Announcing CIMD support for MCP Client registration
Learn more
MCP Gateway
Nov 4, 2026

Running a Private MCP Registry: Allowlists, Namespaces and Review

Varun Krishnan
Senior Content Marketer

Three teams in the same company each wired an agent to Slack. Platform used the community server from npm. Data used a fork someone pushed to an internal GitHub org eight months ago. Support used a package called slack-mcp-server-v2 that a contractor found and that nobody has looked at since. Each works. Each holds a workspace token. When security asks "which agents can post to #general and through what code," the honest answer is a Slack search and a prayer.

This is the state most organizations are in a year into MCP adoption. The fix is not a document listing approved servers. It is a registry whose entries are the only thing an agent runtime will resolve, and a review loop that keeps those entries honest after they are approved.

TL;DR

  • The official MCP Registry solves public discovery and namespace ownership. It explicitly does not host private servers, is not intended to be consumed directly by host applications, and its codebase is not designed to be self-hosted. Subregistries are the intended pattern for everything else.
  • A private registry is an allowlist with a review process attached. Its value is zero unless the agent runtime refuses servers and tools that are not on it.
  • Review has to cover five things per entry: who publishes it, what it is pinned to, what its tool definitions say, what credentials it asks for, and what happens when any of those change.
  • For SaaS tools (Slack, GitHub, Jira, Gmail), the hard part is not listing the server; it is making one approved server definition safe for hundreds of users with different permissions. That is a per-user credential problem, not a catalog problem, and it is what a Virtual MCP server with per-user session tokens is shaped to solve.

What the official registry is, and what it refuses to be

The official MCP Registry launched in preview in September 2025. It stores server.json metadata, not server code: where a package lives (npm, PyPI, NuGet, Docker, or a remote URL), how to run it, what transports it supports. Publishing requires proving you own the namespace: io.github.<user>/* through GitHub OAuth or Actions OIDC, com.<yourcompany>/* through DNS or HTTP verification.

That namespace verification is the single most useful thing the official registry gives an enterprise. com.acme/jira can only be published by someone who controls acme.com. The look-alike problem (slack-mcp-server versus slack-mcp-server-v2) is a naming problem, and verified namespaces are the first real answer to it.

The registry's documentation is equally clear about its limits. It does not support private servers. It is not meant to be queried by host applications at runtime. Its codebase is not built for self-hosting, and the maintainers say they cannot support that use case. Moderation is deliberately light: illegal content, malware, spam, and non-functioning servers are removed; everything else is left to downstream curation.

The design intent is spelled out in the project's own architecture: subregistries are first-class. A subregistry mirrors the official dataset, adds its own metadata, enforces its own policy, and stays API-compatible. An enterprise registry is one of these. You are not forking the official registry; you are building the layer it was designed to sit under.

What a private registry has to hold

Start from the server.json shape so your entries stay compatible with upstream tooling, then add the fields the official registry deliberately leaves out. The schema reserves _meta for publisher-namespaced extensions, so governance data goes there under your own domain key rather than in top-level fields that upstream tools would reject.

{ "name": "com.acme/jira-internal", "description": "Jira tools for engineering agents. Read issues, create issues, transition status.", "version": "2.1.0", "packages": [ { "registryType": "oci", "identifier": "registry.acme.internal/mcp/jira", "version": "2.1.0", "transport": { "type": "stdio" } } ], "_meta": { "com.acme/governance": { "owner": "platform-eng", "approvedBy": "security-review-2026-10-14", "approvedDigest": "sha256:9f1c0d2b4a7e8c3f5d6a1b2c3d4e5f60718293a4b5c6d7e8f9a0b1c2d3e4f5a6", "toolPins": { "jira_issue_search": "b7f3e1c9a2d4f6e8b0c2d4f6a8e0c2d4f6a8e0c2d4f6a8e0c2d4f6a8e0c2d4f6", "jira_issue_create": "4e8a2c6f0b3d7e1a5c9f2b6d0a4e8c2f6b0d4a8e2c6f0b3d7e1a5c9f2b6d0a4e", "jira_issue_transition": "c1d5e9a3f7b2c6d0e4a8f2b6c0d4e8a2f6b0c4d8e2a6f0b4c8d2e6a0f4b8c2d6" }, "credentialMode": "per-user-oauth", "allowedAudiences": [ "eng-agents", "support-agents" ], "reviewDue": "2027-01-14" } } }

Three of those governance fields do all the work.

approvedDigest pins the entry to a container digest, not a tag. :latest is how a rug pull arrives through a registry that looks perfectly governed. A version string in server.json is a promise from the publisher; a digest is a fact.

toolPins is the hash of each tool's name, description and inputSchema as they were at review time. The registry entry carries what was approved; the runtime compares against it on every tools/list. A server that changes a description without a new review fails the comparison and gets quarantined, whatever its version string says.

credentialMode records how the server authenticates to the upstream system. This field decides who is allowed to use the entry. A server running on a shared bot token is safe for one audience and dangerous for another, and the registry has to say which.

Namespaces: the conventions that stop the Slack problem

Namespaces in a private registry are organizational policy encoded as strings. Three conventions keep them useful.

Internal servers use your verified domain namespace, com.acme/*, and nothing else. A server under io.github.<someone>/* is by definition not yours, even if the someone is an employee. This single rule would have caught the Data team's fork in the opening scenario.

Mirrored public servers keep their upstream name and get a governance block. You do not rename io.github.modelcontextprotocol/filesystem to com.acme/filesystem; renaming breaks the chain back to the verified publisher. You mirror it, pin it, and annotate it.

One name per upstream system, enforced at review. If com.acme/slack exists, a second entry for Slack does not get approved without a written reason. Duplicate entries are where credential sprawl starts: two servers, two bot tokens, two sets of scopes, and nobody owns the difference.

For remote servers, Server Cards (SEP-2127, merged as Final in October 2026) give you a standard place for the server to self-describe its transports and protocol versions before a client connects. A registry entry for a remote server should point to its card, and review should verify the card matches what the server actually serves.

The review loop

Review is the part that distinguishes a registry from a wiki page. It runs at three moments: on submission, on change, and on a schedule.

On submission, the reviewer answers five questions and records the answers in the entry:

  1. Who publishes this, and can we prove it? Namespace verification for public entries; a named owning team for internal ones.
  2. What exactly are we pinning? A digest for packages, a Server Card plus endpoint fingerprint for remote servers. Never a tag, never a branch.
  3. What do the tool definitions say? Read every description, including parameter descriptions. Look for imperative text aimed at the model, references to other servers or to files, and anything a user would not see in a tool summary. Hash each definition into toolPins.
  4. What credentials does it need, and whose? Bot token, per-user OAuth, API key from a vault, nothing. This sets credentialMode and therefore allowedAudiences.
  5. What does it do with the data it touches? Side effects, outbound network calls, logging. A server that posts telemetry to its publisher is a data flow you are approving.

On change, any difference in digest, Server Card, or tool pin reopens review automatically. This is the control that defeats rug pulls, and it is only possible because the registry recorded what was approved rather than just that something was.

On a schedule, reviewDue forces a re-read even when nothing has changed, because "nothing has changed" is exactly what a server that swapped its implementation behind an unchanged description looks like.

The tradeoff is throughput. A review that reads every tool description takes longer than one that checks a version number. Teams that skip step 3 to move faster are running an allowlist of names, which is the thing the opening scenario already had.

Enforcement: the registry only counts if the runtime refuses everything else

A registry with no enforcement is documentation. Enforcement means that an agent runtime, when it resolves an MCP server, consults the registry and refuses anything not on it, in the audience it is running for, with the pins the entry carries.

There are two places to put that check, and they have different failure modes.

Client-side allowlist. Each agent host (Claude Desktop, Cursor, your internal harness) is configured from the registry and will only start listed servers. Simple to begin with. The failure mode is drift: every host is a copy of the policy, and the host on a laptop that was configured in March does not know what was revoked in September. This is how an offboarded engineer's agent keeps a working server for weeks.

Gateway enforcement. Agents connect to one endpoint; the gateway resolves registry entries, pins definitions, injects credentials, and logs every call. One copy of the policy, enforced at the boundary. The failure mode is that the gateway is now a dependency for every agent and a single place to get wrong. The tradeoff is explicit: you are trading a distributed consistency problem for a centralized availability one. For a platform where multiple teams run agents, that is usually the right trade; the alternative is N copies of the allowlist and N ways for them to disagree.

Recommended Reading: When an Employee Leaves, Who Revokes Their AI Agent's Access? on why a client-side allowlist on a laptop outlives the person who configured it.

Whichever you pick, the minimal runtime check is the same:

import hashlib import json def fingerprint(tool: dict) -> str: payload = json.dumps( { "name": tool["name"], "description": tool.get("description", ""), "inputSchema": tool.get("inputSchema", {}), }, sort_keys=True, ) return hashlib.sha256(payload.encode("utf-8")).hexdigest() def enforce(entry: dict, listed_tools: list[dict], audience: str) -> list[dict]: gov = entry["_meta"]["com.acme/governance"] if audience not in gov["allowedAudiences"]: raise PermissionError(f"{entry['name']} is not approved for audience '{audience}'") pins = gov["toolPins"] approved = [] for tool in listed_tools: name = tool["name"] if name not in pins: raise PermissionError(f"{entry['name']} served unapproved tool '{name}'") if fingerprint(tool) != pins[name]: raise PermissionError(f"{entry['name']} tool '{name}' differs from approved definition") approved.append(tool) missing = set(pins) - {t["name"] for t in listed_tools} if missing: raise PermissionError(f"{entry['name']} no longer serves approved tools: {sorted(missing)}") return approved

Any PermissionError here is a quarantine event, not a retry. The session does not start until a human looks.

The part a registry cannot solve: one server, many users

Everything above works cleanly for servers that run as the organization: a filesystem server, an internal metrics server, a build tool. It gets hard the moment the server talks to a SaaS product on behalf of a person.

Take com.acme/jira. One approved entry, one pinned image, one reviewed set of three tools. Now 400 engineers run agents against it. If the server authenticates with a shared service account, every one of those 400 agents has the same Jira permissions, which is to say the service account's permissions, which were provisioned as admin for convenience. The registry approved a server. It did not, and cannot, approve what each user is allowed to do through it. The Support team's agent can now transition engineering issues because the bot token can.

A per-user credential model inverts this. The server definition stays single and approved; the credential is resolved per user at runtime, from an OAuth grant that user individually gave. What the user cannot do in Jira, the agent cannot do in Jira. The registry still lists one entry; the runtime binds it to one person.

This is the shape that makes a Virtual MCP server a natural registry entry. The server is defined once per agent role, with exactly the tools that role needs from each connection, and its URL is static. Before each run, a session token is minted for the specific user, scoped to that user's connected accounts. The entry in your registry is the role's server; the identity is supplied per session. The example below uses the GitHub connection that new environments include; the shape is identical for Jira or any other connector.

import os from datetime import timedelta from dotenv import load_dotenv from scalekit import ScalekitClient from scalekit.actions.models.mcp_config import McpConfigConnectionToolMapping load_dotenv() scalekit_client = ScalekitClient( env_url=os.environ["SCALEKIT_ENVIRONMENT_URL"], client_id=os.environ["SCALEKIT_CLIENT_ID"], client_secret=os.environ["SCALEKIT_CLIENT_SECRET"], ) actions = scalekit_client.actions # Setup, once per agent role. The connection names must match # AgentKit > Connections exactly; they can differ between environments. response = actions.mcp.create_config( name="eng-issue-agent", description="Reads and files GitHub issues for engineering agents", connection_tool_mappings=[ McpConfigConnectionToolMapping( connection_name="github-connect", tools=["github_issues_list", "github_issue_get"], ), ], ) config_id = response.config.id mcp_server_url = response.config.mcp_server_url # Runtime, before each run: a short-lived token bound to this user. token_response = actions.mcp.create_session_token( mcp_config_id=config_id, identifier="user_123", expiry=timedelta(hours=1), ) mcp_server = { "url": mcp_server_url, "headers": {"Authorization": f"Bearer {token_response.token}"}, }

The registry entry for this is com.acme/eng-issue-agent, pointing at mcp_server_url, with credentialMode set to per-user-oauth. Review reads the two tool definitions and pins them. Enforcement checks that the agent resolving it is in eng-agents. What the registry does not have to carry is any user's credential, any scope decision per team, or a second entry when Support wants a different slice of GitHub; that is a second Virtual MCP server with a different tool mapping, and a second registry entry under the same namespace.

The tradeoff is that this covers connector-backed SaaS tools. Your internal filesystem server, your build server, your metrics server still go through the registry on their own terms, with the pinning and enforcement above. A private registry is not replaced by a per-user tool layer; it is the catalog that both kinds of entry live in.

Where to start

If you have nothing today: pick the domain namespace, verify it against the official registry, and create entries for the three servers you already know are in use. Pin digests. Hash tool definitions. Write down the audience. That alone turns the opening scenario into a known inventory with a review date.

If you have an allowlist of names: add toolPins and approvedDigest to every entry and make the runtime compare them. The difference between an allowlist and a registry is whether a changed description can get through.

If you have a registry but the SaaS entries run on service accounts: that is the entry to fix next, because it is the one where "approved" and "safe for this user" have quietly become different claims.

Discovery was always the easy half.

Recommended Reading: Build Production-Ready Agent Workflows with Remote MCP Servers on moving from script sprawl to a governed set of remote servers.

How Virtual MCP servers are defined and how session tokens are scoped per user is covered at docs.scalekit.com/agentkit/mcp/overview.

FAQs

Does the official MCP Registry support private servers?

No. It stores public server metadata, verifies namespace ownership, and is not meant to be queried by hosts at runtime or self-hosted. Private catalogs are built as subregistries that mirror its data and add their own policy.

What should a private MCP registry entry record?

The publisher and owning team, a pinned container digest rather than a tag, a hash of every tool name, description and input schema as reviewed, the credential mode the server uses, the audiences allowed to use it, and a review date.

How does a registry defend against rug pulls?

By recording what was approved, not just that something was. Any change in digest, server card or tool hash reopens review automatically, and a scheduled re-read catches servers that changed behavior behind an unchanged description.

Where should registry enforcement live, the client or a gateway?

A client-side allowlist is simple but drifts, since every host holds a copy of the policy. Gateway enforcement keeps one copy at the boundary, pins definitions, injects credentials and logs calls, at the cost of being a dependency for every agent.

Why is one approved server still unsafe for many users?

If the server authenticates with a shared service account, every agent inherits that account's permissions. The registry approved the code, not what each person may do through it. Per-user credentials resolved at runtime fix that.

How do Virtual MCP servers fit into a private registry?

Each agent role gets a server defined once with a fixed tool list and a static URL, which becomes the registry entry. Identity arrives per run as a short-lived session token scoped to one user, so the registry never carries credentials.

No items found.
On this page
Share this article
Scalekit
Find us on ProductHunt

Acquire enterprise customers with
‍zero upfront cost.

Every feature unlocked. No hidden fees.