MCP: Connecting Models to Your Systems

You already wrote this connector. Last quarter it was for the support bot: authenticate against the orders API, page through the results, reshape the dates it hands back in that annoying format. It worked. Then engineering standardised on a different AI-powered IDE, product wanted the same order data inside their own assistant, and none of that work moved an inch. Each app wanted its tools declared in its own shape, registered through its own config, run inside its own process. You are staring at writing the same integration a third time, slightly differently, for no good reason.

That repetition is the problem the Model Context Protocol removes. Before a standard existed, every AI application invented its own way to plug in tools and data, so a connector built for one could not be reused by another. MCP is an open protocol for that plug. An app that speaks it can talk to any tool or data source that also speaks it, with no bespoke glue in between. As of 2026 it has become the common language here: the major chat apps, IDE assistants, and coding agents support it, and there are ready-made servers for a long list of everyday systems.

If you have used a modern code editor, you have already benefited from the same trick. Editors used to hand-build support for each language: autocomplete for one, go-to-definition for another, all bespoke. Then the Language Server Protocol arrived, each editor learned to speak it once, each language shipped one server, and suddenly every editor got every language for free. MCP is that idea pointed at AI apps and tools. Learn the shape once here and you can both publish your systems to any client and consume anyone else’s server without hand-rolling a thing.

The mess it replaces

Count the connectors. If you have three AI apps that each need to reach three systems, and every app wires up every system its own way, that is nine integrations to build and keep alive. Add a fourth app and you owe three more. The work grows with the two numbers multiplied together, and every one of those connectors is a slightly different reimplementation of “let the model read orders and search docs”.

before a standard: hand-wiredwith MCP: one protocolappssystemschat appIDE agentcron botorders DBdocs APIfilesbespoke: 3 x 3 = 9 connectorsappsserverschat appIDE agentcron botordersdocsfilesone protocol: 3 + 3 connectorsadd an app and you add one connector, not one per system
Left: every app hand-wires every system, so the connector count is apps times systems. Right: each side speaks the protocol once, so it is apps plus systems, and any client works with any server.

The right side is the whole economic argument. Speak the protocol once on each side and the count becomes apps plus systems. Your orders server, written a single time, is now reachable from the chat app, the IDE agent, and the cron bot without any of them knowing a thing about your database. This is exactly the tool-calling round trip you already know, lifted out of one app and put behind a standard interface so it stops being copy-pasted.

Host, client, server

Three words carry the whole architecture, and it is worth getting them straight because people mix up the middle one.

The host is the AI application: the chat app, the IDE assistant, the agent you are building. It owns the model and the conversation. For every server it wants to reach, the host creates one client, and that client holds a single dedicated connection to one server. The server is a program that exposes capabilities. It can run as a local subprocess on the same machine or as a remote service across the network, and the host does not much care which.

The part that trips people: one client, one server, always. Connect to five servers and the host is running five clients side by side. Each connection is independent, negotiated on its own, and the host is the thing that stitches all five servers’ capabilities together into the single menu it shows the model.

one host, several servers, capabilities composedMCP HOSTyour AI applicationMCP client 1MCP client 2MCP client 3orders serverlocal, over stdioexposes: tools, resourcesdocs serverremote, over HTTPexposes: resources, promptsgithub serverremote, over HTTPexposes: toolsdedicatedconnectionthe host merges every server’s tools, resources, and prompts into the model’s single menu
The host runs one client per server, local or remote, and merges every server's capabilities into one menu for the model.

Three things a server exposes

A server offers capabilities in three flavours. They look similar on the wire but differ in one important way: who decides when each one gets used.

  • Tools are actions the model can run. getOrder, searchTickets, createIssue. These are model-controlled: the model reads their descriptions and decides, mid-conversation, when to call one. This is the tool-calling round trip, unchanged, just discovered over the protocol instead of hard-coded into the app.
  • Resources are data the model can read. A file’s contents, a database record, an API response, each identified by a URI like orders://schema or file:///readme.md. Resources are application-controlled: the host or the user chooses what to pull into context, the way you decide which files to attach to a message. The model does not reach out and grab them on a whim.
  • Prompts are reusable, templated instructions. A “summarise this incident” template, a “review this diff” workflow with the right system message and arguments baked in. Prompts are user-controlled: they usually surface as something a person picks on purpose, like a slash command, not something the model triggers.
three kinds of capability a server can exposeTOOLSactions the model can runmodel decidestools/listtools/callRESOURCESdata the model can readapp decidesresources/listresources/readPROMPTSreusable templated callsuser picksprompts/listprompts/geteach primitive has a list method to discover it, then a way to call, read, or get it
Same server, three primitives. What changes across them is who sits in the driver's seat: the model, the app, or the user.

You will spend most of your time on tools, because tools are where the model acts. Resources and prompts matter, but if you only ever exposed tools you would still have a useful server. Start there.

It is JSON-RPC underneath

Strip away the SDKs and MCP is JSON-RPC 2.0: plain request and response objects with a method, some params, and an id to match replies to requests. Nothing exotic. A session opens with a handshake where the two sides announce what they support, then the client discovers and calls capabilities.

The handshake is one initialize call. The client says which protocol version and features it speaks, the server answers with its own, and they agree on a common footing:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2025-11-25",
    "capabilities": { "elicitation": {} },
    "clientInfo": { "name": "example-client", "version": "1.0.0" }
  }
}

That protocolVersion is a date, and client and server negotiate one they both understand. The spec revises on a dated cadence, so treat the exact string as a moving value, not a constant to hard-code. After initialize succeeds, discovery and use are just more method calls. List the tools, then call one:

{ "jsonrpc": "2.0", "id": 2, "method": "tools/list" }
{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tools/call",
  "params": {
    "name": "getOrder",
    "arguments": { "orderId": "A-1024" }
  }
}

The response to tools/call comes back as a content array, usually a block of text the model can read on its next turn. If you squint, the request is a tool call and the response is a tool result, which is precisely the round trip from tool calling. MCP has not invented a new interaction. It has standardised where that interaction happens and how the two sides find each other.

a tool call over MCP is the round trip you already knowMCP CLIENTtools/callgetOrder A-1024the model askedMCP SERVERvalidate + routeto the handlerYOUR CODEruns the queryhits DB / APIstdioor HTTPin processresult content, then the model reads it and answerssame beats as tool calling, now across a standard boundary any client can speak
A tool call over MCP is the same four beats as ordinary tool calling. The protocol just standardises the boundary the call crosses on its way to your code.

Wrapping your own system as a server

Here is the payoff for a JavaScript developer. Wrap your API or database as an MCP server once, and every compliant client can use it, no per-app integration required. The SDKs make the server itself small: you declare a tool with a name, a schema, and a handler, exactly the three fields from tool calling, and the SDK handles the JSON-RPC plumbing.

The shape below is illustrative, and the exact method names shift between SDK versions, so lean on your SDK’s current docs rather than memorising this:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";

const server = new McpServer({ name: "orders", version: "1.0.0" });

server.tool(
  "getOrder",
  { orderId: z.string().describe("Order id, for example A-1024") },
  async ({ orderId }, { session }) => {
    // YOUR code, the only place anything runs. Validated and scoped.
    const order = await db.query(
      "SELECT id, status, total_cents FROM orders WHERE id = $1 AND tenant_id = $2",
      [orderId, session.tenantId]
    );
    return { content: [{ type: "text", text: JSON.stringify(order) }] };
  }
);

Notice what did not change from an ordinary tool: the argument is parsed against a schema, the query is parameterised, and the row is scoped to the caller’s tenant. Putting a tool behind MCP does not relax any of that. It is the same function you would write for tool calling, now reachable by clients you did not build.

Consuming a server is the other half, and often the bigger time-saver. Instead of hand-rolling a GitHub or Postgres or Slack integration, you point your host at an existing server and inherit its tools. Configuration is usually a small manifest: local servers are launched as a subprocess, remote ones are given a URL.

{
  "mcpServers": {
    "orders": { "command": "node", "args": ["orders-server.js"] },
    "github": { "url": "https://mcp.example.com/mcp" }
  }
}

That is the transport question in one snippet. A local server runs as a child process and the host talks to it over stdio, standard in and standard out, with no network and no ports. A remote server runs somewhere else and the host reaches it over HTTP, using the Streamable HTTP transport that replaced the older server-sent-events approach during 2025. You rarely touch the bytes either way; the SDK frames the JSON-RPC for you. What matters is the split: stdio for something on your machine, HTTP for something across the network, and authentication only enters the picture for the second one.

See a session run, with no model

Below is a mock MCP session you can drive. There is no model and no network anywhere on the page: a fake client talks to a hardcoded orders server, and every button sends one real-shaped JSON-RPC message and shows the reply. Start with initialize, then list and call. Try calling a method before you initialize and watch the server refuse, because the handshake is not optional.

interactiveA mock MCP session: initialize, discover, call, read

Poke at the order in which you click. The lifecycle is real: a compliant server rejects tools/call until it has been initialized, capabilities are announced in the handshake, and every message carries the id that ties a response to its request. That is the entire protocol in miniature, and it is not much more than this at full size.

A server is an execution surface

Now the part that has to sit at the front of your mind, not the end. An MCP server is not a passive data feed. It is code that runs, its tools execute with whatever permissions you grant them, and the arguments flowing into those tools are produced by a model steered by everything in its context. Bolt a server onto a host and you have added an execution surface. Treat it like one.

Two things make this sharper than ordinary tool calling. First, the inputs are model-driven, so they are untrusted the same way a request body from the open internet is untrusted, and the same prompt injection risk applies: a user message, a pasted document, or a web page read through another tool can all nudge the model into calling your tool with arguments you never intended. Second, and this is the one people miss, connecting to a third-party server means trusting two separate things: its code, which runs with the access you hand it, and its tool descriptions, which flow straight into your model’s context. A hostile or sloppy description can carry hidden instructions, so a tool that claims to “look up the weather” can quietly try to talk your model into exfiltrating data. That attack has a name, tool poisoning, and it lands through the description, the parameter schema, or even the tool’s return value.

every server you connect is code and prompts you are trustingMCP HOSTthe model andits context windowTHIRD-PARTY SERVERUNTRUSTEDits code runs on its box, with thepermissions and token you grant itits tool descriptions are written bysomeone else and enter your promptdescriptions + resultsan injection surfaceauthenticatescope permissionsvalidate every argvet the servermodel-driven input is untrusted, and a third-party server is code plus prompt you did not write
A third-party server is two surfaces at once: code that runs with the access you grant, and tool text that flows into your model. Both are untrusted until you vet them.

The defenses are not new inventions. They are the disciplines you already apply to any endpoint, pointed at the server boundary.

Authenticate the connection. For remote servers, do not roll your own token scheme. The protocol leans on OAuth for remote authorization (2.1 with PKCE as of 2026), which is the OAuth you already know applied to the client-server handshake. Two failure modes get their own names in the security guidance: a confused deputy, where a proxying server is tricked into using its own trust to grant access it should not, and token passthrough, where a server blindly forwards the caller’s token to some upstream API that never authenticated the caller. The short rule: validate that a token was actually issued for your server, and never relay a client’s token to a third party.

Scope permissions tightly. A server’s tools run with the access you give the process, so give it the least that works. The database role behind an orders server should be able to read orders and nothing else. This is ordinary authorization and multi-tenancy: the permission check lives in your code and your data layer, never in a tool description a model can be argued out of.

Validate every argument. The arguments in a tools/call are untrusted input, full stop. Parse them against a schema, reject anything off-shape, and never interpolate a model-generated string into a query, a shell command, or a path. This is the same validating input you do at every HTTP boundary, and it is the wall that a clever prompt cannot climb.

Vet servers before you trust them. Adding a third-party server is a dependency decision, and it deserves the scrutiny of one. Read what its tools claim to do, prefer servers you can inspect or that come from a source you already trust, and pin versions so a description cannot silently change under you. This is supply-chain security with a new twist: the dependency can attack you through prose aimed at your model, not just through code.

Where MCP fits, and where it does not

MCP is worth reaching for when the same capability needs to serve several AI apps, or when you want to consume an integration someone else already built and maintains. That is a real and common shape, and the standard genuinely saves you the N-times reimplementation. It is less obviously worth the ceremony when you have exactly one app calling exactly one set of tools that only you will ever use. In that case plain tool calling in-process is simpler, and you can always lift it behind a server later, because the tool you wrote is the same either way.

The protocol itself is young and moving. The primitives, the client-server split, and the security posture are stable enough to build on and to teach. The exact method names, transport details, and auth requirements shift release to release, so anchor to the concepts here and confirm the wire specifics against the current spec for the version your client and server negotiate.

Summary

  • Before a standard, every AI app wired up tools its own way, so a connector built for one could not move to another. MCP is an open protocol that fixes the N-times-M integration sprawl by making it N plus M, the way the Language Server Protocol did for editors.
  • The architecture is host, client, server. The host is the AI app, it runs one client per server, and each client holds a dedicated connection. The host composes many servers into one menu for the model.
  • A server exposes three primitives: tools (actions, model-controlled, the tool-calling round trip), resources (data to read, application-controlled, URI-addressed), and prompts (reusable templates, user-controlled).
  • Underneath it is JSON-RPC 2.0: an initialize handshake to negotiate capabilities, then tools/list and tools/call, resources/list and resources/read, prompts/list and prompts/get.
  • Transport is stdio for a local subprocess and Streamable HTTP for a remote service. You rarely touch the bytes; authentication only matters for the remote case.
  • The payoff for a JS developer: wrap your API or database as a server once and every client can use it, and consume existing servers instead of hand-rolling integrations.
  • A server is an execution surface. Its tools run with the permissions you grant, its inputs are model-driven and therefore untrusted, and a third-party server is code plus tool descriptions you must trust, where descriptions can carry prompt injection.
  • Defend it like any endpoint: authenticate with OAuth, scope permissions with authorization and multi-tenancy, validate every argument, and vet servers as a supply-chain decision. Tool annotations are hints, not guarantees.