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.
31 KiB
MCP Server Development — TypeScript Best Practices
When to use this skill: Building TypeScript-based MCP servers from scratch. Use when creating new integrations, API wrappers, or data sources for Claude Desktop.
What this covers: Complete TypeScript MCP server patterns extracted from building 30+ production servers (ServiceTitan, Gusto, Mailchimp, Calendly, Toast, Zendesk, Trello, etc.).
1. Project Structure (Standard Pattern)
my-mcp-server/
├── src/
│ └── index.ts # Main server file
├── dist/ # Compiled output (git ignored)
├── package.json
├── tsconfig.json
├── .env.example # Template for required env vars
├── .gitignore
├── Dockerfile # Optional: for containerization
├── railway.json # Optional: for Railway deployment
└── README.md
The One-File Pattern (Preferred for Most Servers)
For most MCP servers, keep everything in one src/index.ts file unless you have 20+ tools. This includes:
- Configuration
- API client class
- Tool definitions
- Tool handler function
- Server setup
Why: Easier to read, debug, and maintain. Split into modules only when file exceeds ~500 lines.
2. File Template (src/index.ts)
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
// ============================================
// CONFIGURATION
// ============================================
const MCP_NAME = "my-service";
const MCP_VERSION = "1.0.0";
const API_BASE_URL = "https://api.example.com";
// ============================================
// API CLIENT
// ============================================
class MyServiceClient {
private apiKey: string;
private baseUrl: string;
constructor(apiKey: string, baseUrl: string = API_BASE_URL) {
this.apiKey = apiKey;
this.baseUrl = baseUrl;
}
async request(endpoint: string, options: RequestInit = {}) {
const url = `${this.baseUrl}${endpoint}`;
const response = await fetch(url, {
...options,
headers: {
"Authorization": `Bearer ${this.apiKey}`,
"Content-Type": "application/json",
...options.headers,
},
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`API error: ${response.status} ${response.statusText} - ${errorText}`);
}
return response.json();
}
async get(endpoint: string) {
return this.request(endpoint, { method: "GET" });
}
async post(endpoint: string, data: any) {
return this.request(endpoint, {
method: "POST",
body: JSON.stringify(data),
});
}
async put(endpoint: string, data: any) {
return this.request(endpoint, {
method: "PUT",
body: JSON.stringify(data),
});
}
async delete(endpoint: string) {
return this.request(endpoint, { method: "DELETE" });
}
}
// ============================================
// TOOL DEFINITIONS
// ============================================
const tools = [
{
name: "get_items",
description: "List items with optional filters. Returns paginated results.",
inputSchema: {
type: "object" as const,
properties: {
page: { type: "number", description: "Page number (default 1)" },
pageSize: { type: "number", description: "Results per page (default 50, max 100)" },
status: { type: "string", description: "Filter by status: active, inactive, all" },
createdAfter: { type: "string", description: "Filter items created after (ISO 8601)" },
},
},
},
{
name: "get_item",
description: "Get detailed information about a specific item by ID.",
inputSchema: {
type: "object" as const,
properties: {
item_id: { type: "string", description: "Item ID" },
},
required: ["item_id"],
},
},
{
name: "create_item",
description: "Create a new item. Returns the created item with ID.",
inputSchema: {
type: "object" as const,
properties: {
name: { type: "string", description: "Item name" },
description: { type: "string", description: "Item description" },
status: { type: "string", description: "Status: active or inactive" },
},
required: ["name"],
},
},
];
// ============================================
// TOOL HANDLER
// ============================================
async function handleTool(client: MyServiceClient, name: string, args: Record<string, unknown>) {
switch (name) {
case "get_items": {
const { page = 1, pageSize = 50, status, createdAfter } = args;
const params = new URLSearchParams();
params.append("page", String(page));
params.append("pageSize", String(Math.min(Number(pageSize), 100)));
if (status) params.append("status", String(status));
if (createdAfter) params.append("createdAfter", String(createdAfter));
return await client.get(`/items?${params}`);
}
case "get_item": {
const { item_id } = args;
return await client.get(`/items/${item_id}`);
}
case "create_item": {
const { name, description, status = "active" } = args;
return await client.post("/items", { name, description, status });
}
default:
throw new Error(`Unknown tool: ${name}`);
}
}
// ============================================
// SERVER SETUP
// ============================================
async function main() {
const apiKey = process.env.MY_SERVICE_API_KEY;
if (!apiKey) {
console.error("Error: MY_SERVICE_API_KEY environment variable required");
process.exit(1);
}
const client = new MyServiceClient(apiKey);
const server = new Server(
{ name: `${MCP_NAME}-mcp`, version: MCP_VERSION },
{ capabilities: { tools: {} } }
);
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools,
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
const result = await handleTool(client, name, args || {});
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return {
content: [{ type: "text", text: `Error: ${message}` }],
isError: true,
};
}
});
const transport = new StdioServerTransport();
await server.connect(transport);
console.error(`${MCP_NAME} MCP server running on stdio`);
}
main().catch(console.error);
3. Package.json Template
{
"name": "mcp-server-myservice",
"version": "1.0.0",
"type": "module",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "tsx src/index.ts"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^0.5.0"
},
"devDependencies": {
"@types/node": "^20.10.0",
"tsx": "^4.7.0",
"typescript": "^5.3.0"
}
}
Key points:
"type": "module"— Always use ESM"main": "dist/index.js"— Points to compiled outputbuild→ Compile TypeScriptstart→ Run compiled versiondev→ Run directly with tsx (development)
4. TypeScript Config (tsconfig.json)
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "node",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
5. Tool Naming Conventions
Pattern: verb_noun (lowercase, snake_case)
CRUD Operations:
list_contacts→ List/search with optional filtersget_contact→ Get one item by IDcreate_contact→ Create new itemupdate_contact→ Update existing itemdelete_contact→ Delete item
Other Actions:
search_contacts→ Full-text searchsend_email→ Action-basedschedule_appointment→ Action-basedexport_report→ Action-based
Anti-patterns (avoid):
- ❌
getContacts(camelCase) - ❌
ContactsList(PascalCase) - ❌
contacts(no verb) - ❌
list-contacts(kebab-case)
6. Input Schema Best Practices
Use type: "object" as const
The as const ensures TypeScript infers literal types correctly.
Always describe parameters
properties: {
page: {
type: "number",
description: "Page number (default 1)" // ✅ Good
},
email: {
type: "string" // ❌ Missing description
},
}
Mark required fields
inputSchema: {
type: "object" as const,
properties: {
contact_id: { type: "string", description: "Contact ID" },
name: { type: "string", description: "Contact name" },
},
required: ["contact_id"], // ✅ Explicitly mark required
}
Default values in descriptions
page: { type: "number", description: "Page number (default 1)" },
pageSize: { type: "number", description: "Results per page (default 50, max 100)" },
Use enums for fixed options
status: {
type: "string",
description: "Status: active, inactive, pending",
enum: ["active", "inactive", "pending"] // Optional but helpful
},
7. API Client Patterns
OAuth Token Management (Example: ServiceTitan)
class ServiceTitanClient {
private accessToken: string | null = null;
private tokenExpiry: number = 0;
async getAccessToken(): Promise<string> {
// Return cached token if still valid (with 5 min buffer)
if (this.accessToken && Date.now() < this.tokenExpiry - 300000) {
return this.accessToken;
}
// Request new token
const response = await fetch(AUTH_URL, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "client_credentials",
client_id: this.clientId,
client_secret: this.clientSecret,
}),
});
const data = await response.json();
this.accessToken = data.access_token;
this.tokenExpiry = Date.now() + (data.expires_in * 1000);
return this.accessToken!;
}
async request(endpoint: string, options: RequestInit = {}) {
const token = await this.getAccessToken();
// ... use token in headers
}
}
API Key Auth (Most Common)
class MyServiceClient {
private apiKey: string;
async request(endpoint: string, options: RequestInit = {}) {
return fetch(url, {
...options,
headers: {
"Authorization": `Bearer ${this.apiKey}`, // or "X-API-Key"
"Content-Type": "application/json",
...options.headers,
},
});
}
}
Error Handling Pattern
async request(endpoint: string, options: RequestInit = {}) {
const response = await fetch(url, options);
if (!response.ok) {
const errorText = await response.text();
throw new Error(
`API error: ${response.status} ${response.statusText} - ${errorText}`
);
}
return response.json();
}
8. Tool Handler Switch Pattern
Always use a switch statement for clarity and type safety:
async function handleTool(client: MyClient, name: string, args: Record<string, unknown>) {
switch (name) {
case "list_items": {
// Destructure with defaults
const { page = 1, pageSize = 50, status } = args;
// Build query params
const params = new URLSearchParams();
params.append("page", String(page));
params.append("pageSize", String(Math.min(Number(pageSize), 100)));
if (status) params.append("status", String(status));
return await client.get(`/items?${params}`);
}
case "get_item": {
const { item_id } = args;
// No validation needed if required in schema
return await client.get(`/items/${item_id}`);
}
case "create_item": {
const { name, description, status = "active" } = args;
return await client.post("/items", { name, description, status });
}
default:
throw new Error(`Unknown tool: ${name}`);
}
}
Pattern notes:
- Use block scope
{ }for each case - Destructure args with defaults
- Build query params explicitly (type-safe)
- Return API response directly (let caller handle formatting)
- Always include
defaultcase with error
9. Environment Variables
Required Pattern
async function main() {
const apiKey = process.env.MY_SERVICE_API_KEY;
const apiSecret = process.env.MY_SERVICE_API_SECRET;
if (!apiKey) {
console.error("Error: MY_SERVICE_API_KEY environment variable required");
process.exit(1);
}
if (!apiSecret) {
console.error("Error: MY_SERVICE_API_SECRET environment variable required");
process.exit(1);
}
const client = new MyServiceClient(apiKey, apiSecret);
// ... rest of setup
}
.env.example Template
# MyService MCP Server Configuration
MY_SERVICE_API_KEY=your_api_key_here
MY_SERVICE_API_SECRET=your_api_secret_here
# Optional: Override base URL for testing
# MY_SERVICE_BASE_URL=https://sandbox.example.com
Best practices:
- Prefix all env vars with service name
- Use SCREAMING_SNAKE_CASE
- Provide
.env.examplewith all required vars - Document what each var is for
- Exit with clear error if missing (don't fail silently)
10. Pagination Handling
Standard Pattern
{
name: "list_contacts",
description: "List contacts with pagination. Use page and pageSize to navigate results.",
inputSchema: {
type: "object" as const,
properties: {
page: {
type: "number",
description: "Page number (default 1, starts at 1)"
},
pageSize: {
type: "number",
description: "Results per page (default 50, max 100)"
},
},
},
}
In Tool Handler
case "list_contacts": {
const { page = 1, pageSize = 50 } = args;
const params = new URLSearchParams();
params.append("page", String(page));
params.append("pageSize", String(Math.min(Number(pageSize), 100))); // Cap at API max
return await client.get(`/contacts?${params}`);
}
Why cap pageSize:
- Most APIs have max limits (50, 100, 200)
- Prevents user from requesting 10,000 results
- Documents the actual API limitation
11. Error Handling (Server Level)
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
const result = await handleTool(client, name, args || {});
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return {
content: [{ type: "text", text: `Error: ${message}` }],
isError: true,
};
}
});
Pattern:
- Always wrap in try-catch
- Extract error message safely (
error instanceof Error) - Return with
isError: true - Format response consistently
12. Common Patterns Across 30+ Servers
1. List Operations
- Always support pagination: page, pageSize
- Common filters: status, createdAfter, updatedAfter, search/query
- Return format: Matches API response (usually
{ data: [], total: N, page: 1 })
2. Get Operations
- Single required param: Usually
id,contact_id,job_idetc. - 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 fromlist_ - Support query string + optional filters
- Document what fields are searched
5. Date/Time Handling
- Always use ISO 8601:
"2025-02-03T14:30:00Z" - Document timezone: UTC unless specified
- Filter params:
createdAfter,createdBefore,updatedAfter, etc.
13. Build & Run Commands
Development
# Install dependencies
npm install
# Run in development (with hot reload)
npm run dev
# Build TypeScript
npm run build
# Run compiled version
npm start
Add to Claude Desktop
{
"mcpServers": {
"my-service": {
"command": "node",
"args": ["/absolute/path/to/dist/index.js"],
"env": {
"MY_SERVICE_API_KEY": "your_key_here"
}
}
}
}
14. Testing Checklist
Before considering an MCP server "done":
- All tools defined with clear descriptions
- All parameters documented
- Required parameters marked explicitly
- Pagination implemented (page, pageSize)
- Error handling returns clear messages
- Environment variables validated on startup
.env.exampleprovided with all required vars- Compiles without errors (
npm run build) - Runs successfully (
npm start) - Test at least one tool in Claude Desktop
- README.md with setup instructions
15. When to Split Into Multiple Files
Keep in one file (src/index.ts) if:
- Under 500 lines
- Fewer than 15 tools
- Single API client
Split into modules when:
- Over 500 lines
- 20+ tools
- Multiple API clients (different auth patterns)
- Shared utilities across tools
Suggested structure when splitting:
src/
├── index.ts # Server setup + main
├── client.ts # API client class
├── tools.ts # Tool definitions array
├── handlers.ts # Tool handler function
└── types.ts # TypeScript interfaces
16. Modern MCP Features (Labels, Lazy Loading, Progress)
Tool Metadata & Labels
Use _meta to add rich metadata to tools:
{
name: "search_contacts",
description: "Search contacts with filters",
inputSchema: { /* ... */ },
_meta: {
// Human-readable labels for categorization
labels: {
category: "contacts",
access: "read",
complexity: "simple",
},
// Visibility control
visibility: ["app", "model"], // or just ["model"] to hide from apps
// UI hints (for MCP Apps)
ui: {
resourceUri: "ui://myservice/contact-search",
preferredPresentation: "inline", // or "modal", "sidebar"
},
},
}
Common label patterns:
// By category
labels: { category: "contacts" | "deals" | "analytics" | "admin" }
// By operation type
labels: { access: "read" | "write" | "delete" }
// By complexity (for model selection)
labels: { complexity: "simple" | "complex" | "batch" }
// By data sensitivity
labels: { sensitivity: "public" | "internal" | "confidential" }
Benefits:
- Hosts can filter/group tools by labels
- Apps can query only tools they need
- Models can prioritize simpler tools
- Better tool discovery in Claude Desktop
Lazy-Loaded Resources
Pattern: Resources that load on demand (not all upfront)
import { ListResourcesRequestSchema, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js";
// Register resource handlers
server.setRequestHandler(ListResourcesRequestSchema, async () => {
// Return resource metadata (not content)
return {
resources: [
{
uri: "myservice://contacts/list",
name: "Contact List",
description: "All contacts in the system",
mimeType: "application/json",
},
{
uri: "myservice://analytics/dashboard",
name: "Analytics Dashboard",
description: "Real-time analytics data",
mimeType: "application/json",
},
],
};
});
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const { uri } = request.params;
switch (uri) {
case "myservice://contacts/list": {
// Fetch contacts on-demand when requested
const contacts = await client.get("/contacts");
return {
contents: [{
uri,
mimeType: "application/json",
text: JSON.stringify(contacts, null, 2),
}],
};
}
case "myservice://analytics/dashboard": {
// Fetch analytics on-demand
const analytics = await client.get("/analytics/dashboard");
return {
contents: [{
uri,
mimeType: "application/json",
text: JSON.stringify(analytics, null, 2),
}],
};
}
default:
throw new Error(`Unknown resource: ${uri}`);
}
});
When to use lazy-loaded resources:
- Large datasets that shouldn't load upfront
- Real-time data that changes frequently
- Expensive API calls
- User-specific data (load per request)
Resource URI patterns:
myservice://type/identifier— Custom schemefile:///path/to/data.json— File systemui://myservice/component— UI components (for MCP Apps)
Resource Templates
Pattern: Dynamic resource URIs with parameters
server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resourceTemplates: [
{
uriTemplate: "myservice://contact/{id}",
name: "Contact Details",
description: "Detailed information for a specific contact",
mimeType: "application/json",
},
{
uriTemplate: "myservice://report/{year}/{month}",
name: "Monthly Report",
description: "Monthly analytics report",
mimeType: "application/json",
},
],
};
});
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const { uri } = request.params;
// Parse template parameters
const contactMatch = uri.match(/^myservice:\/\/contact\/(.+)$/);
if (contactMatch) {
const contactId = contactMatch[1];
const contact = await client.get(`/contacts/${contactId}`);
return {
contents: [{
uri,
mimeType: "application/json",
text: JSON.stringify(contact, null, 2),
}],
};
}
const reportMatch = uri.match(/^myservice:\/\/report\/(\d{4})\/(\d{2})$/);
if (reportMatch) {
const [, year, month] = reportMatch;
const report = await client.get(`/reports/${year}/${month}`);
return {
contents: [{
uri,
mimeType: "application/json",
text: JSON.stringify(report, null, 2),
}],
};
}
throw new Error(`Unknown resource: ${uri}`);
});
Use cases:
- Per-entity detail views
- Time-based data (reports, analytics)
- Filtered datasets
- Generated documents
Progress Notifications (Long-Running Operations)
Pattern: Send progress updates for slow operations
import {
CallToolRequestSchema,
LoggingLevel,
ProgressNotificationSchema
} from "@modelcontextprotocol/sdk/types.js";
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args, _meta } = request.params;
if (name === "import_contacts") {
const { fileUrl } = args;
// Send progress notifications
const progressToken = _meta?.progressToken;
if (progressToken) {
// Download file
await server.notification({
method: "notifications/progress",
params: {
progressToken,
progress: 0.2,
total: 1.0,
},
});
// Parse file
await server.notification({
method: "notifications/progress",
params: {
progressToken,
progress: 0.5,
total: 1.0,
},
});
// Import records
await server.notification({
method: "notifications/progress",
params: {
progressToken,
progress: 0.8,
total: 1.0,
},
});
}
// Do the actual work
const result = await importContactsFromFile(fileUrl);
// Final progress
if (progressToken) {
await server.notification({
method: "notifications/progress",
params: {
progressToken,
progress: 1.0,
total: 1.0,
},
});
}
return {
content: [{
type: "text",
text: `Imported ${result.count} contacts successfully`
}],
};
}
// ... other tools
});
When to use progress notifications:
- Operations taking >5 seconds
- Multi-step workflows
- File uploads/downloads
- Batch operations
- Data imports/exports
Logging for Debugging
Pattern: Send structured logs to host
// In tool handler
try {
await server.notification({
method: "notifications/message",
params: {
level: LoggingLevel.Info,
logger: "myservice",
data: {
operation: "create_contact",
contactId: newContact.id,
timestamp: new Date().toISOString(),
},
},
});
const result = await client.post("/contacts", data);
return { content: [{ type: "text", text: JSON.stringify(result) }] };
} catch (error) {
await server.notification({
method: "notifications/message",
params: {
level: LoggingLevel.Error,
logger: "myservice",
data: {
operation: "create_contact",
error: error.message,
timestamp: new Date().toISOString(),
},
},
});
throw error;
}
Log levels:
LoggingLevel.Debug— Detailed debug infoLoggingLevel.Info— Informational messagesLoggingLevel.Warning— Warning conditionsLoggingLevel.Error— Error conditionsLoggingLevel.Critical— Critical failures
Prompts (for Auto-Completion)
Pattern: Provide predefined prompt templates
import { ListPromptsRequestSchema, GetPromptRequestSchema } from "@modelcontextprotocol/sdk/types.js";
server.setRequestHandler(ListPromptsRequestSchema, async () => {
return {
prompts: [
{
name: "analyze_pipeline",
description: "Analyze sales pipeline health and suggest actions",
arguments: [
{
name: "pipelineId",
description: "Pipeline ID to analyze",
required: false,
},
],
},
{
name: "contact_summary",
description: "Generate comprehensive contact summary with activity history",
arguments: [
{
name: "contactId",
description: "Contact ID",
required: true,
},
],
},
],
};
});
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
switch (name) {
case "analyze_pipeline": {
const { pipelineId } = args || {};
// Fetch data for the prompt
const pipeline = pipelineId
? await client.get(`/pipelines/${pipelineId}`)
: await client.get("/pipelines/main");
const opportunities = await client.get(`/opportunities?pipelineId=${pipeline.id}`);
return {
description: `Analyzing pipeline: ${pipeline.name}`,
messages: [
{
role: "user",
content: {
type: "text",
text: `Please analyze this sales pipeline and suggest actions:\n\n${JSON.stringify({ pipeline, opportunities }, null, 2)}`,
},
},
],
};
}
case "contact_summary": {
const { contactId } = args;
const contact = await client.get(`/contacts/${contactId}`);
const activities = await client.get(`/contacts/${contactId}/activities`);
return {
description: `Summary for ${contact.name}`,
messages: [
{
role: "user",
content: {
type: "text",
text: `Generate a comprehensive summary of this contact:\n\nContact: ${JSON.stringify(contact, null, 2)}\n\nRecent Activity: ${JSON.stringify(activities, null, 2)}`,
},
},
],
};
}
default:
throw new Error(`Unknown prompt: ${name}`);
}
});
Use cases:
- Common analysis workflows
- Report generation
- Data summarization
- Quick actions for users
Roots Listing (for File Systems)
Pattern: List root directories/containers
import { ListRootsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
server.setRequestHandler(ListRootsRequestSchema, async () => {
return {
roots: [
{
uri: "myservice://workspaces/",
name: "All Workspaces",
},
{
uri: "myservice://contacts/",
name: "Contacts Database",
},
{
uri: "myservice://reports/",
name: "Reports Archive",
},
],
};
});
When to use roots:
- File system-like data structures
- Multiple top-level containers
- Workspace/project organization
- Document hierarchies
Sampling (for AI Completions)
Pattern: Request LLM completions from the host
// NOTE: Most servers don't need this - it's for servers that help with AI tasks
import { CreateMessageRequestSchema } from "@modelcontextprotocol/sdk/types.js";
// If your server needs to request completions FROM the model
// (rare - usually the model calls your tools, not the other way around)
server.setRequestHandler(CreateMessageRequestSchema, async (request) => {
const { messages, maxTokens } = request.params;
// This would be handled by the HOST, not your server
// Only implement if your server orchestrates AI workflows
throw new Error("Sampling not implemented");
});
When to use: Almost never. Only for meta-servers that orchestrate AI workflows.
17. Resources & References
- 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
# 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.