~10–14 hrs
Prereq skills: Projects 05, 06, 08
Elective · Advanced · off-spine

MCP Interface Layer

Stop hand-wiring tools into one loop. Turn a subsystem into a reusable protocol surface that many hosts can consume.

📖 Read the lesson ✍ Fill in understanding 🛠 Wrap P05 as a server 🔌 Consume from two hosts 🚫 Break the trust boundary

Learning Objectives

By the end of this elective, you will be able to:

  • Explain MCP as an interface protocol, not an agent framework — it standardizes how an AI app obtains context and actions, and says nothing about the loop, prompt, or model.
  • Distinguish host, client, and server ("one client per server") and say where the agent loop now lives.
  • Distinguish tools vs. resources vs. prompts by their control model (model- / application- / user-controlled) and pick the right primitive for a capability.
  • Explain stdio vs. Streamable HTTP transport conceptually — local single-client vs. remote multi-client + auth — and why the same surface runs over either.
  • Define JSON input schemas that are the contract the model sees, and validate against them.
  • Implement safe handlers for memory search/save, expose entries as resources, and provide one reusable prompt.
  • Consume the same server from two hosts — proving the M×N → M+N decoupling.
  • Analyze the trust boundary: untrusted arguments and URIs crossing a process boundary, and the failure modes (URI escape, unbounded inputs, stdout corruption, over-broad capability).

1. Motivation

Why This Exists

In Projects 06, 08, and 09 you built tools the only way you knew how: a Python function (read_file, search_memory), a hand-written JSON schema beside it, and a dispatch block inside your agent loop. It worked — but every one of those tools is trapped inside the app that defined it. Your copilot's search_memory can't be used by your agent without copy-pasting it; neither can be used by Claude Desktop or a teammate's app at all. You've built the same handful of capabilities several times, once per app, because there was no shared way to expose a capability so any AI application could consume it.

That missing shared way is the Model Context Protocol (MCP): an open, client-server protocol that standardizes how an AI application ("host") obtains context and actions from an external program ("server"). MCP "focuses solely on the protocol for context exchange — it does not dictate how AI applications use LLMs or manage the provided context" mcp-architecture.md. It is a plug, not a brain. This elective's single new idea is the provider/consumer split: stop hand-wiring tools into a loop; turn a capability into a server behind a standard surface that many hosts consume through their own clients.

The Core Problem (M×N → M+N)

Before a standard, connecting M AI applications to N capabilities is an M×N problem: every app needs a bespoke adapter for every capability. With one protocol, each host speaks MCP once and each capability is exposed once — the integration surface collapses to M+N. (This is the standard engineering articulation of the documented "any server, any host" property — an interpretation, not a verbatim protocol claim.)

What Breaks Without It

Without a protocol you live in the M×N world: capabilities are duplicated per app, drift out of sync, and can't be shared or swapped. A vendor who wants their service usable from "any AI app" must write and maintain one integration per app. And inside a single team, the memory system you spent Project 05 building stays locked to the one script that imported it — the moment a second app wants memory, you copy code.

Startup Lens — StarcallOS

A personal OS is exactly a host that wants to consume many capabilities — memory, calendar, files, search — without re-implementing each. MCP is how StarcallOS would expose its memory once and consume third-party capabilities without bespoke glue. The capability is the commodity; the reusable, governed interface is the leverage.

2. Mental Model

Explain Like I'm 12

Imagine every plug and socket in your house was a different shape — the toaster, the lamp, the charger each needed its own special outlet, and buying a new lamp meant rewiring the wall. That's how AI apps used to talk to tools. Then someone invented a standard plug (think USB-C): the wall doesn't care what you plug in, the device doesn't care which wall it's in — as long as both speak the standard, they just work. MCP is that standard plug for AI. Your memory system becomes a device with the plug; any AI app with the socket can use it, and you added the plug once. The plug carries three things: buttons the AI can press (tools), things the AI can read (resources), and ready-made question cards (prompts). You already built the memory gadget — your job is to put the right plug on it.

Explain Like I'm a Software Engineer

  • MCP is a JSON-RPC 2.0 protocol with two layers: the data layer (lifecycle/capability negotiation + the primitives tools/resources/prompts) — the surface you design — and the transport layer (stdio or Streamable HTTP) — how bytes move. "The transport layer abstracts communication details … enabling the same JSON-RPC 2.0 message format across all transport mechanisms" mcp-architecture.md.
  • Three participants: host (the AI app — coordinates clients), client (one per server, a dedicated connection), server (the program providing context — what you build).
  • A connection opens with an initialize handshake doing capability negotiation — each side declares what it supports before any call. Then the client discovers with */list and executes with tools/call / reads with resources/read.
  • The shift from P06/P08: you are no longer writing the loop that calls tools. You are writing the tools (and resources and prompts) that any loop can call. The loop is the host's program now.

Real-World Analogy

Analogy — the device-driver / USB model

Before standardized drivers, every printer shipped bespoke software for every OS — an M×N mess. A driver model + a bus standard (USB) inverted it: the OS speaks the bus once; each device ships one driver exposing a standard interface (enumerate, read, write). Map it: USB/driver contract → MCP; operating system → host; the USB controller, one per device → client; a peripheral → server (your memory); device enumeration → capability negotiation + */list; plugging a printer into two laptops → consuming one server from two hosts. The analogy carries the trust boundary, too: a USB device can be malicious (BadUSB), so the OS mediates and the user grants access — exactly how a host gates an MCP server's tools.

How It Works (Diagram)

 HOST (Claude Desktop / your agent / the smoke-test client)
   │  owns the loop, the prompt, the model
   │  creates ONE client per server
   ▼
 CLIENT ──── stdio / Streamable HTTP ────►  SERVER  (you build this)
   │  initialize        (capability negotiation: "I do tools + resources")
   │  tools/list        (discovery)                 │
   │  tools/call ───────────────────────────────►  ├─ memory_search / memory_save   (TOOLS, model-controlled)
   │  resources/read ──────────────────────────►   ├─ memory://entries/{id}         (RESOURCES, app-controlled)
   │  prompts/get ─────────────────────────────►   └─ reflect_on(topic)             (PROMPT, user-controlled)
   ▼                                                       │
   answer                                          MemoryBackend  (Project 05 — the wrapped capability)

 TRUST BOUNDARY = the arrows INTO the server: every argument + URI is untrusted.

3. Technical Explanation

Formal Definition

MCP is a client-host-server protocol over JSON-RPC 2.0 with stateful sessions and capability negotiation mcp-architecture.md. A host aggregates context and enforces consent/security; it creates one client per server; a server exposes focused capabilities and "should not see the whole conversation or into other servers." The data layer defines the primitives; the transport layer (stdio / Streamable HTTP) carries them — the same surface over either.

The Three Server Primitives

PrimitiveWhat it isWho drives itMCP methods
ToolAn executable function that performs an actionModel decides to call (host gates w/ user approval)tools/list, tools/call
ResourceA data source providing context, addressed by URIApplication reads it as contextresources/list, resources/read
PromptA reusable, parameterized message templateUser invokes itprompts/list, prompts/get

In this project: memory_search/memory_save are tools (the model acts); a stored entry is read-only context, so it's a resource (memory://entries/{id}); reflect_on is a prompt a user invokes. Choosing the right primitive by its control model is a graded design decision.

Transports

TransportWhereClientsAuth
stdiolocal subprocess, stdin/stdouttypically onenone (same machine)
Streamable HTTPremote, HTTP POST + optional SSEtypically manybearer / OAuth

The same server runs over either with a one-line change (mcp.run(transport="stdio")). The data/transport split is the spine of the lesson: what you expose is independent of how it's carried.

How It Works Step by Step

  1. Negotiate. The host's client sends initialize; the server declares its capabilities (tools, resources, prompts). Unsupported operations are never attempted.
  2. Discover. The client calls tools/list / resources/list / prompts/list. Each tool advertises name, description, and an inputSchema (JSON Schema) — "the description is the API" (Project 06), now portable.
  3. Execute. When the model chooses a tool, the client sends tools/call with arguments; the server validates, runs the handler, returns a content array.
  4. Validate at the boundary. Arguments and URIs come from a model in someone else's host — untrusted. The schema is advisory; the handler (via security.py) enforces bounds and contains URIs, failing closed.
  5. Log to stderr. On stdio the protocol is stdin/stdout; a stray print corrupts the JSON-RPC stream and breaks the server.
The #1 footgun

"For STDIO-based servers: Never write to stdout. Writing to stdout will corrupt the JSON-RPC messages and break your server" mcp-build-server.md. Use print(..., file=sys.stderr) or logging (which defaults to stderr).

4. Worked Examples

Simple Case — one tool, one host

Expose memory_search only; connect the programmatic client; initializelist_toolscall_tool. The client never imports the backend — it learns the tool from discovery and calls it over the protocol. The consumer is fully decoupled from the provider.

@mcp.tool()
def memory_search(query: str, k: int = 5) -> str:
    """Search the personal memory store; return the top-k most relevant entries."""
    return memory_tools.handle_search({"query": query, "k": k}, backend)["text"]
mcp.run(transport="stdio")   # the loop lives in the HOST, not here

Real-World Case — tools + resources + prompt, two hosts

Add memory_save, expose entries as memory://entries/{id} resources, and a reflect_on prompt. Run the same server from (a) the stdio smoke-test client and (b) Claude Desktop or the MCP Inspector. Save a note in one session; read it as a resource in the other. One governed surface, multiple independent consumers — the M+N collapse, made concrete.

# consumer #1 — a programmatic stdio client
async with stdio_client(StdioServerParameters(command="python", args=["server.py"])) as (r, w):
    async with ClientSession(r, w) as session:
        await session.initialize()                 # capability negotiation
        print(await session.list_tools())          # discovery
        await session.call_tool("memory_save", {"text": "demo on June 20"})
        print(await session.call_tool("memory_search", {"query": "demo"}))
// consumer #2 — Claude Desktop (claude_desktop_config.json), no code
{ "mcpServers": { "personal-memory": {
    "command": "python", "args": ["C:\\ABSOLUTE\\PATH\\TO\\code\\server.py"] } } }

Failure Case — the trust boundary

Send memory_search a k of 10,000,000 and a 5 MB text to memory_save; request memory://entries/../../etc/passwd and file:///secret. A naive handler hangs, stores junk, or escapes the namespace. With security.py validating bounds and containing the URI (scheme + id allow-list), each is rejected and fails closed. Across a protocol boundary, every input is hostile until validated.

5. Reflection

Answer these in UNDERSTANDING.md before writing code (and revisit them after). Write in your own words.

Knowledge Check

  1. Why is "MCP is a protocol, not an agent framework" the most important framing here? What did you stop writing that you wrote in P06/P08?
  2. Explain host vs. client vs. server to someone who knows P08 but not MCP. Where did the agent loop go?
  3. "The current weather," "my saved notes," "a draft-a-summary template" — which is a tool, which a resource, which a prompt, and why (use the control models)?
  4. Why does the same server run unchanged over stdio and Streamable HTTP? What does change, and when would you pick each?
  5. Where exactly is the trust boundary in your server? Name two inputs that cross it and the check that guards each.
  6. Restate the M×N → M+N argument with your memory server as the example. Who are the M and the N?
  7. The schema/description "is the API." How is that claim stronger here than in Project 06?
  8. What did wrapping P05 (rather than rebuilding memory) teach you about the difference between a capability and an interface to a capability?

6. Project Assignment

See PROJECT.md and source/project.md for the full specification including the file-by-file spec and extended requirements.

Core Requirement

Wrap Project 05's memory system as an MCP server, in code/. The backend and the SDK/transport wiring are provided; the learning target is the protocol surface:

  • memory_tools.pyTOOL_DEFINITIONS (the JSON schemas) + handle_search / handle_save (M1–M2) are the learner core.
  • security.py — the three validators (M1–M3): bound every input, contain the resource URI, fail closed.
  • memory_resources.pylist_resources + read_resource (M3): expose entries as memory://entries/{id}.
  • prompts.pyPROMPT_DEFINITIONS + get_prompt (M4): one reusable reflect_on prompt.
  • memory_backend.py — the provided Project 05 memory store being wrapped (reference — complete).
  • server.py / client_smoke_test.py — the provided FastMCP/stdio adapter + consumer #1 (no loop in the server).

Extended: run the same server over Streamable HTTP; expose memory://search/{query} as a dynamic resource; flip the backend to real provider embeddings; point your Project 08 agent at the server as a third consumer; add elicitation confirmation on memory_save.

Definition of Done

The learner modules are SDK-agnostic; memory_search/memory_save have JSON schemas (required + bounds + a kind enum) and the handlers re-validate; entries are exposed as resources with URI containment; one prompt injects recalled memories; the server logs to stderr; two consumers of the one server are demonstrated (save in one, read in the other); guiding tests pass offline; UNDERSTANDING.md done before any code; FAILURE_ANALYSIS.md has ≥3 experiments (incl. stdout corruption + a trust-boundary breach); EVALUATION.md records the two-consumer demo; STARCALLOS_REFLECTION.md names ≥1 capability to expose via MCP.

Start building

Open code/README.md for setup, the milestone build order, and the file roles (which files are provided vs. learner-owned). Run python -m pytest to see the failing guiding tests, then implement the learner-owned functions in milestone order until they pass.

7. Project Milestones

Work through these in order. Each should produce runnable/inspectable progress before moving on.

1

M1 — Tool schemas + the search handler

Define the inputSchema for memory_search/memory_save (types, required, bounds, kind enum) and implement handle_search + validate_search_args. Validation: tests/test_tools.py + test_security.py search cases pass; a malformed arg is rejected.

2

M2 — Save handler + the trust boundary

Implement handle_save and the security.py bounds (clamp k, cap text, range-check importance, whitelist kind). Validation: valid + hostile inputs; save→search round-trips (offline).

3

M3 — Resources

list_resources + read_resource expose entries as memory://entries/{id}, with URI containment. Validation: list + read one by URI; a traversal/foreign-scheme URI is rejected (offline).

4

M4 — A reusable prompt

get_prompt("reflect_on", {topic}) returns a parameterized message injecting recalled memories. Validation: the message contains the topic and the recalled entries (offline).

5

M5 — Two consumers (Definition of Done)

Run client_smoke_test.py (consumer #1), then connect a second host — Claude Desktop or the MCP Inspector (consumer #2). Validation: the same server's tools/resources/prompt work from both; save in one, read in the other.

6

M6 — Break it

Corrupt the stream with a stdout print; remove a schema bound and send a hostile input; expose save with no validation; classify a read-only entry as a tool. Validation: each failure recorded in FAILURE_ANALYSIS.md.

8. Self-Evaluation

After building, honestly evaluate your implementation against these criteria. Record your answers in EVALUATION.md.

CriterionDoes your implementation...Pass?
SDK-agnostic corekeep memory_tools/memory_resources/prompts/security free of any import mcp, so they're pure and offline-testable?
Schema as contractdefine JSON schemas with required, bounds, and a kind enum — and re-validate in the handler (the schema is advisory)?
Right primitiveuse tools for search/save, a resource for entries, a prompt for reflect-on — and can you justify each by control model?
URI containmentreject a foreign scheme and a .. traversal, resolving only to a known entry id, failing closed?
Bounded inputsclamp k, cap text length, range-check importance, whitelist kind?
stderr onlylog to stderr in the stdio server, never print to stdout?
Two consumersdemonstrate the one server from two hosts — and round-trip a save in one to a read in the other?
Red Flags

Your implementation may have problems if:

  • You put a planning/looping step in the server — the loop belongs to the host.
  • A learner module imports mcp — the surface is no longer SDK-agnostic or offline-testable.
  • Everything is a tool — a read-only entry should be a resource; a user template a prompt.
  • Handlers trust their arguments / resource URIs — across the boundary the caller is untrusted.
  • The stdio server prints to stdout — the JSON-RPC stream is corrupted and the server "breaks."
  • You only demonstrated one consumer — the M+N decoupling is unproven.

9. Common Mistakes

MistakeWhy It HappensConsequenceFix
Treating MCP as an agent framework ("where's the loop?")Coming from P06/P08 where you owned the loopYou put planning/looping in the server; it belongs in the hostThe server only exposes capabilities; the host owns the loop, prompt, model
print() to stdout in a stdio serverHabit; debuggingCorrupts the JSON-RPC stream — server "breaks" mysteriouslyLog to stderr (print(..., file=sys.stderr) or logging)
Putting everything in toolsTools are the familiar primitiveA read-only note becomes a model-invoked action; wrong control modelData to read = resource; an action = tool; a template a user runs = prompt
Vague description / loose schema"The function name says it all"The model mis-calls or can't choose the tool; bad inputs slip throughThe description + JSON schema are the API; be precise — required, bounds, enum
Trusting tool args / resource URIsIn-process habit — callers were trustedUntrusted input reaches your store; URI escapes the namespaceValidate every argument; contain the URI (scheme + id allow-list), fail closed
Unbounded inputsNo limits on k, text, importanceA huge k or megabyte text exhausts memory/latencyClamp k, cap text, range-check importance — in security.py
Hardcoding a transport assumptionOnly ever ran stdioCan't move the capability off-box without a rewriteKeep handlers transport-agnostic; transport is a one-line mcp.run(...) choice

10. Connections

Builds On

This elective generalizes the hand-wired tool use of Projects 06 and 08 into a portable protocol, and wraps the memory system of Project 05 as the concrete capability. The schema/description discipline is Project 06's agent-computer interface ("the description is the API"), now the contract every host's model sees. The URI containment is Project 06's path-sandboxing lesson, lifted across a process boundary. Nothing here re-teaches memory — the backend is provided precisely because you already built it.

Enables

Once a capability is an MCP server, it's consumable by your agent (P08 as a host), the Learning OS (P09 could route to MCP servers instead of in-process workers), Claude Desktop, the MCP Inspector, and a teammate's app — without any of them importing your code. The pattern you build — a governed surface of tools/resources/prompts behind a negotiated, transport-independent protocol — is how real AI products integrate external capabilities.

Production Patterns

Hosts (Claude Desktop, VS Code, Claude Code) launch/connect to servers and merge their tools into the model's toolset. Local servers use stdio (e.g. the reference filesystem server); remote servers use Streamable HTTP with OAuth/bearer (e.g. the hosted Sentry server). The MCP Inspector exercises a server with no client code; official SDKs (Python mcp, TypeScript @modelcontextprotocol/sdk) and reference servers model the shape. What separates MCP from a plain REST API is that it's AI-native: capability negotiation, the tool/resource/prompt control models, an LLM-friendly content format, and client primitives (sampling, elicitation) that let a server stay model-independent.

StarcallOS Relevance

StarcallOS Connection

StarcallOS is a personal OS — a host that wants many capabilities behind one surface. MCP is how it would expose its own memory once as a server (reusable by any host, including third-party AI apps) and consume external capabilities (files, calendar, search) without writing a bespoke integration for each. The protocol surface is StarcallOS's plug standard; the trust boundary is its governance layer (untrusted inputs in, least-privilege capability out); the transport choice is per deployment (stdio for a local capability, Streamable HTTP for a hosted one). Wrap your memory system here and you've prototyped the integration spine StarcallOS grows on.

Sources

See source/resources.md for the complete annotated source list.

Tier 1 — Official Documentation / Engineering Guidance

  • sources/official-docs/mcp-architecture.md — MCP architecture: client-host-server (one client per server), JSON-RPC + stateful sessions with capability negotiation, the data/transport layers, stdio vs Streamable HTTP, and the three server primitives (tools/resources/prompts) with their control models. The conceptual spine; the basis for "a protocol, not an agent framework."
  • sources/official-docs/mcp-build-server.md — building a server in Python with the mcp SDK / FastMCP (type hints + docstrings → tool definition), registering tools/resources/prompts, running over stdio, the stdout-corruption hazard, and configuring a host (Claude Desktop mcpServers). The implementation scaffolding the project provides and you fill in.
  • sources/official-docs/anthropic-tool-use.md — the hand-wired tool loop (tool = name + description + JSON-schema input; tool_use → execute → tool_result) that this elective generalizes into a protocol. (Carried from P06/P08.)
  • sources/articles/building-effective-agents.md — the augmented LLM and the agent-computer interface ("the description is the API"). MCP makes the ACI portable. (Carried from P06.)

Tier 2 — Foundational Papers

  • None new — MCP is a protocol/engineering standard, not a research result. The wrapped capability (memory) is grounded in Project 05's papers (generative-agents.md, memory-systems-taxonomy.md, memgpt.md), carried over rather than re-taught.

Tier 3 — Engineering Guides

  • The MCP Inspector (github.com/modelcontextprotocol/inspector) and official reference servers (github.com/modelcontextprotocol/servers) — practical references for exercising and modeling a server.

Tier 4 — Educational Sources

  • The prior projects are the prerequisites: Project 05 built the memory system you wrap; Projects 06 and 08 built the hand-wired tool loops MCP generalizes.
  • Note: the M×N → M+N framing is the standard engineering interpretation of the documented "any server, any host" property, not a verbatim protocol claim (see source/resources.md). No source is invented for it.