# 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.**
