# 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) { 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 { // 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) { 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.