MCP Interface Layer
Stop hand-wiring tools into one loop. Turn a subsystem into a reusable protocol surface that many hosts can consume.
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.
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.
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
initializehandshake doing capability negotiation — each side declares what it supports before any call. Then the client discovers with*/listand executes withtools/call/ reads withresources/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
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
| Primitive | What it is | Who drives it | MCP methods |
|---|---|---|---|
| Tool | An executable function that performs an action | Model decides to call (host gates w/ user approval) | tools/list, tools/call |
| Resource | A data source providing context, addressed by URI | Application reads it as context | resources/list, resources/read |
| Prompt | A reusable, parameterized message template | User invokes it | prompts/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
| Transport | Where | Clients | Auth |
|---|---|---|---|
| stdio | local subprocess, stdin/stdout | typically one | none (same machine) |
| Streamable HTTP | remote, HTTP POST + optional SSE | typically many | bearer / 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
- Negotiate. The host's client sends
initialize; the server declares its capabilities (tools, resources, prompts). Unsupported operations are never attempted. - Discover. The client calls
tools/list/resources/list/prompts/list. Each tool advertisesname,description, and aninputSchema(JSON Schema) — "the description is the API" (Project 06), now portable. - Execute. When the model chooses a tool, the client sends
tools/callwitharguments; the server validates, runs the handler, returns acontentarray. - 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. - Log to stderr. On stdio the protocol is stdin/stdout; a stray
printcorrupts the JSON-RPC stream and breaks the server.
"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; initialize → list_tools → call_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
- 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?
- Explain host vs. client vs. server to someone who knows P08 but not MCP. Where did the agent loop go?
- "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)?
- Why does the same server run unchanged over stdio and Streamable HTTP? What does change, and when would you pick each?
- Where exactly is the trust boundary in your server? Name two inputs that cross it and the check that guards each.
- Restate the M×N → M+N argument with your memory server as the example. Who are the M and the N?
- The schema/description "is the API." How is that claim stronger here than in Project 06?
- 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.py—TOOL_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.py—list_resources+read_resource(M3): expose entries asmemory://entries/{id}.prompts.py—PROMPT_DEFINITIONS+get_prompt(M4): one reusablereflect_onprompt.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.
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.
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.
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.
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).
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).
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).
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.
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.
| Criterion | Does your implementation... | Pass? |
|---|---|---|
| SDK-agnostic core | keep memory_tools/memory_resources/prompts/security free of any import mcp, so they're pure and offline-testable? | ☐ |
| Schema as contract | define JSON schemas with required, bounds, and a kind enum — and re-validate in the handler (the schema is advisory)? | ☐ |
| Right primitive | use tools for search/save, a resource for entries, a prompt for reflect-on — and can you justify each by control model? | ☐ |
| URI containment | reject a foreign scheme and a .. traversal, resolving only to a known entry id, failing closed? | ☐ |
| Bounded inputs | clamp k, cap text length, range-check importance, whitelist kind? | ☐ |
| stderr only | log to stderr in the stdio server, never print to stdout? | ☐ |
| Two consumers | demonstrate the one server from two hosts — and round-trip a save in one to a read in the other? | ☐ |
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
| Mistake | Why It Happens | Consequence | Fix |
|---|---|---|---|
| Treating MCP as an agent framework ("where's the loop?") | Coming from P06/P08 where you owned the loop | You put planning/looping in the server; it belongs in the host | The server only exposes capabilities; the host owns the loop, prompt, model |
print() to stdout in a stdio server | Habit; debugging | Corrupts the JSON-RPC stream — server "breaks" mysteriously | Log to stderr (print(..., file=sys.stderr) or logging) |
| Putting everything in tools | Tools are the familiar primitive | A read-only note becomes a model-invoked action; wrong control model | Data 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 through | The description + JSON schema are the API; be precise — required, bounds, enum |
| Trusting tool args / resource URIs | In-process habit — callers were trusted | Untrusted input reaches your store; URI escapes the namespace | Validate every argument; contain the URI (scheme + id allow-list), fail closed |
| Unbounded inputs | No limits on k, text, importance | A huge k or megabyte text exhausts memory/latency | Clamp k, cap text, range-check importance — in security.py |
| Hardcoding a transport assumption | Only ever ran stdio | Can't move the capability off-box without a rewrite | Keep 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 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 themcpSDK / FastMCP (type hints + docstrings → tool definition), registering tools/resources/prompts, running over stdio, the stdout-corruption hazard, and configuring a host (Claude DesktopmcpServers). 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.