Skip to content
ArmorCode2025 – 2026MCPTool AuthorizationReliability

Governed MCP Tool Registry

Nine of the fourteen production MCP servers behind a shared enterprise registry, with tool-level authorization under multi-tenant AppSec constraints.

production MCP servers in the shared registry delivered
9 of 14production MCP servers in the shared registry delivered
authorization checks passing across three permission tiers
20 / 20authorization checks passing across three permission tiers
failing queries traced to two independent root causes and fixed
100+ → 0failing queries traced to two independent root causes and fixed
connectors flagged for the same latent misconfiguration
14connectors flagged for the same latent misconfiguration

Giving agents real tools inside a multi-tenant security platform means a single authorization slip leaks another customer's vulnerability data. I delivered nine of the fourteen production servers in the shared registry, solved the distribution problem for non-technical teams, and root-caused the outage that took the codebase service down on two of three client surfaces.

The problem

Agents are only as useful as the tools they can reach, and in an AppSec platform the tools reach customer vulnerability data. Every server needed authentication, scoped permissions, explicit denial behavior, and audit attribution before it could be exposed — and then it needed to actually reach the people who needed it, including teams with no terminal and no device management to push configuration for them.

Constraints

  • Multi-tenant AppSec data. A single authorization slip exposes one customer's findings to another.
  • Claude Enterprise connectors relay through a vendor cloud on a fixed egress range, so they structurally cannot reach VPN-hosted internal endpoints.
  • The users who needed access most — Customer Success, Product, Documentation — had no terminal, no npx, and no MDM to push configuration on their behalf.
  • Tool descriptions are part of the interface. Every agent that connects inherits whatever the description implies.

Architecture

Governed MCP registry authorization pathRequests carry identity from the client through the gateway, where RBAC resolves a permission tier before any tool executes. Denials are explicit and attributed rather than silent.CLIENTSDesktop extensiondownload · double-click · no terminalAgent runtimesOffice OS agentsCLI clientsengineersidentity travels with the requestGATEWAYAuth + RBAC resolutionOAuth2 · scoped keys · tenant scope · audit attributionPER-TOOL DECISIONPermittedtool executes, call attributedDeniedexplicit refusal, never a silent empty resultREGISTRY · 14 SERVERS9 delivered by meread + write paths, tiered5 by teammatessame registry policyCanonical registrysource of truth · drift corrected
Requests carry identity from the client through the gateway, where RBAC resolves a permission tier before any tool executes. Denials are explicit and attributed rather than silent.

Decisions

What I chose, why, and what I turned down to get there.

Desktop extension bundles for distribution

Each bundle wraps a remote MCP client pointed at the internal HTTPS endpoint, with auth tokens marked sensitive in user configuration. The install path became download, double-click, drag to Applications — no terminal at any step. That was the only option that actually reached a non-technical user without weakening the network boundary.

Considered and rejected

  • Public endpoint with an IP allowlist — exposes internal surface to reach an internal user
  • Tunnel or reverse proxy — ongoing ops burden and a fragile dependency in the request path
  • CLI-based client — correct for engineers, unusable for the teams who needed it
  • Vendor-managed MCP tunnels — wrong product tier for the access pattern

Permission tiers at the tool level, not the server level

A single server often exposes both read and write paths over the same data. Gating at the server would have meant either denying useful reads or permitting unreviewed writes, so authorization resolves per tool, with denial explicit and attributed rather than a silent empty result.

Considered and rejected

  • Server-level allow or deny — too coarse for servers that mix read and write tools

Continuous integration for the bundle artifacts

Extension bundles and organization-level agent skills had no structured home and no build pipeline — they were artifacts being produced by hand. Moving them into a repository with automated builds on push and tag closed a real gap in how the tool layer shipped.

One registry as the source of truth

Fourteen servers across multiple teams drift in naming and capability without a single owner reconciling them. Maintaining the canonical list — and correcting drift against it — is what makes registry policy enforceable rather than advisory.

When it broke

The codebase-search service began returning empty answers on two of three client surfaces while the third kept working — the signature of a bug that looks surface-specific and is not. More than a hundred logged calls, none of them successful with content: empty responses, timeouts at just over sixty seconds, and server-unavailable errors. Reading the SDK source and the host runtime turned up two independent root causes.

  • The tool advertised an output schema it could never populate. The handler's second return parameter was a named empty struct, and the SDK derives an output schema from that type, skipping it only when the type is exactly `any`. An empty struct is not `any` — so the tool advertised a schema satisfiable only by an empty object, and returned exactly that alongside the real answer. Clients that support output schemas prefer the structured field, so they read the empty object and discarded every answer. The third surface predated that preference, read the content field, and was unaffected.
  • The query deadline was measured against the wrong client budget. Two host tiers existed: a main process at 300 seconds and agent runtimes at roughly 60. The deadline was set to 240 seconds — correct for the first tier, unreachable in the second. The failing surfaces were cancelled before it fired, so the gateway's 'still running, call again' recovery path never reached them. The job kept running, the cache filled, and nothing ever collected the result.
  • Shipped with an isolated two-tool reproduction differing only in the return type, and a negative-control test that fails if the typed return is reinstated. Verified build, vet, and tests across nine packages. Fourteen connectors were flagged carrying the same timeout misconfiguration before anyone hit it.

Ruling things out

Two otherwise-obvious fixes were eliminated by reading the host and SDK rather than guessing: the host never passes the flag that resets a timeout on progress, so heartbeats could not extend the deadline; and the SDK discards late results silently on cancellation, so letting the job finish and return would never have delivered anything. Knowing what will not work is most of the value of an RCA.

An earlier failure class

A separate investigation traced four-minute silent hangs to a server-side idle timeout closing sessions while the client held a dead reference, burning its entire budget before failing — which is why retrying often appeared to work. Both incidents pointed at the same lesson: in a tool protocol, timeout tiers and schema contracts are the interface, and mismatches there present as unrelated symptoms.

Stack

  • Model Context Protocol
  • Go
  • OAuth2
  • RBAC
  • Python
  • n8n
  • LiteLLM
  • AWS
  • Docker
  • GitHub Actions

What was mine

Nine of the fourteen production servers are mine, along with the canonical registry, the distribution mechanism, and the reliability work. The remaining servers were built by teammates against the same registry policy. Which models are approved is a senior engineering decision; I own the configuration and enforcement.