# MCP Server Blueprint — February 2026 **This is the definitive template for building production-ready MCP servers in 2026.** Use this checklist for EVERY new MCP server. No skipping steps. These patterns ensure your server is: - ✅ Usable (not just functional) - ✅ Fast (lazy loading, efficient queries) - ✅ Discoverable (labels, descriptions) - ✅ Interactive (MCP Apps where appropriate) - ✅ Debuggable (logging, progress) - ✅ Production-ready (error handling, deployment) --- ## Phase 1: Planning (Before Writing Code) ### 1.1 Define Server Scope - [ ] What API/service are you integrating? - [ ] What are the 5-10 most important operations? - [ ] Who is the target user? (developers, business users, etc.) - [ ] What data is most frequently accessed? ### 1.2 Identify Tool Categories Label your tools by category. Common patterns: - [ ] **CRUD operations** (create, read, update, delete) - [ ] **Search/Filter** (find data with queries) - [ ] **Analytics/Reporting** (stats, dashboards, summaries) - [ ] **Workflows** (multi-step operations) - [ ] **Admin** (configuration, settings) ### 1.3 Identify UI Opportunities Which operations benefit from visual display? - [ ] **Data grids** — Contact lists, search results, tables - [ ] **Dashboards** — Metrics, KPIs, analytics - [ ] **Cards** — Detail views (invoices, opportunities, profiles) - [ ] **Timelines** — Activity feeds, history - [ ] **Forms** — Quick actions (booking, creating records) - [ ] **Kanban** — Pipeline views, project boards If you have 3+ UI opportunities, plan for MCP Apps. --- ## Phase 2: Core Server Setup ### 2.1 Project Structure ```bash mkdir mcp-server-myservice cd mcp-server-myservice npm init -y npm install @modelcontextprotocol/sdk npm install -D typescript @types/node tsx fs-extra @types/fs-extra ``` ### 2.2 File Structure ``` mcp-server-myservice/ ├── src/ │ ├── index.ts # Main server (or server.ts) │ ├── clients/ │ │ └── api-client.ts # API client │ ├── apps/ # If using MCP Apps │ │ └── index.ts # Apps manager │ ├── ui/ # If using MCP Apps │ │ ├── contact-grid.html │ │ └── dashboard.html │ └── types/ │ └── index.ts # Shared types ├── dist/ # Build output ├── scripts/ │ └── copy-ui.js # UI build script ├── package.json ├── tsconfig.json ├── .env.example ├── .gitignore ├── .npmignore ├── Dockerfile ├── railway.json └── README.md ``` ### 2.3 Package Configuration ```json { "name": "mcp-server-myservice", "version": "1.0.0", "type": "module", "main": "dist/index.js", "bin": { "mcp-server-myservice": "dist/index.js" }, "scripts": { "build": "npm run build:ts && npm run build:ui", "build:ts": "tsc", "build:ui": "node scripts/copy-ui.js", "dev": "tsx src/index.ts", "start": "node dist/index.js" }, "files": ["dist", "README.md", "LICENSE"], "keywords": ["mcp", "mcp-server", "model-context-protocol", "myservice"] } ``` --- ## Phase 3: Tool Design (The Most Important Phase) ### 3.1 Tool Naming Convention ✅ **Use:** `verb_noun` (snake_case) ❌ **Avoid:** camelCase, PascalCase, kebab-case **CRUD patterns:** - `list_contacts` (with pagination + filters) - `get_contact` (by ID) - `create_contact` (returns created object) - `update_contact` (partial updates) - `delete_contact` (confirm before delete) - `search_contacts` (full-text search if different from list) **Other patterns:** - `send_email`, `schedule_appointment`, `export_report`, `analyze_pipeline` ### 3.2 Tool Metadata & Labels ⭐ CRITICAL Every tool MUST have `_meta` with labels: ```typescript { name: "search_contacts", description: "Search contacts with filters. Returns paginated results.", inputSchema: { /* ... */ }, _meta: { labels: { category: "contacts", // Group by feature access: "read", // read | write | delete complexity: "simple", // simple | complex | batch }, }, } ``` **Label categories to use:** - `category`: contacts, deals, analytics, calendar, email, admin, workflows - `access`: read, write, delete - `complexity`: simple (1 API call), complex (multiple calls), batch (loops) - `sensitivity`: public, internal, confidential (optional) ### 3.3 Input Schemas — Best Practices ```typescript inputSchema: { type: "object" as const, properties: { // Always describe parameters page: { type: "number", description: "Page number (default 1, starts at 1)" }, pageSize: { type: "number", description: "Results per page (default 50, max 100)" }, // Use enums for fixed options status: { type: "string", description: "Filter by status", enum: ["active", "inactive", "pending"], }, // ISO 8601 for dates createdAfter: { type: "string", description: "Filter created after (ISO 8601: 2026-02-03T14:00:00Z)" }, }, // Mark required fields explicitly required: ["contactId"], } ``` ### 3.4 Pagination (Mandatory for List Operations) Every `list_` or `search_` tool MUST support pagination: ```typescript { name: "list_contacts", description: "List contacts with pagination and filters", inputSchema: { type: "object" as const, properties: { page: { type: "number", description: "Page number (default 1)" }, pageSize: { type: "number", description: "Results per page (default 50, max 100)" }, query: { type: "string", description: "Search query (optional)" }, }, }, _meta: { labels: { category: "contacts", access: "read", complexity: "simple" }, }, } ``` **In handler:** ```typescript case "list_contacts": { const { page = 1, pageSize = 50, query } = args; const params = new URLSearchParams(); params.append("page", String(page)); params.append("pageSize", String(Math.min(Number(pageSize), 100))); // Cap at API max if (query) params.append("query", query); return await client.get(`/contacts?${params}`); } ``` --- ## Phase 4: Lazy-Loaded Resources ⭐ NEW ### 4.1 When to Use Resources vs Tools **Use resources for:** - Large datasets (contact lists, transaction history) - Frequently changing data (real-time dashboards) - Reference data (documentation, schemas) - User-specific data (per-user settings, dashboards) **Use tools for:** - Operations with parameters (search, filter, create) - One-time fetches - Mutations (create, update, delete) ### 4.2 Resource Setup ```typescript import { ListResourcesRequestSchema, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js"; // Declare resources capability const server = new Server( { name: "myservice-mcp", version: "1.0.0" }, { capabilities: { tools: {}, resources: {} } } // ✅ Enable resources ); // List available resources (metadata only) server.setRequestHandler(ListResourcesRequestSchema, async () => { return { resources: [ { uri: "myservice://contacts/all", name: "All Contacts", description: "Complete contact database (lazy-loaded)", mimeType: "application/json", }, { uri: "myservice://analytics/dashboard", name: "Analytics Dashboard", description: "Real-time analytics data", mimeType: "application/json", }, ], }; }); // Fetch resource content on-demand server.setRequestHandler(ReadResourceRequestSchema, async (request) => { const { uri } = request.params; switch (uri) { case "myservice://contacts/all": { const contacts = await client.get("/contacts?limit=1000"); // Fetch when requested return { contents: [{ uri, mimeType: "application/json", text: JSON.stringify(contacts, null, 2), }], }; } case "myservice://analytics/dashboard": { const analytics = await client.get("/analytics/dashboard"); return { contents: [{ uri, mimeType: "application/json", text: JSON.stringify(analytics, null, 2), }], }; } default: throw new Error(`Unknown resource: ${uri}`); } }); ``` ### 4.3 Resource Templates (Dynamic URIs) ```typescript server.setRequestHandler(ListResourcesRequestSchema, async () => { return { resourceTemplates: [ { uriTemplate: "myservice://contact/{id}", name: "Contact Details", description: "Full contact record by ID", mimeType: "application/json", }, ], }; }); server.setRequestHandler(ReadResourceRequestSchema, async (request) => { const { uri } = request.params; const contactMatch = uri.match(/^myservice:\/\/contact\/(.+)$/); if (contactMatch) { const contactId = contactMatch[1]; const contact = await client.get(`/contacts/${contactId}`); return { contents: [{ uri, mimeType: "application/json", text: JSON.stringify(contact, null, 2), }], }; } throw new Error(`Unknown resource: ${uri}`); }); ``` --- ## Phase 5: MCP Apps (If Applicable) ### 5.1 Should You Build Apps? Build MCP Apps if you have: - ✅ Visual data (grids, cards, dashboards) - ✅ 3+ UI opportunities identified in Phase 1 - ✅ Complex data relationships (better shown than described) - ✅ Interactive workflows (drag-drop, forms) Skip apps if: - ❌ Simple CRUD operations only - ❌ All operations return small JSON objects - ❌ No visual benefit ### 5.2 App Architecture See `mcp-apps-integration` skill for full details. Quick checklist: - [ ] Create `src/apps/index.ts` — MCPAppsManager class - [ ] Create `src/ui/` directory — HTML components - [ ] Register resource handlers for UI files - [ ] Add app tools with `_meta.ui.resourceUri` - [ ] Implement `ListResourcesRequestSchema` handler - [ ] Implement `ReadResourceRequestSchema` handler - [ ] Add `build:ui` script to copy HTML to `dist/app-ui/` ### 5.3 App Tool Naming **Pattern:** `view_` or `show_` prefix ```typescript { name: "view_contact_grid", description: "Display contact search results in a data grid (visual UI component)", inputSchema: { /* ... */ }, _meta: { labels: { category: "contacts", access: "read", complexity: "simple" }, ui: { resourceUri: "ui://myservice/contact-grid" }, }, } ``` ### 5.4 Common App Patterns - **Contact Grid** — Search results table - **Dashboard** — Multi-widget analytics view - **Pipeline Board** — Kanban with drag-drop - **Opportunity Card** — Detail view for single record - **Calendar View** — Appointment/event calendar - **Timeline** — Activity feed Reference: 11 production GHL apps in `/Users/jakeshore/.clawdbot/workspace/mcp-diagrams/ghl-mcp-apps-only/` --- ## Phase 6: Progress & Logging ### 6.1 Progress Notifications (For Long Operations) Any operation taking >5 seconds MUST send progress updates: ```typescript if (name === "import_contacts") { const progressToken = request.params._meta?.progressToken; if (progressToken) { await server.notification({ method: "notifications/progress", params: { progressToken, progress: 0.3, // 30% total: 1.0, }, }); } // ... do work if (progressToken) { await server.notification({ method: "notifications/progress", params: { progressToken, progress: 1.0, total: 1.0 }, }); } } ``` ### 6.2 Structured Logging Log important operations for debugging: ```typescript import { LoggingLevel } from "@modelcontextprotocol/sdk/types.js"; await server.notification({ method: "notifications/message", params: { level: LoggingLevel.Info, logger: "myservice", data: { operation: "create_contact", contactId: newContact.id, timestamp: new Date().toISOString(), }, }, }); ``` **When to log:** - Info: Successful operations (create, update, delete) - Warning: Rate limits, retries, fallbacks - Error: API failures, validation errors - Debug: Detailed request/response data (dev only) --- ## Phase 7: Error Handling (Production-Ready) ### 7.1 Tool Handler Error Wrapping ```typescript server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { const result = await handleTool(client, name, args || {}); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], }; } catch (error) { const message = error instanceof Error ? error.message : String(error); // Log the error await server.notification({ method: "notifications/message", params: { level: LoggingLevel.Error, logger: "myservice", data: { tool: name, error: message }, }, }); return { content: [{ type: "text", text: `Error: ${message}` }], isError: true, }; } }); ``` ### 7.2 API Client Error Handling ```typescript async request(endpoint: string, options: RequestInit = {}) { const response = await fetch(url, options); if (!response.ok) { const errorText = await response.text(); // Parse API error if JSON try { const errorJson = JSON.parse(errorText); throw new Error( `API error: ${response.status} - ${errorJson.message || errorJson.error || errorText}` ); } catch { throw new Error( `API error: ${response.status} ${response.statusText} - ${errorText}` ); } } return response.json(); } ``` --- ## Phase 8: Prompts (Optional but Recommended) ### 8.1 When to Add Prompts Add prompts for: - Common analysis workflows (e.g., "Analyze pipeline health") - Report generation (e.g., "Generate contact summary") - Quick actions (e.g., "Find overdue tasks") - Data exploration (e.g., "Show top performers") ### 8.2 Prompt Implementation ```typescript import { ListPromptsRequestSchema, GetPromptRequestSchema } from "@modelcontextprotocol/sdk/types.js"; server.setRequestHandler(ListPromptsRequestSchema, async () => { return { prompts: [ { name: "contact_summary", description: "Generate comprehensive contact summary with recent activity", arguments: [ { name: "contactId", description: "Contact ID", required: true }, ], }, ], }; }); server.setRequestHandler(GetPromptRequestSchema, async (request) => { const { name, arguments: args } = request.params; if (name === "contact_summary") { const { contactId } = args; const contact = await client.get(`/contacts/${contactId}`); const activities = await client.get(`/contacts/${contactId}/activities`); return { description: `Summary for ${contact.name}`, messages: [{ role: "user", content: { type: "text", text: `Generate a comprehensive summary:\n\n${JSON.stringify({ contact, activities }, null, 2)}`, }, }], }; } throw new Error(`Unknown prompt: ${name}`); }); ``` --- ## Phase 9: Testing Checklist ### 9.1 Local Testing - [ ] All tools compile without errors (`npm run build`) - [ ] Server starts successfully (`npm start`) - [ ] Environment variables validated on startup - [ ] Test each tool in Claude Desktop - [ ] Test pagination (page 1, page 2) - [ ] Test error cases (invalid IDs, missing params) - [ ] Test apps render correctly (if applicable) - [ ] Check logs in Claude Desktop console ### 9.2 Performance Testing - [ ] List operations return in <2 seconds - [ ] Lazy-loaded resources only fetch when requested - [ ] No unnecessary API calls - [ ] Pagination caps at API maximum - [ ] Progress notifications for operations >5 seconds --- ## Phase 10: Documentation ### 10.1 README.md Structure ```markdown # MCP Server for MyService MCP integration for MyService. Enables Claude Desktop to [core value prop]. ## Features - ✅ List/search/CRUD contacts - ✅ Analytics dashboard (MCP App) - ✅ Pipeline visualization (MCP App) - ✅ Progress tracking for imports ## Installation [npx / manual / docker options] ## Configuration [Environment variables with .env.example] ## Available Tools [List of tools with descriptions] ## MCP Apps (Rich UI) [List of app tools with screenshots] ## Development [Build/dev instructions] ``` ### 10.2 .env.example ```bash # MyService API Credentials MY_SERVICE_API_KEY=your_api_key_here MY_SERVICE_API_SECRET=your_secret_here # Optional: Override base URL # MY_SERVICE_BASE_URL=https://sandbox.api.myservice.com # Optional: Logging # LOG_LEVEL=debug ``` --- ## Phase 11: Deployment ### 11.1 Docker - [ ] Multi-stage Dockerfile - [ ] .dockerignore file - [ ] Test build locally - [ ] Test run locally ### 11.2 Railway - [ ] railway.json with build + start commands - [ ] Environment variables documented - [ ] Test deployment ### 11.3 npm Publishing - [ ] `bin` field in package.json - [ ] `files` field includes only dist/ - [ ] .npmignore excludes src/, .env - [ ] Keywords for discoverability - [ ] Test `npx` installation locally ### 11.4 GitHub - [ ] README.md complete - [ ] LICENSE file - [ ] .gitignore excludes node_modules, dist, .env - [ ] GitHub Actions for CI/CD (optional) --- ## Production Checklist (Final Review) ### Code Quality - [ ] All tools have `_meta.labels` - [ ] All parameters have descriptions - [ ] Required fields marked explicitly - [ ] Pagination implemented for list operations - [ ] Error handling in all tool handlers - [ ] No hardcoded API keys or secrets - [ ] Logging for important operations ### Features - [ ] Lazy-loaded resources for large datasets - [ ] Progress notifications for long operations (>5s) - [ ] MCP Apps for visual data (if applicable) - [ ] Prompts for common workflows (if applicable) ### Documentation - [ ] README with installation instructions - [ ] .env.example with all required variables - [ ] Tool descriptions clear and helpful - [ ] Examples in README ### Deployment - [ ] Compiles without errors - [ ] Runs in Claude Desktop - [ ] Docker image builds (if using Docker) - [ ] Railway deploys successfully (if using Railway) - [ ] npm package installs via npx (if publishing) --- ## Anti-Patterns to Avoid ❌ **No labels on tools** — Always add `_meta.labels` ❌ **Loading all data upfront** — Use lazy-loaded resources ❌ **No pagination** — Every list operation needs page/pageSize ❌ **Silent failures** — Always log errors and return clear messages ❌ **No progress for slow ops** — Add progress notifications for >5s operations ❌ **Building apps when not needed** — Only build apps if visually beneficial ❌ **Missing descriptions** — Every parameter needs a description ❌ **No environment validation** — Check env vars on startup ❌ **Skipping error handling** — Wrap all tool handlers in try-catch ❌ **Generic error messages** — Be specific ("Contact not found" not "Error") --- ## Reference Materials - **Skills:** - `mcp-server-development` — Full TypeScript patterns - `mcp-apps-integration` — MCP Apps guide - `mcp-deployment` — Docker/Railway/npm - **Example Servers:** - `/Users/jakeshore/.clawdbot/workspace/mcp-diagrams/mcp-servers/` - 30 production servers with all patterns - **Example Apps:** - `/Users/jakeshore/.clawdbot/workspace/mcp-diagrams/ghl-mcp-apps-only/` - 11 production apps with UI components --- ## TL;DR — The Golden Rules 1. **Labels on every tool** — category, access, complexity 2. **Lazy-load large datasets** — Use resources, not tools 3. **Paginate everything** — page/pageSize on all lists 4. **Progress for slow ops** — >5 seconds = progress notifications 5. **Apps for visual data** — Grids, dashboards, cards, timelines 6. **Log important operations** — Info, Warning, Error levels 7. **Handle errors gracefully** — Clear messages, no silent failures 8. **Document thoroughly** — README, .env.example, descriptions 9. **Test before shipping** — All tools work in Claude Desktop 10. **Deploy with confidence** — Docker, Railway, npm ready to go **Follow this blueprint and your MCP servers will be production-ready, usable, and optimized for February 2026.**