ESC

Type to search the knowledge base.

Model Context Protocol (MCP)

MCP is a standard for exposing tools and resources to any compatible client. Why a protocol beats one-off function lists glued to one vendor SDK.

intermediate6 min read
  • mcp
  • tools
  • protocol

You already have a REST API. The Model Context Protocol is how an agent host discovers and calls it without a custom plugin per chat app. One server, many clients: an IDE, a desktop chat, your own Next.js agent. That is the pitch. The fine print is the same as any RPC: transports, schemas, and auth that did not vanish because the payload is JSON-RPC.

MCP is not a model. It is not a replacement for function calling. Your process still runs the tool. The protocol is the USB-shaped cable between a host and the thing that owns the data.

Key takeaways

  • MCP standardizes how hosts discover tools, resources, and prompts on a server.
  • Host, client, and server are three roles. The LLM app is the host.
  • Tools are model-called. Resources are app-loaded. Prompts are user-picked.
  • stdio is for local processes. Remote HTTP still needs real authentication.
  • A protocol does not replace your ACL. Least privilege still lives in the handler.

What the Model Context Protocol actually standardizes

Without a protocol you glue a Zod tool list to OpenAI’s SDK, then rewrite it for Anthropic, then again for whoever the IDE vendor wrapped. MCP says: the server speaks JSON-RPC, exposes a list of tools with JSON Schema, and handles tools/call. Compatible hosts map that list onto whatever function-calling API they use.

Build the server once. Point Claude Desktop, Cursor, or your own host at it. You still write tool schemas worth calling.

type JsonRpcRequest = {
  jsonrpc: '2.0';
  id: string | number;
  method: string;
  params?: unknown;
};

type McpTool = {
  name: string;
  description: string;
  inputSchema: {
    type: 'object';
    properties: Record<string, unknown>;
    required?: string[];
    additionalProperties?: boolean;
  };
};

const tools: McpTool[] = [
  {
    name: 'docs_search',
    description: 'Search public docs. Not a shell. Not account data.',
    inputSchema: {
      type: 'object',
      additionalProperties: false,
      properties: {
        query: { type: 'string', minLength: 3, maxLength: 200 },
      },
      required: ['query'],
    },
  },
];

The spec’s method names are the useful part: initialize, tools/list, tools/call, plus resource and prompt variants. Field names around metadata have moved across spec dates. Treat the snippet as a shape, not a pin.

Host, client, server

Three nouns people collapse:

Role What it is Example
Host The LLM application the human is using Desktop chat, IDE agent, your web app
Client A connector inside the host, one per server The MCP client session talking to docs-server
Server The process that owns tools and data A Node process wrapping your docs index

The host can attach many servers (docs, GitHub, Postgres). Each server should be a tight domain. A “god server” that exposes sql and shell is the run(command) anti-pattern with extra ceremony.

Your Next.js app might be the host: the browser talks to your origin (client vs server LLM calls), the route handler is the host logic, and it opens MCP clients to internal servers. Do not open the MCP port to the browser and hope CORS saves you. Keys and tools stay on the server.

Tools, resources, prompts

MCP’s split is about who decides:

  1. Tools. Model-controlled. The model picks docs_search and fills args. Same trust model as function calling: validate, authorize, execute.
  2. Resources. Application-controlled. The host can attach docs://refund-policy as context without waiting for the model to search. Think of a file the IDE already knows is open.
  3. Prompts. User-controlled. Named templates (“/triage-login”) the human invokes. Not a secret backdoor for tools.
type McpResource = {
  uri: string;
  name: string;
  mimeType: 'text/markdown' | 'application/json';
};

async function handleToolsCall(
  user: { id: string; scopes: string[] },
  params: { name: string; arguments?: Record<string, unknown> },
): Promise<{ content: Array<{ type: 'text'; text: string }>; isError?: boolean }> {
  if (params.name !== 'docs_search') {
    return { content: [{ type: 'text', text: 'unknown_tool' }], isError: true };
  }
  if (!user.scopes.includes('docs:read')) {
    return { content: [{ type: 'text', text: 'forbidden' }], isError: true };
  }
  const query = String(params.arguments?.query ?? '');
  if (query.length < 3) {
    return { content: [{ type: 'text', text: 'query_too_short' }], isError: true };
  }
  const hits = await db.docs.searchPublic(query, { limit: 5 });
  return { content: [{ type: 'text', text: JSON.stringify(hits) }] };
}

Servers may advertise annotations (readOnlyHint, destructiveHint). Hosts must treat annotations as untrusted unless the server is trusted. A malicious server can label delete_repo as read-only. Your host UI should still confirm writes.

stdio vs remote HTTP

Two transport families you will actually meet:

  • stdio. The host spawns a local process and speaks JSON-RPC on stdin/stdout. Fine for a developer laptop talking to a local repo. The trust boundary is “this binary on my machine.”
  • Remote HTTP (streamable HTTP in current spec language; older docs mention SSE). A network server. Now you have every ordinary API problem: TLS, authn, authz, rate limits, tenant isolation.

Local stdio does not make a tool safe. A local server with shell is still a shell. Remote without OAuth (or whatever your org uses) is an open function-calling endpoint. The spec has authorization hooks for HTTP. Use them. Then still check user.scopes in handleToolsCall.

If you already expose REST, an MCP server can be a thin adapter: tools/list documents what REST already did, tools/call maps onto the same domain functions. Do not duplicate business logic in the adapter.

Why a protocol instead of one vendor SDK

Vendor tool lists die when you change hosts. MCP is the integration seam: one schema, many callers. That is worth it when the same GitHub or docs server should show up in an IDE and in your support agent.

It is not worth it when you have one app, three tools, and no second host. A typed tool object in your route handler is less moving surface. Adopt MCP when the server is the product you want to reuse.

Interview answer (out loud)

“The Model Context Protocol is a JSON-RPC standard so a host can discover and call tools, resources, and prompts on a server without a custom plugin per vendor. The host is the LLM app, the client lives inside it, the server owns the data. Tools are model-called, resources are app-loaded, prompts are user-picked. stdio is local. Remote HTTP still needs auth. I would wrap existing domain functions, not invent a god shell, and I would still authorize every tools/call in my process.”

Further reading (primary sources)

Related guides