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.
20 KiB
MCP Server Blueprint — February 2026
This is the definitive template for building production-ready MCP servers in 2026.
Use this checklist for EVERY new MCP server. No skipping steps. These patterns ensure your server is:
- ✅ Usable (not just functional)
- ✅ Fast (lazy loading, efficient queries)
- ✅ Discoverable (labels, descriptions)
- ✅ Interactive (MCP Apps where appropriate)
- ✅ Debuggable (logging, progress)
- ✅ Production-ready (error handling, deployment)
Phase 1: Planning (Before Writing Code)
1.1 Define Server Scope
- What API/service are you integrating?
- What are the 5-10 most important operations?
- Who is the target user? (developers, business users, etc.)
- What data is most frequently accessed?
1.2 Identify Tool Categories
Label your tools by category. Common patterns:
- CRUD operations (create, read, update, delete)
- Search/Filter (find data with queries)
- Analytics/Reporting (stats, dashboards, summaries)
- Workflows (multi-step operations)
- Admin (configuration, settings)
1.3 Identify UI Opportunities
Which operations benefit from visual display?
- Data grids — Contact lists, search results, tables
- Dashboards — Metrics, KPIs, analytics
- Cards — Detail views (invoices, opportunities, profiles)
- Timelines — Activity feeds, history
- Forms — Quick actions (booking, creating records)
- Kanban — Pipeline views, project boards
If you have 3+ UI opportunities, plan for MCP Apps.
Phase 2: Core Server Setup
2.1 Project Structure
mkdir mcp-server-myservice
cd mcp-server-myservice
npm init -y
npm install @modelcontextprotocol/sdk
npm install -D typescript @types/node tsx fs-extra @types/fs-extra
2.2 File Structure
mcp-server-myservice/
├── src/
│ ├── index.ts # Main server (or server.ts)
│ ├── clients/
│ │ └── api-client.ts # API client
│ ├── apps/ # If using MCP Apps
│ │ └── index.ts # Apps manager
│ ├── ui/ # If using MCP Apps
│ │ ├── contact-grid.html
│ │ └── dashboard.html
│ └── types/
│ └── index.ts # Shared types
├── dist/ # Build output
├── scripts/
│ └── copy-ui.js # UI build script
├── package.json
├── tsconfig.json
├── .env.example
├── .gitignore
├── .npmignore
├── Dockerfile
├── railway.json
└── README.md
2.3 Package Configuration
{
"name": "mcp-server-myservice",
"version": "1.0.0",
"type": "module",
"main": "dist/index.js",
"bin": {
"mcp-server-myservice": "dist/index.js"
},
"scripts": {
"build": "npm run build:ts && npm run build:ui",
"build:ts": "tsc",
"build:ui": "node scripts/copy-ui.js",
"dev": "tsx src/index.ts",
"start": "node dist/index.js"
},
"files": ["dist", "README.md", "LICENSE"],
"keywords": ["mcp", "mcp-server", "model-context-protocol", "myservice"]
}
Phase 3: Tool Design (The Most Important Phase)
3.1 Tool Naming Convention
✅ Use: verb_noun (snake_case)
❌ Avoid: camelCase, PascalCase, kebab-case
CRUD patterns:
list_contacts(with pagination + filters)get_contact(by ID)create_contact(returns created object)update_contact(partial updates)delete_contact(confirm before delete)search_contacts(full-text search if different from list)
Other patterns:
send_email,schedule_appointment,export_report,analyze_pipeline
3.2 Tool Metadata & Labels ⭐ CRITICAL
Every tool MUST have _meta with labels:
{
name: "search_contacts",
description: "Search contacts with filters. Returns paginated results.",
inputSchema: { /* ... */ },
_meta: {
labels: {
category: "contacts", // Group by feature
access: "read", // read | write | delete
complexity: "simple", // simple | complex | batch
},
},
}
Label categories to use:
category: contacts, deals, analytics, calendar, email, admin, workflowsaccess: read, write, deletecomplexity: simple (1 API call), complex (multiple calls), batch (loops)sensitivity: public, internal, confidential (optional)
3.3 Input Schemas — Best Practices
inputSchema: {
type: "object" as const,
properties: {
// Always describe parameters
page: {
type: "number",
description: "Page number (default 1, starts at 1)"
},
pageSize: {
type: "number",
description: "Results per page (default 50, max 100)"
},
// Use enums for fixed options
status: {
type: "string",
description: "Filter by status",
enum: ["active", "inactive", "pending"],
},
// ISO 8601 for dates
createdAfter: {
type: "string",
description: "Filter created after (ISO 8601: 2026-02-03T14:00:00Z)"
},
},
// Mark required fields explicitly
required: ["contactId"],
}
3.4 Pagination (Mandatory for List Operations)
Every list_ or search_ tool MUST support pagination:
{
name: "list_contacts",
description: "List contacts with pagination and filters",
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)" },
query: { type: "string", description: "Search query (optional)" },
},
},
_meta: {
labels: { category: "contacts", access: "read", complexity: "simple" },
},
}
In handler:
case "list_contacts": {
const { page = 1, pageSize = 50, query } = args;
const params = new URLSearchParams();
params.append("page", String(page));
params.append("pageSize", String(Math.min(Number(pageSize), 100))); // Cap at API max
if (query) params.append("query", query);
return await client.get(`/contacts?${params}`);
}
Phase 4: Lazy-Loaded Resources ⭐ NEW
4.1 When to Use Resources vs Tools
Use resources for:
- Large datasets (contact lists, transaction history)
- Frequently changing data (real-time dashboards)
- Reference data (documentation, schemas)
- User-specific data (per-user settings, dashboards)
Use tools for:
- Operations with parameters (search, filter, create)
- One-time fetches
- Mutations (create, update, delete)
4.2 Resource Setup
import { ListResourcesRequestSchema, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js";
// Declare resources capability
const server = new Server(
{ name: "myservice-mcp", version: "1.0.0" },
{ capabilities: { tools: {}, resources: {} } } // ✅ Enable resources
);
// List available resources (metadata only)
server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resources: [
{
uri: "myservice://contacts/all",
name: "All Contacts",
description: "Complete contact database (lazy-loaded)",
mimeType: "application/json",
},
{
uri: "myservice://analytics/dashboard",
name: "Analytics Dashboard",
description: "Real-time analytics data",
mimeType: "application/json",
},
],
};
});
// Fetch resource content on-demand
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const { uri } = request.params;
switch (uri) {
case "myservice://contacts/all": {
const contacts = await client.get("/contacts?limit=1000"); // Fetch when requested
return {
contents: [{
uri,
mimeType: "application/json",
text: JSON.stringify(contacts, null, 2),
}],
};
}
case "myservice://analytics/dashboard": {
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}`);
}
});
4.3 Resource Templates (Dynamic URIs)
server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resourceTemplates: [
{
uriTemplate: "myservice://contact/{id}",
name: "Contact Details",
description: "Full contact record by ID",
mimeType: "application/json",
},
],
};
});
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const { uri } = request.params;
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),
}],
};
}
throw new Error(`Unknown resource: ${uri}`);
});
Phase 5: MCP Apps (If Applicable)
5.1 Should You Build Apps?
Build MCP Apps if you have:
- ✅ Visual data (grids, cards, dashboards)
- ✅ 3+ UI opportunities identified in Phase 1
- ✅ Complex data relationships (better shown than described)
- ✅ Interactive workflows (drag-drop, forms)
Skip apps if:
- ❌ Simple CRUD operations only
- ❌ All operations return small JSON objects
- ❌ No visual benefit
5.2 App Architecture
See mcp-apps-integration skill for full details. Quick checklist:
- Create
src/apps/index.ts— MCPAppsManager class - Create
src/ui/directory — HTML components - Register resource handlers for UI files
- Add app tools with
_meta.ui.resourceUri - Implement
ListResourcesRequestSchemahandler - Implement
ReadResourceRequestSchemahandler - Add
build:uiscript to copy HTML todist/app-ui/
5.3 App Tool Naming
Pattern: view_ or show_ prefix
{
name: "view_contact_grid",
description: "Display contact search results in a data grid (visual UI component)",
inputSchema: { /* ... */ },
_meta: {
labels: { category: "contacts", access: "read", complexity: "simple" },
ui: { resourceUri: "ui://myservice/contact-grid" },
},
}
5.4 Common App Patterns
- Contact Grid — Search results table
- Dashboard — Multi-widget analytics view
- Pipeline Board — Kanban with drag-drop
- Opportunity Card — Detail view for single record
- Calendar View — Appointment/event calendar
- Timeline — Activity feed
Reference: 11 production GHL apps in /Users/jakeshore/.clawdbot/workspace/mcp-diagrams/ghl-mcp-apps-only/
Phase 6: Progress & Logging
6.1 Progress Notifications (For Long Operations)
Any operation taking >5 seconds MUST send progress updates:
if (name === "import_contacts") {
const progressToken = request.params._meta?.progressToken;
if (progressToken) {
await server.notification({
method: "notifications/progress",
params: {
progressToken,
progress: 0.3, // 30%
total: 1.0,
},
});
}
// ... do work
if (progressToken) {
await server.notification({
method: "notifications/progress",
params: { progressToken, progress: 1.0, total: 1.0 },
});
}
}
6.2 Structured Logging
Log important operations for debugging:
import { LoggingLevel } from "@modelcontextprotocol/sdk/types.js";
await server.notification({
method: "notifications/message",
params: {
level: LoggingLevel.Info,
logger: "myservice",
data: {
operation: "create_contact",
contactId: newContact.id,
timestamp: new Date().toISOString(),
},
},
});
When to log:
- Info: Successful operations (create, update, delete)
- Warning: Rate limits, retries, fallbacks
- Error: API failures, validation errors
- Debug: Detailed request/response data (dev only)
Phase 7: Error Handling (Production-Ready)
7.1 Tool Handler Error Wrapping
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);
// Log the error
await server.notification({
method: "notifications/message",
params: {
level: LoggingLevel.Error,
logger: "myservice",
data: { tool: name, error: message },
},
});
return {
content: [{ type: "text", text: `Error: ${message}` }],
isError: true,
};
}
});
7.2 API Client Error Handling
async request(endpoint: string, options: RequestInit = {}) {
const response = await fetch(url, options);
if (!response.ok) {
const errorText = await response.text();
// Parse API error if JSON
try {
const errorJson = JSON.parse(errorText);
throw new Error(
`API error: ${response.status} - ${errorJson.message || errorJson.error || errorText}`
);
} catch {
throw new Error(
`API error: ${response.status} ${response.statusText} - ${errorText}`
);
}
}
return response.json();
}
Phase 8: Prompts (Optional but Recommended)
8.1 When to Add Prompts
Add prompts for:
- Common analysis workflows (e.g., "Analyze pipeline health")
- Report generation (e.g., "Generate contact summary")
- Quick actions (e.g., "Find overdue tasks")
- Data exploration (e.g., "Show top performers")
8.2 Prompt Implementation
import { ListPromptsRequestSchema, GetPromptRequestSchema } from "@modelcontextprotocol/sdk/types.js";
server.setRequestHandler(ListPromptsRequestSchema, async () => {
return {
prompts: [
{
name: "contact_summary",
description: "Generate comprehensive contact summary with recent activity",
arguments: [
{ name: "contactId", description: "Contact ID", required: true },
],
},
],
};
});
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
if (name === "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:\n\n${JSON.stringify({ contact, activities }, null, 2)}`,
},
}],
};
}
throw new Error(`Unknown prompt: ${name}`);
});
Phase 9: Testing Checklist
9.1 Local Testing
- All tools compile without errors (
npm run build) - Server starts successfully (
npm start) - Environment variables validated on startup
- Test each tool in Claude Desktop
- Test pagination (page 1, page 2)
- Test error cases (invalid IDs, missing params)
- Test apps render correctly (if applicable)
- Check logs in Claude Desktop console
9.2 Performance Testing
- List operations return in <2 seconds
- Lazy-loaded resources only fetch when requested
- No unnecessary API calls
- Pagination caps at API maximum
- Progress notifications for operations >5 seconds
Phase 10: Documentation
10.1 README.md Structure
# MCP Server for MyService
MCP integration for MyService. Enables Claude Desktop to [core value prop].
## Features
- ✅ List/search/CRUD contacts
- ✅ Analytics dashboard (MCP App)
- ✅ Pipeline visualization (MCP App)
- ✅ Progress tracking for imports
## Installation
[npx / manual / docker options]
## Configuration
[Environment variables with .env.example]
## Available Tools
[List of tools with descriptions]
## MCP Apps (Rich UI)
[List of app tools with screenshots]
## Development
[Build/dev instructions]
10.2 .env.example
# MyService API Credentials
MY_SERVICE_API_KEY=your_api_key_here
MY_SERVICE_API_SECRET=your_secret_here
# Optional: Override base URL
# MY_SERVICE_BASE_URL=https://sandbox.api.myservice.com
# Optional: Logging
# LOG_LEVEL=debug
Phase 11: Deployment
11.1 Docker
- Multi-stage Dockerfile
- .dockerignore file
- Test build locally
- Test run locally
11.2 Railway
- railway.json with build + start commands
- Environment variables documented
- Test deployment
11.3 npm Publishing
binfield in package.jsonfilesfield includes only dist/- .npmignore excludes src/, .env
- Keywords for discoverability
- Test
npxinstallation locally
11.4 GitHub
- README.md complete
- LICENSE file
- .gitignore excludes node_modules, dist, .env
- GitHub Actions for CI/CD (optional)
Production Checklist (Final Review)
Code Quality
- All tools have
_meta.labels - All parameters have descriptions
- Required fields marked explicitly
- Pagination implemented for list operations
- Error handling in all tool handlers
- No hardcoded API keys or secrets
- Logging for important operations
Features
- Lazy-loaded resources for large datasets
- Progress notifications for long operations (>5s)
- MCP Apps for visual data (if applicable)
- Prompts for common workflows (if applicable)
Documentation
- README with installation instructions
- .env.example with all required variables
- Tool descriptions clear and helpful
- Examples in README
Deployment
- Compiles without errors
- Runs in Claude Desktop
- Docker image builds (if using Docker)
- Railway deploys successfully (if using Railway)
- npm package installs via npx (if publishing)
Anti-Patterns to Avoid
❌ No labels on tools — Always add _meta.labels
❌ Loading all data upfront — Use lazy-loaded resources
❌ No pagination — Every list operation needs page/pageSize
❌ Silent failures — Always log errors and return clear messages
❌ No progress for slow ops — Add progress notifications for >5s operations
❌ Building apps when not needed — Only build apps if visually beneficial
❌ Missing descriptions — Every parameter needs a description
❌ No environment validation — Check env vars on startup
❌ Skipping error handling — Wrap all tool handlers in try-catch
❌ Generic error messages — Be specific ("Contact not found" not "Error")
Reference Materials
-
Skills:
mcp-server-development— Full TypeScript patternsmcp-apps-integration— MCP Apps guidemcp-deployment— Docker/Railway/npm
-
Example Servers:
/Users/jakeshore/.clawdbot/workspace/mcp-diagrams/mcp-servers/- 30 production servers with all patterns
-
Example Apps:
/Users/jakeshore/.clawdbot/workspace/mcp-diagrams/ghl-mcp-apps-only/- 11 production apps with UI components
TL;DR — The Golden Rules
- Labels on every tool — category, access, complexity
- Lazy-load large datasets — Use resources, not tools
- Paginate everything — page/pageSize on all lists
- Progress for slow ops — >5 seconds = progress notifications
- Apps for visual data — Grids, dashboards, cards, timelines
- Log important operations — Info, Warning, Error levels
- Handle errors gracefully — Clear messages, no silent failures
- Document thoroughly — README, .env.example, descriptions
- Test before shipping — All tools work in Claude Desktop
- Deploy with confidence — Docker, Railway, npm ready to go
Follow this blueprint and your MCP servers will be production-ready, usable, and optimized for February 2026.