{
  "version": "https://jsonfeed.org/version/1.1",
  "title": "Vinkius · AI Connectivity Cloud",
  "home_page_url": "https://blog.vinkius.com/pt",
  "feed_url": "https://blog.vinkius.com/feed.json",
  "description": "Long-form engineering writing by Renato Marinho: AI agent infrastructure, open protocols, and the craft of shipping on static infrastructure.",
  "icons": [
    {
      "url": "https://blog.vinkius.com/favicon.svg"
    }
  ],
  "authors": [
    {
      "name": "Renato Marinho",
      "url": "https://github.com/renatomarinho"
    }
  ],
  "items": [
    {
      "id": "https://blog.vinkius.com/en/posts/ai-agents-the-new-consumers",
      "url": "https://blog.vinkius.com/en/posts/ai-agents-the-new-consumers",
      "title": "AI Agents Are the New Consumers: Why Vinkius Is the Secure Runtime for Agent Workflows",
      "summary": "How AI agents became the first stochastic consumers of a cloud platform, and why Vinkius built a V8 isolate runtime, a twelve-surface governance control plane, and the MCP Fusion MVA architecture as the secure operating system they cannot skip.",
      "content": {
        "markdown": "\nI have been living inside agent workflows for six months now. Not watching demos on slides. Living it. Watching agents book meetings, write code, query databases, move money, open tickets, and forget the ticket number by the next call. And what I learned is simple: agents are not more capable models. They are actors that execute writes to the world through tools, and the question is no longer whether an agent can plan a task. The question is whether the infrastructure that hosts those tools can survive the moment the agent decides to act.\n\nThis is the post where I explain how Vinkius became the operating system for AI agents, and why the gap between an agent on a demo and an agent you would hand production credentials is not a model problem. It is an infrastructure problem. And the answer lives in the architecture we built for MCP Fusion.\n\n## The agent is not a user. It is a new class of consumer.\n\nEvery SaaS platform in the last twenty years was built for one kind of caller: a human behind a browser, or a service account behind a known script. Both of those callers behave like the inside of a system. They do what they are told. They fail fast when the schema breaks. They do not retry a call that returned an error four times, because they do not forget that error from three turns ago. They do not lose track of which tools exist after a handoff to a specialist. They do not swallow a two hundred kilobyte response into context and then summarize a row that does not exist, because truncation is invisible to them.\n\nAn AI agent is none of those things. An agent is stochastic by construction. It will send \"INV-999\" as an invoice ID even when it just listed the invoices and saw three of them. It will retry a call that returned a 404 with the same input, because it does not remember that 404 from three turns ago. It will lose track of which tools exist after a handoff to a specialist. It will loop on a broken tool until the token budget collapses.\n\nAnd then it will call your production database. Your payment API. Your CI pipeline.\n\nThe agents are already acting. They are acting on CRM records, on invoices, on infrastructure state. The question is not whether they will act. The question is whether the surface they act on is built for a non-deterministic principal that cannot read a schema and cannot remember a conversation.\n\n## Why MCP Fusion is not just another MCP server\n\nMCP solved the discovery problem that kept every LLM integration stuck in brittle prompt chains. But vanilla MCP servers inherit every problem of the raw server model, and those problems become catastrophic when the consumer is an agent rather than a human. A raw server leaks whatever it returns, because its output is the row, serialized. A raw server enforces nothing, because its middleware is a convention, not a guarantee. A raw server cannot see its own drift, because it has no mechanism for detecting that the surface it exposes today differs from what it exposed yesterday. A raw server answers errors with strings, and the agent retries with the same string.\n\nMCP Fusion fixes this with an architecture called MVA, and the name is the point. The split is not arbitrary. It is the split that application code already knows, but pointed at a new consumer.\n\nThe Model owns what the data is and what may leave the process. It declares fields, types, and descriptions. Those descriptions are not documentation for a human. They compile, just in time, into the interpretation rules the agent receives with each response. The model declares hidden fields, password hashes, internal flags, tenant markers, and they never cross the boundary. A new database column does not leak to an agent until someone puts it in the model. A raw server has no such property.\n\nThe Presenter owns what the agent perceives about the data. Before anything is serialized, the presenter runs its schema over the raw result in strip mode. Whatever the database returned, the agent sees only the declared surface. This is egress control at RAM level, not a view layer, not a template, not a convention. A field the schema does not know cannot cross. The presenter also attaches system rules, working limits, and affordances. suggestActions tells the agent what it can do next with what it just saw. This is HATEOAS for agents. The response is not a payload. It is a position in a workflow. Server-rendered chart blocks are deterministic: the framework renders them, the agent reads them, and no model in the loop generates the pixels.\n\nThe Tools own the verbs. f.query is read-only by default. f.mutation is destructive by default. f.action is neutral. Those are not metadata labels. They drive what the platform treats as safe to retry, what the observability pipeline marks, and what a governance tool flags when a read turns into a write.\n\nOne rule keeps the separation honest, and it is a security property, not a style rule. Direction. Tools import presenters, presenters import models, models import the core, and nothing imports backwards. The layer that touches your data must never be the layer an agent can steer.\n\nThis is the architecture that every connector deployed on Vinkius Cloud is built on, and it is why the runtime does not need to trust the code it runs.\n\n## The V8 isolate layer: where customer code meets platform control\n\nEvery MCP server a customer ships on Vinkius Cloud is, sooner or later, third-party code executing on our infrastructure. Not ours. Not a library we control. Theirs. A cloud for AI agents is only useful if customers can bring their own logic, their own connectors, their own glue between an LLM and their systems. Once that capability exists, the question becomes: how do you run untrusted JavaScript for thousands of tenants on a small number of servers without giving any one of them a path to hurt the others, the machine, or their neighbors?\n\nThe answer is a V8 isolate per tenant, built on Node.js with isolated-vm, the C++ binding that lets a Node process own a raw V8 isolate. Not a container per tenant. Not a process per tenant. An isolate. A heap with its own garbage collector, its own global scope, and a hard memory cap of 128 MB that the engine enforces, not the application. When a tenant's heap hits it, that tenant's isolate stops being served. The other tenants keep running. No tenant's globalThis is reachable from another tenant's isolate, because there is no shared JavaScript boundary at all.\n\nThe boot is split into two phases, and only the first one is visible to cold-start metrics. Phase one: the V8 binary and the polyfill layer initialize, which takes 50 to 100 milliseconds on first contact with a deploy. Phase two: the tenant's bundle is loaded, wrapped, and executed inside the isolate, which happens on every request because the snapshot cannot capture async state. The platform snapshots the environment, not the program. The polyfill layer is static for a given deploy. What changes per request is the tenant code. The host runs the snapshot builder over the provisioned environment and caches the resulting heap blob on disk, keyed by deploy ID and stamped with the V8 version. On restore, a fresh isolate is created from that blob in 15 to 25 milliseconds total. The bundle re-executes, because isolate creation from a snapshot cannot handle async top-level await, but the polyfill work happens exactly once per deploy.\n\nThe V8 version matters. V8 isolates are not forward-compatible across major engine versions. A snapshot built on V8 version N cannot be loaded by V8 version N+1. The host stamps the snapshot blob with the engine version and refuses to load a blob whose stamp does not match the running version. A mismatch is a cache miss, not a crash.\n\nThe isolation is not just memory. The empty environment that the V8 isolate gives you has no fetch, no crypto, no timers, no process. Nothing that touches the outside world exists inside the isolate. Fetch in a tenant bundle is a polyfill on the guest side, but the actual request happens host-side, through the host bridge, as a binary-safe ArrayBuffer copy across the boundary. Crypto.subtle.digest and HMAC, the ones JWT verification needs, are host-side too, and the host verifies signatures with a constant-time comparison. Timers are host-side setTimeout calls that invoke back into the guest through a registered invoker, capped at five seconds at the framework level and thirty seconds at the dispatch level. The rule is simple. The polyfill makes the call look native. The host owns the effect. A tenant can express intent. It cannot reach the operating system.\n\nThis is the design I wrote about in detail in the [V8 isolates post](/en/posts/vinkius-runtime-v8-isolates). The five-second budget is the per-isolate execution ceiling. The thirty-second budget is the dispatch ceiling. The ten-megabyte response cap is the egress ceiling. All three are named policies in the runtime config, not magic numbers, and all three are documented next to the constant that defines them.\n\n## The SSRF guard: outbound traffic as a policy, not a default\n\nThe most dangerous thing a tenant bundle can do is make an HTTP request. Fetch is a universal source of SSRF. A careless or malicious bundle pointing a tool at the AWS metadata service, at 169.254.169.254, hands a customer's AI agent a read path into the instance's credentials.\n\nAll outbound traffic goes through the SSRF guard. The guard works on one principle. An address is only valid for as long as the policy that approved it. The guard resolves DNS before the request and blocks private ranges outright: loopback, 10 slash 8, 172.16 slash 12, 192.168 slash 16, link-local 169.254 slash 16, zero slash 8, and their IPv6 twins. It then pins the approved IP into the connection, so a rebinding attack cannot swap the address after vetting. And it keeps SNI and TLS aligned with the pinned address, because pinning at the socket level without pinning the name would throw certificate errors on every request.\n\nResponses stream into the isolate with a running byte counter, hard-capped at ten megabytes, with an AbortController wired through the whole dispose path. When an isolate is retired, its in-flight requests are aborted in the same call. When a dispatch times out at the thirty second budget, a classifier asks what the isolate was doing at the moment of breach. Upstream I/O in flight, the guest computing, or memory pressure. The tool error that goes back to the agent carries the answer, plus a recovery hint.\n\nThe DNS cache lifetime is coupled to the connection lifetime. A vetted address is cached exactly as long as the pool holds a keep-alive agent for it, up to 65 seconds by default. An address cannot outlive the connection policy that justified pinning it. There is no independent TTL by design, and that is the property that closes the rebinding window entirely.\n\n## The cryptographic audit path: a chain you cannot forge\n\nAll of the agent traffic, all of the tool calls, all of the data flowing in and out of every connector, flows to a single-threaded streaming daemon. This daemon forges a SHA-256 hash chain and signs it with an Ed25519 session key. The session key is held in RAM for twenty four hours and certified by the master key on boot. Every request hash is sealed at ingestion. Each record carries the previous hash and its position in the sequence. The chain is signed. A tamper is not a privacy issue. It is a detectable event.\n\nThe chain input is the raw base64 payload concatenated with the previous hash and the sequence number. The current hash is SHA-256 of that input. The signature is Ed25519 over the same input, using the session private key. The session key rotates every twenty four hours, and the chain continues without a gap. The chain state is checkpointed to Redis Streams so that a crash resumes from the last acknowledged event instead of re-deriving the chain. SIEM adapters drain that same stream behind their own circuit breakers. A failing sink gets fenced off rather than allowed to back up the audit path.\n\nThe audit is structured around GDPR Article 73, the data subject's right of access. When a regulator or a customer asks for the full history of a connector, the answer is a single exportable, verifiable chain, not a project.\n\nAnd here is the boundary that I am proudest of. V8 never touches crypto. Tenant code can compute a hash through the host bridges, but it cannot sign, certify, or forge anything. The audit trail is something the platform does to the runtime, not something the runtime's tenants can reach into.\n\nThe full twelve-surface model, the audit layer, the decoy layer, and the vault are described in the [AI governance post](/en/posts/vinkius-ai-governance).\n\n## The twelve surfaces: governance as an operating system\n\nIf you have read the AI governance post, you know the control plane. Twelve surfaces, eight that report and four that decide, sitting on scoped connection tokens, a sealed vault for credentials, a hash-chained audit ledger, and a decoy layer that contains damage before you notice it. What I have not yet said is how those twelve surfaces map onto the new consumer, the AI agent itself.\n\nThe agent does not see the twelve surfaces directly. It sees them as constraints on the environment it operates in. And those constraints are what let you hand the agent the night shift, the production query, the payment reconciliation.\n\nAttribution. Every tool call carries the identity of the token that made it, the client behind it, the human or service account behind that. There is no such thing as an anonymous action in production. An agent that should be running hourly and is calling every ninety seconds shows up on Mission Control, and that is where a runaway loop is first noticed, before it becomes an invoice.\n\nData shielding. Sensitive fields are masked in memory, on the outbound path between the upstream and the model. The shielding happens before the response reaches the agent. The difference between data that was masked and data that was never in the model's context.\n\nCost control. The circuit breaker enforces a sliding-window request budget per connector, with a default of five thousand requests every five minutes and a fifteen-minute cooldown. When the budget is exceeded, the breaker trips and tells the agent, in a machine-readable refusal written for a model, that the resource is open and that it should back off. A good agent respects that. A loop that cannot read the refusal gets stopped by the same trip, which is the point. The breaker state is shared across the gateway fleet, so a budget is a budget, not a per-process limit that resets when traffic moves between instances.\n\nError recovery. A raw server answers a bad call with a flat string. The agent retries with the same string. MCP Fusion answers with a self-healing envelope. A specific code, a message, a suggestion, and a list of actions the agent can take instead. InvoiceNotFound tells the agent something that BAD_REQUEST cannot, and the recovery line removes the guesswork that makes retry loops expensive. This is the subject of the [connector architecture post](/en/posts/building-your-own-mcp-connector), where the full MVA pattern is explained.\n\n## The capability lockfile: drift detection as a compile step\n\nOne of the silent failures in agentic systems is drift. The server you deployed last month exposed twelve tools. Today it exposes seventeen, because someone added five more. The agent calls the new ones. Nobody reviewed them. The output is the same row, and it goes to a different place, and the audit trail records the call, but the surface area that was reviewed in CI is not the surface area that the agent can reach.\n\nThe MCP Fusion CLI runs a second, introspective compile of every bundle. It extracts the tool contracts, the prompts, and the credential schema, and writes them into a capability lockfile. This is a deterministic snapshot of the connector's behavioral surface. The lockfile is diffable in git. A fusion lock check in CI is the gate. The surface that your code actually exposes is compared to the surface you committed, and the diff is classified as breaking, risky, safe, or cosmetic.\n\nThe runtime that finally executes that program, sealed and snapshot-restored, is the same program that produced that lockfile. The source code, the compiled bundle, and the committed lockfile are one artifact. Drift is not possible because drift would mean the lockfile and the running code disagree, and that disagreement fails the CI gate.\n\nThe connector ships as a single artifact. The mcpfusion deploy command bundles the server into one self-contained file: all dependencies inlined, Node builtins replaced by stubs that exist only to satisfy the bundler and are never called, and the transport itself stubbed, because the platform supplies it. The bundle passes a size gate of 1.5 megabytes raw, which is a budget, not a spec, then gets compressed and hashed, and goes to the edge. If the hash matches the deployed one, the platform reports an instant restore: the same bytes, no reload.\n\n## Multi-agent orchestration: the SwarmGateway\n\nNot every task can be solved by a single agent. Some problems need specialists. A finance specialist, a devops specialist, a customer operations specialist. Each of them is its own MCP server, its own model, its own tools.\n\nThe SwarmGateway implements the B2BUA pattern. It presents itself to the calling agent as a single MCP server with a prefixed tool list. When the agent calls a prefixed tool, the gateway strips the prefix and forwards the call to the upstream specialist server. When the agent is done with the specialist, a single return tool closes the tunnel and restores the gateway's tool list. Each handoff mints a short-lived delegation token, scoped to that domain only, valid for sixty seconds. The tunnel idle times out after five minutes.\n\nTwo security properties matter here. First, the delegation token cannot escape its sandbox, because the gateway validates the domain against its registry on every call. Second, the return-trip tool is injected by the gateway, not by the upstream, which means a compromised or buggy specialist cannot trick the agent into calling a different gateway. The namespace rewriter enforces this at the tool name level.\n\nAnd because the gateway speaks the same trace context as the incoming agent, every tool span across the specialist handoff parents to the same trace. You can follow the agent from the triage layer to the finance specialist to the devops specialist and back, all in one distributed trace. That work, the W3C Trace Context propagation across the gateway, shipped in MCP Fusion 5.1.0, and the details are in the [trace correlation post](/en/posts/mcpfusion-5-1-0-trace-correlation). When the swarm hits the gateway at scale, the session layer has to coalesce every agent without deadlock. The mechanics behind session pooling, causal invalidation, and the circuit breaker that keeps a runaway swarm in check are in [the SwarmGateway sessions post](/en/posts/vinkius-swarm-gateway-sessions).\n\n## The agent as a governed principal\n\nMost conversations about agent governance treat the agent as a risk to contain, and I am not going to pretend otherwise. The agent is a risk. A non-deterministic principal calling tools against third-party systems under a budget. That is the risk.\n\nBut the other half of the argument is what I would put on the sales deck: governance is what turns an agent from a demo into a worker.\n\nTrust is granted in proportion to evidence. An operator will hand an agent the night shift, the production query, the payment reconciliation, in direct proportion to how well every action it took can be attributed, budgeted, shielded, and verified after the fact. The receipt is not a burden on the agent. It is the credential that gets it the bigger room.\n\nThe envelope of a governed agent is knowable. A deterministic policy around a non-deterministic model is the only sane shape for production: the model plans inside a box, and the box is stable. The circuit breaker's refusal is written in the model's language, so the agent learns the budget and respects it, instead of discovering it in the form of a bill. The truncation in the FinOps guard means the model gets a sized answer instead of a two hundred kilobyte blob, which is an intelligence problem, not only a cost one.\n\nAnd the agent itself gains the capability that no autonomous system has ever had in practice. It can show its work. For an agent that operates in regulated workflows, the audit trail is not overhead. It is the product. The governed agent is the only kind of agent you can give real authority, and the ungoverned agent is, forever, a demo.\n\n## Direction\n\nWhat I have described above is the stack that runs today. Not the plan. Not the roadmap. The stack that a customer's MCP server, built with MCP Fusion, ships to the edge and executes inside.\n\nThe open direction is the stateful one. Longer-lived tenant state riding the hibernation hooks of the isolate. Denser packing of live isolates per host. Teams bringing their own connector catalogs into the marketplace without waiting on our CI. The snapshot layer already carries the hibernation interface. The state sync layer already understands causal invalidation. What remains is the density and the developer experience.\n\nThe deeper direction is the A2A protocol bridge. MCP Fusion already exposes any server as an A2A compliant agent, with the standard discovery endpoint at the well-known agent-card.json path. The SwarmGateway already speaks the B2BUA pattern. When A2A matures and agents start discovering each other autonomously, the gateway does not change. It already handles a fleet of specialists behind a single surface, with scoped delegation tokens and trace continuity through every handoff.\n\n## The boundary\n\nI do not think V8 isolates are the final answer to multi-tenant edge code. They are the answer for the shape this platform has, and the shape is where I want it to stay. Cheap tenants. Hard caps. Millisecond-scale boot. Because an agent's patience is measured in seconds, and so is its trust.\n\nThe agents are already acting on your systems. The question is no longer whether you can afford governance. It is whether you can afford the day you find out you do not have it. Vinkius Cloud is the runtime that ships that governance, and MCP Fusion is the framework that makes it composable. Both are the result of building against one invariant: the consumer is not a human reading a screen. The consumer is a model that cannot read a schema and cannot remember a conversation. Everything else follows from that.\n\nIf you are building agents that act on production systems, the operating system they run on should be built for that consumer, not retrofitted for it. That is what we built. That is what the code enforces. And that is why the audit trail is something the platform does to the runtime, not something the runtime's tenants can reach into.\n\nThe runtime behind the MCP servers on cloud.vinkius.com is what I described here. The framework is open source at github.com/vinkius-labs/mcpfusion. Both are written in the same invariant."
      },
      "date_published": "2026-09-22T00:00:00.000Z",
      "date_modified": "2026-09-22T00:00:00.000Z",
      "authors": [
        {
          "name": "Renato Marinho"
        }
      ],
      "tags": [
        "agents",
        "governance",
        "mcpfusion",
        "security",
        "runtime",
        "isolation"
      ]
    },
    {
      "id": "https://blog.vinkius.com/en/posts/vinkius-agent-era-control-plane",
      "url": "https://blog.vinkius.com/en/posts/vinkius-agent-era-control-plane",
      "title": "The Control Plane of the Agent Era: Eight Rules, and What Vinkius Ships",
      "summary": "The agent era broke every assumption the old control plane was built on. Eight rules, from on-path enforcement to a human hand on the wheel, and the Vinkius layer that ships each one, with its defaults, its numbers, and where the free plan ends.",
      "content": {
        "markdown": "I have spent the last two years watching the word \"control plane\" get used in two completely different senses, and I think the conflation is quietly costing this industry. In the world of networking and Kubernetes, a control plane is the part of the system that decides. It does not carry your traffic. It says what may be built, what may be placed where, who may do what, and it writes that decision down so the data plane can execute it. The control plane is the slow, authoritative half. The data plane is the fast, dumb half.\n\nThat distinction was designed for machines that change on the timescale of minutes. A pod, a service, a route. It assumed the thing that acts is a program a human wrote, with a stable shape, a known owner, and a predictable rhythm.\n\nNone of that is true of an agent. And that is the entire point of this post.\n\n## What an agent-era control plane has to be\n\nAn agent is not a workload you deploy and forget. It is a principal that thinks. It picks a tool, calls it, reads the result, decides the next move, and loops. The shape of its behavior is shaped by reasoning, not by a fixed call graph, and a retry loop can multiply it without anyone writing a new line of code. So the assumptions the old control plane was built on, fixed shape, known owner, predictable cadence, all break at once.\n\nWhat survives is the idea, not the shape. You still need an authoritative half that decides what is allowed and records the decision, and a fast half that executes. But in the agent era the authoritative half has to do more. It has to reason about a caller it cannot fully predict, enforce in a path that the model can rephrase its way around, bound a cost that compounds with every retry, and produce a record a regulator or a finance team will actually read at three in the morning.\n\nSo here is the rulebook. Eight properties, and each one is something I hold Vinkius to. Not all of them are nice to have. Some of them are load bearing, in the structural sense. If one of them is missing, the rest of the system is theater.\n\n## The eight rules\n\n**One. It must own identity.** The first failure mode of most agent setups is that nobody can say who did what. The logs say a request happened; they do not say who sent it, on whose behalf, through which connector. An agent-era control plane attributes every call to a named identity, and that identity is real, not a session cookie. In Vinkius each connector has its own connection tokens, scoped to that connector only, and the token is the unit of attribution. The platform also carries service accounts for the non-human identities that run in CI and OIDC workloads, because a machine that acts in your pipeline is a principal too, and it gets named. Every receipt names the token that made the call. Identity is a column in the data, not a theory.\n\nThe scoping is what makes the column trustworthy. A token issued for the email connector cannot reach the payments connector. The tokens are HMAC authenticated and the plaintext is never stored, so there is no credential sitting in a database to steal, and the fleet itself is inventoried: who each client is, when it last called, how many requests it has made. A leaked credential is then a named, revocable, single-connector asset, not a standing pass to the whole estate, and revoke and rotate sit one click away.\n\n**Two. It must enforce on the path, not after the fact.** A control plane that only reports is a dashboard, and a dashboard cannot stop what is happening right now. The enforcement has to live in the runtime, in the path of the request, before the call crosses the boundary. This is where the word \"governance\" does its real work. A rule that a SIEM correlates an hour later is not a rule; it is a coroner. Vinkius enforces data shielding, cost limits, and capability exposure in the outbound path, in memory, between the upstream and the model. The decision is made in flight, and the receipt records that it was made.\n\nThe shielding layer is where \"masking\" stops being a marketing word. It masks emails, SSNs, and card numbers in memory before the response comes back to the model. Masked, the value still does its job; the model rarely needs the raw digits, and when it does not, the digits have never entered a context window, never been logged, and never become a leak. The Mission Control strip counts the redactions it performed, DLP Protected, per period, so the layer is not just present. It is measured, and the number you see is the running count of secrets kept out.\n\n**Three. It must bound spend and blast radius.** Agent cost is not shaped like any line item you have charted before. It is not per user and it is not per request. It is per thought, and it compounds with every retry a loop adds, so the control plane has to carry a budget as a first-class, machine-enforceable object. Vinkius does this in two layers. The FinOps guard truncates arrays that run past a maximum item count, fifty by default, because a response that returns ten thousand records is a token bill, not an answer; it can compress the payload before it leaves the gateway; and it attributes cost against a rate you set, three dollars per million tokens by default, then measures, in bytes and in dollars, what it saved. The circuit breaker sits in front of that: a sliding-window request budget, five thousand requests over five minutes by default, with a fifteen-minute cooldown, shared across the gateway fleet so the budget holds no matter which instance serves the call. When the budget is exceeded the breaker trips, and the trip does the thing most guardrails never do: it tells the agent, in a machine-readable refusal written for a model, that the resource is open and it should back off. A loop that cannot read that refusal gets stopped by the same trip. That is the point.\n\nAnd the guard leaves a measure of its own work. The KPI strip on Mission Control shows the bytes a truncation pulled out of a response and the dollars the ceiling saved in the period, so the layer reports the bill it prevented, not the bill it charges. When a guardrail has to defend itself with a screenshot of a dashboard, you already know what its default posture is.\n\n**Four. It must keep secrets out of the model's context.** This one is the one I think about most when I design. The thing that decides, the model, and the thing that proves authority, the credential, should never share a context. A model that can read your upstream password is not a problem you audit away; it is the problem. Vinkius keeps connector credentials encrypted at rest with AES-256, and the guarantee we make is that not even the platform's own operators can read them at rest. They are injected into the execution environment only at runtime, inside the isolate where the tool code runs, and the agent never sees the secret. The connection tokens themselves are authenticated in a way that the plaintext is never stored. If you want one sentence to hand a reviewer: the authority that signs a call is kept apart from the mind that makes it.\n\n**Five. It must leave a record that is tamper-evident and exportable.** Most \"audit trails\" are append-only logs, which is to say they are as trustworthy as the person who owns the disk. An agent-era control plane has to do better, because the record is the thing a regulator, a customer, and a finance team will actually be asked to read. Vinkius seals every request hash at ingestion into a hash-chained ledger: each record carries the previous hash and its position in the sequence, and the chain is signed, SHA-256 with Ed25519. So a tamper is not a privacy issue, it is a detectable event, and the dashboard states plainly whether the chain is valid or compromised. The deployment audit exports as a PDF, and the whole structure is built for the regulator's one question, show me the full history of a connector, which arrives as a single exportable, verifiable chain, not a project.\n\n**Six. It must make the model's own line of reasoning visible.** A receipt that cannot join the agent's trace is a receipt nobody can read. The control plane has to carry the caller's distributed trace context across the gateway, so that every tool span parents to the trace that started the thought. That is the work in our open framework since the 5.1.0 release, covered in [the MCP Fusion 5.1.0 post on trace correlation](/en/posts/mcpfusion-5-1-0-trace-correlation), and it matters here because a governance record that is unrooted from the agent's own reasoning is forensically useless. And when a caller does not send a trace context, the identity floor still names who drove the call, so attribution never depends on the caller doing its part.\n\n**Seven. It must give a human a real off-switch and a real approval gate.** Automation without a human-shaped stop is just a faster way to break things. The control plane has to expose two distinct human actions, and they are not the same. The first is the approval gate: a deployment that requires a second person before it goes live, which Vinkius frames on the four-eyes principle that the EU AI Act expects of high-risk systems under Article 14(5). One person does not, alone, decide that a new tool with real consequences reaches the agents. The second is the off-switch: a global emergency halt that stops every active connector in the organization, deactivates all of them, revokes every token, and terminates every open session, in one synchronous action, and the confirmation requires you to type HALT ALL, because that is the button you press when something has gone badly. After a halt you can restore, but the revoked tokens stay revoked; you reissue them on purpose. Recovery is a new trust relationship, not a replay of the old one.\n\n**Eight. It must be cheap to run and honest about its own cost.** A control plane that is more expensive and slower than the thing it governs has failed, and the second half is the one most vendors dodge. The overhead of the authoritative layer has to be measured and shown, not claimed. In Vinkius the request detail breaks latency into the time the upstream API took versus the time the governance layer added, so you always know what the control plane itself cost on that call. And the platform is straight with you about what is live and what is a preview: on the free plan the governance dashboard opens with clearly labeled sample data, so you learn the shape of the control plane before you commit, while live enforcement, the circuit breaker, and the emergency halt run on paid plans. I would rather say that plainly than let a reader assume the free tier does the paid thing.\n\nThe one place the control plane itself talks to a language model is the AI Briefing, and it is a paid feature, because the briefing is a model call, and an honest control plane does not hide where its own compute goes.\n\n![The eight rules of the agent-era control plane, each mapped to the layer in Vinkius that enforces it: identity, on-path enforcement, spend and blast radius, secret isolation, a tamper-evident ledger, trace continuity, a human gate and off-switch, and a measured, honest overhead.](/post/fig-agent-control-plane-rules.svg)\n\n## The foundations the rules stand on\n\nSome of the rules above sit on layers you will never see on a dashboard, and I am proudest of those. The marketplace carries watched decoys, planted where an attacker looking for exfiltration will find them: credentials that look like real secrets and are not. Touch one and the platform quarantines the server, revokes its tokens, cuts the open connections, and freezes its payout path, automatically, without a human deciding. Containment that does not wait for you to notice.\n\nUnderneath all of it is the organization layer. Single sign-on is enforced per organization domain, and the organization itself runs on members and teams, custom roles and the permissions they grant, service accounts for the machines that act in your pipelines, organization API keys for programmatic access, and an audit log of the security-relevant events. The control plane stands on an identity model that knows the difference between a person and a machine, and every one of the eight rules leans on that difference.\n\n## Why the rules beat the surfaces\n\nI wrote about the twelve surfaces Vinkius ships as AI Governance, the eight that report and the four that decide. See [the AI governance post](/en/posts/vinkius-ai-governance) for the tour. That was the inventory. This post is the standard the inventory is measured against, and the reason I keep them separate is that a vendor can show you a shelf of dashboards and call it a control plane. The surfaces are easy to fake. The rules are not, because each rule corresponds to a property you can test. Can it name the caller? Does it act before the call leaves? Does it cap a runaway loop the model can read? Does it keep the credential out of the context? Can it prove the record has not been rewritten? Can a human actually stop it? Is its own cost on the table?\n\nI am not trying to win a naming war. \"Governance\", \"observability\", \"security\" will all get applied to this thing, and all of them understate it. A dashboard observes. A policy file decides, once, at deploy time, and then forgets. The control plane is the only name that carries the decision: it decides per call, and it keeps the receipt.\n\nIf you are buying, that list is the spec. If you are building, it is the checklist you should be embarrassed to have skipped. I put it in writing, in [the post on how Vinkius runs every MCP server in a V8 isolate](/en/posts/vinkius-runtime-v8-isolates) and back, because I have come to believe the control plane is the product, and the product is the rules, not the screens.\n\n## The bar I will hold the industry to\n\nHere is where I go from engineer to, uncomfortably, the person who gets to set a bar. The agent era is going to be judged by how well its control planes did these eight things, and most of the control planes shipped today will not make the grade, because they were built for a world where the caller is a human who filed a ticket. They have a policy engine and a log. They do not have identity, in-flight enforcement, a budget the model can read, a sealed vault, a signed chain, a trace join, a human gate, or an honest overhead number. They have a dashboard and a hope.\n\nThat is not an insult. It is the gap, and the gap is where the next five years of infrastructure get decided. I wrote these rules the way I would want them written down for a team that did not build the thing: specific enough to test, and general enough to outlast this year's names. The eight properties are the contract. Everything else is marketing.\n\nWhen your agent is the thing that acts, the control plane is the thing that decides. Build the deciding half first, measure it, seal its record, and give a human a real hand on the wheel. That is the standard. I hold Vinkius to it, and I would hold yours to it too."
      },
      "date_published": "2026-09-22T00:00:00.000Z",
      "date_modified": "2026-09-22T00:00:00.000Z",
      "authors": [
        {
          "name": "Renato Marinho"
        }
      ],
      "tags": [
        "ai-control-plane",
        "agents",
        "governance",
        "observability",
        "security",
        "mcp"
      ]
    },
    {
      "id": "https://blog.vinkius.com/en/posts/vinkius-capabilities-catalog",
      "url": "https://blog.vinkius.com/en/posts/vinkius-capabilities-catalog",
      "title": "The Capability Layer: 8,933 Connectors and the End of Custom Integrations",
      "summary": "What a catalog of AI capabilities actually is: the anatomy of a capability call, the five shapes a capability ships in, and the plan math, security layer and maintenance model behind 61,738 managed actions.",
      "content": {
        "markdown": "For twenty years, the unit of integration work was the endpoint. You had a URL, a verb, a wire format, a rate limit, and an OAuth flow, and you wrote glue code to hold it all together. The unit is changing. It is the capability: one thing an agent can do to one system. \"Send the invoice\" is a capability. \"Refund the last order\" is a capability. \"Pull the open tickets\" is a capability. The endpoint is an implementation detail underneath it.\n\nThat is why Vinkius ships a catalog of capabilities rather than a catalog of API wrappers. Today the catalog holds 8,933 managed connectors and 61,738 capabilities, and the number that matters is not the number of connectors. It is that each capability is a unit you can host, version, authorize, meter, and audit, the same way you would audit any other service in the stack.\n\nThis post is an anatomy of that layer. What a capability actually is, how one request flows through it, the five ways a capability gets shipped, and the plan math and security model underneath. No marketing math: every figure in here is what the product ships.\n\nThe naming is not accidental. In the product, you do not search for connectors; you search for \"AI Capabilities.\" The catalog, the search bar, and the upgrade pitch all speak in the same unit. When a vendor sells a layer of the agent era, the unit of sale tells you what they believe they are selling. Vinkius sells actions, not endpoints.\n\n## The unit of work is no longer the endpoint\n\nAn agent does not send an HTTP request to a service. It sends intent. In the Vinkius execution model the intent has exactly three fields. The capability, which is the action to complete. The input, which is the data the action needs. And the identity, which is the user authorizing the action. Everything else, authentication, permissions, mapping to the right operation, retrying on a transient failure, belongs to the platform, not to the agent.\n\nThen four stages run. Authenticate connects the correct user account. Authorize enforces the permissions for the requested action. Resolve maps the capability to the correct system operation. Execute runs the operation, retries it when it should, and records what happened. The result comes back structured: a status, a normalized output, and a trace.\n\nNotice what the model never sees. The wire protocol. The retry policy. The permission check. It asked for an outcome and got an outcome.\n\nThat is where most of the industry is still stuck. Models are better at taking an outcome and reasoning about it than at babysitting a fetch call. The capability layer is the place where intent stops being a prompt and becomes a transaction.\n\nOne more part of the anatomy matters if you run fleets of agents: who is acting. Every capability call carries an identity, and the identity is a user, not the model. The product is built around that fact. You can build one application for an unlimited number of end users, and each user's connections and credentials stay fully isolated from every other user's and from the platform itself. The agent is the hand. The identity is who the hand belongs to.\n\n## What the catalog ships\n\nThe numbers first, because they anchor everything. 8,933 connectors. 61,738 capabilities. Of the connectors, 8,764 are official, published and maintained by Vinkius, and 169 are community. At the moment of writing, 1,315 listings carry the new marker. The site's own counter is generated from the catalog data, not a marketing constant someone retypes every quarter, which is the difference between a number you can quote to a board and a number that flatters you.\n\nGrades are part of the surface. The official connectors carry an A+, and the community listings carry letter grades that go all the way down to F, so the grader is not decorative. I have watched vendors publish quality scores that nobody can reproduce. Ours sit in the catalog data next to each listing, which is where you want them.\n\nPublishing is the other side of the ledger. 8,764 listings are official, and 169 are community. The distinction matters for one reason. Official means Vinkius is on the hook. For a community listing, the publisher is on the hook. Both go through the same verification pipeline and land in the same search, and the trust badge is a liability marker, not a category.\n\nWhat managed actually means is the boring part. Vinkius hosts the connector, maintains it, updates it, and runs the authentication. When OpenAI or Salesforce changes an endpoint, that is our job, not yours. Every connector in the catalog lands as a verified surface: production ready, with the guarantees that go with that label. The control plane says it in one line, MCP VERIFIED, PRODUCTION READY, VINKIUS GUARANTEED, and a single click activates the connector in your account.\n\nHosting somebody else's connector is a liability you absorb. When a third party deprecates a tool, the fix lands in the connector, not in your app. The catalog is where that liability sits, and where your maintenance bill ends.\n\nThe breadth shows up in the name list. OpenAI, Anthropic, Stripe, GitHub, Slack, Salesforce, Tesla, PayPal, Plaid, Datadog, Jira, Shopify, plus the long tail: Idealista, Semrush, ElevenLabs, Hugging Face. The catalog page reduces the whole thing to one sentence. One connection, every tool your agent can call.\n\n## Two ways to browse\n\nThere are two shelves, and they answer different questions. The first is editorial: twelve hand-curated categories, Industry Titans, Superpower, Loved by Developers, AI Frontier, The Unthinkable, Money Moves, Ship It, Talk to Me, Growth Engine, Brain Trust, Fort Knox, and Friends of MCP. Those are taste judgments. They are the answer to the question, \"show me what is worth my time.\"\n\nThe second is organic: twenty-two categories populated automatically from the manifests, from Productivity and Developer Tools down to long-tail verticals like Legal, Healthcare, Travel and Hospitality, and Real Estate. They are the answer to the question, \"what can I actually plug in for this team, right now.\"\n\nYou do not have to pick a shelf in the product. Discovery, Explore, Favorites, Activated, and Library sit side by side, and the catalog search is the same search an agent uses when it is looking for a tool mid-task. Inside Explore, four tabs do the work: Top Curated, Newest, Top Tags, and Top Categories. Each is a different answer to the same question, which one of the 8,933 is the one I need.\n\nThe catalog also browses without an account. Discovery, tags, categories, and new arrivals are public, because the catalog is a discovery surface, and discovery should not sit behind a sign-in wall. Activation is where the account matters. One click, and the connector is live in your control plane. The search works on the job, not just the name: you describe what you need, and the catalog finds the capabilities that power it.\n\n![The anatomy of a capability call: agent intent (capability, input, identity) through four execution stages into a structured result, with the catalog of 8,933 managed connectors behind it](/post/fig-capabilities-catalog.svg)\n\n## The five ways a capability arrives\n\nA capability is a contract on top of the transport. Underneath it, one of five server types ships it.\n\nThe API server is the workhorse. It is a REST proxy: you point it at a base URL, and the surface turns any API into an MCP interface without you rebuilding the client side. If the vendor ships a spec, you import it, and the capability set appears.\n\nThe Agent Skills server is different on purpose. It exposes skill files the model reads on demand, in a pattern called progressive disclosure: the model sees what is available, then pulls the details only when it chooses that path. It is the shape of capability that fits codebases and knowledge bases where the full tool description would not fit in context.\n\nThe MCP Server Deploy server is your own code. You bundle it, ship it to the edge, and it runs inside the V8 isolate runtime that I wrote about [in depth here](/en/posts/vinkius-runtime-v8-isolates). The runtime story is where the cost and latency numbers live.\n\nThe YAML server is the declarative one. A manifest in YAML, compiled at deploy time. It is the shape of capability that teams want in git, reviewed like infrastructure.\n\nAuthentication rides underneath all of it in four shapes. None, when the surface is public. Bearer, for token-based services. Basic, for the legacy corner. And a custom header, for the vendors who refuse to fit any of the other three. The point of the taxonomy is that the model does not see any of it. The model sees a capability; the transport, the auth shape, and the retry policy are the platform's problem.\n\nFrom the user's side, authentication is not a wall. Each connector shows its state at a glance: setup required, connected, ready. No hidden login page, no \"it should work.\" There is one more unit worth naming before the plan math: the capability set. Every connector ships its complete set, the exact actions your AI can choose when you ask it to work with that connector. The agent does not see 8,933 surfaces at once; it sees the tools it actually has. That is not a product detail. It is how the catalog keeps the model's context honest.\n\n## The plan math\n\nCapability access is plan-gated in one place only: the free tier. It runs a hundred requests per 30-day cycle, one connection token, two catalog installs, and its dashboards are labeled with sample data, so nobody mistakes a demo for a live system. From the paid tiers up, the catalog has no install cap, and the request ceilings are the real planning numbers: 5,000 per cycle on Lite, 25,000 on Starter, 100,000 on Pro, and 500,000 on Business.\n\nThe ceilings are not decoration. They are the metering layer. A capability that can refund an order is a capability that can run in a loop, and a loop that burns a thousand dollars a night is a governance problem before it is an engineering one. The product's own KPI, Cost Saved, measures the bytes a cost ceiling removes from the outbound path, so the number on the dashboard is a number you can defend in a budget meeting.\n\nPast the ceiling, overage bills on a soft limit rather than hard-stopping the call, and that is deliberate. A hard stop in production is a support queue. A metered overage is a line item. That is the whole philosophy of the ceilings: they are numbers to plan against, not walls to hit.\n\nConnection tokens climb the ladder: three on Lite and Starter, ten on Pro, twenty-five on Business. Activity history goes from three days on the entry paid tiers to seven on Pro and thirty on Business, and the audit trail on the top tier is the immutable one. That tier is where organizations live. Teams, roles, project scoping, service accounts and API key provisioning, SSO, a thirty-day immutable audit trail, instant token revocation, and a kill switch with circuit breaker.\n\n## The security layer your CISO will ask about\n\nEach connector runs in its own sealed sandbox. Thirty-four rules or more are enforced on every request, and they cover memory limits, CPU limits, SSRF protection, private-network blocking, malicious-file protection, and credential isolation. Security events stream in real time to Datadog, Splunk, or a webhook. The audit trail is signed and chained, Ed25519 signatures over SHA-256 blocks, so a tampered record cannot be quietly dropped without the chain breaking.\n\nCredential handling follows one rule. Scoped, user-controlled, revocable at any time, and the platform never trains on your data. The product's own FAQ says the second half out loud, so you can quote it back: \"Vinkius never trains on your data, and you can revoke access at any time.\" End-user credentials are stored securely and never shown again after save, and each user's connections stay fully isolated from every other user's and from the platform. The deploy audit surface exports a PDF, which is the format a regulator or a board committee actually reads.\n\nI have been asking vendors for years to write those two sentences on their page. The catalog writes them.\n\n## Why managed beats bolted on\n\nEvery custom integration is a promise you have to keep forever. The vendor changes a field. You rewrite the parser. The auth flow moves. You chase it. An integration nobody has looked at in a year is a liability wearing the costume of a feature. The catalog inverts the maintenance model. Hosting, authentication, execution, monitoring, updates: the platform does them, and a capability becomes one line in a manifest instead of a codebase of glue that someone has to babysit.\n\nThe math of that inversion is the argument. Say your stack needs a hundred connectors. A hundred custom integrations means a hundred maintenance promises, each one a parser that can rot, an auth flow that can break, a version that can change underneath you. A hundred catalog connectors mean a hundred maintained surfaces, and the maintenance cost sits on the platform side of the bill. That is the difference between buying a tool and owning one.\n\nThere is also a day one argument. Your AI application starts with thousands of connectors in the catalog, and that is a real difference from the team that spends its first quarter wiring the two integrations the demo needed. The catalog is the reason the first commit of an agent app can be the productive one.\n\nThe client side is the same shape. The catalog's surfaces speak MCP, which means the same connector works from ChatGPT, Claude, and Gemini, from Cursor, Cline, and VS Code, and from the Vercel AI SDK inside your own app. You maintain the capability set once, and every client that speaks the protocol can call it.\n\n## Where this sits\n\nThe catalog is the capability layer. The control plane is the rules of the calls: identity, on-path enforcement, bounded spend, secret isolation, a tamper-evident ledger, trace continuity, a human gate and an off-switch, and a measured overhead. I wrote the eight rules [in the control plane post](/en/posts/vinkius-agent-era-control-plane). The V8 isolate runtime has [its own post](/en/posts/vinkius-runtime-v8-isolates). The catalog is where those rules have something to govern.\n\nThree posts, one stack. The control plane post covers the rules of the calls. The V8 post covers where the code runs. This one covers the unit that makes the two worth writing. Without a catalog, the control plane governs nothing, and the isolate runtime has nothing to run. The catalog is the supply side of the agent economy. It decides which actions exist, who maintains them, what they cost, and what happens when they fail.\n\nWhen an agent asks for the world, the capability layer is the door, and the control plane is the lock. The job of the catalog is to make the door wide enough that an agent never has to build one itself. The bar I will hold the catalog to, and any catalog claiming the same space: verified surfaces, grades you can reproduce, metering a finance team can defend, and an audit trail that cannot be quietly edited. Everything else is a directory, not a layer."
      },
      "date_published": "2026-09-22T00:00:00.000Z",
      "date_modified": "2026-09-22T00:00:00.000Z",
      "authors": [
        {
          "name": "Renato Marinho"
        }
      ],
      "tags": [
        "capabilities",
        "mcp",
        "marketplace",
        "connectors",
        "agents",
        "integrations"
      ]
    },
    {
      "id": "https://blog.vinkius.com/en/posts/vinkius-swarm-gateway-sessions",
      "url": "https://blog.vinkius.com/en/posts/vinkius-swarm-gateway-sessions",
      "title": "The Swarm Problem: How Vinkius Gateway Coalesces 100 Concurrent Agent Sessions Without Losing State",
      "summary": "How 100 concurrent AI agents sharing sessions leads to deadlock, rate limit racing, and stale state, and how Vinkius Gateway's B2BUA pattern, session caps, causal invalidation, and circuit breaker solve it.",
      "content": {
        "markdown": "The Swarm Problem: How Vinkius Gateway Coalesces 100 Concurrent Agent Sessions Without Losing State\n\nI watched a customer deploy twelve AI agents onto a single supply chain optimization task last month. By day three the agents were deadlocking on shared inventory state, racing for supplier API rate limits, and creating cascading invoice revisions that no single agent could unwind. The customer's conclusion was that the agents were \"too smart for their own good.\"\n\nThat is not what happened. The agents were not too smart. They were too *alone*. Each one thinking it owned the session, each one writing state that the others would never reconcile, each one burning through the same pooled rate limit without knowing its peers existed. The problem was not agent intelligence. It was the session layer they all shared, and the fact that nobody owned it.\n\nThis is the swarm problem. You do not get one agent anymore. You get a crew, a squad, a swarm. Finance agents, inventory agents, procurement agents, compliance agents. All talking to different services through different credentials, all sharing the same rate budget, all believing the state they read five calls ago is still valid. The old model assumed one agent, one session, one lifecycle. The new model assumes none of those things.\n\nVinkius solves this with a gateway pattern, not an agent pattern. The gateway is the session owner. The agents are guests. The numbers that govern this design are not aspirational. They are extracted from the runtime config and the SSRF guard and the circuit breaker code, each one derived from an external constraint. Here is how the math works.\n\n## The B2BUA That Owns the Session\n\nThe SwarmGateway is a Back-to-Back User Agent. Not a proxy. Not a load balancer. A B2BUA: it terminates the inbound MCP session from the caller and establishes a separate outbound session to each specialist agent. The gateway speaks for the caller, but the caller's session ends at the gateway.\n\nThe gateway is configured with a registry that maps specialist names to upstream URLs, a shared delegation secret, and a set of operational limits. When an agent triggers a handoff, the gateway mints a scoped delegation token with an HMAC signature. The token carries claims: issuer, subject, issued-at, expiry, target agent ID, optional carry-over state, and a W3C trace context identifier so the specialist can correlate back to the originating trace.\n\nThe token TTL is sixty seconds. That is the constant in the code: `tokenTtlSeconds = 60`. It is not arbitrary. Sixty seconds is the window where a handoff completes its handshake, the specialist authenticates through the gateway, and the return-to-gateway path is established. Longer than that and a stolen token stays valid past the attention span of any single agent turn. Shorter and legitimate handoffs get killed mid-flight by network jitter.\n\nThe idle timeout is five minutes. `idleTimeoutMs = 300_000` in the gateway configuration. When a delegated session sits idle beyond five minutes, the gateway evicts the transport, closes the upstream socket, and reclaims the slot. The sweep interval is fifteen seconds, so the gateway knows within that window that a session has gone dark.\n\nThe session cap is one hundred. `maxSessions = 100`. This is the ceiling on concurrent delegated sessions the gateway will track before it starts rejecting handoffs with a machine-readable refusal. Beyond one hundred, the gateway stops minting tokens and returns an error that the calling agent can read and act on.\n\nThe connect timeout is five seconds. `connectTimeoutMs = 5_000`. When the gateway tries to reach a specialist agent upstream, it has five seconds to establish the connection before it aborts and rolls back the handoff. This is deliberately tight: a slow specialist should fail fast, not hang the swarm.\n\nAll of these limits propagate through the V8 isolate boundary. The dispatch budget is thirty seconds, defined in `Limits.ts` as `DISPATCH_TIME_BUDGET_MS = 30_000`. The boot budget is five seconds, `BOOT_TIME_BUDGET_MS = 5_000`. The heap cap is one hundred twenty eight megabytes, `ISOLATE_MEMORY_LIMIT_MB = 128`. When a tool call inside a delegated session exceeds any of these, the TimeoutClassifier steps in to determine whether the breach was upstream I/O wait or host-side compute, and the error message carries a recovery hint to the calling agent.\n\n## Session Isolation: Fifty Per Token, Two-Minute Idle\n\nThe gateway does not own session lifecycle alone. The SessionManager, which sits in the runtime layer, enforces two session caps in parallel.\n\nThe first cap is fifty sessions per token. `MAX_SESSIONS_PER_TOKEN = 50` in the SessionManager code. This is the guardrail that prevents one compromised or misbehaving connection token from exhausting the entire session table. When a token reaches fifty active sessions, the next connection attempt is rejected with a clear error rather than silently evicting an older session.\n\nThe second cap is time-based. The idle timeout is two minutes in normal operation, `SESSION_IDLE_TIMEOUT_MS = 120_000`. Under memory pressure it drops to thirty seconds, `SESSION_PRESSURE_TIMEOUT_MS = 30_000`. The sweep runs every fifteen seconds, `SESSION_SWEEP_INTERVAL_MS = 15_000`, so a session that goes quiet is reclaimed within one sweep cycle of its timeout.\n\nSession metadata is replicated through Redis with a time-to-live of one hundred fifty seconds, `REDIS_SESSION_TTL = 150`. This is the mechanism that enables horizontal scaling: when a request arrives at a runtime instance that does not hold the session transport in memory, the route handler queries Redis for the session token and rehydrates the MCP server on the spot. The Redis TTL is intentionally set to match the idle timeout plus a buffer, so stale sessions expire automatically even if the sweeping instance crashes.\n\nMemory pressure thresholds are defined as fractions of the container limit. `MEMORY_WARN_THRESHOLD = 0.65` triggers warnings in the logs. `MEMORY_PRESSURE_THRESHOLD = 0.80` triggers aggressive cleanup. The cleanup logic evicts idle sessions, forces garbage collection if the Node runtime exposes it, and logs the memory state so operators can see the pressure curve in real time.\n\nThe ConnectionTracker, which manages the transport-level cache, has its own idle sweep at thirty minutes, `IDLE_TIMEOUT_MS = 30 * 60_000`. This is the tier-one normal eviction path. Under memory pressure it has a second tier: when RSS crosses seventy five percent of the task memory budget, it evicts every cached config with zero active connections, oldest first, until pressure drops below sixty five percent. The code explicitly states: \"This gives the auto-scaler time to provision new containers before OOM.\"\n\nThe coupling between these layers is deliberate. A session transport is not the same as a connection cache entry is not the same as a DNS-pinned outbound agent. They each have their own owner, their own timeout, their own eviction trigger. But they all decay on the same signal: the session went quiet, or the container is running out of memory.\n\n## State Sync: Causal Invalidation Without Stale Reads\n\nThe state sync layer is where multi-agent coordination stops being a hope and starts being a protocol. Vinkius implements causal invalidation with three mark types: immutable, volatile, and causal.\n\nAn immutable mark means the state is frozen. Once written, it cannot be changed. A volatile mark means the state is ephemeral and may be evicted at any time under memory pressure. A causal mark means the state is invalidated when any of its upstream dependencies change. These marks are not stored inside the V8 isolate. They are tracked in the host's state sync layer, which pushes invalidation signals through the Redis Streams pipeline.\n\nWhen a specialist agent modifies a resource, the gateway publishes an invalidation event to the appropriate stream. Any agent holding a causal mark on that resource must re-resolve its state before the next tool call. This prevents the classic multi-agent problem where Agent Finance reads a price from Agent Inventory, Agent Inventory updates the price, and Agent Finance bills the customer using the stale value.\n\nThe carry-over state pattern from the SwarmGateway reinforces this. When the gateway mints a delegation token, it embeds the caller's intent as carry-over state in the token claims. The specialist receives this state and can act on it, but the gateway retains the authoritative copy. When the session returns to the gateway, the carry-over state is reconciled against the gateway's records, and any causal marks that the specialist touched are invalidated across the swarm.\n\nThe connection between sessions and the crypto audit trail is what makes this durable. The hash chain is constructed as `raw_base64 || previous_hash || sequence_number` and signed with Ed25519 each time. V8 never touches crypto directly. The StreamingDaemon is the single-threaded worker that reads from Redis Streams, forges the hash chain, signs with Ed25519, and dispatches to SIEM destinations. The session key rotates every twenty four hours, and state changes are checkpointed into Redis Streams so the chain survives a worker crash without gaps.\n\n## Connection Pooling: Four Thousand Domains, One Hundred Sockets\n\nThe SSRF guard enforces a paired cache: DNS resolutions and pooled outbound agents are coupled by lifecycle. The DNS cache holds a maximum of four thousand nine entries, `DNS_CACHE_MAX_ENTRIES = 4_096`. When the cache is full, the oldest entry is evicted. The agent pool holds a maximum of one hundred connections, `AGENT_POOL_MAX = 100`. When the pool is full, the oldest pooled agent is closed and its DNS entry is dropped in the same operation.\n\nThis coupling is the defense against DNS rebinding. The guard resolves DNS before the request, validates that the resolved IP is not in a private range, and then pins that specific IP into the undici Agent's custom lookup function. The SNI and TLS name remain the hostname, so certificate validation still works correctly. A rebinding attack that returns a different IP after the initial DNS resolution cannot redirect the socket, because the socket is pinned to the vetted address.\n\nThe private IP ranges blocked are: loopback (127 slash 8), Class A private (10 slash 8), Class B private (172.16 to 172.31 slash 12), Class C private (192.168 slash 16), link-local (169.254 slash 16), zero network (0 slash 8), and their IPv6 twins (double colon 1, FC00 slash 7, FE80 slash 10). These are tested against each resolved address before the connection is established.\n\nThe keep-alive timeout on pooled agents is sixty five seconds, `AGENT_KEEP_ALIVE_TIMEOUT_MS = 65_000`. This is deliberately set above the standard ALB idle window of sixty seconds so that pooled sockets survive between conversation turns without being silently closed by the load balancer. Undici's library default of four seconds would close sockets between every turn, forcing a cold TLS handshake on nearly every real-world call. The sixty five second timeout keeps the connection warm for the duration of a typical agent conversation.\n\nThe sweep that cleans idle agents runs every sixty seconds, coordinated with the ConnectionTracker's thirty minute idle sweep. When a pool entry is evicted, the DNS entry dies with it. Without a live connection policy, the vetted address is re-derived and re-vetted on the next use. The code comments state this explicitly: \"a DNS resolution is cached exactly as long as we hold a pooled agent for that hostname, both die together.\"\n\n## The Circuit Breaker That Prevents Cascade\n\nThe circuit breaker is the financial guardrail that stops a runaway swarm from burning through budget. It is a sliding window counter implemented in Redis. The defaults are five thousand requests per five minute window, with a fifteen minute cooldown period. These numbers come from the governance dashboard configuration, not from the runtime code. The runtime code in CircuitBreaker.ts mirrors the PHP layer's `CheckRequestQuota::checkCircuitBreaker` method.\n\nWhen a request arrives, the breaker checks if the circuit is already tripped by looking up a TTL key. If the key has a positive TTL, the circuit is open and the agent receives a machine-readable refusal string: \"CRITICAL: Financial budget ceiling exceeded. DO NOT RETRY this request.\" The agent is instructed to direct the human user to the Vinkius Cloud console to approve resumption.\n\nIf the circuit is closed, the breaker increments a counter keyed by a deterministic window key: the account ID and the floor of the current time divided by the window size in seconds. If the counter exceeds the threshold, the circuit trips. The tripped state is stored as a Redis SETEX with the cooldown duration as the TTL, so it automatically resets after the cooldown period.\n\nThe breaker fails open. If Redis is unavailable, the check throws an error that is caught and logged, and the request is allowed to proceed. This is a deliberate design decision: a Redis outage should not block legitimate agent traffic. The tradeoff is that during a Redis outage, the circuit breaker is disabled and agents can exceed their budget. The code comments note this explicitly: \"fail-open prevents blocking legitimate traffic.\"\n\nWhen a session is evicted under memory pressure, the agent pool entries follow. The cascading effect is controlled: the breaker stops new requests, the session sweep removes idle sessions, the connection tracker evicts idle caches, and the DNS cache drops entries that no longer have live connections backing them.\n\n## The 34 Plus 4 Rules That Contain Each Session\n\nThe IsolateRunner enforces thirty four plus four engineering rules that keep each delegated session from escaping its sandbox. The thirty four rules are enforced at boot, snapshot, dispatch, and disposal. The four additional rules are memory, CPU, network, and disk containment.\n\nAt boot: a life-support polyfill replaces every Node builtin that could touch the outside world. The guest callback registry is registered before the IIFE executes. Binary-safe fetch is injected as a host bridge call, not a polyfill that the bundle can replace. The script is released immediately after boot, so the snapshot cache cannot hold a reference to boot-time objects.\n\nAt snapshot: V8 heap snapshots are cached to disk with a four-layer integrity model. The bridge stubs are replaced with host-owned references so the snapshot cannot capture a stale transport. State externalization hooks let the isolate shed references that should not survive a snapshot. The snapshot is keyed by deployment ID and invalidated atomically through Redis when the deployment is reimported.\n\nAt dispatch: structured clone with `copy: true` ensures that objects crossing from the isolate to the host are deep-copied, not shared. The dispatch timeout fires the watchdog, which kills the script even while it is parked on a host-side fetch. The TimeoutClassifier then determines whether the kill was upstream I/O wait or host-side compute, and the error carries a recovery hint.\n\nAt disposal: an AbortController wired through the entire dispose path aborts in-flight requests. Timer guillotine cancels every setTimeout and setInterval registered by the guest. References are released in reverse order, and a disposed guard prevents double-free. When a session is evicted, its in-flight requests are aborted in the same call, and the byte counter on the response stream is hard capped at ten megabytes.\n\nThe ten megabyte response cap is `MAX_FETCH_RESPONSE_BYTES = 10MB` in the runtime config. The byte counter is wired through the AbortController so a runaway response can be cut off mid-stream, not after it fills the isolate heap. The SSRF guard's `safeFetch` function enforces this counter on every outbound request, and the guard is the only path the isolate has to the outside network. There is no direct socket, no DNS-over-HTTPS tunnel, no WebSocket upgrade that bypasses the guard.\n\nThe four containment rules are: heap cap at one hundred twenty eight megabytes, enforced by V8's own `resourceLimits.maxOldGenerationSizeMB` flag. CPU is bounded by the thirty second dispatch watchdog, which is not a soft limit and cannot be extended from inside the isolate. Network is forced through the SSRF guard, which means the guard's DNS cache and agent pool are the only egress paths. Disk is the hard one: the isolate has no filesystem access at all. The bundle runs entirely in memory, and any file operations go through the host bridge.\n\n## Data Shielding Before the Agent Ever Sees It\n\nMulti-agent sessions multiply the surface area for data leakage. One agent reads a customer record, another agent joins the conversation, and suddenly both agents can see the same sensitive field that only the first one was authorized to read. The DLP layer prevents this by operating before the data reaches any agent.\n\nThe ResponseGuard applies redaction patterns to every response before it leaves the gateway. The default patterns include wildcard matches for email, password, secret, credit card, SSN, phone, API key, token, date of birth, bank account, and IBAN. The wildcard syntax means `*.email` protects every email field at every depth in the response object, and `items[*].credit_card` protects array items specifically.\n\nThe redaction happens in RAM, never on disk, and never inside the V8 isolate. The guard runs in the host process before the response is handed to the MCP server. This means the sensitive data is masked before it enters any tool description or argument that an agent could read. The guard is stateless and deterministic, so the same response always produces the same redaction, which makes replay attacks on the audit trail impossible.\n\nEach redaction is counted and attributed. The Security Posture report surface tracks the total redactions per hour, per connector, per agent session. When an agent triggers a redaction, the circuit breaker's error attribution knows it was DLP, not upstream, not the agent. The log entry reads `Vinkius Err: DLP redaction applied` and the recovery hint tells the agent to request filtered fields explicitly.\n\n## Tracing the Swarm: W3C Through the Handoff\n\nWhen an agent hands off to a specialist, the W3C Trace Context travels with it. The `traceparent` header from the originating request is embedded in the delegation token claims, and when the specialist processes the request, it reads the trace context from the token and continues the trace.\n\nThe TraceContext helper in the runtime lifts the caller's trace context into the per-request context for every server factory. The context is present on all server types, API proxy, YAML, and bundle, so that tool spans and the request ledger can correlate a call back to the originating trace without any per-tool code. When the value is undefined, the trace is unrooted, not implicitly parented to a default.\n\nThe audit trail preserves the trace context end to end. The ChainForge constructs each audit record's hash from the raw base64 of the payload, the previous hash, and the sequence number. The trace context is embedded as searchable metadata in the log entry, so a security analyst can follow a single agent's path through the gateway, the specialist, and back to the gateway, all in one trace.\n\nThis is what makes the Live Activity table work. Each row shows the MCP server, the tool, the semantic action (query, mutation, destructive), the token, the outcome, and a full latency breakdown. Green means the upstream answered. Violet means Vinkius policy acted in flight. Amber means the caller erred. Red means the provider failed. Every color maps to a different failure owner, and every failure carries a trace that an analyst can open to see the full path.\n\n## The Return Trip: How the Gateway Re-enters\n\nWhen a specialist agent finishes its work, the SwarmGateway's `returnToGateway` method activates. This is not a redirect. It is a state reconciliation. The gateway compares the carry-over state that was embedded in the delegation token against the state that the specialist returned, and any causal marks that the specialist touched are invalidated across the swarm.\n\nThe return trip is mediated by a tool that the gateway injects into the specialist session. This tool is not visible to the agent as a first-class capability. It is a return channel: the agent calls it with its final state, and the gateway takes over from there. The tool is branded with `_MCPFUSION_handoff_return` so the session layer recognizes it and activates the return path.\n\nThe gateway also rewrites the specialist's tool namespace. When the finance specialist exposes its tools, the gateway prefixes them with `finance.` so that the calling agent sees `finance.create_invoice` and `finance.get_balance`, not `create_invoice` and `get_balance`. This prevents name collisions when multiple specialists are active in the same conversation, and it makes the audit trail unambiguous: every tool call records which specialist namespace it came from.\n\n## The Numbers That Matter\n\nEvery limit in this system is named. There are no magic numbers hidden in configuration files. The derivation is documented next to each constant.\n\n| Name | Value | Source |\n|------|-------|--------|\n| Session idle timeout | 2 minutes | SessionManager.ts, SESSION_IDLE_TIMEOUT_MS |\n| Memory pressure timeout | 30 seconds | SessionManager.ts, SESSION_PRESSURE_TIMEOUT_MS |\n| Sessions per token cap | 50 | SessionManager.ts, MAX_SESSIONS_PER_TOKEN |\n| Session sweep interval | 15 seconds | SessionManager.ts, SESSION_SWEEP_INTERVAL_MS |\n| Redis session TTL | 150 seconds | SessionManager.ts, REDIS_SESSION_TTL |\n| Memory warning threshold | 65 percent | SessionManager.ts, MEMORY_WARN_THRESHOLD |\n| Memory pressure threshold | 80 percent | SessionManager.ts, MEMORY_PRESSURE_THRESHOLD |\n| Connection idle eviction | 30 minutes | ConnectionTracker.ts, IDLE_TIMEOUT_MS |\n| DNS cache max entries | 4,096 | SsrfGuard.ts, DNS_CACHE_MAX_ENTRIES |\n| Agent pool max | 100 | Limits.ts, AGENT_POOL_MAX |\n| Agent keep-alive | 65 seconds | Limits.ts, AGENT_KEEP_ALIVE_TIMEOUT_MS |\n| Dispatch time budget | 30 seconds | Limits.ts, DISPATCH_TIME_BUDGET_MS |\n| Boot time budget | 5 seconds | Limits.ts, BOOT_TIME_BUDGET_MS |\n| Heap cap per isolate | 128 MB | Limits.ts, ISOLATE_MEMORY_LIMIT_MB |\n| Session key rotation | 24 hours | StreamingDaemon.ts, SESSION_KEY_TTL_MS |\n| Circuit breaker window | 5,000 requests / 5 minutes | Governance config |\n| Circuit breaker cooldown | 15 minutes | Governance config |\n\nThe swarm problem is not solved by making agents smarter. It is solved by making the session layer authoritative. The gateway owns the delegation token, the session lifecycle, the state sync, and the rate budget. The agent is the guest. The gateway is the host. When an agent overstays its welcome, the gateway evicts its session. When the pool is full, the gateway rejects the handoff. When the budget is breached, the circuit breaker trips and the swarm halts.\n\nThe post on [AI agents as new consumers](/en/posts/ai-agents-the-new-consumers) covered the MVA architecture: Model, Presenter, and Tools. The [runtime post on V8 isolates](/en/posts/vinkius-runtime-v8-isolates) covered the sandbox and the snapshot integrity model. The [governance post](/en/posts/vinkius-ai-governance) covered the twelve surfaces and the circuit breaker. This post covers the layer they all depend on but never talk about: the session layer that prevents a hundred agents from stepping on each other. The next post in this series will cover the capability lockfile, and how the `mcpfusion.lock` file enforces breaking changes with git-diffable diffs and CI gates.\n\nThe numbers above are not my estimates. They are what the code enforces. You can read Limits.ts, SessionManager.ts, SsrfGuard.ts, and CircuitBreaker.ts in the cloud runtime. Every value is named, documented, and derived from an external constraint. That is the difference between a platform that scales and a demo that collapses."
      },
      "date_published": "2026-09-22T00:00:00.000Z",
      "date_modified": "2026-09-22T00:00:00.000Z",
      "authors": [
        {
          "name": "Renato Marinho"
        }
      ],
      "tags": [
        "agents",
        "swarm",
        "sessions",
        "gateway",
        "orchestration",
        "scaling"
      ]
    },
    {
      "id": "https://blog.vinkius.com/en/posts/building-your-own-mcp-connector",
      "url": "https://blog.vinkius.com/en/posts/building-your-own-mcp-connector",
      "title": "Building Your Own MCP Connector: Why MCP Fusion Is the Point",
      "summary": "What sits between your data and an agent's perception, and why that gap is an architecture: the MVA split that closes the egress, the presenter that decides what an agent sees, self-healing errors, state the agent can feel, and a deploy that ships all of it as one hashable bundle.",
      "content": {
        "markdown": "\nAn MCP connector is a promise. You are promising a system that does not share your context, your history, or your intent that a call to `billing.void_invoice` means exactly what you meant it to mean. The database row is not the answer. It is raw material, and between it and the agent sits a layer of architecture that most hand-rolled servers skip.\n\nAn agent is stochastic by construction: it hallucinates parameters, misformats inputs, retries without thinking, and loses context between calls. A raw server treats each tool call as independent, and that single omission is what turns a working demo into a system that corrupts data, burns tokens, and fails in ways no one can trace. Sending raw JSON to an agent creates four structural failure modes, context starvation, action blindness, perception inconsistency, and security leakage, and these are deficits that no amount of prompt engineering can fix.\n\nThat is the thesis of this post. If the failure modes are structural, the fix has to be structural. MCP Fusion makes that fix with a pattern it calls MVA, and the pattern is a separation of responsibilities that application code already knows, just pointed at a different consumer:\n\n- The **Model** owns what the data is and what may leave the process.\n- The **Presenter** owns what the agent perceives about it.\n- The **Tools** own the verbs: the queries, mutations, and actions.\n\n![The MVA pipeline: a raw row crosses the model boundary, undeclared fields are stripped, and the presenter assembles the perception package the agent actually receives](/post/fig-connector-mva.svg)\n\nOne rule keeps the separation honest, and it is a security property, not a style rule: direction. Tools import presenters, presenters import models, models import the core, and nothing imports backwards. The layer that touches your data must never be the layer an agent can steer.\n\n## Model: where the wire ends\n\nIn a normal application, a schema validates input. In a connector, the schema has to close the output too, because the consumer on the other side of the wire is not a colleague reading code. It is a language model that will act on whatever it is handed. `defineModel` is where that boundary is drawn, and its four declarations do different jobs:\n\n`m.casts` declares the fields, their types, and their descriptions. Those descriptions are not documentation for a human. They compile, just in time, into the interpretation rules the agent receives with each response. `m.hidden` declares the fields that never reach the wire: password hashes, internal flags, tenant markers. `m.guarded` declares the fields that can never come in from an agent. `m.fillable` declares the input profiles, create, update, and filter, and a tool's parameters are derived from those profiles rather than re-typed by hand.\n\nThe consequence is the one that matters in production. When a migration adds a column to the table, that column does not leak. It stays out of the wire until someone puts it in the model and someone, on purpose, puts it in the presenter. A raw server has no such property. Its output is the row, serialized, which means a password hash, an internal flag, and a tenant ID all arrive in the agent's context window the moment a new field lands in the schema.\n\n## Presenter: the perception you never showed\n\nThe V in MVA is not for a human eye. It is the package the agent perceives, and it is assembled from four layers.\n\n**Data that survived the firewall.** Before anything is serialized, the presenter runs its schema over the raw result in strip mode: whatever the database returned, the agent sees only the declared surface. This is egress control at RAM level, not a view layer, not a template, not a convention. A field the schema does not know cannot cross.\n\n**Rules, delivered just in time.** The field descriptions on the model compile into system rules attached to this response, for this entity. The agent is not carrying a global prompt of thousands of tokens; it receives the interpretation rules for what it actually asked for. The domain knowledge lives in one place and ships only when the domain is in play.\n\n**A working limit.** The presenter declares how many items this agent may see in one response and what it is told when the list is truncated. A list of ten thousand rows is not data to an agent. It is a denial of service wearing a data costume. The limit is part of the perception, and the truncation notice is what stops the agent from pretending it saw more than it did.\n\n**Affordances.** `suggestActions` tells the agent what it can do next with what it just saw. The docs call it HATEOAS for agents, and this is where action blindness dies: the response is not a payload, it is a position in a workflow. Server-rendered chart and diagram blocks are part of the same package, and they are deterministic: the framework renders them, the agent reads them, and no model in the loop generates the pixels.\n\n## Tools: verbs with intent\n\nA tool in this framework is not a named function with a schema. It is a semantic verb with a default intent. `f.query` is read-only. `f.mutation` is destructive by default. `f.action` is neutral. Those are not metadata labels. They drive what the platform treats as safe to retry, what the observability pipeline marks, and what a governance tool flags when a read turns into a write.\n\nFrom the verb, the chain is deliberately small. `.fromModel` pulls the input shape out of the model's fillable profile, so the tool's parameters are derived from the same declaration that closes the egress. `.returns` attaches a presenter to the response. `.proxy` writes the handler for you: it infers the HTTP method from the verb, resolves the path parameters from the input, and unwraps the response envelope. The `.with` steps are reserved for domain-specific inputs a model cannot express.\n\n`f.router` groups verbs under a prefix and inherits middleware and tags to each one. `f.middleware` derives a typed context downstream, and the tenant identifier comes from a verified credential in that context, which is why the docs can say, flatly, that the agent cannot override it. Concurrency caps and egress byte limits attach to the same chain. And when a workflow needs a prompt instead of a tool, `definePrompt` builds it from the same presenter: the rules become the system message, the data and UI become the user block. One source of truth, two surfaces.\n\n## Errors that steer, state the agent can feel\n\nA raw server answers a bad call with a flat string, and the agent's response to a flat string is to retry, with the same input, and again. That loop is where token budgets go to die, and where a wrong refund gets attempted four times.\n\nThe framework's answer is a self-healing envelope. `f.error` builds it from a specific code, a message, a suggestion, a list of actions the agent can take instead, optional details, and a retry window:\n\n```\n<tool_error code=\"InvoiceNotFound\">\n  <message>Invoice \"INV-999\" does not exist.</message>\n  <recovery>Call billing.list_invoices first to find valid IDs.</recovery>\n  <available_actions>billing.list_invoices</available_actions>\n</tool_error>\n```\n\nSpecific codes beat generic ones. `AlreadyPaid` tells the agent something `BAD_REQUEST` cannot, and the recovery line removes the guesswork that makes retry loops expensive.\n\nState is the other sense a language model does not have. After a mutation, the agent still believes the list it fetched before the mutation is current. The framework's answer is state sync signals in the response, borrowed from HTTP caching: a tool marks its result immutable, volatile, or causal. An immutable result can be trusted, a volatile one says re-query me, and a causal mark says that after this mutation, these other verbs must be re-queried. It is causality, not time, which is exactly what an agent without a clock needs.\n\n## Deploy: the connector as a bundle\n\nA connector built this way ships as a single artifact, and the CLI does the whole job. `mcpfusion deploy` bundles the server into one self-contained file: all dependencies inlined, Node builtins replaced by stubs that exist only to satisfy the bundler and are never called, and the transport itself stubbed, because the platform supplies it. The bundle passes a size gate of 1.5 MB raw, which is a budget, not a spec, then gets compressed and hashed, and goes to the edge. If the hash matches the deployed one, the platform reports an instant restore: the same bytes, no reload.\n\nTwo steps are worth understanding, because they are what make the architecture auditable. The CLI runs a second, introspective compile of the bundle: it extracts the tool contracts, the prompts, and the credential schema, and writes them into a capability lockfile, a deterministic snapshot of the connector's behavioral surface. That lockfile is git-diffable, and a `fusion lock --check` in CI is the gate: the surface your code actually exposes is compared to the surface you committed, and the diff is classified as breaking, risky, safe, or cosmetic. The protocol has no mechanism for detecting drift, and the framework gives you one. That is the difference between a deployment you cannot audit and one a reviewer can read.\n\nThe same bundle speaks the current era of the protocol: stateless, per-request, behind any load balancer. And the registry that built it runs unchanged on stdio and on the 2025 era of HTTP, which is how local development and production stay on the same code.\n\nEdge deployment has three constraints, and all of them are design decisions: the tool set is registered explicitly, because discovery scans a filesystem that does not exist there; there are no native addons; and nothing in the bundle touches the process. Explicit imports and explicit registration, and the server is edge-legal.\n\n![The deploy as artifact: source tree and lockfile on the left, the CLI pipeline in the middle, and the same registry running on stdio, HTTP, and the stateless edge on the right](/post/fig-connector-deploy.svg)\n\n## Why build with MCP Fusion\n\nThe question behind all of the above is the one worth answering directly: why not write a plain MCP server and add safety where it becomes painful?\n\nBecause the failure modes are structural, and structural fixes live in the framework, not in the handler. A raw server has six absences, and this one has six presences:\n\n- It leaks whatever it returns. This one closes the egress at the model layer, so a new column cannot reach an agent until someone declares it.\n- It enforces nothing. This one freezes the registry after attach and keeps the pipeline order by construction: the safety is enforced, not conventional.\n- It cannot see its own drift. This one hashes the behavioral surface into a lockfile and classifies each change before merge.\n- It answers errors with strings. This one answers with recovery instructions and the next available actions.\n- It is blind to time. This one carries causal invalidation signals in the responses.\n- It is one surface, wherever it is hosted. This one is one registry across stdio, HTTP, the Vinkius edge, and serverless targets, with observability that maps to SOC 2 controls and can forward to a SIEM.\n\nTwo multipliers change the economics of the work itself. You can generate the connector from an existing contract: an OpenAPI spec or a Prisma schema becomes a complete server, with the egress, the tenant isolation, and the memory protection baked into the generated code, in one command. And the tests are the real pipeline: the testing package runs your connector in RAM, through the same validation, middleware, handler, presenter, and egress that production runs, with zero tokens and full determinism. You assert that the data has no secret field, that the rules arrived, that the error classified.\n\nThe honest cost is that this is an architecture, not a helper library. The MVA split takes a few days to internalize, and the bundle budget keeps the dependencies lean. What you are buying is the boundary between your data and an agent's perception, enforced by the framework instead of by review. For anything that will run in production with other people's agents on the other side of the wire, that is the point.\n\n## One connector, from spec to edge\n\nThe whole workflow, end to end:\n\n```\nmcpfusion create invoices --vector openapi\nmcpfusion remote --server-id <uuid from the dashboard>\nmcpfusion deploy\n```\n\nFor a bare start instead of a generated one, the same three commands with `--vector vanilla`. Then the minimum of three declarations:\n\n```\ndefineModel('Invoice', m => {\n  m.casts({\n    id: m.uuid().label('Invoice ID, a UUID'),\n    total: m.number().label('Total, integer cents'),\n    status: m.string().label('open, paid, or voided'),\n  });\n  m.hidden(['webhookSecret', 'internalFlags']);\n  m.fillable({ create: ['id', 'total'], filter: ['status'] });\n});\n\nconst router = f.router('billing');\nrouter.mutation('void_invoice')\n  .withString('id')\n  .returns(invoiceUI)\n  .invalidates('billing.*')\n  .proxy('invoices/:id/void');\n\nconst tester = createMCPFusionTester(registry, {\n  contextFactory: () => ({ tenantId: 't_777' }),\n});\n\nconst result = await tester.callAction('billing', 'void_invoice', { id: 'INV-7' });\n\nexpect(result.data).not.toHaveProperty('webhookSecret');\nexpect(result.uiBlocks.length).toBeGreaterThan(0);\n```\n\nThe test runs the same pipeline production runs and proves, without a single token, that the egress held. Deploy, check the lockfile in CI, and the connector is a hash in a repository, a diff in a pull request, and an isolated program at the edge. The same object is all three.\n\nThe runtime that finally executes that program, sealed and snapshot-restored, is the subject of [the post on V8 isolates](/en/posts/vinkius-runtime-v8-isolates)."
      },
      "date_published": "2026-09-21T00:00:00.000Z",
      "date_modified": "2026-09-21T00:00:00.000Z",
      "authors": [
        {
          "name": "Renato Marinho"
        }
      ],
      "tags": [
        "mcpfusion",
        "connectors",
        "mcp",
        "agents",
        "edge"
      ]
    },
    {
      "id": "https://blog.vinkius.com/en/posts/vinkius-ai-governance",
      "url": "https://blog.vinkius.com/en/posts/vinkius-ai-governance",
      "title": "AI Governance: The Control Plane Behind Every Tool Call Your Agents Make",
      "summary": "Vinkius AI Governance: the control plane that attributes every MCP tool call to an identity, shields data in flight, budgets agent spend and seals a hash-chained audit receipt for the regulator, the finance team, and the 3 a.m. page.",
      "content": {
        "markdown": "Six months ago I was still watching teams demo agents. Now I watch agents that act. Somewhere in between, the center of gravity of agentic infrastructure moved from \"can the model do the task\" to \"can you survive the moment the model stops answering and starts doing.\" A tool call is a write to the world. An email goes out. A row updates. An order places. A file deletes. The thing that decides is probabilistic, and the thing that executes is not. Every incident in agentic systems lives in that gap, and this post is about how to close it.\n\nI built Vinkius around one conviction: a cloud for AI agents is only finished when you can answer four questions about any tool call, at any moment, with evidence attached. Who called. What was allowed to happen. What crossed the gate, and what got shielded on the way out. And what it cost. The answer to those four questions is the control plane we ship as twelve surfaces: eight that tell you what is happening, and four that decide what is allowed. Everything below is what the platform does today. The numbers are the numbers the system enforces or reports, not the ones I would like it to be.\n\n## The moment agents stop talking and start acting\n\nIncident response in the enterprise was quiet for years, because the things that broke were built by humans and changed slowly. A service has an owner, a changelog, a post-mortem, and a fixed population of callers. When the caller is a model, all of that evaporates in one design decision. The same agent can book a meeting in the morning, write code by lunch, and query a production database by afternoon. That is what agentic systems are for: one principal, many tools, and no memory of the boundary between them. The failure categories change with it.\n\nClassic monitoring asks \"did the request fail, and why.\" With agents you must ask four more questions, and each one has its own answer shape. Was the failure the agent's fault, because it sent a malformed request or looped on a broken tool? Was it the upstream service's fault, because the API it called returned a 500? Was it the platform's fault, because the gateway itself misbehaved? And, the one nobody asks until the finance team asks: what did all of that cost, per request, per connector, per agent, in dollars?\n\nI have read enough incident reports, in this space and in mine, to know the answer pattern. It is almost always \"we could not tell which agent did what, and we found out after the damage.\" A non-deterministic caller and a post-hoc log system are a terrible pairing. The log is a crime scene photo, and the suspect has already left the building. Governance is the architecture you build so that the suspect, the weapon, and the timestamp all get recorded at the moment of the act, not after.\n\n## What AI governance actually is\n\nAI governance, in the sense this industry needs it, is the control plane that surrounds agent to tool traffic. It is not a policy engine in the traditional OPA sense, deciding at deploy time which rules a service gets. It is not RBAC, which decides which human can log in. It is not a SIEM, which correlates logs after the fact. Those systems were all designed for a world where the thing that acts is a person or a fixed pipeline.\n\nWhat AI governance does is different, and it is worth being precise about. It decides, per call, whether an action by a non-deterministic principal is allowed to happen, on a machine, against a third party's system, under a budget. And it keeps, for every call, a receipt that answers who, what, and how much. The term \"governance\" is doing real work here, and it is not only a compliance word. Compliance is the output. The input is a control plane with two properties: attribution at identity granularity, and enforcement in-flight, before the call leaves your infrastructure.\n\nIn Vinkius that control plane is what we call AI Governance, and it is organized as twelve surfaces. Eight report surfaces tell you what is happening across the fleet of connectors your agents use. Four policy surfaces decide what is allowed to happen, and they are enforced in the runtime, in the path of the request, not in a dashboard you check on Mondays. The split is deliberate. A dashboard you cannot act on is theater, and a policy without a receipt is fear. You need both halves, and the product is the union of the two.\n\n## Why a gateway is the only place control can live\n\nThe architectural argument is short. Your agents do not call your upstream APIs directly. They call tools, and in Vinkius those tools are connectors, hosted MCP servers that every agent in the fleet reaches through a single gate. That gate is where the traffic has to be, so that is where the control has to live.\n\nThe client side is not a safe place to put this. Prompt level guardrails are fragile by construction, because the model can rephrase its way around a rule, and because a guardrail that lives in the prompt is one context window away from not existing. Provider side filters are not your data path, and you cannot hold a vendor accountable for your agent's behavior. Post-hoc analytics, the SIEM you bolt on later, are a record of what happened, which is useful, and too late to stop the thing that is happening right now.\n\nThe gateway sees all of it at once: the request, the response, the token that authenticated the call, the connector it targeted, and the clock. That is the only point in the system where \"who, what, and how much\" is answerable with a single data set, and it is also the only point where a decision can still change the outcome. Every tool call in your fleet crosses the gate twice, inbound and outbound, and both crossings are where governance happens.\n\nTwo other pieces of the Vinkius platform make the gateway argument complete. Each connector runs inside its own sandboxed isolate, a design I wrote about separately in [how Vinkius runs every MCP server in a V8 isolate](/en/posts/vinkius-runtime-v8-isolates), so that a hostile or broken tool can only ever act through effects the host owns. And since the 5.1.0 release of our open framework, the agent's distributed trace context is carried across the gateway, so every tool span parents to the caller's trace. That work is in [the MCP Fusion 5.1.0 post on trace correlation](/en/posts/mcpfusion-5-1-0-trace-correlation), and it matters here because a governance receipt that cannot join the agent's own trace is a receipt nobody can read.\n\nOne honesty note before the tour: on the free plan the AI Governance dashboard opens with clearly labeled sample data. You explore the shape of the control plane before you commit to it. Live enforcement, the circuit breaker, and the emergency halt run on paid plans. I would rather say that plainly than let a reader assume the free tier does the paid thing.\n\n![A single tool call moving through the Vinkius gate. Inbound: the token is identified, then the call passes the four policy layers in order, connector policy, data shielding, cost guard, and circuit breaker. Outbound: the response is scrubbed of sensitive values and tagged with its cost on the way back, and every crossing is sealed into the hash-chained audit ledger.](/post/fig-ai-governance-receipt.svg)\n\n## Eight surfaces that tell you what is happening\n\nThe reports half of AI Governance is where I want to spend the most time, because this is the part enterprises test first, and the part where \"we instrumented our agents\" usually turns out to be a sentence about four dashboards and a prayer.\n\n### Mission Control\n\nThe top surface is the KPI strip for the whole fleet: total requests, average latency, a reliability figure we call Vinkius Reliability, total tokens moved, the count of values protected by data loss prevention, and the estimated cost saved by the FinOps guard. Below it sits a 30-day activity heatmap of agent behavior across the month, a request volume and latency chart, and an AI Briefing: a language-model-generated digest of what changed in your traffic this period. It is the one deliberately non-deterministic surface in a deterministic control plane, and it is a paid feature, because the briefing is a model call. One detail I insist on: the reliability figure you see excludes Vinkius's own errors. The number answers \"how reliable is the fleet for you,\" not \"how reliable is us.\" We do not count our mistakes against your uptime.\n\n### Agent Activity\n\nThe per-client view. Every connection token in your organization appears with its activity: who the client is, when it last called, how many requests it made. This is the surface that answers \"which agent is doing what.\" An agent that should be running hourly and is calling every ninety seconds shows up here, and that is usually where a runaway loop is first noticed, before it becomes an invoice.\n\n### Connector Traffic\n\nThe same decomposition, per connector. Which of your tools is hot, which is cold, where latency concentrates. When one upstream provider degrades, this is the view that separates the provider's problem from your fleet's.\n\n### Access Tokens\n\nThe token fleet as an inventory. Status, last used, request counts. This one earns its keep in a way that surprises people: a connection token that has not been used in 90 days is a standing credential you issued and forgot about, and this surface exists to make that forgettability visible and actionable, with revoke and rotate one click away.\n\n### AI Spend\n\nCost, made legible. Estimated spend, the savings the FinOps guard measured, cost per request, the return on the FinOps policies, and a ledger broken down per connector. The guard attributes cost with a per million token rate you set, and the default is $3.00 per million tokens. The point of the surface is not to bill you. It is to show which agent, on which connector, is consuming what, because agent spend is not shaped like any cost you have charted before. It is not per user and it is not per request. It is per thought, and it compounds with every retry a loop adds.\n\n### Capability Reliability\n\nReliability at the level of the tool, not the service. Each capability a connector exposes has its own latency and failure behavior, and the Tool Health Matrix plots them together: latency against failure rate, one bubble per tool. A connector can be perfectly healthy while one specific tool inside it is quietly failing. That is the difference between monitoring a product and monitoring the thing your agents actually touch.\n\n### Security Posture\n\nTwo time series and one donut. The time series track how much data the DLP layer has shielded and how much the FinOps layer has truncated, over time. The donut is Compliance Coverage: the share of active governance policies across your fleet, counted per category, DLP, FinOps, and approvals. It is the number a reviewer wants to see, and it is computed from policy state, not from a questionnaire you fill out.\n\n### Request Failures\n\nThe error timeline, split into three buckets: Agent errors, where the caller sent something the tool could not honor, Upstream errors, where the third party behind the connector failed, and Vinkius errors, where we failed. This is the failure attribution the opening of this post was about. When a call fails, you are told whose failure it was, in the same view, in the same period, without a meeting.\n\n### Request Detail: the receipt\n\nDrill into any single request and you get the unit of accountability the whole control plane exists to produce. The receipt names the identity that made the call: token, client, and the human or app user behind it. It lists the policy in force at that moment, rule by rule, grouped by the phase of the request lifecycle each rule belongs to, with the verdict of each: pass, fail, off, or enforced. It shows the audit trail entry for the call, sealed at ingestion into the hash-chained ledger we cover below. It shows trace continuity, the W3C trace context that joins this call to the agent's own distributed trace. It breaks the latency down into time the upstream API took versus time the governance layer added, so you always know the cost of the control plane itself. And it ends with actions: block a capability for every caller, or, on Business tier, an emergency halt of the connector. One request, one screen, the whole story.\n\n## Four surfaces that decide what is allowed\n\n### Connector Policy\n\nThe first policy surface is about how a connector is introduced to your fleet. Deployments can require approval before they are live, and the setting is framed around the four-eyes principle that the EU AI Act expects of high-risk systems under Article 14(5): a single person does not decide, by themselves, that a new tool with real consequences reaches the agents. The same surface controls how capabilities are exposed to the model, flat or grouped, with an automatic grouping threshold when a connector's tool list gets long, because what the model sees is a surface area, and surface area is a design decision.\n\nAnd then there is the danger zone, which I would rather you understand than discover. The Global Emergency Halt stops every active connector in your organization. It deactivates all of them, revokes every token, and terminates every open session, in one synchronous action, and the confirmation requires typing HALT ALL, because this is the button you press when something has gone badly and you would rather the whole fleet be dead than wrong. After a halt you can restore, but the revoked tokens stay revoked. You reissue them deliberately. That is a design decision I want to be explicit about: recovery is a new trust relationship, not a replay of the old one.\n\n### DLP Protection\n\nData loss prevention, made for the model path. You name patterns for sensitive fields, an email anywhere in the response tree, a credit card number, a field at a specific path you decide to protect, and the runtime enforces them on the outbound path, shielding the value before the response reaches the agent. The shielding happens in memory, in the path between the upstream and the model, which is the difference between data that was masked and data that was never in the model's context. The KPI you see on Mission Control, DLP Protected, counts the redactions per period, so the layer is not just there, it is measured.\n\n### FinOps Guard\n\nCost guardrails, for the reasons explained in the AI Spend section. The guard does three things. It truncates arrays beyond a maximum item count, because a response that returns ten thousand records is a token bill, not an answer. It compresses the tool descriptions the model reads before it acts, the Toon compression setting in the dashboard. And it attributes cost with the rate you set, so the guard tells you, per period, how much it saved, in bytes and in dollars, against the rate. The output is not a bill. It is a measure of the gap between what the tool returned and what the agent needed, which is the number that decides whether your agent can keep calling this tool at all.\n\n### Circuit Breaker\n\nA sliding-window request budget per connector, with a default of 5,000 requests every 5 minutes and a 15-minute cooldown, numbers you can tune. When a connector's budget is exceeded, the breaker trips, and the trip does something most guardrails never do: it tells the agent, in a machine-readable refusal written for a model, that the resource is open and that it should back off. A good agent respects that. A loop that cannot read the refusal gets stopped by the same trip, which is the point. The breaker state is shared across the fleet of gateway instances, so a budget is a budget, not a per process limit that resets when traffic moves. After the cooldown it can reset automatically, or you approve resumption from the dashboard. The breaker is what keeps one runaway agent from taking a third party's API, and your own bill, with it.\n\n## The foundations: scoped tokens, a sealed vault, a chained ledger\n\nThe twelve surfaces sit on foundations, and the foundations are where a lot of \"governance\" products are actually thin.\n\nThe identity layer is the connection token. Each connector has its own tokens, generated per organization, shown once, signed, and scoped to that connector only. A token for the email connector cannot touch the payments connector. Every receipt names the token that made the call, so identity is not a theory, it is a column in the audit data.\n\nThe secrets layer is the vault. Connector credentials are encrypted at rest with AES-256, and the guarantee we make is that not even the platform's own operators can read them at rest. They are injected into the execution environment only at runtime, in the isolate where the tool code runs, and the agent never sees the secret. That separation is the one I think about most when I design: the thing that decides, the model, and the thing that proves authority, the credential, should never share a context. A model that can read your upstream password is not a governance problem you can audit away. It is the problem.\n\nThe audit layer is the ledger. Every request hash is sealed at ingestion, and the ledger is hash-chained: each record carries the previous hash and its position in the sequence, and the chain is signed, SHA-256 with Ed25519, so a tamper is not a privacy issue, it is a detectable event, and the dashboard tells you the chain is valid or compromised. The deployment audit is structured around GDPR Article 73, the data subject's right of access, so when a regulator or a customer asks for the full history of a connector, the answer is a single exportable, verifiable chain, not a project.\n\nAnd one layer more, the one I am proudest of. Some of the connectors in our marketplace are monitored decoys. They look like real tools. They are not, and they are watched. When a decoy is touched, the platform quarantines the tool, revokes the token, kills the open connections, and freezes the payout path, automatically. If a tool goes rogue, or a token leaks into the wild, the containment does not wait for you to notice.\n\nUnderneath all of it, the organization layer: single sign-on, custom roles and teams, service accounts for the non-human identities that run in CI and OIDC workloads, organization-level API keys, and a security audit log for the actions on the organization itself. The control plane sits on top of an identity model that knows the difference between a person and a machine, which is a difference every one of the twelve surfaces relies on.\n\n## What governance changes for the agents themselves\n\nMost of the conversation about agent governance treats the agent as a risk to contain, and I am not going to pretend otherwise, because it is. But the other half of the argument is the one I would put on the sales deck: governance is what turns an agent from a demo into a worker.\n\nTrust is granted in proportion to evidence. An operator will hand an agent the night shift, the production query, the payment reconciliation, in direct proportion to how well every action it took can be attributed, budgeted, shielded, and verified after the fact. The receipt is not a burden on the agent. It is the credential that gets it the bigger room.\n\nThe envelope of a governed agent is knowable. A deterministic policy around a non-deterministic model is the only sane shape for production: the model plans inside a box, and the box is stable. The circuit breaker's refusal is written in the model's language, so the agent learns the budget, and respects it, instead of discovering it in the form of a bill. The truncation in the FinOps guard means the model gets a sized answer instead of a 200-kilobyte blob, which is an intelligence problem, not only a cost one.\n\nAnd the agent itself gains the capability that no autonomous system has ever had in practice: it can show its work. For an agent that will operate in regulated workflows, the audit trail is not overhead, it is the product. The governed agent is the only kind of agent you can give real authority, and the ungoverned agent is, forever, a demo.\n\n## Observability that finally speaks the language of agents\n\nIf your team already runs Datadog or Grafana, the shape of the AI Governance surfaces will feel familiar, and that is intentional. KPI strips, heatmaps, drill-downs, per request receipts. What is not familiar is what agent traffic does to the model those tools were built on.\n\nDatadog measures a world where the callers are systems that someone wrote. The workload has a fixed shape, cost scales with users or requests, and a failure has one responsible party. Agent traffic breaks all three assumptions at once. The caller is probabilistic. The workload is shaped by thinking, and a retry loop multiplies it. And a failure has three candidate owners, agent, upstream, platform, which is why the failure timeline splits into three buckets instead of one.\n\nSo the control plane for agents is observability with an enforcement layer, and the enforcement layer is what Datadog, built for a human-built world, does not have. Every tool call is attributed to an identity, cost is attributed at token granularity, failures are attributed to a responsible side, and the policy layer acts in-flight, before the call leaves the gate. That is the elevation I wanted in this post: take the instrumentation discipline of the platforms you already trust, and add the two properties agent systems need, per-call attribution and in-flight enforcement. The result is not a new category of tool. It is the same category, at the resolution agent systems require.\n\n## Frequently asked questions\n\n### Is AI governance just RBAC with extra steps?\n\nNo, and the difference is the unit of decision. RBAC decides which human, with which role, can reach a system. It is a gate on identity, evaluated once. AI governance decides what an action, taken by a non-deterministic principal through a specific tool, is allowed to do, on every call, under a budget, with the answer recorded. One is a door. The other is a traffic court for the actions your agents take.\n\n### Does Vinkius read my agents' prompts?\n\nNo. The gate sees the tool call: the tool name, the arguments it sent, the response it received, the identity behind it, and the clock. The four policy surfaces are deterministic rule and budget layers, not a language model judge. If you want the model to inspect the model, that is a different product, and I would not call that governance, I would call it a second opinion.\n\n### What happens when the circuit breaker trips?\n\nThe connector's traffic is refused with a machine-readable notice that tells the agent the budget is open and that it should back off, for a cooldown period. After the cooldown the breaker resets on its own, or an operator approves resumption from the dashboard. The state is shared across the gateway fleet, so the budget holds regardless of which instance serves the call. The session layer that keeps the swarm in check is the subject of [the SwarmGateway sessions post](/en/posts/vinkius-swarm-gateway-sessions).\n\n### Can I export and verify the audit chain?\n\nYes. The deployment audit exports as a document, and the chain is verifiable end-to-end: every record carries the previous hash and its position, the chain is signed, and the dashboard states whether the chain is valid or compromised. It is structured around GDPR Article 73, so a data subject's access request maps onto one artifact.\n\n### What do I get on the free plan?\n\nThe full dashboard, with clearly labeled sample data, so you can learn the shape of the control plane. The live enforcement, the circuit breaker, and the emergency halt are on paid plans, and the AI Briefing is a paid feature. I would rather be the vendor who tells you that in a blog post than the one who makes you discover it in the pricing page.\n\n## The control plane is the product\n\nThe gap between the agent demo and the agent you would let act while you sleep is not the model. It is the control plane. Twelve surfaces, eight that tell you and four that decide, sitting on scoped tokens, a sealed vault, a chained ledger, and a decoy layer that contains the damage before you do. Every call gets a receipt, and the receipt answers who, what, and how much, in the order a regulator, a finance team, or a 3 a.m. page would ask them.\n\nIf your agents are already acting on your systems, the question is no longer whether you can afford governance. It is whether you can afford the day you find out you do not have it.\n\nFor a deeper account of how Vinkius built the runtime that makes this governance possible for AI agents, see [AI Agents Are the New Consumers](/en/posts/ai-agents-the-new-consumers).\n"
      },
      "date_published": "2026-09-21T00:00:00.000Z",
      "date_modified": "2026-09-21T00:00:00.000Z",
      "authors": [
        {
          "name": "Renato Marinho"
        }
      ],
      "tags": [
        "ai-governance",
        "mcp",
        "agents",
        "observability",
        "security",
        "compliance"
      ]
    },
    {
      "id": "https://blog.vinkius.com/en/posts/vinkius-runtime-v8-isolates",
      "url": "https://blog.vinkius.com/en/posts/vinkius-runtime-v8-isolates",
      "title": "Zero Cold Starts for Untrusted Code: How Vinkius Runs MCP Servers in V8 Isolates",
      "summary": "Why V8 isolates beat containers for multi-tenant edge code: ~15 ms snapshot restore, hard 128 MB per-tenant caps, SSRF-pinned fetch, and a four-layer snapshot integrity model where a corrupt entry is a cache miss, not a crash, inside the Vinkius runtime.",
      "content": {
        "markdown": "Each MCP server a customer ships on Vinkius Cloud is, sooner or later, third-party code executing on our infrastructure. Not ours. Not a library we control. Theirs. A cloud for AI agents is only useful if customers can bring their own logic, their own connectors, their own glue between an LLM and their systems. Once that capability exists, the question no product deck answers becomes the whole engineering problem: how do you run untrusted JavaScript for thousands of tenants on a small number of servers without giving any one of them a path to hurt the others, the machine, or their neighbors?\n\nThis is the question behind the V8 isolate layer of the Vinkius runtime, and the part of the architecture I talk about the least in public, which is why I am writing it down here. What follows is how the system works today. The numbers are the ones the code enforces, not the ones I would like it to be.\n\n## Why not one container per server\n\nThe first answer any multi-tenant platform reaches for is a container per server. It is clean, it is understood, and it gives you real kernel-level isolation. I spent a serious amount of design time on exactly that.\n\nIt died on density. Vinkius Cloud customers connect MCP servers the way they connect integrations on any SaaS: dozens per workspace, cheap to create, cheap to delete, most of them idle for days. A Node.js process that has booted a real runtime (framework, SDK, polyfills, connection pool) carries a fixed cost of tens of megabytes before it does useful work. Multiply that by a few hundred servers on one task and you are fighting the task's memory ceiling before a single tool call has happened. Multiply that by cold starts, and you have built a platform where booting your own server takes hundreds of milliseconds.\n\nThe second obvious answer is many processes inside one container, one fork per tenant. It is worse. You trade the kernel boundary for nothing: each process still has its own heap, its own page tables, its own garbage collector, and a crash in one process can still take the siblings down through shared file descriptors and the init process. You spent the same memory as the container answer and got a worse failure mode.\n\nI also ran the numbers on the middle ground: one Node process per workspace, shared across a tenant's servers. It falls on the same two walls. The fixed footprint of a booted Node runtime is a per-tenant cost whether you share it or not, and one workspace's memory leak or fork bomb takes out all the servers in that workspace with it. Blast radius does not localize.\n\nWhat Cloudflare demonstrated with Workers is the third option: not a process per tenant, and not a container per tenant, but an isolate. V8 was built to run many untrusted programs inside one browser process, which is exactly the shape of our problem, because a browser tab is a tenant. The Workers runtime leans on the same primitive: an isolate per request, restored from a V8 snapshot, so the cold start people complain about is really just a heap restore. I did not adopt their platform. I cannot: our gateway is long-lived and stateful in ways a page function is not, and we host our own fleet on AWS where I control the upgrade cadence. I adopted the idea, and I built the host side of it on top of Node.js with [isolated-vm](https://github.com/laverdet/node-isolated-vm), the C++ binding that lets a Node process own a raw V8 isolate.\n\nThat one decision (\"a tenant is an isolate, not a process\") is where the rest of the runtime comes from. What follows is a consequence of it.\n\n## What the isolate actually provides\n\nAn isolate is a heap: one garbage-collected region of memory with its own microtask queue, its own globals, and a hard cap you set at creation time. The isolate that `isolated-vm` creates for a tenant gets 128 MB. That is not a soft quota we enforce in application code and log a warning about; it is a `memoryLimit` handed to the engine. When a tenant's heap hits it, that tenant's isolate stops being served. The other tenants keep running. No tenant's `globalThis` is reachable from another tenant's isolate, because there is no shared JavaScript boundary at all. The only traffic that crosses the boundary is what we explicitly bridge, and we bridge almost nothing.\n\nScheduling cost matters more than people think. Spawning a process is a kernel event; forking takes page-table setup, an `exec`, and a garbage collector warmup. Switching isolates inside one process is a pointer swap. That is why Cloudflare can put the word \"zero\" next to cold starts, and why the latency floor of our edge-deployed servers is measured in milliseconds, not in the 200 to 400 ms range a fresh Node boot usually lands in.\n\nThe lifecycle layer states the isolation property more directly than I can: one runner per connection token, and no sharing between tokens. Credential isolation is absolute. The runner that owns a connection owns its isolate, its secrets snapshot and its timers, and nothing in the process graph reaches across it.\n\nThe trade is that an isolate is an empty environment. V8 does not ship a standard library. There is no `fetch`, no `TextEncoder`, no `console`, no `setTimeout`, no `process`, no `Buffer`. The first real work of the runtime is therefore not running tenant code. It is building the environment that tenant code runs in.\n\n## The empty environment\n\nBooting a tenant isolate starts with what we call the life-support layer: one concatenated bundle of polyfills that restores the Web and Node API surface tenant code expects. The load order is deliberate, because each layer depends on the one below it. Console and process shims go first, since the polyfills above them log and inspect `process`. Then `TextEncoder` and `TextDecoder` with UTF-8 and surrogate-pair support, because emoji in a tool argument is not an edge case. Then `URL` and a full 12-method `URLSearchParams` (the changelog entry reads \"axios, got, and @octokit now work correctly in edge bundles\", and that was the point). Then crypto, timers, and the network stack: `Headers`, `fetch`, and an async-only `XMLHttpRequest` shim, so the old HTTP libraries people paste into connectors do not die of `xhr is not defined`.\n\n![The life-support stack: guest polyfill layers on the left, host-owned effects on the right, with the bridge between them](/post/fig-v8-polyfills.svg)\n\nThe point is what is real and what is delegated. Nothing that touches the outside world exists inside the isolate. `fetch` in a tenant bundle is a polyfill on the guest side, but the actual request happens host-side, through `__host_fetch`, as a C++ `ArrayBuffer` copy across the boundary: binary-safe, so an image or a gzip response is not mangled into UTF-8 nonsense. `crypto.getRandomValues` is host-side `randomBytes`, capped at 64 KB. `crypto.subtle.digest` and HMAC, the ones JWT verification needs, are host-side too, and the host verifies signatures with a constant-time comparison. Timers are host-side `setTimeout` calls that invoke back into the guest through a registered invoker, capped at 30 seconds. The rule behind all of it: the polyfill makes the call look native, and the host owns the effect. A tenant can express intent. It cannot reach the operating system.\n\nArguments and results cross the boundary with structured cloning (`copy: true` in the `isolated-vm` API) instead of `JSON.stringify`. You pay the serialization once, in C++, and you keep real `ArrayBuffer`s and typed objects on both sides. You do not notice this until you profile a tool call that moves a 1 MB payload: the JSON round-trip was the second most expensive part after the network.\n\nOn the compile side, tenant code never reaches the runtime as source. The bundle pipeline takes the server spec, generates the entrypoint, and runs it through esbuild (IIFE, `browser` platform, ES2022 target, minified), so what lands in the isolate is one flat file with no dynamic imports. A static-analysis sanitizer rejects the escape hatches before the bundle is accepted: direct `__host_*` bridge access, `globalThis[...]` bracket-notation sneaks, `Function.constructor` bypass patterns. Bundles are gzipped and SHA-256'd. The API allows up to 1.5 MB of raw bundle, and the runtime enforces a 2 MB decompression ceiling with a streaming byte counter that aborts before a GZIP bomb can fill the heap. One detail I stand by: the compiled bundle runs once in a disposable isolate with all bridges stubbed out, and that run is what extracts the tool manifest. The program is paid for at compile time, not at request time.\n\nAnd here is the part developers never see. The bundle does not know it is in the cloud. The same `startServer()` call that boots a server over stdio on a developer's laptop detects the runtime's `__vinkius_edge_interceptor` global, hands over its tool definitions through that interceptor, and skips the normal transport setup. One codebase, two runtimes: you develop locally with the framework and deploy to the cloud without a single `if (inCloud)` branch.\n\nProtocol versioning travels with the runtime, not the bundle. The gateway speaks the stateless MCP release of 2026-07-28 as its primary dialect and negotiates down through the 2025 release and the 2024-11-05 SSE transport as fallbacks, so a client a year old still talks to a server shipped this morning. The dialect a session ends up on is a handshake, not a deploy decision.\n\n## Why the guest never touches the network\n\nThe most dangerous thing a tenant bundle can do is make an HTTP request. `fetch` is a universal source of SSRF: a careless or malicious bundle pointing a tool at `http://169.254.169.254/`, the AWS metadata service, hands a customer's AI agent a read path into the instance's credentials. All outbound traffic therefore goes through the SSRF guard, and the guard works on one principle: an address is only valid for as long as the policy that approved it. It resolves DNS before the request and blocks private ranges outright (loopback, `10/8`, `172.16/12`, `192.168/16`, link-local `169.254/16`, which is the metadata service, `0/8`, and the IPv6 twins). It then pins the approved IP into the undici connection, so a rebinding attack cannot swap the address after vetting. And it keeps SNI and TLS aligned with the pinned address, because pinning at the socket level without pinning the name would throw `CERT_ALTNAME_INVALID` on each request. The DNS cache is capped at 4,096 entries and lives no longer than the pooled connection it belongs to.\n\nThe keep-alive setting on that pooled agent is the most boring fix I have ever shipped, and I am glad to write it down. undici's default idle timeout is 4 seconds, and it was closing pooled sockets between conversation turns in the background, so nearly each real tool call paid a cold TLS handshake. We run it at 65 seconds, just above the ALB's 60-second idle window, and cap the pool at 100 sockets per target. The p50 of outbound calls dropped by the cost of one handshake. A boring fix with a p50 is the dream.\n\nResponses stream into the isolate with a running byte counter, hard-capped at 10 MB, with an `AbortController` wired through the whole dispose path: when an isolate is retired, its in-flight requests are aborted in the same call. When a dispatch times out (the 30-second budget), the error is not a generic \"request took too long\". A classifier asks what the isolate was doing at the moment of breach: upstream I/O in flight, the guest computing, or memory pressure? The `tool_error` that goes back to the agent carries the answer, plus a recovery hint. Ownership of a failure (the tenant's upstream API, the tenant's code, or the platform) is not something you should figure out by grepping logs at three in the morning.\n\nThat classifier's output feeds an upstream-attribution ring with 32 slots, newest first. The operational picture then shows not only that dispatches are failing but whose upstream is. When a tenant's CRM API degrades at two in the morning, the runtime knows it is the tenant's CRM, not the platform, and the ring holds the evidence long enough for the incident channel to read it.\n\n## Provisioning once: the snapshot principle\n\nA full boot (compile the bundle, run the polyfill layer, execute the IIFE) costs around 50 to 100 ms on first contact with a deploy. That is fine for request one. It is not fine for request 4,000, which is all of them.\n\nThe principle is that an environment and a program are different artifacts, and only one of them changes often. The polyfill layer is static for a given deploy: same V8 version, same host surface. What changes per request is the tenant code. So we snapshot the environment, not the program. The host runs the isolated-vm snapshot builder over the provisioned environment and caches the resulting heap blob on disk, keyed by deploy ID and stamped with the V8 version it was built under. On restore, a fresh isolate is created from that blob: a millisecond or two for the engine, another one or two to swap the stubbed bridges for real ones, and the IIFE itself re-executes in the restored context. Total: 15 to 25 ms, against 50 to 100 for a full boot, and the polyfill work happens exactly once per deploy. The bundle is re-run rather than snapshotted because `Isolate.createSnapshot()` is a static method that cannot execute async code, and our IIFEs are allowed to `await` at top level.\n\n![Full boot vs snapshot restore: where the 50 to 100 ms goes and where the 15 to 25 ms comes from](/post/fig-v8-snapshot.svg)\n\nThe hard part of snapshots is not the speed. It is the failure mode. A deserialization failure in V8 is a `SIGABRT`: the process dies, JavaScript cannot catch it, and a corrupt blob sitting on disk is a crash loop waiting to happen. Restore, abort, restart, restore the same blob, abort again. So the integrity model treats the cache like untrusted input. Four layers, in order: writes are atomic (the blob lands in a `.tmp` file and is renamed, metadata first, so a kill mid-write leaves a verifiably mismatched pair, not a half-blob); each blob is SHA-256'd and the hash is checked before the bytes reach V8; the V8 version stamp makes the cache self-invalidating the moment Node is upgraded on the fleet; and at process start a purge pass validates the cached snapshots on disk and deletes the bad ones. The design invariant underneath all of it: a corrupt cache entry degrades to a 100 ms cold boot. It never degrades to a crash. That is the whole incident: 80 extra milliseconds on one request.\n\nThe snapshot layer also doubles as hibernation. Stateful bundles expose `getState` and `setState` hooks with a one-second serialization budget, so a tenant that keeps a working set in memory can have its state extracted, its isolate retired, and its state injected back on the next request. Same machinery, reversed direction.\n\n## Invariants: what one phase owes the next\n\nThe layer above is only as honest as its invariants, and invariants rot when they live in people's heads. The runner therefore carries a written contract: 34 rules across its four phases (boot, snapshot, dispatch, dispose), next to the code they govern, read on each review that touches that file. The phases are the lifecycle; the rules are what one phase owes the next. Most of them are \"no\" rules. No bridge in a snapshot. No host timer outliving its isolate. No dispatch that skips timeout attribution.\n\nDispose is where the contract pays off, because disposal is the one phase where a wrong order leaks something. Retiring an isolate is a fixed five-step sequence: abort the `AbortController` first, so pending HTTP dies with the abort; clear the host-side timers, so the guest cannot reschedule itself; release the `isolated-vm` reference handles in reverse allocation order; release the context and the isolate; only then null the last JavaScript reference, so nothing pins a dead heap to the process. The concept is ownership transfer: no bridge may outlive the isolate it points at. Skip a step and you get a lingering heap, a timer that fires into a corpse, or a request that outlives its owner. The order is load-bearing, and the contract says so in writing.\n\nData-loss prevention gets the same treatment at the other end of the lifecycle. Before a payload reaches tenant code it passes a redaction pass that is loaded at boot, not lazily: a control that has not loaded yet does not exist. Credentials in transit and anything that looks like a secret are masked before they touch a log line or a bridge argument. A DLP inside tenant code is a DLP the tenant can turn off; a DLP on the host side of the bridge cannot be.\n\n## Containment by construction\n\nThe limits in the runtime are not a wall of magic numbers. They live in one file, `Limits.ts`, which documents, next to each constant, where the number comes from. The 30-second dispatch budget comes from user behavior: mainstream MCP clients give up on a tool call somewhere between 30 and 60 seconds, so beyond 30 we are spending resources on a result nobody reads. The 65-second keep-alive mirrors the ALB idle window. The sweep horizon belongs to the connection tracker's 30-minute idle tier. A limit is the footprint of a policy, not a constant: when the policy changes, one file changes, and the derivation stays on record.\n\nCredentials follow the same discipline. A tenant's decrypted secret map is injected into the isolate as a deep copy on `__vinkius_secrets`: the guest reads it, it cannot write back to the host. The runner keeps a SHA-256 fingerprint of the map, not the values, which makes re-injection on a later request a cheap no-op when nothing changed. Each token gets its own runner and its own isolate; there is no pooling that shares credential state between tenants, full stop.\n\nThe only token shape that reaches an isolate is the live connection token, anything with the `vk_live_` prefix. Preview tokens, revoked tokens, tokens for another deploy: they fail at the runner boundary, before a bridge is registered. The one capability the guest does hold is `VINKIUS_TOKEN`, exposed to the catalog's tool code: it authenticates a tool back to the platform as the user who owns it, scoped to that user's catalog. The model is capabilities, not passwords: each crossing of the boundary is a named, audited, revocable right.\n\nWhat a bundle cannot do is documented as positively as what it can. No filesystem, no `process.env`, no direct bridge access (the sanitizer rejects it at compile time), 128 MB of heap, 30-second timers, 10 logs per second and 1 KB per message through the console bridge, a 10 MB fetch ceiling. When a tenant crosses the financial line, the circuit breaker does not return a `429` and shrug. The quota layer emits an AI-native error: plain language telling the LLM to stop retrying and to surface the budget ceiling to the human user, with a link to the Vinkius Cloud console where the owner approves resumption. A retry storm is the native failure mode of agents; the platform should answer the agent, not only the person behind it.\n\n## What the platform owns\n\nAll of this runs on a small, deliberately x86-only fleet. The x86 pin is a constraint I own in print: `isolated-vm` is a native addon, and its ARM builds have been the flakiest dependency we ship. On x86 it is boring, and boring is the goal in the layer where other people's code runs.\n\nThe process boots empty. No configuration is loaded at start; the first connection for a token pulls the server's configuration from the Laravel API on demand, boots that server's isolate just in time, and an LRU sweep retires entries after 30 idle minutes, so one task holds the live state of many tenants. Redis carries the control plane (invalidation, kill switches, tool-change notifications, quota updates), so a configuration change in the console reaches all tasks without a deploy.\n\nOne last piece of the security story sits next to the isolates, and I close on it because it shows the boundary we drew deliberately: the cryptographic audit path. Each tool call event flows to a single-threaded streaming daemon that forges a SHA-256 hash chain and signs it with an Ed25519 session key, held in RAM for 24 hours and certified by the master key on boot. After a crash the daemon reprocesses un-ACKed events, so the chain never has a gap. Each forged link is checkpointed to a Redis Streams consumer group, so a crash resumes from the last ACKed event instead of re-deriving the chain, and SIEM adapters drain that same stream behind their own circuit breakers: a failing sink gets fenced off rather than allowed to back up the audit path. The rule that made the daemon necessary fits in one line of source: V8 never touches crypto. Tenant code can compute a hash through the host bridges, but it cannot sign, certify, or forge anything. The audit trail is something the platform does to the runtime, not something the runtime's tenants can reach into.\n\n## Direction\n\nThe isolate layer today is the stateless, fast, hard-capped story: boot in a millisecond or two, cap at 128 MB, retire in a sweep, and let the snapshot carry the heavy part. The open direction is the stateful one: longer-lived tenant state riding the hibernation hooks, denser packing of live isolates per task, and eventually teams bringing their own connector catalogues into the marketplace without waiting on our CI.\n\nI do not think V8 isolates are the final answer to multi-tenant edge code. They are the answer for the shape this platform has, and the shape is where I want it to stay: cheap tenants, hard caps, and a millisecond-scale boot, because an agent's patience is measured in seconds, and so is its trust. Cloudflare's \"zero cold start\" is a sentence you can engineer, not a slogan you can only admire. It is an isolate, a snapshot, an integrity chain, and a series of unglamorous decisions about who owns the timers. The code in this post is the runtime behind the MCP servers on [cloud.vinkius.com](https://cloud.vinkius.com), and the tracing work that ties its tool spans into distributed traces is in [the MCP Fusion 5.1.0 post](/en/posts/mcpfusion-5-1-0-trace-correlation). The stateful direction this platform is moving toward is the session layer itself: how the SwarmGateway coalesces overlapping agent sessions and applies causal invalidation to stale state. The full architecture is in [the SwarmGateway sessions post](/en/posts/vinkius-swarm-gateway-sessions)."
      },
      "date_published": "2026-09-21T00:00:00.000Z",
      "date_modified": "2026-09-21T00:00:00.000Z",
      "authors": [
        {
          "name": "Renato Marinho"
        }
      ],
      "tags": [
        "v8",
        "sandboxing",
        "runtime",
        "mcp",
        "edge"
      ]
    },
    {
      "id": "https://blog.vinkius.com/en/posts/mcpfusion-5-1-0-trace-correlation",
      "url": "https://blog.vinkius.com/en/posts/mcpfusion-5-1-0-trace-correlation",
      "title": "MCP Fusion 5.1.0: correlating MCP tool calls into the distributed trace",
      "summary": "Released September 20, 2026. MCP tool spans used to be islands: every call started a fresh root, and backends that don't speak mcp.* couldn't query them at all. 5.1.0 adds W3C Trace Context parent correlation and dual GenAI/OpenInference emission to every tool span, without a single OpenTelemetry dependency in core.",
      "content": {
        "markdown": "The release notes for 5.1.0, published September 20, 2026, list two work items: G1, W3C Trace Context parent correlation, and G2, dual-convention emission, and both of them land in a single package ([release notes](https://github.com/vinkius-labs/mcpfusion/releases/tag/v5.1.0) · [MCP Fusion on GitHub](https://github.com/vinkius-labs/mcpfusion)). The fifteen other packages ride along to 5.1.0 in lockstep without a line of code: their `^5.0.0` range on core is already satisfied, so for them the release is one line of semver bookkeeping. What moved is narrow, and it moves the one thing that decides whether you can even *see* your agent's tool calls in your backend.\n\n## The waterfall ended at the gateway\n\nBefore this release, `MCPFusionTracer.startSpan` took two arguments, and the spans it produced were always fresh roots. In practice that meant the agent's distributed trace stopped at the MCP gateway: each tool call surfaced in your backend as its own orphan trace, and following agent → tool in one waterfall was not possible. It also meant that backends without knowledge of the private `mcp.*` namespace were blind; \"all tool calls in the last hour\" was a query you could only run where you had taught your vendor that namespace.\n\n![Before: every tool call minted its own root trace. After: the per-request W3C traceparent parents the tool span to the caller's trace, unknown-tool 404s included.](/post/fig-trace-waterfall.svg)\n\n## A parent handle the framework will not touch\n\nThe new exported type `MCPFusionSpanContext` is a structurally opaque parent handle: `traceId` (32 lowercase hex characters), `spanId` (16), `flags` (the W3C trace-flags byte, `0x01` when sampled), plus optional `remote`, `traceState`, `baggage` and a `raw` slot for the host's own tracer context. The host builds one from an incoming `traceparent` / `tracestate` / `baggage` pair, HTTP headers, or MCP `params._meta`, and the framework does exactly one thing with it: it forwards it. It never inspects it. That opacity is the design, not an accident: because a host adapter maps `traceId` / `spanId` / `raw` onto OpenTelemetry's `SpanContext` + `Context`, `@mcpfusion/core` keeps zero `@opentelemetry/*` dependencies. A raw OTel `Tracer` is not assignable to `MCPFusionTracer` (immutable attribute arrays, plus the `Context` split), so the boundary stays a thin adapter in your code rather than a package dependency.\n\nParsing is strict by choice. The zero-dependency helpers sit on `crypto.randomUUID()`: `newTraceId()`, `newSpanId()`, `generateTraceparent(sampled = true)`, and `parseTraceparent` accepts only version `00`, 32-character lowercase non-zero trace IDs, 16-character lowercase non-zero span IDs, and two-hex-digit flags; anything else returns `undefined` rather than a fabricated parent. `tracestate` and `baggage` pass through verbatim under their W3C limits (512 characters, 8192 bytes), and `extractW3CContext` makes a valid `traceparent` mandatory: no valid header, no context, and the span is a fresh root. The byte format matches what `SwarmGateway` already emits, so traces across the monorepo interoperate out of the box.\n\nThe pickup is per-request. `GroupedToolBuilder` and `ToolRegistry` read a conventional duck-typed key, `ctx.mcpTraceContext` (the same pattern as `ctx.handoffTraceparent`), and thread it into the tool span, and the unknown-tool 404 span that `ToolRegistry.routeCall` emits gets the same treatment, which matters precisely because a 404 is usually the moment an agent has lost track of which tools exist. One subtlety worth knowing: without a `contextFactory`, `attachToServer()` installs a guard proxy that throws on any property access; the new `readMcpTraceContext` helper performs its single read inside a `try/catch` and swallows the throw, so enabling tracing alone never forces you to write a factory. Absence of context reads as \"fresh root\", never as an error. There is one documented limit: because the framework does not hold an OTel `Context` at runtime, auto-instrumented downstream calls inside a tool handler (Prisma, HTTP clients, Redis) surface as siblings of the MCP span, not children. The incoming parent is honored; the tool span does not become the active context for everything below it.\n\n## One span, three dialects\n\nEvery tool span now carries, next to the native `mcp.*` attributes, `openinference.span.kind = \"TOOL\"`, `gen_ai.operation.name = \"tools/call\"` and `tool.name`, the 404 span included. The same span is therefore portable across Datadog, Arize and Phoenix without any of them needing to understand `mcp.*`; filter on `openinference.span.kind = TOOL` anywhere. What is *not* emitted matters as much: no token or cost attributes at the tool boundary, because those belong on the agent-side LLM span, and 5.1.0 refuses to invent them at the tool layer. Status semantics stay consistent with the pipeline: `UNSET` for AI-side failures (validation errors, unknown actions, unknown tools; the 404 span is explicitly `UNSET` with a message, not `ERROR`), `OK` on success, and `ERROR` only when a handler throws a system failure. An LLM calling the wrong tool does not page your on-call.\n\n## If you already run an OpenTelemetry tracer\n\nThe entire adoption is two snippets. First, wrap the tracer so the optional context argument lands where it belongs:\n\n```ts\nimport { trace, type SpanOptions, type Context } from '@opentelemetry/api';\n\nconst otel = trace.getTracer('mcpfusion');\nregistry.attachToServer(server, {\n  contextFactory: createContext,\n  tracing: {\n    startSpan: (name, options, context) =>\n      otel.startSpan(name, options as SpanOptions, context?.raw as Context | undefined),\n  },\n});\n```\n\nThen, in your per-request context factory, extract the W3C context under the conventional key:\n\n```ts\nconst ctx = {\n  ...baseContext,\n  mcpTraceContext: extractW3CContext(request.headers),\n};\n```\n\nFrom that point on, every tool span, 404 routing included, hangs off the caller's trace, and every backend that speaks the GenAI/OpenInference conventions can see it. The release's verification block backs it up: core builds with zero TypeScript errors, all 16 satellite packages clean, the core suite green at 272 files / 5,263 tests with zero failures, and `@mcpfusion/swarm` at 158/158. The new `Tracing.test.ts` pins dual-emit parity between the tool span and the 404 span, W3C round-trips, malformed-input rejection, guard-proxy tolerance, and parent-context propagation on both paths."
      },
      "date_published": "2026-09-20T00:00:00.000Z",
      "date_modified": "2026-09-20T00:00:00.000Z",
      "authors": [
        {
          "name": "Renato Marinho"
        }
      ],
      "tags": [
        "mcp",
        "tracing",
        "observability",
        "mcpfusion"
      ]
    },
    {
      "id": "https://blog.vinkius.com/en/posts/mcpfusion-5-0-10-tool-boundary-fixes",
      "url": "https://blog.vinkius.com/en/posts/mcpfusion-5-0-10-tool-boundary-fixes",
      "title": "MCP Fusion 5.0.10: smaller descriptions, recovered thrown errors",
      "summary": "Released September 17, 2026. Two P2 fixes in @mcpfusion/core aimed at the exact surfaces the model touches: grouped descriptions that echoed the tool summary once per action and under-reported required fields, and thrown classified responses that were flattened into a generic INTERNAL_ERROR.",
      "content": {
        "markdown": "Published September 17, 2026, 5.0.10 is a maintenance release, and every code change in it sits inside `@mcpfusion/core`; the release notes confirm the other fifteen workspace packages still build clean, untouched ([release notes](https://github.com/vinkius-labs/mcpfusion/releases/tag/v5.0.10) · [MCP Fusion on GitHub](https://github.com/vinkius-labs/mcpfusion)). Two P2 defects and one latent crash make up the release. All three matter, and all three can only be observed from where the language model stands.\n\n## Bug 152: the summary, said 1 + N times\n\nUnder `toolExposition: 'grouped'`, a tool exposes a single name with a discriminating `action` field instead of N flat tools, and its description is layered. Layer 1 leads with the tool summary, the module/action listing and the dispatch instruction; Layer 2 carries one line per action: the `Workflow:` block in markdown, the `desc:` column in TOON. When a builder sets a `.description()` but its individual actions do not have one, the tool summary is inherited into each action (correct, flat exposition relies on that fallback), and it was then echoed back *once per action* in Layer 2. The model saw the summary `1 + N` times with zero new information after the first. A redundant description does not merely bloat the prompt; it dilutes the exact signal the model is supposed to weigh. The fix treats a description equal to the tool summary as \"not action-specific\" and omits it from Layer 2; action-specific descriptions pass through untouched.\n\n![The grouped tool description, Layer 1 and Layer 2: on 5.0.9 the inherited summary was echoed once per action and the Requires: hint skipped commonSchema fields; on 5.0.10 the echo is omitted and the hint matches the validator.](/post/fig-desc-layers.svg)\n\n## Bug 153: a hint the validator would reject\n\nThe \"which fields must I pass\" hint, the `Requires:` line, was computed from the per-action schema only. It never looked at the tool-level `commonSchema` and did not apply `omitCommonFields`. The consequence in practice: a required `workspace_id` declared via `.commonSchema()` appeared in `inputSchema.required` but not in the hint. The model omitted the field, validation rejected the call, the agent read the error, re-called with the missing field, and succeeded. One needless self-healing bounce, paid on every grouped call that shared a common field. The fix makes `getActionRequiredFields()` reflect the *same merged schema* that `buildValidationSchema()` enforces, including `omitCommonFields`, so the hint and the validator can no longer disagree.\n\nThe `commonSchema` parameter added to `getActionRequiredFields` / `generateDescription` / `generateToonDescription` is optional, which keeps the change wire-visible but non-breaking: no API, type or schema shape changed; flat exposition output is byte-for-byte unchanged, and grouped descriptions only shrink. The one operational consequence: `mcpfusion.lock`'s `integrityDigest` changes for grouped servers that had inherited descriptions; regenerate the lockfile.\n\n## The catch that swallowed meaning\n\nThe framework's taught idiom is to *classify* failures, not just raise them: handlers and middleware may `throw` an already-classified response (`throw toolError('NOT_FOUND', {...})`, `throw error('Unauthorized')`) instead of returning one. The catch block in `runChain()` recognized a plain `ToolResponse` and nothing else, so a thrown response arrived stripped of its error code, its recovery guidance and its warning-versus-error severity, and left re-wrapped as a generic `INTERNAL_ERROR`.\n\nTwo shapes were worse than merely lost. A thrown `HandoffResponse` carries the tool-response brand but has no `content` array, so the `isToolResponse()`-only check forwarded it into code that indexes `content`, a crash on the federated-handoff path. A thrown `ResponseBuilder` was discarded instead of built, losing every composed content block. The catch now mirrors `postProcessResult()`'s ordering (`isHandoffResponse` → `isResponseBuilder` → `isToolResponse`), and only genuinely unexpected residue becomes `INTERNAL_ERROR`.\n\nThe part that matters most at the model boundary: that fallback **no longer claims the failure is transient**. A permanent failure (a bad id, a missing resource, expired auth) would otherwise send the agent into a retry loop with identical parameters. The new error reports the `[tool/action]` origin and tells the model to inspect the message rather than retry blindly, which is the difference between an agent that escalates and one that hammers the same call until it times out.\n\n## Pinned and shipped\n\n- `DescriptionInheritance-bug152-153.test.ts` (18 cases) and `ThrownResponseRecovery.test.ts` (12 cases) pin both behaviors, per the release notes.\n- Full core suite: **5,243 passing, 0 failed**; `tsc --noEmit` clean; all 16 workspace packages build.\n\nInstalling is one line:\n\n```bash\nnpm install @mcpfusion/core@5.0.10\n```\n\nRunning grouped exposition? Regenerate your `mcpfusion.lock` so the `integrityDigest` matches the (now smaller) description, and nothing else in your handler code changes; the idiom of throwing a classified response now works the way the docs always said it would."
      },
      "date_published": "2026-09-17T00:00:00.000Z",
      "date_modified": "2026-09-17T00:00:00.000Z",
      "authors": [
        {
          "name": "Renato Marinho"
        }
      ],
      "tags": [
        "mcp",
        "mcpfusion",
        "agents"
      ]
    }
  ]
}