Skip to content

Version awareness

  • Lesson 3
  • 45 min
  • Module 1: Understand the integration problem

What you will be able to do

MCP changes fast, and old examples are everywhere. Write down the protocol revision, the SDK version and the hosts that your project depends on. Recognize older code and messages, and make a test fail when the installed SDK is not the one you chose.

Before you startWhat to know and to have readyAbout 45 minutes

What you need to know

Your project

  • ~/projects/support-mcp from the last lesson, with the virtual environment active: python -m pytest -q prints 10 passed.
  • The labs repository in ~/projects/nextia-learning-labs. No account and no key.

Think of a recipe card from an old family cookbook. It says “a cup of flour” and “a moderate oven, gas mark 4”. You follow it in a new kitchen with an electric oven and metric cups. The cake comes out flat.

Nothing was wrong with the recipe. It was written for another kitchen, in another time, and the card did not say so. A careful cook writes the details on the card: which oven, which cup, which year’s flour. Then the next cook knows what to change.

Code examples work the same way. An example that worked for its author can fail for you, because it was written for another version. If the example does not say which version, you find out by failing.

Tomás wants a server quickly. He finds a tutorial and copies its code:

tutorial_server.py (copied from an older tutorial)
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("hello-server")
@mcp.tool()
def hello(name: str) -> str:
"""Say hello to someone."""
return f"Hello, {name}!"
if __name__ == "__main__":
mcp.run()

He runs it in the project’s virtual environment, with SDK 2.3.0 installed:

Traceback (most recent call last):
File "…/tutorial_server.py", line 1, in <module>
from mcp.server.fastmcp import FastMCP
File "…/.venv/lib/python3.12/site-packages/mcp/server/fastmcp.py", line 16, in <module>
raise ModuleNotFoundError(_MESSAGE, name=__name__)
ModuleNotFoundError: No module named 'mcp.server.fastmcp'. This is mcp 2.x, where FastMCP was renamed to MCPServer (from mcp.server.mcpserver import MCPServer) and other APIs changed; see the migration guide at https://py.sdk.modelcontextprotocol.io/v2/migration/#fastmcp-renamed-to-mcpserver or pin 'mcp<2' to keep running v1 code.

This failure is the lucky kind. It is loud, and the message says what changed. Other differences are quieter. A client and a server can disagree about the messages themselves. Then a request fails with an error that does not mention versions at all, as you see below. To understand both kinds, you need to know which version numbers exist.

An MCP connection depends on three versioned things. Each one has its own number.

  1. The specification revision. The specification is the document that defines MCP’s messages and rules. Each revision is named by its release date. This course uses 2026-07-28, the newest stable revision. The earlier ones are 2024-11-05, 2025-03-26, 2025-06-18 and 2025-11-25. A date makes the order clear: a later date is a newer protocol.
  2. The SDK version. The SDK is the library that writes and reads the messages for you. This course uses the official MCP Python SDK 2.3.0, the pip package mcp. Its first number, 2, is the major version. A new major version may rename things, as Tomás just saw: FastMCP became MCPServer. The 1.x line still gets critical bug fixes and security fixes, but no new features.
  3. The host and its client. Each host brings its own client, and each client speaks some revisions and not others. In this course you use three clients: the SDK’s Client, MCP Inspector 2.10.1 in Module 3, and the course’s own host in Module 4.

So the course says it in one sentence: this course uses MCP specification revision 2026-07-28 with the official MCP Python SDK 2.3.0. Write such a sentence for every MCP project that you build.

A row of five specification revisions: 2024-11-05, 2025-03-26, 2025-06-18, 2025-11-25 and 2026-07-28. Above the first four, a grey dashed band: older revisions, the initialize handshake first, legacy. Above 2026-07-28, a blue band: current, no handshake, server/discover and _meta. Below, a grey dashed band: SDK 1.x, FastMCP and McpError, maintenance only, legacy code in old tutorials. A blue band across all five: SDK 2.3.0, this course, MCPServer and MCPError, speaks 2026-07-28 and still accepts the handshake of the four older revisions from older clients. A grey dashed box: HTTP+SSE, the remote transport of 2024-11-05, legacy, not taught. A key: grey and dashed means recognize it in old examples, do not copy it; blue is what this course builds on.
The five specification revisions and the two SDK lines. Grey is legacy: recognize it, but do not copy it.
Text description of the diagram

Five specification revisions, oldest first: 2024-11-05, 2025-03-26, 2025-06-18, 2025-11-25 and 2026-07-28.

  • The first four (legacy): a connection starts with the initialize handshake. Old tutorials use SDK 1.x code, with FastMCP and McpError; SDK 1.x gets only critical bug fixes and security fixes. The revision 2024-11-05 also had a remote transport called HTTP+SSE, which this course does not teach.
  • 2026-07-28 (current): no handshake. Discovery is a request, server/discover, and every request carries _meta.
  • SDK 2.3.0 (this course): MCPServer and MCPError. It speaks 2026-07-28, and it still accepts the handshake of the four older revisions from older clients.

The newest revision changed how a connection starts. The course team sent both forms by hand to your hello server, one JSON line at a time, and recorded the answers. In Module 3, you write such messages yourself.

An older client (revision 2025-06-18) starts with a handshake: a first exchange that agrees on the revision before any other request.

-> {"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": {"name": "old-tutorial-client", "version": "1.0"}}}
<- {"jsonrpc":"2.0","id":1,"result":{"capabilities":{…},"protocolVersion":"2025-06-18","serverInfo":{"name":"hello-server","version":"1.0.0"}}}
-> {"jsonrpc": "2.0", "method": "notifications/initialized"}
-> {"jsonrpc": "2.0", "id": 2, "method": "tools/list"}
<- {"jsonrpc":"2.0","id":2,"result":{"tools":[{"description":"Say hello to someone.", … "name":"hello", …}]}}

The client proposes 2025-06-18, and the server agrees. The client confirms with notifications/initialized. A notification is a message that gets no answer: it has no id. After the handshake, tools/list carries no version at all. The agreement holds for the whole connection. That shared state is called a session.

A current client (revision 2026-07-28) has no handshake and no session. Every request carries the version itself, in a field called _meta:

-> {"jsonrpc": "2.0", "id": 1, "method": "server/discover", "params": {"_meta": {"io.modelcontextprotocol/protocolVersion": "2026-07-28", "io.modelcontextprotocol/clientInfo": {"name": "by-hand", "version": "1.0"}, "io.modelcontextprotocol/clientCapabilities": {}}}}
<- {"jsonrpc":"2.0","id":1,"result":{"cacheScope":"private","capabilities":{…},"resultType":"complete","supportedVersions":["2026-07-28"],"ttlMs":0,"_meta":{"io.modelcontextprotocol/serverInfo":{"name":"hello-server","version":"1.0.0"}}}}
-> {"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {"_meta": {…the same _meta…}}}
<- {"jsonrpc":"2.0","id":2,"result":{"cacheScope":"private","resultType":"complete","tools":[{"description":"Say hello to someone.", … "name":"hello", …}],"ttlMs":0,"_meta":{…}}}

Discovery is now an ordinary request, server/discover. Its answer lists the revisions that the server supports. Each later request repeats the _meta, so each one stands alone.

A request in neither form gets an error. This is tools/list with no handshake before it, and no _meta:

-> {"jsonrpc": "2.0", "id": 1, "method": "tools/list"}
<- {"jsonrpc":"2.0","id":1,"error":{"code":-32602,"message":"Invalid request parameters","data":""}}

The lines are shortened with … where the content repeats. Here are the three cases side by side:

Two columns of messages. Left, an older client, revision 2025-06-18, legacy: client to server initialize id 1 with protocolVersion 2025-06-18; server to client result id 1 with protocolVersion 2025-06-18 and serverInfo hello-server 1.0.0; client to server notifications/initialized, a notification with no answer; client to server tools/list id 2 with no _meta; server to client result id 2, 1 tool, hello. Right, a current client, revision 2026-07-28: client to server server/discover id 1 with _meta protocolVersion 2026-07-28, clientInfo and clientCapabilities; server to client result id 1 with supportedVersions 2026-07-28 and hello-server 1.0.0; client to server tools/list id 2 with the same _meta; server to client result id 2, 1 tool, hello. Below, in red: neither form, tools/list id 1 with no handshake and no _meta, gets error -32602 Invalid request parameters.
The same hello server and SDK 2.3.0 answer both eras. A request in neither form is refused.
Text description of the diagram

An older client, revision 2025-06-18 (legacy):

  1. Client to server: initialize (id 1), protocolVersion: 2025-06-18.
  2. Server to client: result (id 1), protocolVersion 2025-06-18, serverInfo: hello-server 1.0.0.
  3. Client to server: notifications/initialized, a notification with no answer.
  4. Client to server: tools/list (id 2), with no _meta.
  5. Server to client: result (id 2), one tool, hello.

A current client, revision 2026-07-28:

  1. Client to server: server/discover (id 1), with _meta: protocolVersion 2026-07-28, clientInfo, clientCapabilities.
  2. Server to client: result (id 1), supportedVersions ["2026-07-28"], hello-server 1.0.0.
  3. Client to server: tools/list (id 2), with the same _meta.
  4. Server to client: result (id 2), one tool, hello.

Neither form: tools/list (id 1) with no handshake and no _meta gets error −32602, “Invalid request parameters”.

Why does one server answer both? SDK 2.3.0 speaks 2026-07-28, and it still accepts the handshake from older clients. The client side is similar: the SDK’s Client first tries server/discover. If the server is older and does not know it, the client falls back to initialize. So participants.py printed protocol 2026-07-28: both sides spoke the newest revision.

Amira does three things for support-mcp. She pins the SDK, writes the versions in the code, and adds a test that fails when they disagree.

  1. Copy this lesson’s files from the snapshot end-of-m01:

    Terminal window
    S=../nextia-learning-labs/C28/snapshots/end-of-m01
    cp "$S/requirements.txt" "$S/pyproject.toml" "$S/README.md" .
    cp "$S/support_mcp/__init__.py" support_mcp/
    cp "$S/scripts/versions.py" scripts/
    cp "$S/tests/test_versions.py" "$S/tests/conftest.py" tests/

    The new conftest.py adds two test helpers for Module 3. They change nothing yet.

  2. Open requirements.txt. It pins exact versions, with ==:

    requirements.txt
    # The MCP Python SDK. It brings Starlette, uvicorn, httpx2, pydantic, PyJWT and cryptography.
    mcp==2.3.0
    PyJWT[crypto]==2.15.1

    PyJWT is for the sign-in of Module 5. pyproject.toml lists the same two pins.

  3. Open support_mcp/__init__.py. The versions are now part of the code:

    support_mcp/__init__.py
    __version__ = "1.0.0"
    SERVER_NAME = "support-mcp"
    # Versions are part of the design: MCP changes fast. This project is built and tested with:
    SPEC_REVISION = "2026-07-28" # the MCP specification revision (the latest stable one)
    SDK_VERSION = "2.3.0" # the official MCP Python SDK (pip package "mcp")
  4. Install the pinned packages:

    Terminal window
    python -m pip install -r requirements.txt

    Most of them are installed already, so pip reports them as satisfied.

  5. Print the version report:

    Terminal window
    python -m scripts.versions
    specification revision (this project): 2026-07-28
    MCP Python SDK (pinned): 2.3.0
    MCP Python SDK (installed): 2.3.0
    newest revision the SDK speaks: 2026-07-28
    older revisions it still accepts (initialize handshake): 2024-11-05, 2025-03-26, 2025-06-18, 2025-11-25
    Python: 3.12.13

    Your Python version can differ. It must be 3.12 or later.

  6. Run the tests:

    Terminal window
    python -m pytest -q

    The last line is 13 passed. Your project now equals the snapshot end-of-m01.

Each line of the report comes from a different place, and that is the point. “Pinned” comes from SDK_VERSION, the project’s decision. “Installed” comes from the package that pip really installed. “Newest revision” and “older revisions” come from the SDK’s own constants, so they say what the code really speaks. When a line disagrees with another, something changed without a decision.

tests/test_versions.py turns this into three tests. The first fails when the installed SDK is not the pinned one, or when requirements.txt does not pin it. The second fails when the SDK’s newest revision is not 2026-07-28. The third checks that the report names the older revisions. So a silent upgrade becomes a red test, not a surprise in a lesson three modules later.

You will read many MCP examples. Use these signs to place an example in time before you copy it.

In the example you see It is written for What to do
from mcp.server.fastmcp import FastMCP SDK 1.x Use MCPServer from mcp.server.mcpserver, as in hello_server.py.
McpError SDK 1.x The 2.x name is MCPError.
camelCase attributes, such as result.serverInfo SDK 1.x 2.x uses snake_case, such as client.server_info in participants.py.
initialize, then notifications/initialized revisions 2025-11-25 or older Recognize it. SDK 2.3.0 still accepts it from older clients. Current clients send server/discover.
A session ID kept between requests revisions 2025-11-25 or older 2026-07-28 has no session. Module 5 shows the current HTTP headers.
The HTTP+SSE transport revision 2024-11-05 Legacy. Module 5 uses Streamable HTTP.

If an example names no version at all, look at its date and at its imports. Then decide: port it to the current names, or do not use it. Never mix 1.x and 2.x code in one project.

A version record also says which clients you tested. “It works with MCP” is not a useful promise, because each host has its own client. Here is the record for this course:

Participant Version Tested by the course
Specification revision 2026-07-28 yes
Server and client library official MCP Python SDK 2.3.0 yes
Older clients with the handshake revision 2025-11-25 yes, in Module 7; the three older revisions are accepted by the SDK, but the course does not test them
MCP Inspector 2.10.1, needs Node.js 22.19 or later yes, in Module 3
The course’s own host the project’s host/, Module 4 yes
Desktop assistants and code editors their own no: the course shows their configuration only
Operating system Python 3.12 on macOS yes; Windows and Linux commands are written, not tested

Module 7 turns this table into a compatibility matrix with real test results.

Pin exact versions in every project that other people run: a course, a team’s service, a partner integration. Then every install gets the same code, and every message in the lessons matches yours.

Pinning has a cost. A pinned version does not get fixes by itself. So plan an upgrade as a decision: read the SDK’s changelog, change the pin and the constant, run the tests, and read what fails. The test that you added today is the first alarm in that process.

Note one limit. This project pins its two direct packages exactly, but it has no lock file. The 27 packages that the SDK brings can still get newer versions on a later install. For a course project that is acceptable. For a production service, a lock file pins every package.

Common mistake

The installed SDK is not the pinned one

Symptom: after a separate install of another version, python -m scripts.versions ends with a warning, and one test fails:

MCP Python SDK (pinned): 2.3.0
MCP Python SDK (installed): 2.2.0
…
WARNING: the installed SDK is not the pinned one. Run: pip install -r requirements.txt
> assert version("mcp") == support_mcp.SDK_VERSION
E AssertionError: assert '2.2.0' == '2.3.0'
…
FAILED tests/test_versions.py::test_the_installed_sdk_is_the_pinned_one - Ass...
1 failed, 12 passed in 1.68s

Cause: something installed another SDK version into the virtual environment, for example pip install mcp==2.2.0 copied from an old note. The course team made this failure on purpose, in a second environment.

Fix: run python -m pip install -r requirements.txt. The test passes again, and the report shows 2.3.0 twice.

Common mistake

No module named 'mcp.server.fastmcp'

Symptom: code copied from an older tutorial stops at its first import, with the message at the top of this lesson.

Cause: the code was written for SDK 1.x. The project uses SDK 2.3.0, where FastMCP became MCPServer.

Fix: change the import to from mcp.server.mcpserver import MCPServer, and FastMCP(…) to MCPServer(…). Then check the rest against the table above. Do not change the pin to mcp<2: that would break every other file of the project.

Reset. If your project is broken, copy the snapshot end-of-m01 as in Where MCP fits, with end-of-m01 instead of start. Then make the virtual environment and install requirements-dev.txt.

Module practice: map an assistant’s integrations

Section titled “Module practice: map an assistant’s integrations”

Guided example. docs/integration-map.md is the worked example of this module. Amira made it in four steps. First, list the AI applications and who uses them. Second, list what each one needs from the service. Third, write how each one connects today, without MCP. Fourth, write what changes with MCP, and the cases where MCP is not needed. The last table of the file says what MCP standardizes and what stays with Larkfield. Read it once more before the task.

Your turn. Harbour Cycles is a made-up bike rental shop. Its website chat assistant needs five things:

  1. Check bike availability in the shop’s booking system.
  2. Read the rental terms and the safety guide.
  3. Get the weather forecast from a public weather API.
  4. Refund a customer’s deposit.
  5. The mechanics’ phone app and the staff’s editor assistant also want the same availability lookup.

Write a short integration map for Harbour Cycles. For each need, decide: a direct API call, a tool in one application, or a capability of an MCP server. For each MCP capability, name one decision that stays with Harbour Cycles.

Show a worked solution
Need Choice Why What stays with Harbour Cycles
1. Availability MCP server, a read-only tool Three applications need it: the chat assistant, the phone app and the editor assistant. Which bikes and dates a caller may see; limits on the size of a result.
2. Terms and safety guide The same MCP server, as readable documents (resources, Module 2) The same three applications quote them. Which version is in force; staff-only documents.
3. Weather A tool in the chat assistant, or a direct API call Only one application needs it, and the public API is already simple. A server adds a program to run for no reuse. The weather API’s key and its terms of use.
4. Deposit refund Not yet. Later, at most a proposal that a person approves (Module 6) It moves money. A model must never decide it alone. Who may approve, the amount limit, and the record of each approval.
5. Phone and editor Their own MCP clients, to the same server as need 1 That is the reason for the server: one capability, three hosts. Sign-in and permissions for each app’s users (Module 5).

Count: without MCP, needs 1 and 2 in three applications make 3 × 2 = 6 custom integrations. With MCP, they make 1 server. Your table can differ in the details. It must not give the refund to a model alone, and it must keep the permission checks in the shop’s own code.

Check your understanding

Name the three things in an MCP connection that have their own version, and give the values that this course uses.

Show the answer

The specification revision: 2026-07-28, a date that names the protocol’s messages and rules. The SDK version: the official MCP Python SDK 2.3.0, the library that writes and reads the messages. The host and its client: each host has its own client. In this course, they are the SDK’s Client, MCP Inspector 2.10.1 and the course’s own host. Python 3.12 is a fourth version worth writing down.

Check your understanding

Predict: an older client sends initialize with protocolVersion 2025-11-25 to the hello server, then notifications/initialized, then tools/list without _meta. What does it get? And a client that sends only tools/list, with no handshake and no _meta?

Show the answer

The first client gets a normal answer, because SDK 2.3.0 accepts the handshake of the older revisions. The server answers initialize with protocolVersion 2025-11-25. The notification gets no answer, and tools/list returns the hello tool. The second client gets error −32602, “Invalid request parameters”. Its request is in neither form: there was no handshake, and the request has no _meta.

Check your understanding

Practical check: run python -m scripts.versions and python -m pytest -q. What do the lines 'pinned' and 'installed' say, and how many tests pass?

Show the answer

Both say 2.3.0: MCP Python SDK (pinned): 2.3.0 and MCP Python SDK (installed): 2.3.0. The report also names revision 2026-07-28 and the four older revisions. python -m pytest -q prints 13 passed. If the two lines differ, the report ends with a warning, and test_the_installed_sdk_is_the_pinned_one fails.

Check your understanding

Module completion check: explain what MCP standardizes, and what remains the responsibility of the application and the server. Use Larkfield's support-mcp as the example.

Show the answer

MCP standardizes five things:

  • how a client discovers what a server offers (server/discover, tools/list);
  • the format of the messages (JSON-RPC 2.0);
  • the transports (stdio and Streamable HTTP);
  • how a call is made, and how a result or an error comes back;
  • how a remote server says where to get a token.

It also names its revisions, so both sides can agree on one.

The application and the server still decide everything about meaning and permission. The server decides which capabilities exist and how narrow they are, for example get_ticket instead of a database query tool. The server and the business service check who the caller is and what they may read. For example, knowledge.py answers Not found. for a Bramble Books ticket on a Larkfield request. The host decides which proposed calls run and when a person must approve, and the project decides its limits, logs and pinned versions. MCP is not an agent framework, and it does not make a tool safe.

Recap

  • Three things have versions: the specification revision (2026-07-28), the SDK (the official MCP Python SDK 2.3.0) and each host’s client. Write them down in one sentence for every project.
  • Legacy, to recognize: the initialize handshake and its session, SDK 1.x names such as FastMCP and McpError, and the HTTP+SSE transport. Current: server/discover and a _meta on every request.
  • SDK 2.3.0 answers both eras. A request in neither form gets −32602.
  • scripts/versions.py and tests/test_versions.py turn a silent version change into a warning and a failed test.

Module recap. You can now explain where MCP fits, and when a direct API call or a tool in one application is enough. You can name the host, the client and the server, and say which part must make each check. You ran the smallest server and read its six messages. And you wrote down the versions that your project depends on. Your project equals the snapshot end-of-m01: 13 passed.

Next, you design support-mcp’s capabilities before you build them, starting with its tools.

Next: Tools