Tooling Lab
40 minutes
mcp
architecture
demo
MCP Local Stub Interface
Conceptual explanation of Model Context Protocol with a practical local stub implementation.
What is MCP?
Model Context Protocol (MCP) is an open standard for connecting AI assistants to external data sources and tools. Think of it as a USB standard for AI - a consistent way to plug capabilities into language models.
Core Concepts
The Protocol Flow
┌──────────────┐ JSON-RPC ┌──────────────┐
│ AI Host │◀──────────────▶│ MCP Server │
│ (Claude) │ │ (Your App) │
└──────────────┘ └──────────────┘
│
▼
┌──────────────┐
│ Resources │
│ (DB, Files) │
└──────────────┘
Three Primitives
- Resources - Read-only data (files, database records, API responses)
- Tools - Functions the AI can call (create, update, delete operations)
- Prompts - Pre-built prompt templates for common tasks
Building a Local Stub
Let's create a minimal MCP server that demonstrates all three primitives.
Project Setup
Server Implementation
Create server.ts:
typescriptimport { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; // In-memory data store const notes: Map<string, string> = new Map([ ["welcome", "Welcome to the MCP demo!"], ["readme", "This is a sample note."], ]); // Create the server const server = new Server( { name: "notes-server", version: "1.0.0" }, { capabilities: { resources: {}, tools: {}, prompts: {} } } ); // RESOURCES: List available notes server.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: Array.from(notes.keys()).map((name) => ({ uri: `note:///${name}`, name: `Note: ${name}`, mimeType: "text/plain", })), })); // RESOURCES: Read a specific note server.setRequestHandler(ReadResourceRequestSchema, async (request) => { const match = request.params.uri.match(/^note:\/\/\/(.+)$/); if (!match) throw new Error("Invalid URI"); const name = match[1]; const content = notes.get(name); if (!content) throw new Error("Note not found"); return { contents: [{ uri: request.params.uri, mimeType: "text/plain", text: content }], }; }); // TOOLS: List available tools server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: "create_note", description: "Create a new note", inputSchema: { type: "object", properties: { name: { type: "string", description: "Note name" }, content: { type: "string", description: "Note content" }, }, required: ["name", "content"], }, }, { name: "list_notes", description: "List all note names", inputSchema: { type: "object", properties: {} }, }, ], })); // TOOLS: Execute tool calls server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; if (name === "create_note") { const { name: noteName, content } = args as { name: string; content: string }; notes.set(noteName, content); return { content: [{ type: "text", text: `Created note: ${noteName}` }] }; } if (name === "list_notes") { const list = Array.from(notes.keys()).join(", "); return { content: [{ type: "text", text: `Notes: ${list}` }] }; } throw new Error(`Unknown tool: ${name}`); }); // PROMPTS: List available prompts server.setRequestHandler(ListPromptsRequestSchema, async () => ({ prompts: [ { name: "summarize_notes", description: "Summarize all notes", }, ], })); // PROMPTS: Get a specific prompt server.setRequestHandler(GetPromptRequestSchema, async (request) => { if (request.params.name === "summarize_notes") { const allNotes = Array.from(notes.entries()) .map(([name, content]) => `${name}: ${content}`) .join("\n"); return { messages: [ { role: "user", content: { type: "text", text: `Please summarize these notes:\n\n${allNotes}`, }, }, ], }; } throw new Error("Prompt not found"); }); // Start the server const transport = new StdioServerTransport(); await server.connect(transport); console.error("MCP Notes Server running on stdio");
Testing the Server
Run the server directly to test:
bashnpx tsx server.ts
The server communicates via stdio, so you'll need an MCP client to interact with it fully. For testing, you can use the MCP Inspector.
Connecting to Claude Desktop
Configure Claude Desktop to use your server:
Architecture Patterns
Read-Only vs Read-Write
For safety, consider separating:
- Read-only server: Resources only (safe for exploration)
- Read-write server: Tools included (requires more trust)
Error Handling
Always return meaningful errors:
typescriptif (!content) { throw new Error(`Note "${name}" not found. Available: ${Array.from(notes.keys()).join(", ")}`); }
Validation
Use Zod for input validation:
typescriptimport { z } from "zod"; const CreateNoteSchema = z.object({ name: z.string().min(1).max(100), content: z.string().max(10000), });
Verification
Your MCP stub is working if:
- Server starts without errors
- Resources are listed correctly
- Tools can create and list notes
- Prompts return formatted messages
- Claude Desktop recognizes the server
Next Steps
- Add persistence (file or database storage)
- Implement authentication
- Add rate limiting
- Create domain-specific tools for your project