Development

One MCP Server for Several APIs: Namespacing Tools

Antonio Evans9 min read

A single server rack center-left with three labeled upstream API boxes on the right (Billing, CRM, Shipping) connected by distinct colored lines that pass through a small credential keyhole per line.
Key points

Key points

  1. 1Register every upstream tool under a stable upstream.action prefix so collisions between APIs cannot happen at the client.
  2. 2Keep one permission table with a row per tool, holding the upstream name, required scope and allowed tenant tiers, and reject any call without a matching row.
  3. 3Resolve credentials at call time from a store keyed by tenant id and upstream name, never from the tool name alone.
  4. 4Log tenant id, tool name and upstream request id on every call so a bad write can be traced back to one credential and one API.

Why one spec stops being enough

When you build an MCP server from a single OpenAPI description, the mapping is mechanical and mostly safe: a path becomes a tool, path parameters become required arguments, and query parameters become optional ones with their defaults preserved from a step-by-step conversion walkthrough. Tool names come from operationId when it exists, otherwise from the method and path with non-alphanumeric characters replaced by underscores.

That works until the second API arrives. Now create_invoice from the billing spec and create_invoice from the accounting spec both want the same name. MCP tools are identified by unique names, and a client that sees two identical names can refuse to connect at all; duplicate function names across servers have broken real MCP clients as reported in this LiteLLM issue. The fix proposed there is exactly the one this article builds on: prefix each tool with its server or upstream name.

There is a second, quieter problem. A single-spec server can assume one credential set, often read from the environment. A multi-API server serves several tenants against several upstreams, and now the credential depends on who is calling and which API the tool reaches. Those two concerns, naming and credentials, have to be designed together because both key off the same identifier: the upstream name.

A third problem is scale. Hosts that connect to many servers and accumulate hundreds of tools cannot load every definition into context; the MCP client guidance recommends switching to progressive discovery once tool definitions take up a meaningful share of the context window per the MCP client best practices. A clean prefix makes search and filtering across a large catalog far more effective, because a query for "billing" matches every billing tool without embedding tricks.

A naming convention that survives regeneration

The rule: <upstream>.<action>, lower snake case on both sides, where upstream is a short, stable slug you own and action is derived from the upstream's operation identifier.

billing.create_invoice
billing.get_invoice
crm.find_contact
crm.upsert_contact
search.query_index

Three properties matter.

Stability. Names must not change when you regenerate from an updated spec. The upstream slug is decided by you and written into configuration, not derived from the spec's info.title, which marketing may edit. The action comes from operationId when present; if the generator does not produce a usable one, fall back to the synthesized method-plus-path form described in the conversion walkthrough on TrueFoundry's blog. Either way, capture the mapping in a file you commit, so a spec change that renames an operation shows up as a diff rather than a silent break.

Uniqueness. Dots create a hierarchy that maps onto SDK namespaces too: multi-level tags turn into nested method names in generated clients per Speakeasy's OpenAPI guidance. The same discipline keeps tool names readable and collision-free.

Legibility for the model. Clear, descriptive names and descriptions are the first listed best practice for MCP tools in the MCP tools documentation. A prefixed name tells the model both what the tool does and which system it touches, which reduces wrong-upstream calls.

Two edge cases worth deciding up front. First, some clients are strict about name characters; if you cannot be sure the dot survives every client, use an underscore instead (billing_create_invoice). The LiteLLM thread suggests both slash and underscore styles, and the important thing is consistency, not the separator. Second, keep each segment under the roughly 50-character guidance used for operation identifiers and schema names from APIMatic's OpenAPI best practices, because long identifiers cause problems in generated code and portals.

DecisionRecommendedWhy
Separator. (fallback _)Reads as hierarchy, matches SDK namespace convention
Upstream segmentShort, stable slug from configSurvives spec edits and title changes
Action segmentoperationId, else synthesizedStable across regenerations
Caselower_snake_caseMost generators and clients accept it
Segment lengthUnder 50 charactersAvoids codegen and portal issues
VersioningIn the URL behind the tool, not the nameRenaming a tool breaks agent prompts and evals

That last row is the decision rule most teams get wrong: never put the upstream API version in the tool name. billing_v2.create_invoice forces every prompt, saved workflow and evaluation to change when the upstream bumps a version. Version belongs in the base URL your adapter holds, invisible to the model.

Where namespacing goes in the code

There are two places to enforce the prefix, and the right answer is both.

At registration time, the tool is created with its prefixed name, so tools/list never shows an unprefixed tool. At call time, the server re-checks the name against the permission table before doing anything. The second check matters because a client can send any string in tools/call, and the handler in a naive implementation often just switches on the name and assumes it is known.

A compact registry that combines both:

UPSTREAMS = {"billing", "crm", "search"}

PERMISSIONS = {
    "billing.create_invoice": ("billing", "invoices:write", {"pro", "enterprise"}),
    "billing.get_invoice":    ("billing", "invoices:read",  {"free", "pro", "enterprise"}),
    "crm.find_contact":       ("crm",     "contacts:read",  {"pro", "enterprise"}),
    "search.query_index":     ("search",  "index:query",    {"free", "pro", "enterprise"}),
}

def resolve(tool_name, tenant):
    row = PERMISSIONS.get(tool_name)
    if row is None:
        raise PermissionError(f"unregistered tool: {tool_name}")
    upstream, scope, tiers = row
    if tool_name.split(".", 1)[0] != upstream:
        raise PermissionError(f"prefix mismatch for {tool_name}")
    if tenant.tier not in tiers:
        raise PermissionError(f"tier {tenant.tier} not allowed for {tool_name}")
    return upstream, scope

The prefix-versus-row check is redundant by construction, which is the point: if someone adds a row with a mismatched upstream field, the server refuses every call to that tool instead of silently routing to the wrong API with the wrong credential. Fail closed on a table error.

The per-tenant credential broker

The short answer: resolve credentials at call time from a store keyed by tenant id and upstream name, never from the tool name alone. The tool name is model-supplied input; the tenant id comes from the authenticated session, not from the tool arguments.

The lookup is a two-part key:

credential = credentials.get(tenant_id, upstream_name)

If the tool name were the key, a model that invents billing.create_invoice_v2 or a prompt-injected instruction could steer the call to a different upstream, and if that key happened to exist you would hand over another API's token. Keying on (tenant_id, upstream_name) means the worst a malicious tool name can do is fail the permission table lookup.

Practical rules for the broker:

  1. The tenant id comes from the transport, not the payload. MCP authorization is built around OAuth-style flows where the client proves who the user is before tools run, and the server publishes protected resource metadata so clients can find the authorization server per the MCP authorization tutorial. Bind the tenant from that validated session.
  2. Store secrets encrypted, fetch on demand, cache briefly. A short in-memory cache per tenant and upstream cuts latency; make the TTL explicit and invalidate on revocation.
  3. Ask for scopes, not full tokens, where the upstream supports it. Requesting a token scoped to invoices:write limits the blast radius if the server is compromised mid-request.
  4. Refresh without leaking. If the upstream token expires, refresh inside the broker and retry once. Never return the upstream token in a tool result; scrub it from error messages too.
  5. One credential per upstream per tenant. If the upstream requires both an API key and an account id, store them as one record so atomic rotation is possible.

For local, stdio-based servers where there is no remote session, environment-based credentials are an accepted pattern per the same MCP authorization tutorial. That is fine for a single operator and wrong for multi-tenant hosting. If you are hosting multiple upstreams and want to skip building the broker, routing model calls through one endpoint per model family while your MCP server stays focused on tools is one way to split the load; Quanta Inference exposes text, image, video, audio and embedding models behind one OpenAI-compatible key, so the broker only has to manage your upstream API credentials.

The permission table is the contract

The permission table is more than a lookup map. It is the document reviewers read to answer "what can this agent do on behalf of this customer." Keep it in source control and treat changes to it like changes to an access policy.

ToolUpstreamRequired scopeAllowed tiersWrites?Rate limit
billing.create_invoicebillinginvoices:writepro, enterpriseyes20/min
billing.get_invoicebillinginvoices:readfree, pro, enterpriseno120/min
crm.find_contactcrmcontacts:readpro, enterpriseno60/min
crm.upsert_contactcrmcontacts:writeenterpriseyes20/min
search.query_indexsearchindex:queryfree, pro, enterpriseno300/min

Four columns earn their place. The scope column is what you request from the upstream token endpoint. The tier column is a coarse commercial gate that stops a free tenant from calling a write tool even if a stale token would allow it. The writes column lets a host apply extra human-in-the-loop checks: MCP tools are model-controlled with a human in the loop to grant approval per the MCP tools documentation, and knowing which tools mutate state is what makes that approval prompt meaningful. The rate limit column keeps one tenant from exhausting an upstream quota shared by everyone.

The insight the first search result will not give you: derive the table from the spec, then review it by hand, rather than authoring it by hand. A script can walk each OpenAPI description, extract operations, and emit a row with a guessed scope and empty tier set. Nobody is allowed to publish a tool until every row has a reviewed scope and at least one tier. This keeps the table complete as APIs grow: an operation added upstream that nobody mapped cannot be called, because it has no row. The same single-source-of-truth discipline that documentation teams apply to specs applies here; keeping one description authoritative prevents drift between code, SDKs and docs per the OpenAPI Initiative's best practices.

Observability: one log line that answers the right question

When a tenant reports a wrong invoice, you need to know which credential was used, which upstream handled it and what the upstream called it. Log, at minimum:

  • the tenant id from the session
  • the tool name as invoked
  • the resolved upstream name
  • the upstream request id from the response header
  • the outcome and latency

That triple, tenant id plus tool name plus upstream request id, is what lets you join your logs to the upstream vendor's logs during an incident. It also gives you the data to answer a question the permission table cannot: which tools are never called, and which are called by tiers you did not expect.

On the client side, hosts that accumulate many tools benefit from progressive discovery, which keeps context small by loading definitions on demand when tool definitions exceed a threshold of the context window per the MCP client best practices. Your prefixed names make keyword and embedding search across the catalog work well: a search for "crm" returns every CRM tool, and a search for "invoice" returns billing tools across all upstreams. Treat observability as part of the discovery design, not a separate concern.

A rollout checklist

  1. Inventory the upstreams. For each, record the slug, base URL, auth scheme and the spec's location.
  2. Generate the tool list per spec, capture the operation-to-name mapping in a committed file, and apply the prefix rule.
  3. Flag collisions at build time. Any duplicate full name fails CI, the same discipline docs-as-code teams use when they block merges on broken references described by Write the Docs.
  4. Draft the permission table from the specs, then review every scope and tier by hand.
  5. Implement the broker with (tenant_id, upstream_name) as the key and a short cache TTL.
  6. Enforce the table at call time, before any upstream request, and fail closed.
  7. Add the log line with tenant, tool, upstream and upstream request id.
  8. Test the negative paths: unknown tool, prefix mismatch, tier not allowed, missing credential, expired credential.

The build-time collision check and the call-time table check are the two controls that carry most of the weight. Everything else is hygiene.

The same docs-as-code habits that keep a single spec honest, version control, review and automated checks, are what keep a multi-API registry honest from Fern's docs-as-code overview. If you would rather wire the whole pipeline, prefixed tool registry, permission table review and generated documentation, into one place, Quanta takes a description of the site, app or plan you need and a specialist builds it with you.

FAQ

FAQ

Why not let each upstream keep its natural tool names and deduplicate in the client?

Because not every client will do it, and some will simply refuse to connect. Duplicate function names across MCP servers have rejected connections outright in at least one client integration per this LiteLLM issue. Centralizing the prefix at registration also keeps names stable for prompts and evaluations, which client-side deduplication cannot promise.

Should the tool name include the upstream API version?

No. Put the version in the base URL your adapter holds. A version in the tool name forces every saved prompt, evaluation and workflow to change when the upstream bumps. Keep the name tied to the upstream slug and the operation instead.

What happens when a new operation appears in an upstream spec?

It is generated with a prefixed name, but it cannot be called until someone adds a reviewed permission-table row with a scope and at least one allowed tier. That gate is deliberate: an unreviewed write operation should not become reachable just because a vendor deployed it.

Can I skip the credential broker for a small internal deployment?

For a local stdio server run by one operator, environment-based credentials are an accepted pattern per the MCP authorization tutorial. Once more than one tenant or more than one person depends on the server, you need per-tenant resolution, because a shared environment token cannot be revoked without breaking everyone.

How do I keep the permission table from rotting?

Generate a draft from each OpenAPI description and require review before publishing, then regenerate on every spec change so new operations surface as unapproved rows. Commit the table to source control so changes go through review like code, the same argument docs-as-code teams make for specifications per the OpenAPI best practices.

Sources

Sources

  1. Converting OpenAPI Specs into MCP Servers, Step by Step
  2. Namespaced MCP Tool Names #12271 - BerriAI/litellm
  3. Client Best Practices
  4. OpenAPI overview and best practices - Speakeasy
  5. Tools
  6. 14 Best Practices to Write OpenAPI for Better API Consumption - APIMatic
  7. Understanding Authorization in MCP
  8. Best Practices - OpenAPI Documentation
  9. Docs as Code - Write the Docs
  10. Docs-as-code: What is it, how it works, and ways to start | Fern
About the author

About the author

Antonio Evans
Antonio Evans

Antonio Evans is the founder of Quanta Intelligence and a fractional CTO who builds HIPAA-ready AI systems for health and life-science companies. Before that he was technical co-founder of FriendsEAT for twenty years, CEO of Socialdraft, and a data engineer at IBM. Antonio lives with his dog Santino in Lisbon, Portugal.

All articles