Plugging knowledge and capabilities into AI agents.
David Cai · github.com/davidcai/kungfu-mcp
An AI agent is smart, but alone is not enough.
It needs domain knowledge and capabilities.
In this analogy, Neo is the AI agent
— then, what will be the MCP?
What is MCP
Model Context Protocol — an open standard for connecting AI applications to external tools and data. Open-sourced by Anthropic, Nov 2024; now adopted across Claude, OpenAI, Google DeepMind, IDEs, and inspector tooling.
Over a transport — stdio for local, HTTP for remote. Anything speaking the protocol interoperates.
TypeScript, Python, Kotlin, C#, Java. Today's server is TypeScript.
modelcontextprotocol.io
Where MCP fits
An agent is a model calling tools in a loop. Without tools, an LLM is a well-read brain with frozen knowledge and no hands. MCP is the standardized socket where tools and data plug into that loop.
model = brain · MCP tools = hands · MCP resources = reference library
In The Matrix, an operator plugs a cable into Neo's brain — Kung Fu downloaded directly into his mind. He opens his eyes, fully trained.
What is MCP?
Smart, but isolated in a box — no hands, no tools, no world to act in.
An open standard connecting AI models to external tools, software, and databases.
A standardized translator linking the AI to browsers, calendars, and files.
MCP SERVER
The kungfu-mcp server (our demo) catalogs 8 kung fu factions, narrates sparring matches, and ships an interactive arena UI. This one MCP server can serve many hosts.
Host
Claude · Cursor · MCPInspector
Anything that speaks MCP
Server
kungfu-mcp
Hono + Streamable HTTP · :3001/mcp · tools · resources · app
The economic argument
Before
Every app writes its own Slack, GitHub, and database connector. M×N custom adapters to build, test, and maintain forever.
After
M + N. One port, like USB — the laptop doesn't care if you plug in a monitor, a drive, or a keyboard.
Same server, unchanged, in Claude, the Inspector, a custom host, your internal agent.
Protocol anatomy
| Primitive | Who decides to use it | In kungfu-mcp |
|---|---|---|
| Tools | The model — function calling | list_factions, get_faction, spar, spar_arena |
| Resources | The application / user — attached as context | kungfu://kungfu/roster · kungfu://factions/{id} |
| Prompts | The user — slash-command templates | — not used here (mention only) |
| Apps (extension) | Tool + ui:// resource → interactive iframe | spar_arena + ui://spar-arena/app.html |
The control-plane distinction: tools are model-controlled, resources are application-driven. Tools and resources render the same markdown via a shared formatter — they differ only in who decides to fetch the content.
Primitive 01 · Tools
server.registerTool(
"list_factions",
{
description: "List all major kungfu factions known to the kung fu world…",
inputSchema: {},
outputSchema: {
factions: z.array(z.object({
id: z.string(), name: z.string(), catchphrase: z.string(),
})),
},
},
async () => ({
content: [{ type: "text", text: rosterMarkdown() }],
structuredContent: { factions: /* typed data */ },
}),
);
Three parts: a name, a schema contract (Zod → JSON Schema over the wire), and a handler. The description is the API documentation the model reads to decide when to call. Bad descriptions = tools that never get called.
Primitive 02 · Resources
const factionTemplate = new ResourceTemplate("kungfu://factions/{id}", {
list: async () => ({
resources: KUNGFU_FACTIONS.map((f) => ({
uri: `kungfu://factions/${f.id}`,
name: `${f.name} profile`,
mimeType: "text/markdown",
})),
}),
complete: { id: () => KUNGFU_FACTIONS.map((f) => f.id) },
});
Static resource: kungfu://kungfu/roster · Template: kungfu://factions/{id} — enumerable via list() and autocompletable via complete.id(). The URI is the contract: rename it and you break every client that references it.
Live demo
Host #1 · http://localhost:3001/mcp
Markdown roster + structuredContent JSON in one result.
Techniques with threat scores. Try a bogus id — error lists valid ids.
3 rounds, random picks, threat-score verdict. Run twice — different each time.
Read roster; browse factions/{id} with id autocomplete.
Land the line: "The Inspector is host #1. Same server, zero changes, meets host #2 in ten minutes." Full script: Appendix A, Demo 1.
Resources extend what the agent knows — 8 Kung Fu factions, and signature techniques. Tools extend what it can do — run a real sparring match with computed scoring.
Design pattern
return {
content: [{ type: "text",
text: sparNarration(a, b, nameA, nameB, outcome) }],
structuredContent: outcome, // { rounds, verdict, winnerId }
};
content → prose
"The crowd gasps. A chicken flees." — what the model summarizes to the user.
structuredContent → JSON
{ rounds, verdict, winnerId } — what the arena UI animates. No prose-parsing, no drift.
Beyond text · MCP Apps
registerAppTool(server, "spar_arena", {
title: "Kung Fu Spar Arena",
inputSchema: {},
_meta: { ui: { resourceUri: "ui://spar-arena/app.html" } }, // ← the hook
}, async () => ({
content: [{ type: "text", text: "Arena ready. Select two factions…" }],
}));
One line. Point the host at a ui:// resource; it fetches the HTML, renders it in a sandboxed iframe, and your tool's answer comes alive. The bundle is one self-contained Vite file — one fetch, no asset pipeline for the host.
Beyond text · MCP Apps
Same tools, same server. The LLM called them via content; the iframe calls them via structuredContent — one source of truth, two consumers.
Host (e.g., Cursor, Claude Desktop, or MCP Inspector) → call spar_arena → the arena renders inline. Pick factions, Begin the Spar, watch rounds animate from structuredContent. View Profile fires a live get_faction call.
What to watch for: inline iframe render · dropdowns filled via list_factions · animated rounds · winner's catchphrase
How to build · Anatomy
const transport = new WebStandardStreamableHTTPServerTransport({
sessionIdGenerator: () => newSessionId,
});
const server = new McpServer({ name: "kungfu-mcp", version: "1.0.0" });
registerTools(server); // src/tools.ts — 4 tools
registerDataResources(server); // src/resources.ts — roster + profiles
registerApp(server, bundledHtml); // src/app.ts — arena tool + ui:// resource
await server.connect(transport);
Hono + hono/cors (must expose the mcp-session-id header for browser hosts). One McpServer + transport per session, tracked in a Map. File-per-primitive layout: tools.ts · resources.ts · app.ts · shared data.ts + format.ts.
How to build · Recipe
npm i @modelcontextprotocol/sdk zod — TS, Python, Kotlin, C#, Java.
Name + description + schema + handler via server.registerTool.
Context users and apps attach — server.registerResource.
stdio for local CLIs · Streamable HTTP for remote or app-capable.
Before touching any real host — schemas, results, autocomplete.
Register a ui:// resource — @modelcontextprotocol/ext-apps.
Reference implementation: github.com/davidcai/kungfu-mcp — npm install && npm start gives you everything shown today.
Resources are knowledge, tools are capability, apps are living UI.
github.com/davidcai/kungfu-mcp