CourseModel Context Protocol: The Complete Course · Module 1: Foundations · part 7 of 77
Part 7 · Module 1: Foundations

Lesson 1: Why MCP exists: one protocol, many applications

14 min read·13 Oct 2026
Module 1 · Lesson 1 · About 40 minutes · Needs: basic Python; no model key for this lesson

The situation

It is Monday 7 September 2026. Nare runs a small sleep and memory lab and keeps her research notes as Markdown files in one folder. She wants to type "what did we decide about the nap study?" into any of the four AI applications she uses and get the answer from her notes.

You are the engineer. Before building anything, you need to know why a plug-in per application gets expensive, what the Model Context Protocol (MCP) changes, and when it is not worth using. This lesson answers all three with arithmetic, one small server and a stopwatch.

A real case, first

In April 2025 Angie Jones of Block described how the company adopted MCP for goose, its open-source AI agent ("MCP in the Enterprise: Real World Adoption at Block", goose blog, 21 April 2025). Block's reason was Nare's problem at company scale: "We didn't want to build one-off integrations or hardwire AI into a specific vendor ecosystem." Because MCP is "model-agnostic and tool-agnostic", one agent reaches internal APIs, open-source tools and hosted services the same way; the post names Snowflake, GitHub, Jira, Slack and Google Drive.

The post is also honest about what the protocol did not do. Every MCP server used internally is written by Block's own engineers, and OAuth flows run inside locally run servers, "a practical but temporary solution". Thousands of employees use goose daily (most report, in their own estimate, saving 50 to 75 percent of their time on common tasks). The lesson: a protocol pays off when many applications reach many tools, and it moves work such as security and sign-in rather than removing it.

What you will be able to do

  • Count Nare's integrations: N times M against N plus M.
  • Show what a protocol fixes that a library cannot, by talking to a server with no MCP library.
  • Place MCP next to native function calling, REST and agent-to-agent protocols.
  • Name the official SDKs and what their tiers mean.
  • Decide when MCP is the wrong tool, using measured costs.

1. The problem Nare actually has (Essential)

Nare's notes are only useful if the assistant she is using can read them. Everything in this course follows from that one wish.

ONE QUESTION, FOUR APPLICATIONS Desktop chat Code editor CLI agent Lab assistant notes/ folder 8 Markdown notes “what did we decide about the nap study?”
Nare asks the same question in four AI applications, and every one of them must reach the same notes folder (blue marks the systems you build and connect).

Nare uses a desktop chat assistant, an AI code editor, a command-line coding agent and the lab's own small assistant (the host you build in Module 6). Each is an AI application: a program that puts a language model in front of a user. All four should answer from the same folder.

lab-sync-2026-09-02.md Attendees: Priya, Tomas, Nare. Decision: 24 participants instead of 16 Action: Tomas books the sleep lab Action: consent form by 15 September body line 2
The answer lives on one line of one note: the lab sync of 2 September raised the nap study to 24 participants (green marks the correct answer).

A person finds the answer quickly: the note lab-sync-2026-09-02 says, on its second body line, that the nap study will run with 24 participants instead of 16. The hard part is letting four programs, written by four companies, reach it.

ONE FOLDER, WIRED FOUR TIMES Desktop chat plug-in A Code editor plug-in B CLI agent settings file Lab assistant extension notes/ folder
Without a shared protocol each application wants the notes in its own plug-in format, so the same folder is wired up four times (red marks the duplicated glue).

Each application has its own way of adding a tool: its own plug-in format, settings file and sign-in. So the folder is wired up four times, and each wiring breaks when its application changes. Then Tomas asks for the equipment calendar and Priya for her reference manager, and the wiring multiplies.

2. The N applications times M tools arithmetic (Essential)

The cost of custom wiring is a multiplication. Counting it tells you when a shared protocol starts to pay.

4 X 6 = 24 ADAPTERS Desktop chat Code editor CLI agent Lab assistant Notes Calendar References EEG data Email Spreadsheet
Four applications and six tool systems wired directly need 24 adapters, one for every pair.

Call the number of AI applications N and the number of tool systems M (anything an application should reach). Without a shared agreement, every application needs its own adapter for every system. Nare's lab has 4 applications and 6 systems (notes, calendar, references, EEG data, email, participant spreadsheet): 24 adapters.

MCP 4 + 6 = 10 PIECES Desktop chat Code editor CLI agent Lab assistant Notes Calendar References EEG data Email Spreadsheet
With a shared protocol each application writes one client and each system one server: 10 pieces instead of 24.

With a shared protocol each application implements the client side once and each system the server side once: N plus M, here 10. The vendors already wrote their clients, so Nare writes only the servers nobody has published. The course's MCP cell installs the official Python SDK:

python
%pip install -q mcp==2.3.0
import logging
for name in ("mcp", "httpx2", "uvicorn"):
    logging.getLogger(name).setLevel(logging.WARNING)

Code walkthrough

  • mcp==2.3.0 is the official SDK, pinned to the tested version (checked October 2026); it speaks protocol revision 2026-07-28.
  • The loop turns three libraries' log messages down to warnings so they do not bury your output.

The arithmetic itself needs no library:

python
def integrations(apps, systems):
    """Pieces of glue to write: without a shared protocol, and with one."""
    return apps * systems, apps + systems

SETTINGS = [("Nare's lab", 4, 6), ("A mid-sized team", 10, 50), ("A large company", 30, 400)]
for name, n, m in SETTINGS:
    bespoke, shared = integrations(n, m)
    saved = bespoke - shared
    print(f"{name:17} {n:>3} x {m:<3} = {bespoke:>6,}   {n:>3} + {m:<3} = {shared:>3}   "
          f"saved {saved:,} ({saved / bespoke:.0%})")
print("a fifth application in the lab:", integrations(5, 6))

Code walkthrough

  • integrations returns the product (one adapter per pair) and the sum (one piece per side).
  • SETTINGS holds Nare's lab and two made-up larger sizes for comparison.
  • The output was 24 against 10 (58 percent saved), 500 against 60 (88 percent) and 12,000 against 430 (96 percent). The last line gave (30, 11).
SHARE OF GLUE SAVED Nare's lab 24 vs 10: 58% Mid-sized team 500 vs 60: 88% Large company 12,000 vs 430: 96%
The saving grows with size: 58 percent for Nare's lab, 88 percent for a mid-sized team and 96 percent for a large company.

A product grows faster than a sum. Two caveats: the saving is in wiring, not thinking (each server still needs good descriptions, sign-in and a security review, Modules 3, 7 and 8), and it holds only if the applications really speak the protocol. The Agentic AI Engineer course makes this argument briefly (Module 4, Lesson 1); this lesson goes down to the messages.

without a protocol with MCP +6 +1 adapters to write client, already written
A fifth application costs six new adapters without a protocol and one new client with it.

A fifth application shows the asymmetry: 30 adapters instead of 24, six new things to test, against 11 pieces instead of 10, a client its vendor already wrote.

3. What a protocol standardises that an SDK cannot (Essential)

Why not just a good library? The answer decides how far you can trust code written by people you never meet, which is most of what an MCP host runs.

SDK Protocol Language one any that reads JSON Owner one team public spec, SEPs Discovery read the docs ask the server Versions package numbers dated, per request Process same program separate program
An SDK is code you import; a protocol is an agreement about messages, which is why it works across languages, owners and programs (grey marks the narrower option).

An SDK (software development kit) is code in one language, owned by one team, that you import. A protocol is an agreement about the messages that cross a boundary, so programs that share no code can work together. The difference shows in five places.

Code editors LSP Languages AI applications MCP Tools and data one agreement in the middle
MCP borrows the Language Server Protocol's trick: one agreement in the middle turns a grid of pairings into a sum.

MCP's specification says it "takes some inspiration from the Language Server Protocol", the agreement that lets any code editor support any programming language. One agreement in the middle turned a grid of plug-ins into a sum; MCP does the same for AI applications.

client notes server tools/list what I offer search_notes(query: string) plus its description
A client asks a running server what it offers and gets back each tool's name, description and input schema.

The most practical difference is discovery: a client asks a running server what it offers instead of hard-coding it. Here is a one-tool server, written to a file (Lesson 3 explains it fully):

python
%%writefile notes_server.py
from mcp.server import MCPServer

NOTES = {"lab-sync-2026-09-02": "Decision: run the nap study with 24 participants instead of 16."}
mcp = MCPServer("notes")

@mcp.tool()
def search_notes(query: str) -> list[str]:
    """Search Nare's notes and return the ids of notes that mention the query."""
    return [nid for nid, body in NOTES.items() if query.lower() in body.lower()]

if __name__ == "__main__":
    mcp.run()          # stdio by default

Code walkthrough

  • %%writefile saves the cell as a file, so another program can start it.
  • NOTES holds one line of the anchor note; Lesson 2 loads all eight.
  • @mcp.tool() publishes the function as a tool, with the docstring as its description and the type hints as its input schema. mcp.run() serves it over standard input and output.

Ask it what it offers, as any client would:

python
import json
import notes_server
from mcp import Client

async with Client(notes_server.mcp) as client:      # in memory: no process, no port
    listed = await client.list_tools()

tool = listed.tools[0].model_dump(by_alias=True, exclude_none=True)
print(json.dumps({k: tool[k] for k in ("name", "description", "inputSchema")}, indent=2))

Code walkthrough

  • Client(notes_server.mcp) connects in memory. Top-level async with and await work in Colab, which already runs an event loop.
  • list_tools() sends one request, tools/list.
  • model_dump(by_alias=True, ...) gives the protocol's own JSON names. The output showed search_notes, its docstring, and a schema with one required string, query.
{"jsonrpc": "2.0", "id": 2, matches reply to request "method": "tools/call", what to do "params": {"name": "search_notes", which tool "arguments": {"query": "nap"}, its input "_meta": {protocolVersion: "2026-07-28", version, every time clientCapabilities: {}}}} what the client can do
Every request names its method and carries the protocol version and the client's capabilities in _meta, so each message stands on its own.

What crossed the boundary is JSON-RPC 2.0, a small standard for "call this method with these parameters" messages. Under revision 2026-07-28 each request also carries _meta with the protocol version and the client's capabilities. The message is the contract, so you do not need the SDK. This cell talks to the server with only the standard library:

python
import json, subprocess, sys

META = {"io.modelcontextprotocol/protocolVersion": "2026-07-28",
        "io.modelcontextprotocol/clientCapabilities": {}}

server = subprocess.Popen([sys.executable, "notes_server.py"], text=True,
                          stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=open("server.log", "w"))

def request(req_id, method, params=None, meta=META):
    """Send one JSON-RPC request as a line on stdin; read one line of reply from stdout."""
    message = {"jsonrpc": "2.0", "id": req_id, "method": method, "params": {**(params or {}), "_meta": meta}}
    server.stdin.write(json.dumps(message) + "\n")
    server.stdin.flush()
    return json.loads(server.stdout.readline())

print(request(1, "server/discover")["result"]["supportedVersions"])
reply = request(2, "tools/call", {"name": "search_notes", "arguments": {"query": "nap"}})
print(reply["result"]["content"][0]["text"])

Code walkthrough

  • META holds the two _meta entries required on every request: version and capabilities (none here).
  • subprocess.Popen starts the server as a child program; its standard input and output are the connection, and its error output goes to server.log.
  • request writes one message as one line and reads one line back: the whole stdio transport.
  • server/discover printed ['2026-07-28']; tools/call printed lab-sync-2026-09-02, the note holding Nare's answer.
plain Python no MCP library MCPServer Python SDK one JSON line on stdin one JSON line on stdout lab-sync-2026-09-02
A few lines of plain Python with no MCP library talk to the SDK's server and get the right note back, because both follow the same messages.

Plain Python, sharing no code with the SDK, used its server; so can a TypeScript editor or a Go agent.

Break it: ask for a version the server does not speak

Protocols change, so a mismatch must fail loudly. Send a made-up future version, then no _meta at all.

python
future = {**META, "io.modelcontextprotocol/protocolVersion": "2099-01-01"}
print(request(3, "tools/list", meta=future)["error"])
empty = request(4, "tools/list", meta={})["error"]
print(empty["code"], empty["message"])
server.terminate()

Code walkthrough

  • future copies META with another version. The reply was error -32022, "Unsupported protocol version", with supported: ['2026-07-28'], so the client knows what to retry with.
  • empty came back as -32602 (invalid parameters), naming the two missing keys.
  • server.terminate() stops the child program.
version 2099-01-01 error -32022 supported: 2026-07-28 version 2026-07-28 result retry
Ask for a version the server does not speak and it answers with error -32022 and the versions it does support, so the client can retry.

The specification requires this: a server that lacks the requested version "MUST respond with an UnsupportedProtocolVersionError listing the versions it does support". A library's version number cannot promise that across languages and owners. Module 2 takes the wire apart properly.

4. Official SDKs in other languages (Optional)

You will meet servers in other languages, and may write one. Know which SDKs exist and how far to rely on each.

Tier 1 TypeScript Python C# Go Rust Ruby Tier 2 Java Tier 3 Swift PHP Kotlin
The MCP site lists ten official SDKs in three tiers (checked October 2026); every one speaks the same messages.

The MCP site lists ten official SDKs in three tiers, by "feature completeness, protocol support, and maintenance commitment" (checked October 2026). Tiers move: on release day, 28 July 2026, the maintainers named four Tier 1 SDKs as speaking the new revision and Rust as supporting it in beta. Check the SDK page before choosing. Every one sends the messages you just sent by hand.

5. MCP versus native function calling, REST and agent-to-agent protocols (Essential)

Three neighbours get confused with MCP, and picking the wrong one costs weeks.

model host with client notes server notes/ function calling MCP
Function calling is how a model asks the host for a tool; MCP is how the host finds and runs that tool in another program (amber marks the model).

Native function calling (also called tool calling) is a feature of a model's API. You send the model tool definitions; it replies "call search_notes with query set to nap"; your code runs it and returns the result. It says nothing about where the tool lives or how another application reuses it. MCP sits one step out: how the host finds tools in another program and runs them there.

MCP tool function-calling tool name name description description inputSchema parameters
The same tool in MCP's shape and in function-calling shape: the host copies two fields and renames the third.

Most hosts use function calling underneath, translating each MCP tool into the model's format:

python
def as_function_tool(mcp_tool):
    """Translate an MCP tool definition into the function-calling format most model APIs accept."""
    return {"type": "function",
            "function": {"name": mcp_tool["name"],
                         "description": mcp_tool["description"],
                         "parameters": mcp_tool["inputSchema"]}}

print(json.dumps(as_function_tool(tool), indent=2))

Code walkthrough

  • as_function_tool wraps the tool listed in section 3 in the widely used OpenAI-style shape.
  • Name and description carry over; inputSchema becomes parameters, as the output showed. Lesson 2 calls a model with a tool in this format; Module 6's host translates automatically.
REST MCP Reader another program a model Finding calls read the docs tools/list Descriptions for developers for the model Ask the user not defined defined
REST serves programs that already know the API; MCP serves a model that must discover the tools and read their descriptions.

REST (an HTTP API, often described with OpenAPI) is how services talk to services, and many MCP servers wrap one. What REST does not define is behaviour for a model: a standard tool list, descriptions written for a model, prompts a user picks, or a way to ask the user a question mid-call.

agent MCP search_notes does what it is told agent A2A another agent plans for itself
MCP hands a tool a precise request; an agent-to-agent protocol such as A2A hands another agent a whole task.

An agent-to-agent protocol connects one autonomous agent to another. Google announced A2A (Agent2Agent) on 9 April 2025 as "an open protocol that complements Anthropic's Model Context Protocol (MCP)". With A2A an agent hands a task to a remote agent that plans for itself; with MCP the other side does what it is told. An agent can use both.

6. When MCP is the wrong tool (Essential)

Every protocol boundary costs a process, a message, a dependency and a review. Knowing when not to pay is part of using MCP well.

Does a model choose? no plain code or REST Shared by several apps? no function calling Handing over a goal? yes A2A otherwise MCP
Three questions decide whether MCP fits; only a capability chosen by a model, shared by several applications, ends at MCP.

MCP fits when several AI applications must reach the same capability and a model decides when to call it. Otherwise it is overhead: a nightly export needs plain code, one script with two functions needs function calling, service traffic with no model needs REST or a queue.

TIME PER CALL, LOG SCALE direct call 0.2 µs MCP in memory 300 to 400 µs MCP over stdio 600 to 900 µs stdio start ~220 ms
Measured on the course run (log scale): a direct call takes a fraction of a microsecond, an MCP call well under a millisecond, and starting a stdio server about 220 ms.

The overhead is measurable. This cell calls search_notes 100 times three ways: directly, through an in-memory client and through a stdio server.

python
import time
from mcp import StdioServerParameters
from mcp.client.stdio import stdio_client

CALLS = 100
async def per_call_ms(client):
    t = time.perf_counter()
    for _ in range(CALLS):
        await client.call_tool("search_notes", {"query": "nap"})
    return (time.perf_counter() - t) / CALLS * 1000

t = time.perf_counter()
for _ in range(CALLS):
    notes_server.search_notes("nap")
print(f"direct call     {(time.perf_counter() - t) / CALLS * 1e6:7.2f} microseconds per call")

async with Client(notes_server.mcp) as client:
    print(f"MCP in memory   {await per_call_ms(client) * 1000:7.0f} microseconds per call")

params = StdioServerParameters(command=sys.executable, args=["notes_server.py"])
with open("server.log", "w") as log:
    t = time.perf_counter()
    async with Client(stdio_client(params, errlog=log)) as client:
        await client.call_tool("search_notes", {"query": "nap"})
        start = (time.perf_counter() - t) * 1000
        print(f"MCP over stdio  {await per_call_ms(client) * 1000:7.0f} microseconds per call, "
              f"after {start:.0f} ms to start")

Code walkthrough

  • per_call_ms averages 100 call_tool requests on a connected client, in milliseconds.
  • The first loop calls the function directly; @mcp.tool() leaves it an ordinary function.
  • stdio_client(params, errlog=log) starts the server as a child program, so its timing includes start-up.
  • On the course's runs (a laptop, not Colab) the output was 0.1 to 0.25 microseconds direct, 300 to 400 in memory, and 600 to 900 over stdio after 210 to 230 ms to start.

Thousands of times slower, yet under a millisecond is nothing beside a model call that takes a second. It matters only with no model involved or in tight loops.

TOKENS THE MODEL READS EVERY TURN Chrome DevTools 26 tools: 18.0k Playwright MCP 21 tools: 13.7k CLI README 225 tokens
Tool definitions cost context: two browser MCP servers took 13.7k and 18.0k tokens, a README of command-line tools 225 (Zechner, November 2025).

The second cost is context: every tool definition is text the model reads each turn. Mario Zechner measured Playwright MCP's 21 tools at 13.7k tokens and Chrome DevTools MCP's 26 at 18.0k, against 225 for a README of his own command-line tools (November 2025). His view: "in many situations, you don't need or even want an MCP server". For an agent that can run shell commands, he is often right.

In production

goose + MCP Block's agent servers built in-house curated default set model allowlists tokens in keychain
Block's controls around MCP sit outside the protocol: servers written in-house, a curated default set, model allowlists and tokens in the system keychain.

Block's post shows what a team adds around the protocol: servers written in-house, "a curated set of approved MCP servers" by default, model allowlists on some servers, tokens in the system keychain, and tools marked read-only or destructive so users confirm risky calls. None of it is in the wire format; the specification says MCP "cannot enforce these security principles at the protocol level". Modules 6, 7 and 8 build these controls for Nare. Expect change here: Block called its sign-in approach temporary, and the specification is revised every few months.

What you added to the notes assistant

  • The lab's integration count: 24 adapters against 10 pieces, and 6 against 1 per new application.
  • A one-tool notes server, reached through the SDK and through plain JSON over stdio.
  • as_function_tool, the translation Module 6's host performs.
  • A measured rule for when not to use MCP.

Check yourself

  1. Nare's lab grows to 6 applications and 8 tool systems. How many pieces of glue with and without a protocol?
  2. Name two things a protocol gives that a Python library cannot.
  3. Where does native function calling end and MCP begin?
  4. A nightly job exports every note to a backup drive. Should it use MCP?
  5. What does a server do when a request names a protocol version it does not support?
  1. 48 adapters without a protocol (6 times 8) and 14 pieces with one (6 plus 8).
  2. Any two of: any language; a public owner and change process; discovery at run time; versions on every request with a defined error; a separate program.
  3. Function calling is the model asking the host for a tool; MCP is how the host finds and runs it in another program.
  4. No: no model decides and nothing is shared, so plain code is simpler.
  5. It returns error -32022 listing the versions it supports.

Exercise

Send tools/list with the raw request function and translate the result with as_function_tool. The check restarts the server and must print ok.

python
server = subprocess.Popen([sys.executable, "notes_server.py"], text=True,
                          stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=open("server.log", "w"))
listed_raw = request(10, "tools/list")["result"]["tools"]
fn = as_function_tool(listed_raw[0])
assert fn["function"]["name"] == "search_notes"
assert fn["function"]["parameters"]["required"] == ["query"]
assert integrations(6, 8) == (48, 14)
server.terminate()
print("ok")

Code walkthrough

  • server = subprocess.Popen(...) restarts the server; request uses whatever server names.
  • request(10, "tools/list") returns the raw tool list, and as_function_tool works on it because it reads the protocol's own field names.
  • The asserts check the name, the required argument and question 1's arithmetic.

Key terms

TermMeaning
Model Context Protocol (MCP)An open protocol for connecting AI applications to tools and data
AdapterCustom glue between one application and one tool system
ProtocolAn agreement about messages, so programs that share no code can work together
SDKA library in one language that implements the protocol for you
DiscoveryA client asking a running server what it offers
JSON-RPC 2.0The message format MCP uses: a method, parameters, and an id to match the reply
Native function callingA model API feature where the model asks your code to run a named function
A2AAgent2Agent, a protocol for one agent to hand a task to another

Sources

  • Angie Jones (Block), "MCP in the Enterprise: Real World Adoption at Block", goose blog, 21 April 2025. https://goose-docs.ai/blog/2025/04/21/mcp-in-enterprise
  • Model Context Protocol, "Specification" (revision 2026-07-28), overview, fetched 13 October 2026. https://modelcontextprotocol.io/specification/2026-07-28
  • Model Context Protocol, "Versioning and Compatibility" (revision 2026-07-28), fetched 13 October 2026. https://modelcontextprotocol.io/specification/2026-07-28/basic/versioning
  • Model Context Protocol, "SDKs", fetched 13 October 2026. https://modelcontextprotocol.io/docs/2026-07-28/sdk
  • David Soria Parra and Den Delimarsky, "The 2026-07-28 Specification", MCP blog, 28 July 2026. https://blog.modelcontextprotocol.io/posts/2026-07-28/
  • Rao Surapaneni, Miku Jha, Michael Vakoc and Todd Segal (Google), "Announcing the Agent2Agent Protocol (A2A)", 9 April 2025. https://developers.googleblog.com/en/a2a-a-new-era-of-agent-interoperability/
  • Mario Zechner, "What if you don't need MCP at all?", 2 November 2025. https://mariozechner.at/posts/2025-11-02-what-if-you-dont-need-mcp/