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.
1243 lines
31 KiB
Markdown
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.
|