MCP architecture patterns for agent platforms

Learn when to use the hosted APIVerve MCP server, your own MCP server or direct function calling, and how to give each agent deployment and tenant its own scoped key.

  • 40 min
  • Expert

The AI Agents certification taught you to build one agent well: connect it over MCP, ground it in live data, handle its errors, test it and fence it in. This course starts where that one ends. Your company now has a support assistant in three editors, a nightly enrichment job, a payout reviewer, and a customer-facing feature where each of your customers gets an agent that calls APIs on their behalf. Each was built sensibly. Together they are a mess: four ways of calling the same catalog, keys copied between them, nobody sure which agent spent last Tuesday's credits, and no single place where a rule such as "agents never fetch URLs outside our allow-list" can be enforced.

An architect's job is to pick the shapes deliberately. This lesson covers the three integration patterns, when each one fits, how to keep the tool surface small on purpose, how keys map to deployments, and what changes when the agents serve many tenants.

Three ways to put an API catalog behind a model

Every agent that uses APIVerve reaches it through one of three shapes. They differ less in what the model sees than in who runs the loop, who owns the tool definitions and where your rules can live.

Hosted APIVerve MCP serverYour own MCP serverDirect function calling
Who runs the loopThe MCP host (an editor, a desktop app, a chat client)Any MCP host your users chooseYour backend code
Who owns tool definitionsAPIVerve, generated from the live catalogYouYou
Where guards can liveThe host, if it is yours; otherwise only key scopingInside your server, for every hostInside your executor
What you see on failureAn isError result with a sentence, or a JSON-RPC errorWhatever your server reads from RESTFull HTTP status, body and headers
What you operateNothingA service, its auth and its uptimeYour existing service

The hosted server is https://api.apiverve.com/v1/mcp. It answers initialize, tools/list and tools/call, builds one tool per catalog endpoint, and forwards each tools/call to the ordinary REST endpoint with your credential, so a call over MCP returns exactly what the REST call returns:

Try it liveNumber to Words1 credit
GEThttps://api.apiverve.com/v1/numbertowords
curl "https://api.apiverve.com/v1/numbertowords?number=2048" \
  -H "x-api-key: YOUR_API_KEY"
Sign in or create a free account to run this with your own key.

Over MCP, this same object arrives as structuredContent. The tool layer adds nothing and removes nothing.

Four properties of the hosted server matter at platform scale, and all of them come from how it is built rather than from configuration you can change:

  • The tool list is generated from the live catalog. A new endpoint is a new tool on the day it ships, and a reworded description is a reworded description for every connected agent. That is convenient for a person exploring in a chat window and a change-control problem for a production agent, which lesson 4 deals with.
  • Input schemas do not forbid extra properties. They list properties and required, but they do not set additionalProperties: false. A model that invents an argument passes schema validation, the call goes through, and the API drops the unknown key silently.
  • Failures are text. A failed tools/call comes back as a normal result with isError: true and a sentence such as Invalid request to NumbertoWords: {"missing":["number"]}. The HTTP status and the credit headers stay on the server's side of the hop. A tool name the server does not know is different again: it comes back as a JSON-RPC error object, not as an isError result.
  • Discovery is free and calls are billed. tools/list costs nothing; tools/call costs what the endpoint costs over REST, and each call has a 60-second limit.

None of these is a flaw. They are the properties of a server designed to plug straight into any client. They become constraints when you need status-aware error handling, strict argument checking or a stable tool contract, and that is what the other two shapes give you.

Your own MCP server sits in front of the REST API. It publishes tools you wrote, with names in your domain, strict schemas and composite tools that hold state. When a host calls one, your server validates the arguments, applies your policies, calls APIVerve over REST with the right key, and returns a result you shaped. Because MCP is the interface, every editor and chat client your teams use gets the same tools and the same rules.

Direct function calling skips the protocol. Your backend sends tool definitions to a model API, runs the loop, and executes each call itself, as you did in Build Agent Workflows. Nothing is reusable across hosts, but nothing sits between you and the response either.

When each pattern fits

The deciding question is not "which is more modern". It is who is in the loop, and do you own the host?

Use the hosted server when a person is driving and the host is not yours. A developer in Cursor, an analyst in a desktop chat app, a support engineer looking something up. The person reads every answer, the tools are lookups, and the cost of a wrong tool choice is a second question. Mount single-tool servers (/v1/mcp/<id>) for anything with a defined job, connect with OAuth so no key lands in a committed file, and let key scoping be the backstop.

Build your own MCP server when several hosts need the same curated toolset with your rules applied. The moment you want "the payout tools, with our argument checks, our budget and our audit log, available in every editor the finance team uses", the enforcement point has to be the server, because you do not control the hosts. Your server is the one place every call passes through.

Use direct function calling when one backend service owns the whole run. Unattended jobs, request handlers that run an agent for a user, anything with an SLA. You want the HTTP status to classify failures, the credit headers for accounting, cf-ray and x-cloud-trace-context for support tickets, and full control of retries and timeouts. A protocol hop adds latency and removes information you need.

Two hybrids are worth naming. The healthy one: your own MCP server whose tool handlers call APIVerve over REST, so you get MCP's reach on the outside and full HTTP information on the inside. The unhealthy one: your own MCP server that forwards to the hosted MCP server. It stacks two protocol hops, inherits the text-only failures, and makes your server a proxy with no information advantage. If you are writing a server anyway, call REST from it.

Here is the core of an MCP server of your own, stripped to the routing so the shape is visible. In production use an official MCP SDK for transport, sessions and streaming; the logic you own is the tools/call branch.

own-mcp-server.mjs
// Node 18+, ESM. The routing core of a tools-only MCP server.
// session = { tools: Map<name, { definition, run(args, ctx) }>, ctx } built per authenticated connection.
const VERSIONS = ['2025-06-18', '2025-03-26', '2024-11-05'];
const ok = (id, result) => ({ jsonrpc: '2.0', id, result });
const toolError = (text) => ({ isError: true, content: [{ type: 'text', text }] });

export async function handleRpc(msg, session) {
  switch (msg.method) {
    case 'initialize': {
      const asked = msg.params?.protocolVersion;
      return ok(msg.id, {
        protocolVersion: VERSIONS.includes(asked) ? asked : VERSIONS[0],
        capabilities: { tools: {} },
        serverInfo: { name: 'acme-agent-tools', version: '3.2.0' },
      });
    }
    case 'tools/list':
      return ok(msg.id, { tools: [...session.tools.values()].map((t) => t.definition) });
    case 'tools/call': {
      const tool = session.tools.get(msg.params?.name);
      // Unknown tool as a readable result, so the model can recover instead of the client failing.
      if (!tool) return ok(msg.id, toolError(`No tool named "${msg.params?.name}". Use one of: ${[...session.tools.keys()].join(', ')}.`));
      return ok(msg.id, await tool.run(msg.params.arguments ?? {}, session.ctx));
    }
    default:
      return { jsonrpc: '2.0', id: msg.id, error: { code: -32601, message: `Method not found: ${msg.method}` } };
  }
}

Notice the one deliberate difference from the hosted server: an unknown tool name becomes an isError result the model can read, rather than a protocol error. When you own the server, you choose which failures the model gets to see.

Check yourself

A finance team wants the same three payout-review tools, with your argument checks and an audit log, available in Cursor, VS Code and a desktop chat app. Which pattern fits?

Run every example on your own key. Free plan, no card.

Scaling up?

Volume pricing, custom SLAs, and dedicated support for high-traffic teams.

Contact sales