What MCP is for
By the end: You can explain why a tool protocol exists and when it is worth using one.
- The problem it solves
- The three things a server exposes
- Client and server, and which you are writing
- When not to bother
New to mcp & tool interfaces? Every tool an agent can use has to be described to it in some format, and for a while each product invented its own — so a connector to a company's wiki, written for one assistant, was useless in the next one. The Model Context Protocol is an agreed format for those descriptions. A server offers its tools, files and prompts in that format, and any client that speaks the protocol can use them without either side being rewritten. Why it matters
You already know how to give a model tools. The awkward part is that each one ends up welded to a single application and a single provider's format, so the same integration gets written again and again with the same auth and error handling each time. MCP is an attempt to standardise that: expose a capability once as a server, and any client speaking the protocol can use it. This section covers what the protocol offers, which side of it you are writing, and when a plain function in your own codebase is still the better answer.
The repetition problem#
Write tools by hand for a while and a pattern appears. The same integration gets rewritten for every application, in every provider's tool format, with the same auth and error handling each time. Nothing is reusable because every tool is welded to the app that hosts it.
The Model Context Protocol is a standard shape for exposing capabilities to any model client. Write a server once, and any client that speaks the protocol can use it.
Without a protocol:
3 applications x 5 integrations = 15 implementations
With one:
5 servers, used by any client that speaks MCPThat arithmetic is only worth anything if the fifteen implementations were actually similar, so it is worth saying what an integration contains. A JSON schema for each argument, in whatever shape the provider expects. Credentials read from somewhere and kept out of the transcript. Pagination, because the underlying API returns more than fits. Truncation, because the model will be handed the result. Errors turned into sentences rather than stack traces. Only the middle bit, the actual call to the underlying system, is specific to the integration. The rest is the same work every time, which is the part a protocol can absorb.
What is actually on the wire#
The protocol is JSON-RPC 2.0 messages passed between a client and a server over a transport, and knowing that much stops it feeling mysterious. A connection opens with an initialize exchange in which both sides state a protocol version, which is a date, and declare their capabilities, so a client can tell whether a server offers tools, resources, prompts or logging before it asks for any of them. After that the client asks what exists and calls what it needs.
client {"jsonrpc": "2.0", "id": 1, "method": "initialize",
"params": {"protocolVersion": "...", "capabilities": {...},
"clientInfo": {"name": "some-ide", "version": "1.4.0"}}}
server {"jsonrpc": "2.0", "id": 1,
"result": {"protocolVersion": "...", "capabilities": {"tools": {}},
"serverInfo": {...}, "instructions": "optional hint text"}}
client {"jsonrpc": "2.0", "method": "notifications/initialized"}
client {"jsonrpc": "2.0", "id": 2, "method": "tools/list"}
server {"result": {"tools": [{"name": "find_orders",
"description": "...",
"inputSchema": {"type": "object", ...}}],
"nextCursor": "optional, when there are more"}}
client {"jsonrpc": "2.0", "id": 3, "method": "tools/call",
"params": {"name": "find_orders", "arguments": {"email": "..."}}}
server {"result": {"content": [{"type": "text", "text": "..."}],
"isError": false}}Two transports matter in practice. Standard input and output, where the client starts your server as a child process on the same machine and talks down a pipe, which is how local servers in an IDE work and needs no ports, no auth and no deployment. And HTTP, for a server that runs somewhere else and is shared, which does need all three. The message shapes above are identical either way, so the choice is an operational one rather than a protocol one.
One thing the shape makes obvious: the description and the schema of every tool are strings a server hands to a client, which the client puts in front of a model. Nothing validates that they are honest. That is the whole of the security story, and it is covered properly in the prompt injection topic; the short version is that installing a server is closer to installing a browser extension than to adding a dependency.
Three kinds of thing#
- Tools: actions the model can take. Search a database, send a message, create a record. These are the ones you will write most.
- Resources: data the client can read. A file, a record, a document. Read-only, addressed by URI.
- Prompts: reusable prompt templates the server offers, which a client can surface to the user as a command.
The distinction that makes them easy to remember is who decides when each is used. A tool is chosen by the model, in the middle of a turn, from the description you wrote. A resource is chosen by the application, which decides what to attach before the model sees anything, so it fits material a user or a client picks rather than material a model goes looking for. A prompt is chosen by the person, usually from a menu of commands. Getting this backwards produces servers that expose a file reader as a tool and then wonder why the model reads the wrong files.
Most servers only implement tools, and that is a perfectly reasonable server.
Which side are you on#
A client is the application holding the conversation, such as an IDE or a chat product. A server exposes capabilities. You will almost always be writing servers, because that is where your organisation's data and actions live.
It is worth being precise about what the client does, because it is more than transport. The client starts or connects to each server, performs the handshake, collects every tool list, hands the combined list to the model as tool definitions, runs the loop when the model asks for a call, routes that call to the right server, and puts the result back into the conversation. The protocol standardises the connection and the descriptions. It does not standardise the loop, the prompt, or how a model decides, and none of those stop being your problem because a server is involved.
What the protocol costs#
Two costs get asserted without numbers in most discussions of this, so here they are measured. The first is the extra hop: a tool that used to be a function call in your process is now a message to another process. The second is the tool list, which is text in the model's context on every request of the conversation.
import json, subprocess, sys, time
# A server that speaks just enough JSON-RPC to answer a tool call.
SERVER = """
import json, sys
for line in sys.stdin:
request = json.loads(line)
reply = {"jsonrpc": "2.0", "id": request["id"],
"result": {"content": [{"type": "text", "text": "ok"}], "isError": False}}
sys.stdout.write(json.dumps(reply) + chr(10))
sys.stdout.flush()
"""
started = time.perf_counter()
proc = subprocess.Popen([sys.executable, "-c", SERVER], stdin=subprocess.PIPE,
stdout=subprocess.PIPE, text=True, bufsize=1)
def call_over_the_pipe(i):
proc.stdin.write(json.dumps({"jsonrpc": "2.0", "id": i, "method": "tools/call",
"params": {"name": "ping", "arguments": {}}}) + "\n")
proc.stdin.flush()
return json.loads(proc.stdout.readline())
call_over_the_pipe(0)
startup = (time.perf_counter() - started) * 1000
def call_in_process(i):
return {"content": [{"type": "text", "text": "ok"}], "isError": False}
for label, fn in (("over the pipe", call_over_the_pipe), ("in process", call_in_process)):
t0 = time.perf_counter()
for i in range(1, 2001):
fn(i)
print("%-14s %8.4f ms per call" % (label, (time.perf_counter() - t0) / 2000 * 1000))
print("%-14s %8.1f ms, once" % ("start the server", startup))
proc.stdin.close()
proc.wait()
# over the pipe 0.0307 ms per call
# in process 0.0002 ms per call
# start the server 30.9 ms, onceTOOL = {"name": "find_orders",
"description": "Find a customer's orders by email address. Use when the user "
"asks about order status, delivery or refunds. Returns at most "
"10 recent orders.",
"inputSchema": {"type": "object",
"properties": {
"email": {"type": "string", "description": "The customer's email address."},
"status": {"type": "string", "description": "Optional: open, shipped, cancelled."}},
"required": ["email"]}}
chars = len(json.dumps(TOOL))
print("one tool definition: %d characters, roughly %d tokens" % (chars, chars / 4))
for n in (5, 20, 50):
per_request = n * chars / 4
print("%2d tools %5.0f tokens per request %7.0f over a 20-step task"
% (n, per_request, per_request * 20))
# one tool definition: 417 characters, roughly 104 tokens
# 5 tools 521 tokens per request 10425 over a 20-step task
# 20 tools 2085 tokens per request 41700 over a 20-step task
# 50 tools 5212 tokens per request 104250 over a 20-step taskSo the honest summary of the cost is that latency is not the problem and context is. A protocol hop you cannot feel, a session startup you pay once, and a tool list that grows with every server you connect and is charged on every request. The failure that follows from that is not slowness, it is a model choosing badly between forty descriptions, which is the subject of the next section.
What it buys over just writing a function#
Against a plain function in your own codebase, a server buys four things and nothing else. A second client can use it without you writing anything. Someone else's client can use it, including ones you have never seen. It runs as a separate process with its own dependencies and its own credentials, which is a real isolation boundary. And the tool list is discovered at connect time rather than compiled in, so a server can gain a tool without the client being rebuilt.
What it costs you is equally concrete. A schema that is now a published interface you cannot change casually, a process that can be absent or out of date when a client expects it, a version negotiation to think about, and a security boundary where previously there was a function call. The question to ask is not whether the protocol is good. It is whether more than one client will ever call this, and whether you would be comfortable with one of them being written by someone else.
When it is not worth it#
One app, three tools, no protocol needed
If you have a single application with a handful of tools and no intention of sharing them, plain function calling is simpler and has less to go wrong. MCP earns its cost when the same capability is wanted by more than one client, or when you want other people's servers to work with your app.
Beyond that, there are cases where a server is the wrong shape rather than merely unnecessary.
- The call is on a hot path with a latency budget. Not because of the hop, which is measured above and is nothing, but because a separate process is a separate thing to keep warm, monitor and restart, and an in-process call has none of that.
- The tool needs the caller's request context: a transaction already open, a user session, an object graph you are halfway through building. Serialising that across a process boundary costs more than the tool does.
- The interface is still changing weekly. A published schema is a promise, and making it while the design moves means breaking clients you cannot see.
- The capability is one line of business logic over data you already hold in memory. Wrapping it earns nothing and adds a process.
The reverse case is worth naming too, because it is the one people miss: consuming servers is often more valuable than writing them. Connecting an existing server for a system you already run gets you an integration you did not have to build, and evaluating whether its tools are well designed is the same skill as designing your own.
Where people go wrong
Adopting the protocol for its own sake on a project with one client. You gain a transport layer and a schema to maintain, and lose the ability to change a tool signature without thinking about it.
If this comes up in an interview
What is MCP and what problem does it solve?
A standard protocol for exposing tools, resources and prompt templates to model clients. It removes the repetition of rewriting the same integration per application and per provider format, so a capability written once works with any client that speaks it.
Then they push further
How is that different from defining tools in your API call?
Tools in an API call are defined by your application for that request. MCP puts them behind a server speaking a standard protocol, so one integration can be used by many clients and agents without each rewriting it.
What are the security concerns with MCP servers?
A server's tool descriptions and results go straight into the model's context, so a malicious or compromised server can inject instructions. Servers often hold credentials too. Install only servers you trust, scope their credentials, and review what they expose.
What gets marked up. Marked up for describing it as a standard interface between agents and tools, and for raising the trust questions unprompted.
What is the difference between a tool and a resource in MCP?
A tool is an action the model can invoke and which usually changes something. A resource is read-only data the client can fetch, addressed by URI. Most servers implement only tools.
When would you not use MCP?
A single application with a few tools that nobody else needs. Plain function calling is simpler and has fewer moving parts. The protocol pays off with multiple clients or when you want to consume servers written by others.
Build this: List the tools in a project you have built and decide honestly which would be genuinely reusable by another application. That answer tells you whether a server is worth writing.
This is one section of MCP & tool interfaces, 8 hours across 2 sections. Read it in full to get the whole topic in order.
Stop 17 of 50 on the AI Engineer route
0 of 50 topics done · 0h behind you, 603h to go
What you have marked done is stored in this browser only. It does not reach a server, and clearing site data will clear it.