AI EngineeringZero to ProductionHome·About·What’s new·Contact
Specialized Topics · Part T3

Model Context Protocol (MCP)

You built tools by hand in Ch 4 — every app re-implementing its own. MCP is the open standard that fixes that: a universal way to plug tools, data, and prompts into any LLM app (it's what powers Claude Code's own tools). This part explains what MCP is, its client/server architecture, its three primitives, and how to build a server — connecting straight back to the tool-use you already know.

⏱️ ~1.5 hours🧪 3 labs🎯 Beginner→Tech-lead

Learning objectives

  • Explain MCP's role in the AI tooling ecosystem.
  • Distinguish MCP from function-calling and plugins.
  • Understand the client-server architecture and discovery.
  • Reason about adopting MCP in an organization.

You've felt this pain even if you've never named it. Your team ships a support agent that reads from your ticketing system. A month later a second team wants their coding assistant to read the same tickets — so they write the integration again, slightly differently. Then someone builds a Slack bot, and it needs the tickets too. Three apps, one data source, three hand-rolled connectors that all drift apart, all break separately, all need re-auth when the ticket API changes. Now multiply that across every data source your org has. That quiet, compounding mess is exactly what MCP exists to kill.

The clean way to see it is with a plug analogy. Before USB, every device had its own cable — printer cables, mouse connectors, serial ports, each incompatible with the others. USB won because it was one contract: any device that speaks USB works in any port that speaks USB, and neither side has to know anything about the other's internals. MCP is USB for AI tools. It's an open protocol — a fixed, published set of rules — for how an LLM application asks an external program "what can you do, and do it." Once both sides speak the protocol, they interoperate without bespoke glue.

Here's the precise idea the section is built around. Call the number of AI applications N and the number of tools/data sources M. In the old world, each app needs a custom connector to each tool, so you build and maintain N × M integrations — and that product explodes: it's the multiplication rule from the complexity track, and doubling either side roughly doubles the work. A protocol changes the math. Each app implements the standard once (that's N), and each tool is wrapped as an MCP server once (that's M), giving N + M total. Addition, not multiplication.

The common mistake here is dismissing this as "just an interface" — a nice-to-have abstraction. It isn't cosmetic; it's the difference between an integration cost that grows like an area and one that grows like a perimeter. At small scale (2 apps, 2 tools) both look the same. At org scale the gap is brutal: 20 apps and 20 tools is 400 bespoke connectors versus 40 standardized ones. The protocol doesn't make any single integration cleverer — it makes each one reusable, which is what turns a quadratic problem into a linear one. Keep that N×M-versus-N+M lens in mind; the diagram and the tiny program below both exist to make it something you can feel rather than just recite.

by hand · every app wires to every source support coding Slack bot tickets docs db 3 × 3 = 9 bespoke connectors via MCP · each side wires once support coding Slack bot MCPcontract tickets docs db 3 + 3 = 6 wires to one contract 3 apps, 3 sources: 9 tangled wires vs 6 through a shared standard and the gap widens — 20 × 20 = 400 by hand becomes 20 + 20 = 40 via MCP Multiplication vs addition, drawn. On the left every app owns a custom connector to every source (N×M); on the right both sides implement one shared contract (N+M). The tangle is exactly the cost MCP removes.
🗺️ How to read this diagram

Compare the two halves side by side — same three apps (blue) and same three data sources (green) on each side, wired two different ways.

  • On the left, grey lines run from every app to every source: 3 apps × 3 sources = 9 separate hand-built connectors, each one to write, re-auth, and maintain when an API changes.
  • On the right, each app connects to the single MCP contract box in the middle, and each source connects to it too — the blue lines. Every app and every source wires up once: 3 + 3 = 6.
  • The payoff is the bottom line: at 3×3 the win is modest (9 vs 6), but at org scale (20×20) it is 400 hand-built connectors versus 40 — multiplication replaced by addition.

In short: Point every app at one shared contract instead of at every source, and integration cost stops growing like an area and starts growing like a perimeter.

▶ Runnable companionEvery code block in this lesson is also a standalone file under code/xt3-mcp/ in the course, with a README on how to run it. Read here, run there — no need to copy-paste.

1 · Why a protocol at all essential

Before MCP, every AI app wrote custom integrations for each tool and data source — N apps × M tools = N×M bespoke connectors. A protocol collapses that: build a tool once as an MCP server (M servers), and any client speaks to all of them (N clients). N+M, not N×M.

N clients apps/agents MCP standard the contract M servers tools/data
🗺️ How to read this diagram

This picture is the one-sentence case for MCP. MCP (Model Context Protocol) is just an agreed-upon standard — a shared set of rules — for how an AI app can get tools and data from an outside program. The diagram shows what that standard buys you. Read it left to right.

  • The left box, "N clients", is your AI apps or agents — the programs that want to use tools (for example, Claude Code, a chatbot, your own script).
  • The right box, "M servers", is the tools and data those apps want to reach — a file system, a database, GitHub, a ticketing system. In MCP, each of these is wrapped in a small program called an MCP server.
  • The middle box, "MCP standard — the contract", is the shared language both sides speak. Because every client and every server follows the same contract, any client can talk to any server without custom glue code.
  • The arrows show the direction of connection: a client connects through the standard to a server. Without the standard you'd need a separate wire from every app to every tool (N×M of them); with it, each side just implements the contract once (N+M).

In short: If you have 5 apps and 8 tools, doing it by hand means 5×8 = 40 separate integrations to build and maintain. With one shared standard it's 5+8 = 13. The next code block proves that with a tiny function.

Example code for learning — review, test, and adapt it before running against real or production systems. Commands can create, change, or delete resources. See the Terms & Disclaimer.
Python · the N×M vs N+M math (runs)
nxm.pydef integrations(n_clients, m_tools, with_protocol):
    return (n_clients + m_tools) if with_protocol else (n_clients * m_tools)

print("5 apps x 8 tools, custom:  ", integrations(5, 8, False), "integrations to build/maintain")
print("5 apps x 8 tools, via MCP: ", integrations(5, 8, True), "(each side implements the standard once)")
5 apps x 8 tools, custom:   40 integrations to build/maintain
5 apps x 8 tools, via MCP:  13 (each side implements the standard once)
▶ How this works

This tiny program turns the diagram's claim into a number you can check. It counts how many integrations you have to build in two worlds: the old custom way, and the MCP way. The whole point is to feel why a shared standard scales better.

  1. The function integrations(n_clients, m_tools, with_protocol) takes how many apps (n_clients), how many tools (m_tools), and a true/false flag for whether a protocol is used.
  2. The one-line body says it all: with a protocol you return n_clients + m_tools (each side wires up to the standard once — that's the N+M from the diagram). Without one you return n_clients * m_tools (every app needs its own connector to every tool — the N×M explosion).
  3. The two print lines call the function for 5 apps and 8 tools — once with False (custom) and once with True (via MCP) — and label each number so the difference is obvious.

What the output means: You get two lines: the custom approach needs 40 integrations, MCP needs 13. Same apps, same tools — the standard replaces multiplication with addition. That gap only grows as you add more of either side.

Try this: Change the numbers to integrations(20, 20, False) vs (20, 20, True). Custom jumps to 400 while MCP is just 40 — this is the scaling argument for a protocol, made concrete.

2 · MCP vs function-calling vs plugins essential

If you've read Ch 4, a fair objection is forming in your head right now: "Wait — I already gave the model tools. I wrote a JSON schema, the model asked to call get_weather, I ran the function and handed back the result. Didn't I already solve this?" You did — for one app. The confusion between function-calling and MCP is the single most common misunderstanding on this topic, and clearing it up is the whole job of this section.

Think of it as the difference between a skill and a marketplace. Function-calling is the model's built-in ability to say "I'd like to call this tool with these arguments." That ability is real and necessary — but the tool definitions live inside your one application's code. If another team wants the same tool, they copy your schema and re-wire the execution. Nothing is shared. MCP sits one layer up: it's the marketplace where tools are published once and any application can walk in and use them. Crucially, MCP doesn't replace function-calling — under the hood, when the model picks an MCP tool, the app still uses ordinary function-calling to invoke it. MCP just standardizes where the tools come from and how they're described.

The table below sorts three approaches you'll hear conflated. Read it along the "reusable across clients?" column, because that's the axis that actually matters. Function calling is app-specific: powerful, but every app re-implements its tools. Vendor plugins (the closed app-store style) are reusable, but only inside one vendor's walled garden — build a plugin for vendor A and it does nothing for vendor B. MCP is the only row that's both reusable and open: any compliant client can talk to any compliant server, regardless of who made either. The mistake to avoid is treating these as competitors to pick between — in practice a real system uses function-calling as the mechanism, MCP as the distribution model, and skips vendor plugins precisely because of the lock-in the third row warns about.

where each approach sits: reusable? · open? function calling the mechanism reusable: no tools live in one app's own code vendor plugins walled garden reusable: 1 vendor works only inside that vendor's stack MCP the distribution model reusable · open any client ↔ any server, any maker red = app-locked · amber = vendor-locked · green = open + reusable Only one row is both open and reusable. Function calling is the in-app mechanism, vendor plugins are reusable but locked to one vendor, and MCP is the open standard any client and any server can share.
🗺️ How to read this diagram

Read the three panels left to right — they get progressively more open, and the colour tells you how far each one travels.

  • The red panel (function calling) is the model's built-in ability to request a tool, but the tool definitions live inside one app's code — so it is not reusable across other apps.
  • The amber panel (vendor plugins) is reusable, but only inside a single vendor's ecosystem — build for vendor A and it does nothing for vendor B.
  • The green panel (MCP) is the only one that is both reusable and open: any compliant client can talk to any compliant server, no matter who built either.

In short: These aren't competitors — a real system uses function calling as the mechanism and MCP as the distribution model, and skips vendor plugins to avoid the lock-in.

ApproachScopeReusable across clients?
Function callingone app's tools, in-codeno — app-specific
Vendor pluginsone vendor's ecosystemonly that vendor
MCPany client ↔ any serveryes — open standard

3 · Intermediate — the architecture intermediate

Here's where beginners tie themselves in knots: the word "server" makes people picture a machine in a data center, and "client" makes them picture a browser. Drop that image — in MCP these words describe roles in a conversation, not big remote boxes. An MCP server is often just a small script running on your own laptop; an MCP client is a component living inside the AI app you're already using. Once you see them as two programs talking, the architecture stops being intimidating.

The clean mental model has three named parts. The host is the AI application the human interacts with — Claude Code, a chat UI, your own agent. The host embeds one or more clients, and each client holds a single connection to one server. The server is the program that actually owns a capability: it wraps a filesystem, a database, GitHub, whatever. So the shape is one host → many clients → one server each. Why split "host" from "client" at all? Because the host can juggle several servers at once (one for your files, one for your database, one for Slack), and each gets its own dedicated client connection — like a restaurant host who seats you while separate waiters handle separate tables.

The sequence matters, and it's the part to actually remember. When a client connects to a server it doesn't immediately start calling things — it first discovers what the server offers: its tools, its resources, its prompts (the three primitives you'll meet next). The client collects those advertised capabilities and hands the model a menu. Then, when the model decides to use something, the client routes that call to the server that owns it and relays the result back. Discover, then route — that two-step is the entire runtime loop. The common mistake is to imagine the model talking directly to the server; it never does. The model only ever produces a request ("call create_ticket with these args"), and the client is the intermediary that knows which server that request belongs to. Hold onto that separation — it's exactly what the capability-discovery section builds on next.

MCP is client-server. The host app (Claude Code) runs an MCP client that connects to one or more servers. On connect, the client discovers each server's tools/resources/prompts, then routes the model's calls to the right server.

one host → many clients → one server each host (the AI app) Claude Code · chat UI · your agent client A client B client C files serverfilesystem db serverdatabase slack serverSlack each client holds exactly one dedicated connection to its server Roles in a conversation, not big remote boxes. The one host embeds a separate client per server, and each client owns a single connection — so the host can juggle files, a database, and Slack at once.
🗺️ How to read this diagram

Read it top-to-bottom: one app at the top fans out into clients, and each client drops down to exactly one server.

  • The top box is the host — the AI application the human actually uses (Claude Code, a chat UI, your own agent).
  • The middle blue row is the clients the host embeds — one per server. They are components inside the host, not separate machines.
  • The bottom green row is the servers, each owning one capability (a filesystem, a database, Slack). A server is often just a small script on your own laptop.
  • Each vertical arrow is a single dedicated connection: one client ↔ one server. That's why the host can talk to many servers at once without them interfering.

In short: One host → many clients → one server each; "server" and "client" are roles two programs play in a conversation, not big remote boxes.

4 · Advanced — capability discovery advanced

Imagine plugging in a new keyboard and having to first tell your operating system every key it has. Absurd — the device announces itself, and the OS just knows. That self-describing handshake is exactly what MCP does with tools, and it's the property that separates MCP from the hand-wired world of Ch 4. There, you hard-coded which tools existed and where they lived. With MCP, the client asks and the servers answer. That question-and-answer is called capability discovery, and it's the reason the protocol can grow without anyone editing the app.

Concretely: when a client connects to a server, it sends a "list your capabilities" request, and the server replies with its tools, resources, and prompts — each with a name and a machine-readable schema describing its arguments. The client does this for every server it's connected to, then merges the answers into a single combined menu. The most useful shape that merge produces is a map from each tool's name to the server that owns it — a routing table. When the model later says "call create_ticket," the client looks up create_ticket in that table, sees it belongs to the tickets server, and sends the call there. No branching logic hard-coded per tool; just a dictionary lookup.

This is precisely why the lesson calls MCP composable. "Composable" means you can add new pieces and the whole simply grows to include them, with no rewiring. Connect a fourth server and its tools automatically appear in the merged menu — the model can use them on the next turn without a single line of app code changing. That's the payoff of discovery: capability becomes data the client gathers at runtime, not code the developer commits ahead of time.

One honest wrinkle to keep in the back of your mind, because the lab that follows will expose it: merging by tool name assumes names are unique. If two servers both advertise a tool called search, a naïve merge silently lets one clobber the other, and calls misroute. Real MCP clients guard against this — typically by namespacing tools per server (so you get github.search and jira.search rather than a bare search) or by an explicit precedence rule. The point isn't the exact fix; it's that discovery is a merge, and every merge has to decide what happens on a collision. Watch for that as you read the code — it's the difference between a toy example and a router you'd trust in production.

Discovery is what makes MCP composable: a client asks each server "what can you do?" and merges the capabilities. Model it.

servers advertise docs search_docs tickets create_ticketlookup_ticket client asks "what can you do?" client merges tools/list one routing table search_docs → docs create_ticket → tickets lookup_ticket → tickets model says "call create_ticket" → client looks it up → routes to the tickets server connect a 4th server and its tools just appear — composable: capability is data gathered at runtime Discovery builds a routing table, not hard-coded branches. Each server advertises its tools; the client merges them into a tool-name → owning-server map, then routes every call by a dictionary lookup.
🗺️ How to read this diagram

Follow the flow left to right: servers announce what they offer, the client merges those answers, and the result is a lookup table.

  • The green boxes on the left are two servers, each advertising the tools it owns — docs offers search_docs; tickets offers create_ticket and lookup_ticket.
  • The purple middle box is the client running discovery (a tools/list request to each server) and merging every answer into one menu.
  • The right column is the payoff: a routing table mapping each tool name to its owning server. When the model says "call create_ticket," the client just looks up the row and routes there — no per-tool branching code.
  • The amber band is why this is called composable: connect a fourth server and its tools appear in the table automatically, with no app code changed.

In short: Discovery turns "which server owns this tool?" into a dictionary lookup that grows itself as you add servers.

Python · merge capabilities from multiple servers (runs)
discovery.pydef discover(servers):
    """servers: {name: {tools:[...], resources:[...]}}. Merge into one capability map."""
    tools, resources = {}, {}
    for name, cap in servers.items():
        for t in cap.get("tools", []): tools[t] = name        # tool -> which server
        for r in cap.get("resources", []): resources[r] = name
    return tools, resources

servers = {
    "docs":    {"tools": ["search_docs"], "resources": ["kb"]},
    "tickets": {"tools": ["create_ticket", "lookup_ticket"], "resources": ["queue"]},
}
tools, res = discover(servers)
print("available tools:", tools)
print("create_ticket lives on:", tools["create_ticket"])
available tools: {'search_docs': 'docs', 'create_ticket': 'tickets', 'lookup_ticket': 'tickets'}
create_ticket lives on: tickets
▶ How this works

This models capability discovery — the moment an MCP client connects to several servers and asks each one "what can you do?", then combines the answers into a single menu. It's how one AI app can offer tools that actually live in many different servers, without you hard-coding where each tool is.

  1. discover(servers) takes a dictionary describing each server: the key is the server's name, and the value lists the tools and resources it offers. (A tool is an action the model can call; a resource is data it can read.)
  2. Inside, two empty dictionaries tools and resources are the combined menu. The loop visits every server, and for each tool or resource it records which server owns it — that's what tools[t] = name does (tool name → server name).
  3. The servers example has a docs server (one search tool) and a tickets server (two tools). After calling discover, the code prints the merged tool list and then looks up who owns create_ticket.
  4. That final lookup is the payoff: given just a tool name, the client instantly knows which server to route the request to — no matter how many servers are connected.

What the output means: First line prints the merged menu — three tools, each mapped to its home server. Second line answers the routing question: create_ticket lives on the tickets server. That name→server map is exactly what a real MCP client builds on connect.

Try this: Add a third server, e.g. "github": {"tools": ["open_pr"], "resources": []}, and rerun. Notice the merged menu grows automatically — that's why MCP is called composable: new servers just add to the shared menu.

5 · Professional — the growing ecosystem professional

The N+M math has a consequence people miss on first read: that "M" doesn't all have to be built by you. Every server anyone else publishes is one you don't have to write. This is where MCP stops being a clever protocol and starts being a leverage multiplier — because it's open, other teams and companies have already wrapped the common systems as servers, and their work drops straight into your stack.

The analogy is package managers. Before pip or npm, you wrote (or copied) every utility yourself; after them, you install a battle-tested library and move on to your actual problem. An open MCP ecosystem does the same thing for AI integrations. Need your agent to touch the filesystem, query Postgres, read GitHub issues, or post to Slack? Rather than building and maintaining that connector — auth, error handling, schema, the lot — you connect an existing community server that already speaks MCP. Your team's scarce time goes to the integrations that are genuinely yours, the ones no one else can build because they wrap your proprietary systems.

Read this section as the shift from author to assembler. At the professional level the interesting decision is no longer "how do I write a connector" but "which of these do I build versus reuse." Anything already in the ecosystem is an N+M reuse — plug it in. Anything unique to your organization is a server you expose once, and from that moment every AI tool in your company can use it without a second integration. The mistake at this stage is reflexively rebuilding what already exists (a filesystem server, say) out of habit or mistrust; the discipline is to reuse aggressively and reserve your engineering for the systems only you have. That reuse-versus-build judgment is what the tech-lead section turns into an actual org strategy.

MCP is open, so an ecosystem of servers exists for common systems (filesystems, databases, GitHub, Slack, etc.). Adopting it means reusing community servers and exposing your own systems once for every AI tool your org uses — rather than rebuilding integrations per app.

at the professional level: which servers to reuse vs build reuse from the ecosystem files Postgres GitHub Slack Drive already published — pip-install style build once yourself your proprietary systems internal billing · custom CRMin-house data expose once → every AI tool in the org uses it author → assembler: reuse aggressively, build only what only you have You don't have to build all the M. Common systems are reused from the open ecosystem (green); your engineering time goes only to the proprietary servers no one else can write (purple).
🗺️ How to read this diagram

Read the two sides of the divider as the one decision this level is about: reuse or build.

  • The green boxes on the left are common systems the community has already wrapped as MCP servers — filesystem, Postgres, GitHub, Slack, Drive. You install and connect them like a library, not write them.
  • The purple box on the right is the work that stays yours: servers for your proprietary systems (internal billing, a custom CRM) that no one else can build.
  • The payoff line for the purple side: you expose each proprietary system once, and from then on every AI tool in your org can use it — no second integration.
  • The bottom band names the shift: from author (writing every connector) to assembler (reusing aggressively, reserving effort for what's unique).

In short: Reuse the ecosystem's servers for common systems; spend your engineering only on the ones unique to your org.

6 · Tech-lead — an org MCP strategy tech-lead

Here's the failure mode that turns MCP from an asset into a liability, and it happens fast. The protocol makes connecting a server so easy that, left ungoverned, every engineer wires up whatever they find. Six months later you have forty ad-hoc connections, three teams that each built the same "internal-docs" server, an unvetted community server someone pointed at production, and zero record of which AI app touched which system when. The very ease that made MCP attractive has recreated the sprawl — this time with a security surface attached. A tech lead's job is to get the N+M leverage without that chaos.

The answer is to treat MCP servers the way a mature org treats any shared infrastructure: as a curated, governed catalog rather than a free-for-all. Concretely, a lead decides four things. First, what to expose — which internal systems become servers, built once and shared, so no team rebuilds them. Second, what to trust — which community servers are vetted and approved, because an MCP server is code you're granting tool access to, and a malicious or sloppy one is a real threat (that vetting connects to the safety work in C4). Third, discoverability — a registry so teams can find the approved servers instead of reinventing them. Fourth, governance — who may call which tools, and an audit trail of every call.

The cleanest way to enforce all four at once is a gateway: a single chokepoint that every client connects through, which registers the approved servers, checks a per-client allowlist before routing a call, and logs the outcome. That one pattern gives you the N+M wiring plus centralized authentication, least-privilege access, collision checks, and audit — the difference between "we use MCP" and "we operate MCP safely." The strategic mistake to avoid is treating high-blast-radius tools (a run_sql against production, say) as ordinary capabilities granted by default; in a governed setup those are granted narrowly, to the few clients that genuinely need them, and denied-by-default to everyone else. The table below contrasts the ungoverned world with the governed one — read it as the before-and-after of applying exactly this thinking.

A lead decides the org's MCP posture: which internal systems to expose as servers, which community servers to trust (vetting them — C4), a registry so teams discover them, and governance (access, audit). This turns scattered integrations into shared, governed infrastructure.

one gateway = the single governed chokepoint support-bot coding agent analyst app MCP gateway registry · allowlist auth · audit log tickets docs run_sql (prod) denied by default granted calls route through (blue); ungranted high-blast-radius calls are blocked + logged (red) The gateway is where governance lives. Every client connects through one chokepoint that registers approved servers, checks a per-client allowlist, and logs every call — so N+M wiring comes with auth, least-privilege, and audit.
🗺️ How to read this diagram

Follow a call left to right: it leaves a client, hits the gateway, and only reaches a server if the allowlist permits it.

  • The left blue boxes are the org's AI clients — a support bot, a coding agent, an analyst app.
  • The purple box in the middle is the gateway: the single point every client routes through. It holds the registry of approved servers, a per-client allowlist, auth, and an audit log.
  • The right column is the servers. Solid blue arrows are granted calls that route through; the dashed red arrow to the red run_sql (prod) box is a high-blast-radius call blocked because that client wasn't granted it — and the block is still written to the audit log.
  • The colours say it plainly: blue = allowed and routed, red = denied by default.

In short: One gateway gives you the N+M wiring plus centralized auth, least-privilege allowlists, collision checks, and audit — the difference between "we use MCP" and "we operate MCP safely."

Without a strategyWith an org MCP strategy
each team rebuilds integrationsbuild each server once, shared
untrusted servers connected ad-hoca vetted, registered catalog
no visibility into tool usecentral audit + access control
Deep MCP engineering is in K6This chapter is the ecosystem/architecture view; the hands-on advanced server work (sampling, transports, roots, robustness) is K6 · MCP advanced. Together they take you from "why a protocol" to production servers.

Exercise XT3.1 — Map your integrations to MCP

Context: The lesson lands when you apply the N+M math to your own world instead of a toy example — deciding what to build as servers and what to reuse from the ecosystem.

Your task: List the tools and data an AI app in your world needs, compute the custom-vs-MCP integration count, and sketch which pieces you'd build as servers versus reuse, then show how a client would see them merged.

Requirements:

  • Enumerate the concrete tools/data sources your app would integrate
  • Use the N×M vs N+M helper to compute both integration counts for your numbers
  • Decide, per tool, whether you'd build an MCP server or reuse an existing one
  • Use the discovery shape to show how a client would see your servers merged into one capability map

💡 Hint: Anything already offered by the ecosystem is an N+M reuse, not a new server you have to build and maintain.

🪜 Practice ladder beginner → industry

Six graded exercises, easy to real-world. Try each before opening its solution.

Exercise 1 · The N×M vs N+M integration math (offline)Beginner

Context: MCP's core value is integration math: a shared protocol turns N clients × M tools of bespoke connectors into N+M standard ones. Building a tool once as a server lets any client speak to it.

Your task: Reproduce integrations(n, m, with_protocol) and show the savings for 5 clients and 8 tools.

Requirements:

  • Without a protocol, return n × m (every app wired to every tool)
  • With a protocol, return n + m (each side wires to the standard once)
  • Stdlib only — a couple of arithmetic branches
  • Print both counts and the number of integrations saved for n=5, m=8
  • Note the savings grow super-linearly as the ecosystem grows

💡 Hint: The two cases are just a multiply versus an add; the whole point is contrasting the 40 against the 13.

Show solution

Runnable, stdlib only:

def integrations(n_clients, m_tools, with_protocol):
    if with_protocol:
        return n_clients + m_tools     # each side wires to the standard once
    return n_clients * m_tools         # every app -> every tool

n, m = 5, 8
print(integrations(n, m, False))       # 40  (N x M explosion)
print(integrations(n, m, True))        # 13  (N + M)
print("saved:", integrations(n, m, False) - integrations(n, m, True))  # 27

Build a tool once as an MCP server; any client speaks to it. The savings grow super-linearly as the ecosystem grows.

Exercise 2 · Merge capabilities discovered from multiple servers (offline)Intermediate

Context: Discovery is what makes MCP composable: a client asks each server "what can you do?" and merges the advertised tools and resources into one map it can route against.

Your task: Reproduce discover(servers) that merges the tools and resources advertised by multiple servers into unified maps, and run it over two servers.

Requirements:

  • Input is a mapping of server name to its tools and resources lists
  • Build one map from each tool name to its owning server, and one for resources
  • Stdlib only; iterate the servers and accumulate into dicts
  • Run it over two example servers and print both merged maps
  • Frame the merged tool map as a routing table: tool name → which server to call

💡 Hint: A dict keyed by tool name with the server as the value is exactly the routing table the next rung consumes.

Show solution

Runnable, stdlib only:

def discover(servers):
    """servers: {name: {tools:[...], resources:[...]}}. Merge into one map."""
    tools, resources = {}, {}
    for name, cap in servers.items():
        for t in cap.get("tools", []):     tools[t] = name
        for r in cap.get("resources", []): resources[r] = name
    return tools, resources

servers = {
    "docs":    {"tools": ["search_docs"],                 "resources": ["kb"]},
    "tickets": {"tools": ["create_ticket", "lookup_ticket"], "resources": ["queue"]},
}
tools, resources = discover(servers)
print(tools)      # {'search_docs':'docs','create_ticket':'tickets','lookup_ticket':'tickets'}
print(resources)  # {'kb':'docs','queue':'tickets'}

The merged map is a routing table: given a tool name, the client knows which server to call.

Exercise 3 · Route a model's tool call to the owning server + detect collisions (offline)Advanced

Context: Once you have a merged capability map you need to route a model's tool call to the owning server — and handle the failure mode where two servers export the same tool name.

Your task: Extend discovery into a router: write route(tools, name) that returns the owning server and detect collisions where two servers export the same tool name. Runnable offline.

Requirements:

  • discover() records a collision when a second server exports an already-seen tool name
  • route() returns the owning server for a known tool
  • route() raises (e.g. KeyError) for an unknown tool
  • Show a clean route succeeding and a collision being reported for a shared name like search
  • Note that namespacing (e.g. gh.search vs jira.search) or precedence resolves collisions

💡 Hint: Detect the collision at merge time by checking whether the tool name is already in the map before assigning it.

Show solution

Runnable, stdlib only. Real MCP namespaces tools per-server; here we model the collision so you handle it explicitly:

def discover(servers):
    tools, collisions = {}, {}
    for name, cap in servers.items():
        for t in cap.get("tools", []):
            if t in tools:                       # two servers export same name
                collisions.setdefault(t, [tools[t]]).append(name)
            else:
                tools[t] = name
    return tools, collisions

def route(tools, name):
    if name not in tools:
        raise KeyError(f"no server exports tool {name!r}")
    return tools[name]

servers = {
    "gh":    {"tools": ["search", "create_issue"]},
    "jira":  {"tools": ["search", "create_ticket"]},   # 'search' collides
}
tools, collisions = discover(servers)
print(route(tools, "create_issue"))   # gh
print("collisions:", collisions)      # {'search': ['gh', 'jira']}

Namespacing (e.g. gh.search vs jira.search) or explicit precedence resolves collisions before they silently misroute a call.

Exercise 4 · A minimal MCP server exposing one tool (needs the SDK)Expert

Context: A real MCP server exposes tools over a standard transport so any compliant client can discover and call them. The SDK derives each tool's schema from your function's type hints and docstring.

Your task: Write a minimal MCP server with the Python SDK that exposes a single add tool over stdio. Needs the SDK (pip install "mcp[cli]").

Requirements:

  • Use the SDK's high-level FastMCP server with a display name
  • Decorate a typed add(a: int, b: int) -> int function as a tool
  • Let the docstring and type hints become the tool schema clients discover
  • Run the server over the stdio transport
  • Explain that on connect the client calls tools/list and later tools/call to invoke the function

💡 Hint: The @mcp.tool() decorator plus type hints is all the schema you need to write by hand.

Show solution

Needs the SDK (mcp, the official Model Context Protocol SDK). Uses FastMCP, the SDK's high-level server:

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("calc")          # server name shown to clients on discovery

@mcp.tool()
def add(a: int, b: int) -> int:
    """Add two integers."""    # docstring + type hints become the tool schema
    return a + b

if __name__ == "__main__":
    mcp.run(transport="stdio") # stdio is the standard local transport

On connect, the client calls tools/list and sees add with a schema derived from the type hints; when the model chooses it, the client sends tools/call and the SDK runs your function.

Exercise 5 · A minimal MCP client that lists + calls tools (needs the SDK)Professional

Context: The other half of MCP is the client: it launches a server, runs discovery, and calls a tool by name — speaking the protocol once so it can drive any compliant server. This is the N+M win in code.

Your task: Write a minimal MCP client that launches the server over stdio, lists its tools (discovery), and calls add. Needs the SDK.

Requirements:

  • Use the SDK's stdio client transport and ClientSession (async)
  • Point the transport at the server command and its script arguments
  • Follow the handshake: initialize, then list_tools, then call_tool
  • Print the discovered tool names and the result of calling add
  • Note the client speaks the protocol once and can drive any compliant server

💡 Hint: Everything runs inside nested async with contexts and an asyncio.run entry point.

Show solution

Needs the SDK. Uses the SDK's ClientSession + stdio transport (async):

import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

async def main():
    params = StdioServerParameters(command="python", args=["calc_server.py"])
    async with stdio_client(params) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()
            tools = await session.list_tools()          # discovery
            print([t.name for t in tools.tools])        # ['add']
            result = await session.call_tool("add", {"a": 2, "b": 3})
            print(result.content[0].text)               # '5'

asyncio.run(main())

This is the N+M win in code: the client speaks the protocol once and can drive any compliant server — initialize then list_tools then call_tool.

Exercise 6 · Design an org MCP strategy: gateway, auth, and least-privilege (offline reasoning + code)Industry scenario

Context: As a tech lead you must connect many internal tools to many AI clients without an N×M mess or a security hole. The answer is one gateway that also becomes the single place for auth, allowlists, and auditing.

Your task: Model an MCP gateway that registers servers, enforces a per-client allowlist of tools, and audits every call. Runnable offline; the registry stands in for real MCP sessions.

Requirements:

  • A register(server, tools) builds a tool → server registry and rejects name collisions
  • A grant(client, tools) records a per-client allowlist (least privilege)
  • A call(client, tool, args) denies unknown tools and any tool not granted to that client
  • Every call appends an OK/DENIED record to an audit trail
  • Demonstrate a granted call succeeding and a high-blast-radius tool (run_sql) being blocked for a client not granted it
  • Frame the strategy: one gateway = N+M wiring plus centralized auth, allowlists, collision checks, and audit

💡 Hint: Check membership in the client's granted set before touching the registry so a denied call never routes anywhere and still lands in the audit log.

Show solution

Runnable offline — models the governance layer you'd put in front of real MCP servers:

class MCPGateway:
    def __init__(self):
        self.registry = {}       # tool_name -> server_name
        self.allow = {}          # client -> set(tool_name)
        self.audit = []

    def register(self, server, tools):
        for t in tools:
            if t in self.registry:
                raise ValueError(f"tool name collision: {t}")
            self.registry[t] = server

    def grant(self, client, tools):
        self.allow[client] = set(tools)

    def call(self, client, tool, args):
        if tool not in self.registry:
            raise KeyError(f"unknown tool {tool!r}")
        if tool not in self.allow.get(client, set()):     # least privilege
            self.audit.append((client, tool, "DENIED"))
            raise PermissionError(f"{client} may not call {tool}")
        self.audit.append((client, tool, "OK"))
        return f"routed {tool} -> {self.registry[tool]}({args})"

gw = MCPGateway()
gw.register("tickets", ["create_ticket"])
gw.register("prod-db", ["run_sql"])
gw.grant("support-bot", ["create_ticket"])   # NOT run_sql
print(gw.call("support-bot", "create_ticket", {"title": "reset"}))
try:
    gw.call("support-bot", "run_sql", {"q": "DROP TABLE users"})
except PermissionError as e:
    print("blocked:", e)
print("audit:", gw.audit)

Strategy: one gateway = N+M wiring plus a single place for auth, per-client allowlists (least privilege), collision checks, and an audit trail. High-blast-radius tools (run_sql) are granted narrowly, never by default.

✓ Checkpoint — you can move on when you can…

  • Explain why a protocol beats N×M custom integrations.
  • Distinguish MCP from function-calling/plugins.
  • Describe the client-server architecture + discovery.
  • Outline an org MCP adoption strategy.

Knowledge check check yourself

✓ Knowledge check

The lesson frames MCP as turning an N×M problem into N+M. What does each side of that comparison mean, and why does the protocol scale better?

Show answer
Without a standard, N apps each need a custom connector to M tools — N×M bespoke integrations to build and maintain (5 apps × 8 tools = 40). With MCP each app and each tool implements the shared contract once, so it's N+M (5+8 = 13). A shared standard replaces multiplication with addition, and the gap widens as you add more of either side.
✓ Knowledge check

What is capability discovery in MCP, and why does the lesson call it what makes MCP "composable"?

Show answer
On connect, an MCP client asks each server "what can you do?" and merges the tools/resources into one capability map (tool name → owning server), so it can route each call to the right server. It's composable because adding a new server just adds its tools to the shared menu automatically — no hard-coding where each tool lives.
© 2026 studybydoing.in · AI Engineering: Zero to Production · All rights reserved. · About · Privacy Policy · Terms · Contact
Educational content, provided as-is and without warranty. Code samples are examples — review, test, and adapt them before using in production. See the Terms of Use & Disclaimer. Use at your own risk.
© studybydoing.in