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

  1. Resources - Read-only data (files, database records, API responses)
  2. Tools - Functions the AI can call (create, update, delete operations)
  3. 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:

typescript
import { 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:

bash
npx 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:

typescript
if (!content) {
  throw new Error(`Note "${name}" not found. Available: ${Array.from(notes.keys()).join(", ")}`);
}

Validation

Use Zod for input validation:

typescript
import { 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