An agent is only as useful as the things it can reach. To read a calendar, search a database, or call a billing API, it needs tools — and every tool is, at bottom, a function the model is allowed to invoke. The hard part was never calling one tool. It was wiring up the dozen an agent needs, keeping those wires working as the tools change, and doing it for every model and every host that wants to use them.
For years each model vendor invented its own format for describing a tool, so the same get_weather tool had to be re-described three different ways. This lesson is about the combinatorial mess that produced — and the shared protocol, the Model Context Protocol (MCP), that replaced it. MCP is how an agent in 2026 gets a clean, versioned set of tools, and it is just a protocol: a fixed set of rules for a conversation, independent of any one model or language.
flowchart TD M["Mesh: every client to every tool<br/>N x M adapters"] --> X["3 clients, 4 tools = 12 wires"] H["Hub: each side joins the protocol<br/>N + M adapters"] --> Y["3 clients, 4 tools = 7 wires"] style X fill:#b91c1c,color:#fff style Y fill:#166534,color:#fff
The N-times-M problem
Call the number of clients (models, IDEs, dashboards) N and the number of tools M. If every client speaks to every tool directly, each in its own private format, you build N x M adapters. Three clients and four tools is twelve hand-written integrations. Add a fifth tool and every client needs a fresh adapter; add a sixth client and it must re-implement all five tools. The work does not add, it multiplies.
MCP collapses that into a hub. Every tool becomes an MCP server that speaks one protocol, and every consumer becomes an MCP client that speaks the same one. The connection count drops to N + M: each of the N clients connects once, and each of the M tools exposes itself once. Three clients and four tools becomes seven connections, not twelve — and the next tool added is one server touching zero clients.
function mesh(clients, tools) {
return clients * tools; // every client wired to every tool
}
function hub(clients, tools) {
return clients + tools; // each side joins the protocol once
}
console.log(mesh(3, 4)); // 12 bespoke integrations
console.log(hub(3, 4)); // 7 connections through the hub
Client, server, and the tool description
Two words do most of the work. An MCP server is the side that has something — a database, an API, a file system — and offers it up. An MCP client is the side that wants something and asks for it: your IDE, an agent framework, a dashboard. The server advertises its tools; the client calls them.
Crucially, a tool is nothing more than a description: a name, a short sentence of human-readable help, and a JSON Schema declaring the arguments it accepts. That description is the entire contract. The client never needs to know how the tool is built, what language the server is written in, or where it runs — only the shape of the request it sends and the shape of the reply it gets back.
flowchart LR T["one tool:<br/>get_weather"] --> O["OpenAI format"] T --> A["Anthropic format"] T --> G["Gemini format"] style T fill:#8b5cf6,color:#fff
function describeTool(tool) {
const args = Object.keys(tool.inputSchema.properties || {});
return tool.name + '(' + args.join(', ') + ')';
}
const getWeather = {
name: 'get_weather',
description: 'Current weather for a city',
inputSchema: { properties: { city: {}, units: {} } }
};
console.log(describeTool(getWeather));
Why a shared protocol wins
The real saving is not the seven-versus-twelve connections. It is that a tool written once, as an MCP server, becomes usable by every client that speaks the protocol — including clients that did not exist when you wrote it. A database vendor publishes a single server, and every compliant IDE, agent, and dashboard can query it. A new model ships, and every existing tool works with it on day one, with no per-vendor adapter to write.
That is the durable idea, and it is why MCP is a protocol rather than a library. The rules of the conversation stay fixed; the things having the conversation can be written in any language and live anywhere. The next lesson opens that conversation up and shows the exact wire format every message uses.
flowchart LR S["one MCP server<br/>exposes get_weather once"] --> P["shared protocol"] P --> C1["client 1"] P --> C2["client 2"] P --> C3["client 3"] style P fill:#166534,color:#fff
What does this print? A mesh multiplies clients by tools; a hub adds them. There are two console.log lines.
function mesh(clients, tools) {
return clients * tools;
}
function hub(clients, tools) {
return clients + tools;
}
console.log(mesh(3, 4));
console.log(hub(3, 4));
mesh is multiplication: 3 times 4.
hub is addition: 3 plus 4. Print them on separate lines.
Write isToolDefinition(obj) that returns true only when obj is a valid tool description: it has a name that is a string, a description that is a string, and an inputSchema that is a (non-null) object. Anything else returns false.
function isToolDefinition(obj) {
// true only when name, description, and inputSchema have the right types
}
Guard null first: typeof null is 'object', so check obj != null before reading its fields.
Use typeof === 'string' for name and description, and typeof === 'object' (plus a null check) for inputSchema.
function isToolDefinition(obj) {
return obj != null
&& typeof obj.name === 'string'
&& typeof obj.description === 'string'
&& obj.inputSchema != null
&& typeof obj.inputSchema === 'object';
}
waste(clients, tools) should report how many MORE adapters a bespoke mesh needs than a hubbed design: the mesh count minus the hub count. But it treats the mesh as if it were a hub, so it always returns 0. Three clients and four tools should waste 5 (12 minus 7). Fix the one operator that computes the mesh.
function waste(clients, tools) {
const mesh = clients + tools; // BUG: a mesh multiplies, it does not add
const hubbed = clients + tools;
return mesh - hubbed;
}
The mesh term must be clients times tools, not clients plus tools.
Change the + in the mesh line to a *.
function waste(clients, tools) {
const mesh = clients * tools;
const hubbed = clients + tools;
return mesh - hubbed;
}
A new model vendor launches. Under bespoke, per-vendor tool glue, how many new adapters must you write to expose your 6 existing tools to that one new vendor's format?
Bespoke glue is per (client, tool) pair, so a new client times your 6 tools means 6 fresh adapters. A shared protocol turns that into zero new adapters — the vendor just speaks MCP.
Write maintainWithoutMCP(tools, vendorFormats) that returns how many separate tool descriptions a team must keep up to date when every one of tools has to be re-described in every one of vendorFormats. (With MCP the answer would just be tools.length; this is the cost the protocol removes.) Both arguments are arrays.
function maintainWithoutMCP(tools, vendorFormats) {
// every tool, re-described for every vendor format
}
Each tool is described once per vendor format, so the counts multiply.
Return tools.length times vendorFormats.length.
function maintainWithoutMCP(tools, vendorFormats) {
return tools.length * vendorFormats.length;
}
Recap
- Wiring every client to every tool directly costs N x M adapters; a shared protocol costs N + M connections.
- An MCP server offers tools; an MCP client consumes them. Both speak the same fixed protocol.
- A tool is a description: a
name, adescription, and a JSON-SchemainputSchemafor its arguments. That description is the whole contract. - One server reaches every compliant client, including ones written later — the real saving is maintenance, not the first connection.
- MCP is a protocol, not a library: the rules of the conversation are fixed and language-agnostic.
Next you will open up the wire format itself — JSON-RPC 2.0 — which is exactly the shape of every MCP message.