Jake Shore 8d65417afe Add 11 MCP agent skills to repo — 550KB of encoded pipeline knowledge
Skills added:
- mcp-api-analyzer (43KB) — Phase 1: API analysis
- mcp-server-builder (88KB) — Phase 2: Server build
- mcp-server-development (31KB) — TS MCP patterns
- mcp-app-designer (85KB) — Phase 3: Visual apps
- mcp-apps-integration (20KB) — structuredContent UI
- mcp-apps-official (48KB) — MCP Apps SDK
- mcp-apps-merged (39KB) — Combined apps reference
- mcp-localbosses-integrator (61KB) — Phase 4: LocalBosses wiring
- mcp-qa-tester (113KB) — Phase 5: Full QA framework
- mcp-deployment (17KB) — Phase 6: Production deploy
- mcp-skill (exa integration)

These skills are the encoded knowledge that lets agents build
production-quality MCP servers autonomously through the pipeline.
2026-02-06 06:36:37 -05:00

31 KiB

MCP Server Development — TypeScript Best Practices

When to use this skill: Building TypeScript-based MCP servers from scratch. Use when creating new integrations, API wrappers, or data sources for Claude Desktop.

What this covers: Complete TypeScript MCP server patterns extracted from building 30+ production servers (ServiceTitan, Gusto, Mailchimp, Calendly, Toast, Zendesk, Trello, etc.).


1. Project Structure (Standard Pattern)

my-mcp-server/
├── src/
│   └── index.ts          # Main server file
├── dist/                 # Compiled output (git ignored)
├── package.json
├── tsconfig.json
├── .env.example         # Template for required env vars
├── .gitignore
├── Dockerfile           # Optional: for containerization
├── railway.json         # Optional: for Railway deployment
└── README.md

The One-File Pattern (Preferred for Most Servers)

For most MCP servers, keep everything in one src/index.ts file unless you have 20+ tools. This includes:

  • Configuration
  • API client class
  • Tool definitions
  • Tool handler function
  • Server setup

Why: Easier to read, debug, and maintain. Split into modules only when file exceeds ~500 lines.


2. File Template (src/index.ts)

#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";

// ============================================
// CONFIGURATION
// ============================================
const MCP_NAME = "my-service";
const MCP_VERSION = "1.0.0";
const API_BASE_URL = "https://api.example.com";

// ============================================
// API CLIENT
// ============================================
class MyServiceClient {
  private apiKey: string;
  private baseUrl: string;

  constructor(apiKey: string, baseUrl: string = API_BASE_URL) {
    this.apiKey = apiKey;
    this.baseUrl = baseUrl;
  }

  async request(endpoint: string, options: RequestInit = {}) {
    const url = `${this.baseUrl}${endpoint}`;
    
    const response = await fetch(url, {
      ...options,
      headers: {
        "Authorization": `Bearer ${this.apiKey}`,
        "Content-Type": "application/json",
        ...options.headers,
      },
    });

    if (!response.ok) {
      const errorText = await response.text();
      throw new Error(`API error: ${response.status} ${response.statusText} - ${errorText}`);
    }

    return response.json();
  }

  async get(endpoint: string) {
    return this.request(endpoint, { method: "GET" });
  }

  async post(endpoint: string, data: any) {
    return this.request(endpoint, {
      method: "POST",
      body: JSON.stringify(data),
    });
  }

  async put(endpoint: string, data: any) {
    return this.request(endpoint, {
      method: "PUT",
      body: JSON.stringify(data),
    });
  }

  async delete(endpoint: string) {
    return this.request(endpoint, { method: "DELETE" });
  }
}

// ============================================
// TOOL DEFINITIONS
// ============================================
const tools = [
  {
    name: "get_items",
    description: "List items with optional filters. Returns paginated results.",
    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)" },
        status: { type: "string", description: "Filter by status: active, inactive, all" },
        createdAfter: { type: "string", description: "Filter items created after (ISO 8601)" },
      },
    },
  },
  {
    name: "get_item",
    description: "Get detailed information about a specific item by ID.",
    inputSchema: {
      type: "object" as const,
      properties: {
        item_id: { type: "string", description: "Item ID" },
      },
      required: ["item_id"],
    },
  },
  {
    name: "create_item",
    description: "Create a new item. Returns the created item with ID.",
    inputSchema: {
      type: "object" as const,
      properties: {
        name: { type: "string", description: "Item name" },
        description: { type: "string", description: "Item description" },
        status: { type: "string", description: "Status: active or inactive" },
      },
      required: ["name"],
    },
  },
];

// ============================================
// TOOL HANDLER
// ============================================
async function handleTool(client: MyServiceClient, name: string, args: Record<string, unknown>) {
  switch (name) {
    case "get_items": {
      const { page = 1, pageSize = 50, status, createdAfter } = args;
      const params = new URLSearchParams();
      params.append("page", String(page));
      params.append("pageSize", String(Math.min(Number(pageSize), 100)));
      if (status) params.append("status", String(status));
      if (createdAfter) params.append("createdAfter", String(createdAfter));
      
      return await client.get(`/items?${params}`);
    }
    
    case "get_item": {
      const { item_id } = args;
      return await client.get(`/items/${item_id}`);
    }
    
    case "create_item": {
      const { name, description, status = "active" } = args;
      return await client.post("/items", { name, description, status });
    }
    
    default:
      throw new Error(`Unknown tool: ${name}`);
  }
}

// ============================================
// SERVER SETUP
// ============================================
async function main() {
  const apiKey = process.env.MY_SERVICE_API_KEY;
  
  if (!apiKey) {
    console.error("Error: MY_SERVICE_API_KEY environment variable required");
    process.exit(1);
  }

  const client = new MyServiceClient(apiKey);

  const server = new Server(
    { name: `${MCP_NAME}-mcp`, version: MCP_VERSION },
    { capabilities: { tools: {} } }
  );

  server.setRequestHandler(ListToolsRequestSchema, async () => ({
    tools,
  }));

  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);
      return {
        content: [{ type: "text", text: `Error: ${message}` }],
        isError: true,
      };
    }
  });

  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error(`${MCP_NAME} MCP server running on stdio`);
}

main().catch(console.error);

3. Package.json Template

{
  "name": "mcp-server-myservice",
  "version": "1.0.0",
  "type": "module",
  "main": "dist/index.js",
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js",
    "dev": "tsx src/index.ts"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^0.5.0"
  },
  "devDependencies": {
    "@types/node": "^20.10.0",
    "tsx": "^4.7.0",
    "typescript": "^5.3.0"
  }
}

Key points:

  • "type": "module" — Always use ESM
  • "main": "dist/index.js" — Points to compiled output
  • build → Compile TypeScript
  • start → Run compiled version
  • dev → Run directly with tsx (development)

4. TypeScript Config (tsconfig.json)

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ES2022",
    "moduleResolution": "node",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

5. Tool Naming Conventions

Pattern: verb_noun (lowercase, snake_case)

CRUD Operations:

  • list_contacts → List/search with optional filters
  • get_contact → Get one item by ID
  • create_contact → Create new item
  • update_contact → Update existing item
  • delete_contact → Delete item

Other Actions:

  • search_contacts → Full-text search
  • send_email → Action-based
  • schedule_appointment → Action-based
  • export_report → Action-based

Anti-patterns (avoid):

  • getContacts (camelCase)
  • ContactsList (PascalCase)
  • contacts (no verb)
  • list-contacts (kebab-case)

6. Input Schema Best Practices

Use type: "object" as const

The as const ensures TypeScript infers literal types correctly.

Always describe parameters

properties: {
  page: { 
    type: "number", 
    description: "Page number (default 1)" // ✅ Good
  },
  email: { 
    type: "string" // ❌ Missing description
  },
}

Mark required fields

inputSchema: {
  type: "object" as const,
  properties: {
    contact_id: { type: "string", description: "Contact ID" },
    name: { type: "string", description: "Contact name" },
  },
  required: ["contact_id"], // ✅ Explicitly mark required
}

Default values in descriptions

page: { type: "number", description: "Page number (default 1)" },
pageSize: { type: "number", description: "Results per page (default 50, max 100)" },

Use enums for fixed options

status: { 
  type: "string", 
  description: "Status: active, inactive, pending",
  enum: ["active", "inactive", "pending"] // Optional but helpful
},

7. API Client Patterns

OAuth Token Management (Example: ServiceTitan)

class ServiceTitanClient {
  private accessToken: string | null = null;
  private tokenExpiry: number = 0;

  async getAccessToken(): Promise<string> {
    // Return cached token if still valid (with 5 min buffer)
    if (this.accessToken && Date.now() < this.tokenExpiry - 300000) {
      return this.accessToken;
    }

    // Request new token
    const response = await fetch(AUTH_URL, {
      method: "POST",
      headers: { "Content-Type": "application/x-www-form-urlencoded" },
      body: new URLSearchParams({
        grant_type: "client_credentials",
        client_id: this.clientId,
        client_secret: this.clientSecret,
      }),
    });

    const data = await response.json();
    this.accessToken = data.access_token;
    this.tokenExpiry = Date.now() + (data.expires_in * 1000);
    
    return this.accessToken!;
  }

  async request(endpoint: string, options: RequestInit = {}) {
    const token = await this.getAccessToken();
    // ... use token in headers
  }
}

API Key Auth (Most Common)

class MyServiceClient {
  private apiKey: string;

  async request(endpoint: string, options: RequestInit = {}) {
    return fetch(url, {
      ...options,
      headers: {
        "Authorization": `Bearer ${this.apiKey}`, // or "X-API-Key"
        "Content-Type": "application/json",
        ...options.headers,
      },
    });
  }
}

Error Handling Pattern

async request(endpoint: string, options: RequestInit = {}) {
  const response = await fetch(url, options);

  if (!response.ok) {
    const errorText = await response.text();
    throw new Error(
      `API error: ${response.status} ${response.statusText} - ${errorText}`
    );
  }

  return response.json();
}

8. Tool Handler Switch Pattern

Always use a switch statement for clarity and type safety:

async function handleTool(client: MyClient, name: string, args: Record<string, unknown>) {
  switch (name) {
    case "list_items": {
      // Destructure with defaults
      const { page = 1, pageSize = 50, status } = args;
      
      // Build query params
      const params = new URLSearchParams();
      params.append("page", String(page));
      params.append("pageSize", String(Math.min(Number(pageSize), 100)));
      if (status) params.append("status", String(status));
      
      return await client.get(`/items?${params}`);
    }
    
    case "get_item": {
      const { item_id } = args;
      // No validation needed if required in schema
      return await client.get(`/items/${item_id}`);
    }
    
    case "create_item": {
      const { name, description, status = "active" } = args;
      return await client.post("/items", { name, description, status });
    }
    
    default:
      throw new Error(`Unknown tool: ${name}`);
  }
}

Pattern notes:

  • Use block scope { } for each case
  • Destructure args with defaults
  • Build query params explicitly (type-safe)
  • Return API response directly (let caller handle formatting)
  • Always include default case with error

9. Environment Variables

Required Pattern

async function main() {
  const apiKey = process.env.MY_SERVICE_API_KEY;
  const apiSecret = process.env.MY_SERVICE_API_SECRET;
  
  if (!apiKey) {
    console.error("Error: MY_SERVICE_API_KEY environment variable required");
    process.exit(1);
  }
  
  if (!apiSecret) {
    console.error("Error: MY_SERVICE_API_SECRET environment variable required");
    process.exit(1);
  }

  const client = new MyServiceClient(apiKey, apiSecret);
  // ... rest of setup
}

.env.example Template

# MyService MCP Server Configuration
MY_SERVICE_API_KEY=your_api_key_here
MY_SERVICE_API_SECRET=your_api_secret_here

# Optional: Override base URL for testing
# MY_SERVICE_BASE_URL=https://sandbox.example.com

Best practices:

  • Prefix all env vars with service name
  • Use SCREAMING_SNAKE_CASE
  • Provide .env.example with all required vars
  • Document what each var is for
  • Exit with clear error if missing (don't fail silently)

10. Pagination Handling

Standard Pattern

{
  name: "list_contacts",
  description: "List contacts with pagination. Use page and pageSize to navigate results.",
  inputSchema: {
    type: "object" as const,
    properties: {
      page: { 
        type: "number", 
        description: "Page number (default 1, starts at 1)" 
      },
      pageSize: { 
        type: "number", 
        description: "Results per page (default 50, max 100)" 
      },
    },
  },
}

In Tool Handler

case "list_contacts": {
  const { page = 1, pageSize = 50 } = args;
  const params = new URLSearchParams();
  params.append("page", String(page));
  params.append("pageSize", String(Math.min(Number(pageSize), 100))); // Cap at API max
  
  return await client.get(`/contacts?${params}`);
}

Why cap pageSize:

  • Most APIs have max limits (50, 100, 200)
  • Prevents user from requesting 10,000 results
  • Documents the actual API limitation

11. Error Handling (Server Level)

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);
    return {
      content: [{ type: "text", text: `Error: ${message}` }],
      isError: true,
    };
  }
});

Pattern:

  • Always wrap in try-catch
  • Extract error message safely (error instanceof Error)
  • Return with isError: true
  • Format response consistently

12. Common Patterns Across 30+ Servers

1. List Operations

  • Always support pagination: page, pageSize
  • Common filters: status, createdAfter, updatedAfter, search/query
  • Return format: Matches API response (usually { data: [], total: N, page: 1 })

2. Get Operations

  • Single required param: Usually id, contact_id, job_id etc.
  • Return full object: Don't filter fields unless API requires it

3. Create/Update Operations

  • Required vs optional: Mark clearly in schema
  • Return created object: Include new ID in response
  • Validation: Let API do validation, don't duplicate

4. Search Operations

  • Separate search_ tool if different from list_
  • Support query string + optional filters
  • Document what fields are searched

5. Date/Time Handling

  • Always use ISO 8601: "2025-02-03T14:30:00Z"
  • Document timezone: UTC unless specified
  • Filter params: createdAfter, createdBefore, updatedAfter, etc.

13. Build & Run Commands

Development

# Install dependencies
npm install

# Run in development (with hot reload)
npm run dev

# Build TypeScript
npm run build

# Run compiled version
npm start

Add to Claude Desktop

{
  "mcpServers": {
    "my-service": {
      "command": "node",
      "args": ["/absolute/path/to/dist/index.js"],
      "env": {
        "MY_SERVICE_API_KEY": "your_key_here"
      }
    }
  }
}

14. Testing Checklist

Before considering an MCP server "done":

  • All tools defined with clear descriptions
  • All parameters documented
  • Required parameters marked explicitly
  • Pagination implemented (page, pageSize)
  • Error handling returns clear messages
  • Environment variables validated on startup
  • .env.example provided with all required vars
  • Compiles without errors (npm run build)
  • Runs successfully (npm start)
  • Test at least one tool in Claude Desktop
  • README.md with setup instructions

15. When to Split Into Multiple Files

Keep in one file (src/index.ts) if:

  • Under 500 lines
  • Fewer than 15 tools
  • Single API client

Split into modules when:

  • Over 500 lines
  • 20+ tools
  • Multiple API clients (different auth patterns)
  • Shared utilities across tools

Suggested structure when splitting:

src/
├── index.ts          # Server setup + main
├── client.ts         # API client class
├── tools.ts          # Tool definitions array
├── handlers.ts       # Tool handler function
└── types.ts          # TypeScript interfaces

16. Modern MCP Features (Labels, Lazy Loading, Progress)

Tool Metadata & Labels

Use _meta to add rich metadata to tools:

{
  name: "search_contacts",
  description: "Search contacts with filters",
  inputSchema: { /* ... */ },
  _meta: {
    // Human-readable labels for categorization
    labels: {
      category: "contacts",
      access: "read",
      complexity: "simple",
    },
    // Visibility control
    visibility: ["app", "model"], // or just ["model"] to hide from apps
    // UI hints (for MCP Apps)
    ui: {
      resourceUri: "ui://myservice/contact-search",
      preferredPresentation: "inline", // or "modal", "sidebar"
    },
  },
}

Common label patterns:

// By category
labels: { category: "contacts" | "deals" | "analytics" | "admin" }

// By operation type
labels: { access: "read" | "write" | "delete" }

// By complexity (for model selection)
labels: { complexity: "simple" | "complex" | "batch" }

// By data sensitivity
labels: { sensitivity: "public" | "internal" | "confidential" }

Benefits:

  • Hosts can filter/group tools by labels
  • Apps can query only tools they need
  • Models can prioritize simpler tools
  • Better tool discovery in Claude Desktop

Lazy-Loaded Resources

Pattern: Resources that load on demand (not all upfront)

import { ListResourcesRequestSchema, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js";

// Register resource handlers
server.setRequestHandler(ListResourcesRequestSchema, async () => {
  // Return resource metadata (not content)
  return {
    resources: [
      {
        uri: "myservice://contacts/list",
        name: "Contact List",
        description: "All contacts in the system",
        mimeType: "application/json",
      },
      {
        uri: "myservice://analytics/dashboard",
        name: "Analytics Dashboard",
        description: "Real-time analytics data",
        mimeType: "application/json",
      },
    ],
  };
});

server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
  const { uri } = request.params;

  switch (uri) {
    case "myservice://contacts/list": {
      // Fetch contacts on-demand when requested
      const contacts = await client.get("/contacts");
      return {
        contents: [{
          uri,
          mimeType: "application/json",
          text: JSON.stringify(contacts, null, 2),
        }],
      };
    }

    case "myservice://analytics/dashboard": {
      // Fetch analytics on-demand
      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}`);
  }
});

When to use lazy-loaded resources:

  • Large datasets that shouldn't load upfront
  • Real-time data that changes frequently
  • Expensive API calls
  • User-specific data (load per request)

Resource URI patterns:

  • myservice://type/identifier — Custom scheme
  • file:///path/to/data.json — File system
  • ui://myservice/component — UI components (for MCP Apps)

Resource Templates

Pattern: Dynamic resource URIs with parameters

server.setRequestHandler(ListResourcesRequestSchema, async () => {
  return {
    resourceTemplates: [
      {
        uriTemplate: "myservice://contact/{id}",
        name: "Contact Details",
        description: "Detailed information for a specific contact",
        mimeType: "application/json",
      },
      {
        uriTemplate: "myservice://report/{year}/{month}",
        name: "Monthly Report",
        description: "Monthly analytics report",
        mimeType: "application/json",
      },
    ],
  };
});

server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
  const { uri } = request.params;

  // Parse template parameters
  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),
      }],
    };
  }

  const reportMatch = uri.match(/^myservice:\/\/report\/(\d{4})\/(\d{2})$/);
  if (reportMatch) {
    const [, year, month] = reportMatch;
    const report = await client.get(`/reports/${year}/${month}`);
    return {
      contents: [{
        uri,
        mimeType: "application/json",
        text: JSON.stringify(report, null, 2),
      }],
    };
  }

  throw new Error(`Unknown resource: ${uri}`);
});

Use cases:

  • Per-entity detail views
  • Time-based data (reports, analytics)
  • Filtered datasets
  • Generated documents

Progress Notifications (Long-Running Operations)

Pattern: Send progress updates for slow operations

import { 
  CallToolRequestSchema,
  LoggingLevel,
  ProgressNotificationSchema 
} from "@modelcontextprotocol/sdk/types.js";

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args, _meta } = request.params;

  if (name === "import_contacts") {
    const { fileUrl } = args;
    
    // Send progress notifications
    const progressToken = _meta?.progressToken;
    
    if (progressToken) {
      // Download file
      await server.notification({
        method: "notifications/progress",
        params: {
          progressToken,
          progress: 0.2,
          total: 1.0,
        },
      });

      // Parse file
      await server.notification({
        method: "notifications/progress",
        params: {
          progressToken,
          progress: 0.5,
          total: 1.0,
        },
      });

      // Import records
      await server.notification({
        method: "notifications/progress",
        params: {
          progressToken,
          progress: 0.8,
          total: 1.0,
        },
      });
    }

    // Do the actual work
    const result = await importContactsFromFile(fileUrl);

    // Final progress
    if (progressToken) {
      await server.notification({
        method: "notifications/progress",
        params: {
          progressToken,
          progress: 1.0,
          total: 1.0,
        },
      });
    }

    return {
      content: [{ 
        type: "text", 
        text: `Imported ${result.count} contacts successfully` 
      }],
    };
  }
  
  // ... other tools
});

When to use progress notifications:

  • Operations taking >5 seconds
  • Multi-step workflows
  • File uploads/downloads
  • Batch operations
  • Data imports/exports

Logging for Debugging

Pattern: Send structured logs to host

// In tool handler
try {
  await server.notification({
    method: "notifications/message",
    params: {
      level: LoggingLevel.Info,
      logger: "myservice",
      data: {
        operation: "create_contact",
        contactId: newContact.id,
        timestamp: new Date().toISOString(),
      },
    },
  });

  const result = await client.post("/contacts", data);
  
  return { content: [{ type: "text", text: JSON.stringify(result) }] };
  
} catch (error) {
  await server.notification({
    method: "notifications/message",
    params: {
      level: LoggingLevel.Error,
      logger: "myservice",
      data: {
        operation: "create_contact",
        error: error.message,
        timestamp: new Date().toISOString(),
      },
    },
  });
  
  throw error;
}

Log levels:

  • LoggingLevel.Debug — Detailed debug info
  • LoggingLevel.Info — Informational messages
  • LoggingLevel.Warning — Warning conditions
  • LoggingLevel.Error — Error conditions
  • LoggingLevel.Critical — Critical failures

Prompts (for Auto-Completion)

Pattern: Provide predefined prompt templates

import { ListPromptsRequestSchema, GetPromptRequestSchema } from "@modelcontextprotocol/sdk/types.js";

server.setRequestHandler(ListPromptsRequestSchema, async () => {
  return {
    prompts: [
      {
        name: "analyze_pipeline",
        description: "Analyze sales pipeline health and suggest actions",
        arguments: [
          {
            name: "pipelineId",
            description: "Pipeline ID to analyze",
            required: false,
          },
        ],
      },
      {
        name: "contact_summary",
        description: "Generate comprehensive contact summary with activity history",
        arguments: [
          {
            name: "contactId",
            description: "Contact ID",
            required: true,
          },
        ],
      },
    ],
  };
});

server.setRequestHandler(GetPromptRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;

  switch (name) {
    case "analyze_pipeline": {
      const { pipelineId } = args || {};
      
      // Fetch data for the prompt
      const pipeline = pipelineId 
        ? await client.get(`/pipelines/${pipelineId}`)
        : await client.get("/pipelines/main");
      
      const opportunities = await client.get(`/opportunities?pipelineId=${pipeline.id}`);

      return {
        description: `Analyzing pipeline: ${pipeline.name}`,
        messages: [
          {
            role: "user",
            content: {
              type: "text",
              text: `Please analyze this sales pipeline and suggest actions:\n\n${JSON.stringify({ pipeline, opportunities }, null, 2)}`,
            },
          },
        ],
      };
    }

    case "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 of this contact:\n\nContact: ${JSON.stringify(contact, null, 2)}\n\nRecent Activity: ${JSON.stringify(activities, null, 2)}`,
            },
          },
        ],
      };
    }

    default:
      throw new Error(`Unknown prompt: ${name}`);
  }
});

Use cases:

  • Common analysis workflows
  • Report generation
  • Data summarization
  • Quick actions for users

Roots Listing (for File Systems)

Pattern: List root directories/containers

import { ListRootsRequestSchema } from "@modelcontextprotocol/sdk/types.js";

server.setRequestHandler(ListRootsRequestSchema, async () => {
  return {
    roots: [
      {
        uri: "myservice://workspaces/",
        name: "All Workspaces",
      },
      {
        uri: "myservice://contacts/",
        name: "Contacts Database",
      },
      {
        uri: "myservice://reports/",
        name: "Reports Archive",
      },
    ],
  };
});

When to use roots:

  • File system-like data structures
  • Multiple top-level containers
  • Workspace/project organization
  • Document hierarchies

Sampling (for AI Completions)

Pattern: Request LLM completions from the host

// NOTE: Most servers don't need this - it's for servers that help with AI tasks
import { CreateMessageRequestSchema } from "@modelcontextprotocol/sdk/types.js";

// If your server needs to request completions FROM the model
// (rare - usually the model calls your tools, not the other way around)
server.setRequestHandler(CreateMessageRequestSchema, async (request) => {
  const { messages, maxTokens } = request.params;
  
  // This would be handled by the HOST, not your server
  // Only implement if your server orchestrates AI workflows
  throw new Error("Sampling not implemented");
});

When to use: Almost never. Only for meta-servers that orchestrate AI workflows.


17. Resources & References


18. Quick Start Command

# Create new MCP server
mkdir mcp-server-myservice
cd mcp-server-myservice

# Init package.json
npm init -y

# Install deps
npm install @modelcontextprotocol/sdk
npm install -D typescript @types/node tsx

# Create structure
mkdir src dist
touch src/index.ts tsconfig.json .env.example .gitignore

# Copy template from this skill into src/index.ts
# Configure package.json scripts
# Configure tsconfig.json
# Build and test

Summary:

  • One-file pattern for most servers
  • Clear tool naming (verb_noun)
  • Comprehensive input schemas with descriptions
  • Tool metadata with labels for categorization
  • Lazy-loaded resources for on-demand data
  • Progress notifications for long operations
  • Structured logging for debugging
  • Prompts for common workflows
  • Environment variable validation
  • Consistent error handling
  • Pagination support
  • ISO 8601 dates
  • Test before shipping

This skill captures patterns from 30+ production MCP servers plus modern MCP features (labels, lazy loading, progress, prompts). Follow these and you'll get it right on attempt 1.