# 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`)

```typescript
#!/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

```json
{
  "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`)

```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
```typescript
properties: {
  page: { 
    type: "number", 
    description: "Page number (default 1)" // ✅ Good
  },
  email: { 
    type: "string" // ❌ Missing description
  },
}
```

### Mark required fields
```typescript
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
```typescript
page: { type: "number", description: "Page number (default 1)" },
pageSize: { type: "number", description: "Results per page (default 50, max 100)" },
```

### Use enums for fixed options
```typescript
status: { 
  type: "string", 
  description: "Status: active, inactive, pending",
  enum: ["active", "inactive", "pending"] // Optional but helpful
},
```

---

## 7. API Client Patterns

### OAuth Token Management (Example: ServiceTitan)
```typescript
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)
```typescript
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
```typescript
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:

```typescript
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
```typescript
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
```bash
# 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
```typescript
{
  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
```typescript
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)

```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);
    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
```bash
# 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
```json
{
  "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:**

```typescript
{
  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:**
```typescript
// 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)

```typescript
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

```typescript
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

```typescript
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

```typescript
// 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

```typescript
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

```typescript
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

```typescript
// 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

- **MCP SDK Docs:** https://modelcontextprotocol.io
- **Example Servers:** `/Users/jakeshore/.clawdbot/workspace/mcp-diagrams/mcp-servers/`
- **Tool Best Practices:** https://modelcontextprotocol.io/docs/tools
- **Schema Validation:** Use Zod if complex validation needed
- **Progress Notifications:** https://modelcontextprotocol.io/docs/concepts/progress
- **Resources Guide:** https://modelcontextprotocol.io/docs/concepts/resources

---

## 18. Quick Start Command

```bash
# 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.
