Courses / Building AI Agents

Session 2 of 11

Model Context Protocol

Overview

Chapter 2 — Model Context Protocol

Course: Building AI Agents

Prerequisites: You can read and run a short Python script and read a JSON document. From Chapter 1 you know what a tool definition is (a name, a description and an input schema), that your application and not the model runs a tool, and how to validate a tool call’s arguments against a schema. Everything else this chapter uses (hosts, clients, servers, JSON-RPC, capabilities, the initialize handshake, the stdio transport, and the two kinds of error) is defined in the notes before it is used.

What the reader can do after this chapter:

  • Explain which part of an MCP connection is the host, the client and the server, and say which of them your code is when it exposes a tool.
  • Write the exact JSON-RPC messages for an initialize handshake, a tools/list request and a tools/call request, and turn a Chapter 1 tool definition into the MCP tool shape.
  • Write a small server loop that answers those messages, reports an unknown tool as a protocol error and a failed call as a tool error, and keeps everything except protocol messages off its output.

Core question this chapter answers: When an AI application needs a tool that someone else built, what has to be agreed in advance so that it can find that tool, call it and read its result without a custom integration, and what stays your responsibility even then?

Connections:

  • Builds on: Chapter 1’s tool definition and the code that validates and runs a call. This chapter puts a standard message format around that definition so that any compatible application can use it.
  • Leads into: Chapter 3, which puts a model in a loop that chooses tools, calls them and reads their results. The server function you write in this chapter’s project is the tool layer that loop calls.

Your answers are scored

This chapter is assessed. The quiz is marked against a key held separately from your materials. Questions 1–8 are multiple choice and are marked by comparing your letter with the key. Questions 9–10 ask for a short answer in an exact format, and each one is marked by a rule printed with the question, so you can check that your answer has the right shape before you submit it.

  • Nothing is judged by reading it for style. An answer either matches the rule or it does not.
  • All three project tasks are scored from an evidence file of key: value lines. Every line’s accepted value, or the rule that decides it, follows from what the task’s code prints, so you can check your own file before you submit it.

Notes

Building AI Agents — Chapter 2: Model Context Protocol

Before you start

Nothing to install for the reading. For the project you need Python 3 and nothing beyond its standard library. No task needs an account with a model provider, an API key or any payment, and no task connects to a real model.

Keep one question in mind while you read: if a different company’s AI application wanted to use the tool I wrote in Chapter 1, what would it need to know in advance, and what would I still have to check myself? By the end of the chapter you should be able to answer it from the message shapes the protocol defines and from the checks your own code runs.

Every protocol fact below is taken from version 2025-06-18 of the Model Context Protocol specification, fetched 2026-10-04. A protocol is revised over time and a newer version may exist, so check the version you build against.

Summary

The Model Context Protocol (MCP) is an open standard for connecting AI applications to external systems. It splits a connection into three roles: a host application that contains the model, a client inside it that holds one connection, and a server that exposes tools. They exchange JSON-RPC messages, which are small JSON documents with a fixed shape. A connection starts with an initialize exchange in which both sides state which optional features they support. A client then asks a server which tools it has with a tools/list request and runs one with a tools/call request. Because those messages are the same for every server, an application written once can use any compatible server, and a tool written once can be used by any compatible application. The protocol standardises how a tool is found and called. It does not make a tool safe: validating inputs, limiting what a tool can reach and keeping a person in the loop for high-impact actions stay the job of the code on each side.

Key concepts

Term Definition
Model Context Protocol (MCP) An open-source standard for connecting AI applications to external systems such as data sources, tools and workflows.
Host The application that contains the model and coordinates the connections: it creates and manages client instances, enforces security policy and consent, and handles user authorization decisions.
Client A component the host creates to hold one connection to one server. Each client keeps one stateful session with exactly one server.
Server A program that exposes tools, resources or prompts through the protocol. It can be a local process or a remote service.
JSON-RPC A small message format built on JSON. A request has jsonrpc, id, method and usually params; a response carries the same id and either a result or an error; a notification has no id and gets no response.
Capability A feature a side says it supports, declared during initialization, such as tools. A side may only use capabilities that were negotiated.
Initialization The first exchange on a connection: the client sends an initialize request with a protocol version, its capabilities and its identity, the server answers with its own, and the client then sends an initialized notification.
Transport The way messages physically travel. The specification defines two standard ones: stdio, and Streamable HTTP.
stdio transport A transport in which the client launches the server as a subprocess and the two exchange messages over the server’s standard input and standard output, one message per line.
tools/list The request a client sends to discover which tools a server offers.
tools/call The request a client sends to run one tool, giving its name and arguments.
Protocol error A standard JSON-RPC error response, for problems such as an unknown tool or an unknown method.
Tool execution error A failure reported inside a successful response, as a result with isError set to true, so that the model can read what went wrong.

Explanations

The failure, before the explanation

A team has built a small internal server that exposes the log-search tool from Chapter 1. They write a client that launches it as a subprocess and talks to it over standard input and output. The first time they run it, the client crashes while reading the server’s very first reply, with a message that the reply is not valid JSON.

The cause is one line in the server, added during development to see what was happening:

print("server starting, loading config...")   # to stdout, not to stderr

The client reads every line the server writes to standard output and parses it as a message. The debug line is not a message. The specification is direct about this for the stdio transport: “The server MUST NOT write anything to its stdout that is not a valid MCP message” (Model Context Protocol, Transports, version 2025-06-18). Debug output belongs on standard error, which the specification says a server “MAY write UTF-8 strings to its standard error (stderr) for logging purposes”. Nothing about the tool was wrong. The failure was a broken agreement about what travels on which channel, which is exactly the kind of agreement the protocol exists to write down.

Why a protocol: the connectors you would otherwise write

Count the work in a concrete case. A company runs three AI applications: a chat assistant, a coding tool and an on-call helper. It wants each of them to reach four systems: tickets, logs, a wiki and a deployment system. With a custom connector for every pair, that is 3 x 4 = 12 integrations, each with its own message format, its own way of finding what a tool accepts, and its own bugs. Add a fifth system and three more connectors are needed.

With one shared protocol, each application implements the client side once and each system implements the server side once: 3 + 4 = 7 pieces, and the fifth system adds one. The arithmetic is not from the specification; it is the reason a shared format helps. The specification’s own description is the one most people remember: “Think of MCP like a USB-C port for AI applications. Just as USB-C provides a standardized way to connect electronic devices, MCP provides a standardized way to connect AI applications to external systems” (Model Context Protocol, What is MCP?, retrieved 2026-10-04).

A second way to picture it: the web. A browser does not ship a special connector for each website; it speaks HTTP, and any site that speaks HTTP can be opened. MCP aims at the same effect for tools: an application that speaks the protocol can discover and call a tool on a server it has never seen, and the server’s author did not have to know which application would arrive.

What the protocol does not remove is the work of building each tool well. A badly described tool or an over-permissioned one is just as bad behind MCP as behind a custom connector, which is the reason the last section of this chapter returns to Chapter 1.

The three roles: host, client and server

Take the on-call helper. It is one program. Inside it, the model decides it needs ticket data and log data. The helper keeps one connection to the ticket server and a separate one to the log server. Those are three of the protocol’s roles in one picture.

The specification names them. The host is the container and coordinator: it “creates and manages multiple client instances”, enforces security policies and consent requirements, and handles user authorization decisions. A client is created by the host and “maintains an isolated server connection”; the host application has multiple clients, “each client having a 1:1 relationship with a particular server”. A server exposes “resources, tools and prompts via MCP primitives” and “can be local processes or remote services” (Model Context Protocol, Architecture, version 2025-06-18).

A second way to hold it: an office building. The host is the front desk that decides who may go where. Each client is a dedicated phone line to one department. A server is the department’s extension. The caller on one line cannot hear the other lines, and the front desk sees all of them.

That separation is deliberate. One of the specification’s design principles is that “servers should not be able to read the whole conversation, nor ‘see into’ other servers”, and that “full conversation history stays with the host”. For you as the author of a tool, this means a server gets the arguments of a call and not the whole chat. For you as the author of a host, it means the consent decisions are yours.

When you wrote the Chapter 1 tool and its validator, you wrote what is, in these terms, the inside of a server. This chapter writes the surface that lets a client reach it.

The messages are JSON-RPC

Here is a complete exchange, as it travels. A client asks for the tool list:

{"jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {}}

and the server answers:

{"jsonrpc": "2.0", "id": 1, "result": {"tools": [ ... ]}}

Three kinds of message are enough to read every example in this chapter. A request has jsonrpc (always the string "2.0"), an id, a method and usually params. A response repeats the request’s id and carries either a result or an error, never both. A notification is a request with no id; the JSON-RPC specification says it “signifies the Client’s lack of interest in the corresponding Response object”, so no response is returned (JSON-RPC Working Group, JSON-RPC 2.0 Specification, section 4.1, retrieved 2026-10-04).

The id is what pairs a response with its request, in the same way the tool call id did in Chapter 1. Because no response needs to be returned for a notification, a server loop must not wait for one, and must not invent an id for it.

The specification says MCP “uses JSON-RPC to encode messages” and that they are UTF-8. On the stdio transport, “Messages are delimited by newlines, and MUST NOT contain embedded newlines”, so one message is exactly one line (Model Context Protocol, Transports).

Starting a connection: initialize and capabilities

Before any tool is listed, the two sides introduce themselves. This is the first message a client sends, from the specification’s own example (shortened):

{"jsonrpc": "2.0", "id": 1, "method": "initialize",
 "params": {"protocolVersion": "2025-06-18",
            "capabilities": {"roots": {"listChanged": true}, "sampling": {}, "elicitation": {}},
            "clientInfo": {"name": "ExampleClient", "version": "1.0.0"}}}

and the server’s answer:

{"jsonrpc": "2.0", "id": 1, "result": {"protocolVersion": "2025-06-18",
 "capabilities": {"tools": {"listChanged": true}},
 "serverInfo": {"name": "ExampleServer", "version": "1.0.0"}}}

The client then sends a notification, notifications/initialized, to say it is ready (Model Context Protocol, Lifecycle, version 2025-06-18).

Think of a two-person call that opens with “which language are we using, and what can you do?” Two things are settled here. Version. The client sends a version it supports. If the server supports it, the server “MUST respond with the same version”; otherwise it “MUST respond with another protocol version it supports”, and a client that cannot work with that version “SHOULD disconnect” (Lifecycle). Capabilities. Each side lists the optional features it supports. A server that offers tools must declare a tools capability, and its listChanged flag says whether it will send notifications when its tool list changes (Model Context Protocol, Tools, version 2025-06-18). During the connection, both sides “only use capabilities that were successfully negotiated” (Lifecycle).

The consequence is practical. A client does not need to guess whether a server supports tools: the specification says tool invocation “requires the server to declare tool capabilities” (Architecture). The same sentence applies to features outside this chapter, such as prompts and resources.

Discovering and calling a tool

Return to the Chapter 1 tool. Its definition used a field called input_schema on one provider’s API. In MCP the same idea is called inputSchema, and a tool is listed like this (the specification’s example, with the weather tool):

{"jsonrpc": "2.0", "id": 1, "result": {"tools": [
  {"name": "get_weather",
   "title": "Weather Information Provider",
   "description": "Get current weather information for a location",
   "inputSchema": {"type": "object",
                   "properties": {"location": {"type": "string", "description": "City name or zip code"}},
                   "required": ["location"]}}]}}

The specification lists the parts of a tool definition: name (a unique identifier), title (optional, for display), description (human-readable), inputSchema (a JSON Schema for the parameters), and optional outputSchema and annotations (Model Context Protocol, Tools). Only the first, third and fourth matter for this chapter. The change from Chapter 1 is a rename and a wrapper: the description and the schema you wrote are carried over unchanged. The wording that Chapter 1 asked for in a description, what the tool does, when to use it and what it will not do, is just as important here, because the client shows the description to its model.

To run the tool the client sends:

{"jsonrpc": "2.0", "id": 2, "method": "tools/call",
 "params": {"name": "get_weather", "arguments": {"location": "New York"}}}

and the server answers with a result whose content is a list of items, each with a type:

{"jsonrpc": "2.0", "id": 2,
 "result": {"content": [{"type": "text", "text": "Current weather in New York: 72°F, partly cloudy"}],
            "isError": false}}

(Model Context Protocol, Tools, version 2025-06-18; the weather text is shortened.) A second way to see the exchange is as two functions: tools/list is “what can I call here?” and tools/call is “call it with these arguments”, the same two steps Chapter 1 had inside one application, now carried over a connection to a server someone else owns.

A tool result can hold more than text. The specification lists text, images, audio, resource links and embedded resources as content types; a server that wants machine-readable output can also declare an outputSchema and return structuredContent. This chapter uses text results only.

When something goes wrong: two kinds of error

Take two failures with the log tool. A client asks for a tool called deploy_nuke, which the server does not have. A different client asks for logs_search with service set to payments, which the tool’s schema rejects. These are not the same kind of failure, and the specification gives them different channels.

Protocol errors are standard JSON-RPC errors “for issues like: Unknown tools, Invalid arguments, Server errors”. The specification’s example is an error response with code -32602 and the message “Unknown tool: invalid_tool_name”. Tool execution errors are “reported in tool results with isError: true” and cover “API failures, Invalid input data, Business logic errors” (Model Context Protocol, Tools, version 2025-06-18). The reasoning matters more than the list: a protocol error says the request itself could not be handled, and a tool execution error says the request was handled and the tool reported a problem the model can read and react to.

The two lists overlap, since both mention invalid input. This chapter’s project settles that overlap by one rule that you can apply and test: an unknown tool is a protocol error, and arguments that fail the tool’s schema are a tool execution error whose text says what to change. That keeps the Chapter 1 habit of writing instructive errors, because the model sees a tool execution error as a result and can correct itself.

The JSON-RPC specification gives standard codes for the other protocol errors: -32601 is “Method not found” and -32602 is “Invalid params”, and -32700 is a parse error for text that is not valid JSON (JSON-RPC Working Group, JSON-RPC 2.0 Specification, section 5.1, retrieved 2026-10-04).

What the protocol does not do for you

Return to the three questions of Chapter 1: what can the tool do, what can its credentials reach, and who approves a high-impact call. MCP puts none of them on the wire for you.

The specification’s own security section says servers MUST “validate all tool inputs”, “implement proper access controls”, “rate limit tool invocations” and “sanitize tool outputs”, and that clients SHOULD “prompt for user confirmation on sensitive operations”, “show tool inputs to the user before calling the server” and “validate tool results before passing to LLM” (Model Context Protocol, Tools, security considerations). For trust and safety it says there SHOULD be “a human in the loop with the ability to deny tool invocations”. It also says that “clients MUST consider tool annotations to be untrusted unless they come from trusted servers”: a server’s description of its own tool is a claim, not a guarantee.

A second way to see the point: USB-C standardises the plug, not what is on the other end of it. A standard port lets you connect a drive or a charger, and it does not tell you whether the thing you plugged in is safe. A standard protocol makes connecting easy, so a larger number of tools are easier to attach, which makes the three questions more important and not less.

Four things to decide about every connection

Before the table, three situations:

  1. A QA engineer writes a server for the team’s test-data tool. A teammate’s client launches it and crashes parsing the first line, because the server printed a banner to standard output.
  2. A backend engineer writes a client that calls tools/call straight away, without the initialize exchange, against a third party’s server. The specification says a client “SHOULD NOT send requests other than pings before the server has responded to the initialize request”, so the call is outside the agreement.
  3. A DevOps engineer connects a community server to the on-call helper. The server’s tool descriptions look helpful, and one of the tools can delete deployments. Nobody has read the code.
What you see What the design allowed What to do instead
The client cannot parse the first reply (situation 1) Non-protocol text on the stdio output channel Send logs to standard error; write only protocol messages to standard output
A call is made outside the agreement (situation 2) The handshake was skipped, so nothing was negotiated Send initialize, wait for the answer, send initialized, then use only negotiated capabilities
A connected tool can do more than the task needs (situation 3) A connection was treated as trust Decide per tool whether to expose it, keep high-impact tools behind a person’s approval, and treat the server’s descriptions as untrusted

Flashcards

Say the answer aloud before revealing it. Speaking it is what exposes the gaps that reading past a written answer hides.

What problem does MCP solve, in one sentence?

It gives AI applications one standard way to connect to external systems, so that an application written once can use any compatible server and a server written once can serve any compatible application, instead of one custom connector for every pair.

What are the three roles in an MCP connection, and what does each do?

The host is the application containing the model; it manages clients and enforces security and consent. A client is created by the host and holds one session with one server. A server exposes tools, resources or prompts.

What are the three kinds of JSON-RPC message?

A request (it has an id and a method), a response (it repeats the id and carries either a result or an error) and a notification (a request with no id, which gets no response).

What happens in the initialize exchange?

The client sends initialize with a protocol version, its capabilities and its identity; the server answers with the version it will use and its own capabilities; the client then sends the notifications/initialized notification. After that, both sides use only the capabilities negotiated.

What does a `listChanged` flag under the `tools` capability mean?

It says whether the server will send a notification when its list of tools changes.

Which two requests do you need to find and run a tool?

tools/list returns the server’s tools, each with a name, description and inputSchema. tools/call runs one, given its name and arguments; the result carries a content list and an isError flag.

How does a tool definition in MCP differ from the one you wrote in Chapter 1?

The parts are the same ideas, a name, a description and a schema for the arguments, and the schema field is called inputSchema. Your description and schema carry over unchanged.

What is the difference between a protocol error and a tool execution error?

A protocol error is a JSON-RPC error response for a request that could not be handled, such as an unknown tool or an unknown method. A tool execution error is a normal result with isError set to true, for a problem the tool itself reports, so that the model can read it.

On the stdio transport, where do debug messages go, and why?

To standard error. The server must not write anything to standard output that is not a valid MCP message, because the client parses every line of standard output as a message.

Does connecting through MCP make a tool safe to use?

No. The protocol standardises how a tool is found and called. Validating inputs, access controls, rate limits, a person’s approval for sensitive operations and distrust of a server’s own tool descriptions remain the job of the code on each side.

Model answer

The question: “We have three AI applications and four internal systems. Why would you put the systems behind MCP servers instead of writing a connector per application, and what actually crosses the wire when an application uses one of our tools?” This is the question an architect asks before approving the approach and a security reviewer asks before a server is connected. A strong answer hits five beats, in this order:

  1. Problem — connectors multiply with every pair, and a shared protocol reduces the pieces to one client per application and one server per system.
  2. Roles — a host holds the model and the policy, one client per server connection, and the server exposes the tools.
  3. Handshake — initialize, then initialized, with the version and capabilities settled before tools are used.
  4. Messages — tools/list to discover, tools/call to run, as JSON-RPC requests and responses, with a protocol error for an unknown tool and an isError result for a tool failure.
  5. Trust — the protocol does not make a tool safe: validate inputs, scope what the tool reaches, keep a person in the loop for sensitive actions.

“With a connector for every pair, three applications and four systems is twelve integrations, and every new system adds three more. If the systems speak a standard protocol, each application implements a client once and each system a server once, so it is seven pieces. In the protocol an application is a host that runs one client per server connection; the server exposes the tools. A connection starts with an initialize request carrying a protocol version and capabilities, the server answers with its own, and the client sends an initialized notification, so both sides know what is available. After that the client sends tools/list to find tools with their names, descriptions and input schemas, and tools/call with a name and arguments to run one; the result comes back as content with an error flag. An unknown tool is a protocol error, and a tool that fails reports it in the result so the model can read it. What the protocol does not give us is safety. We still validate every input on the server, limit what each tool’s credentials can reach, require a person to approve sensitive actions, and treat a server’s own tool descriptions as claims to check.”

Missing a beat is a failure state. Without problem, the answer cannot say why MCP exists. Without roles, it cannot say who enforces consent. Without handshake, it cannot explain how a client knows what a server supports. Without messages, it describes an idea and not a protocol. Without trust, it implies that connecting a server is the same as approving it.

Why this matters

  • Integration work. A team that exposes a system once as a server avoids writing a new connector for each application that wants it, and a team that adopts a client can try servers it did not write.
  • Debugging. Several common failures at the connection are visible in the messages: a banner on the output channel, a missed handshake, an id that does not match. An engineer who can read the JSON-RPC lines can find them.
  • Security review. A reviewer will ask what each exposed tool can reach and who approves it. The protocol’s own security section gives the checklist to answer from.
  • Talking to customers. When a customer asks “can our assistant use our existing systems?”, the honest answer is about which servers exist, what each tool can reach, and who approves what, and not only that a standard exists.

After this chapter

The project for this chapter is Serve the Tool Over MCP (project.md). You will turn the tool definition from Chapter 1 into the MCP shape, write a function that answers the protocol’s messages for one tool, and wrap it in a loop that reads and writes one message per line the way the stdio transport does.

Chapter 3 starts from the function you write in the last task. It puts a model in a loop that chooses a tool, calls it through that function, reads the result and decides what to do next.

Project

Hands-on Project — Serve the Tool Over MCP

Objective

Turn a tool definition into the shape the Model Context Protocol uses, write the function that answers the protocol’s messages for one tool, and wrap it in a loop that reads and writes one message per line as the stdio transport does, so that you can show the exact messages a client and a server exchange and that nothing but protocol messages reaches the output.

What this project needs: Python 3 and nothing beyond its standard library (json). No task needs an account with a model provider, an API key or any payment, and no task connects to a real client or model: the “client’s messages” are fixed data given below. All three tasks are required.

Your evidence file. Keep one plain-text file, chapter2-evidence.txt. Each task tells you which lines to add. Every line has the form key: value, one per line, with the key in lower case exactly as printed. Write what your code printed, not what you expected it to print.

The protocol facts the tasks use come from version 2025-06-18 of the Model Context Protocol specification (the Tools and Transports pages), restated in the tasks so that you do not need to open them.

Task 1 — Convert a tool definition

Take the tool definition you wrote in Chapter 1, saved as tool.json with the keys name, description and input_schema. If you do not have it, use this one:

TOOL = {
    "name": "logs_search",
    "description": "Searches recent log lines for one service. Use this when the user asks what a "
                   "service logged about a topic. Does not modify anything and does not search "
                   "any other data.",
    "input_schema": {
        "type": "object",
        "properties": {
            "service": {"type": "string", "enum": ["api", "worker", "billing"]},
            "query": {"type": "string"},
            "limit": {"type": "integer", "minimum": 1, "maximum": 50},
        },
        "required": ["service", "query"],
        "additionalProperties": False,
    },
}

Write to_mcp(tool), which returns a new dictionary in the MCP tool shape: the same name and description, and the schema under the key inputSchema instead of input_schema. It must not change the schema’s contents and must not add other keys. Run it on your tool and print three things: the sorted list of keys of the result, whether result["inputSchema"] == tool["input_schema"], and whether the result’s name and description equal the original’s.

Add to your evidence file:

t1_keys: <the sorted keys of the result, comma-separated>
t1_schema_unchanged: <true or false>
t1_name_description_unchanged: <true or false>

Output: a function to_mcp and three evidence lines.

Task 2 — Answer the protocol’s messages

Write handle(message), where message is one parsed JSON-RPC message (a dictionary). It returns the response dictionary, or None when no response is due. Every response has "jsonrpc": "2.0" and the id of the request it answers. This server has one tool, logs_search, whose description and input schema are exactly the TOOL dictionary shown in Task 1 (use the converted form from to_mcp). It also needs the validator you wrote in Chapter 1, or this rule set: a missing required field gives missing required field: <name>, an unlisted field gives unexpected field: <name>, a value outside an enum gives <field> must be one of: <values separated by comma and space>, a wrong type gives <field> must be of type <type>, and a value below a minimum or above a maximum gives <field> must be at least <n> or <field> must be at most <n>.

Answer these methods exactly as follows:

  1. A message with no id (a notification, for example notifications/initialized): return None.
  2. initialize: a result of {"protocolVersion": "2025-06-18", "capabilities": {"tools": {"listChanged": false}}, "serverInfo": {"name": "chapter2-server", "version": "1.0.0"}}. The server always answers with this version, whatever version the client asked for, which is the specification’s rule when the server does not support the version requested: it responds with another version it supports.
  3. tools/list: a result of {"tools": [<the converted logs_search tool>]}.
  4. tools/call for a tool name other than logs_search: a protocol error, an error of {"code": -32602, "message": "Unknown tool: <name>"}.
  5. tools/call for logs_search whose arguments fail the schema: a tool execution error, a result of {"content": [{"type": "text", "text": <the error messages joined with "; ">}], "isError": true}.
  6. tools/call for logs_search with valid arguments: a result of {"content": [{"type": "text", "text": "found 3 lines for <query> in <service>"}], "isError": false}. The text is fixed; the tool does not search anything.
  7. Any other method: an error of {"code": -32601, "message": "Method not found"}.

The specification lists a failure in both kinds of error (“invalid arguments” among protocol errors and “invalid input data” among tool execution errors), so rule 5 is this project’s choice, and it keeps the Chapter 1 habit of telling the model what to change.

Run these eight messages, in order, through handle:

MESSAGES = [
    {"jsonrpc": "2.0", "id": 1, "method": "initialize",
     "params": {"protocolVersion": "2025-03-26", "capabilities": {},
                "clientInfo": {"name": "t", "version": "0"}}},
    {"jsonrpc": "2.0", "method": "notifications/initialized"},
    {"jsonrpc": "2.0", "id": 2, "method": "tools/list"},
    {"jsonrpc": "2.0", "id": 3, "method": "tools/call",
     "params": {"name": "logs_search", "arguments": {"service": "api", "query": "timeout"}}},
    {"jsonrpc": "2.0", "id": 4, "method": "tools/call",
     "params": {"name": "deploy_nuke", "arguments": {}}},
    {"jsonrpc": "2.0", "id": 5, "method": "tools/call",
     "params": {"name": "logs_search", "arguments": {"service": "payments", "query": "x"}}},
    {"jsonrpc": "2.0", "id": 6, "method": "resources/list"},
    {"jsonrpc": "2.0", "id": 7, "method": "tools/call",
     "params": {"name": "logs_search",
                "arguments": {"service": "worker", "query": "retry", "limit": 5}}},
]

Add to your evidence file. The text lines are the text of the first item in content:

t2_responses: <how many of the eight calls returned something other than None>
t2_init_version: <protocolVersion in the response to id 1>
t2_tool_names: <the names of the tools in the response to id 2, comma-separated>
t2_id3_text: <text in the response to id 3>
t2_id4_error: <the code, one space, then the message, from the response to id 4>
t2_id5_is_error: <the isError value in the response to id 5, as true or false>
t2_id5_text: <text in the response to id 5>
t2_id6_error_code: <the code in the response to id 6>
t2_id7_text: <text in the response to id 7>

Output: a function handle and nine evidence lines.

Task 3 — Serve lines, keep the output clean

The stdio transport carries one message per line: “Messages are delimited by newlines, and MUST NOT contain embedded newlines”, and the server “MUST NOT write anything to its stdout that is not a valid MCP message” (the specification’s Transports page). Write serve(lines), which takes a list of strings and returns two lists: out (what the server would write to standard output) and log (what it would write to standard error). Do not print inside it.

For every line that is not empty after strip():

  1. Append "line N" to log, where N is the line’s position in the input, counting from 1 and counting empty lines.
  2. Try json.loads. If it fails, append this response, serialised as described below, to out: {"jsonrpc": "2.0", "id": null, "error": {"code": -32700, "message": "Parse error"}}
  3. Otherwise pass the message to handle, and when it returns a dictionary, append it to out.

Serialise every output line with json.dumps(response, separators=(",", ":"), sort_keys=True), so that a line has no spaces after separators, the keys are in alphabetical order, and the line has no newline inside it.

Run serve on these six lines. Lines 1, 2, 3 and 5 are the first four messages of Task 2’s list (the ones with id 1, the notification, id 2 and id 3), serialised with json.dumps and default settings; line 4 is not JSON; line 6 is empty:

LINES = [
    json.dumps(MESSAGES[0]),   # line 1
    json.dumps(MESSAGES[1]),   # line 2
    json.dumps(MESSAGES[2]),   # line 3
    "{not json",               # line 4
    json.dumps(MESSAGES[3]),   # line 5
    "",                        # line 6
]

Add to your evidence file:

t3_out_count: <number of lines in out>
t3_log_count: <number of lines in log>
t3_no_embedded_newline: <true if no line in out contains a newline character, else false>
t3_parse_error_line: <the line in out that answers the not-JSON line, exactly>
t3_list_line_length: <the number of characters in the line in out that answers the tools/list request>

Output: a function serve, a standard-output list that contains only protocol messages, and five evidence lines.

What carries forward

Keep your to_mcp, handle and serve code and chapter2-evidence.txt. The next chapter puts a model in a loop that chooses a tool, calls it through your handle function, reads the result and decides what to do next, so Chapter 3 opens with handle as its tool layer.

TechEazy Consulting training material — not affiliated with or endorsed by OpenAI, Anthropic or any other company named in this course.