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

1243 lines
31 KiB
Markdown

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