13 KiB
13 KiB
MCP Server Gold Standard — MANDATORY REFERENCE
READ THIS before ANY MCP server work. No exceptions. This is the definitive spec for how every MCPEngine server must be built. Last updated: 2026-02-14
1. Server Architecture
main.ts — Entry Point
#!/usr/bin/env node
- Shebang line for CLI execution
- Env validation with clear error messages (e.g., "Get your access token from: Settings > Apps > ...")
- Graceful shutdown handlers for SIGINT/SIGTERM
- Health check support
- Dual transport: stdio (default) + HTTP/SSE option via
--http --port=3000 - Loads config → creates API client → creates server → starts transport
server.ts — Server Class
- MUST be a CLASS (e.g.,
ShopifyMCPServer), not inline code toolModules: Map<string, () => Promise<ToolModule[]>>for lazy loadingsetupToolModules()— registers each tool file via dynamicimport():this.toolModules.set('orders', async () => { const module = await import('./tools/orders.js'); return module.default; });setupHandlers()— registersListToolsRequestSchemaandCallToolRequestSchemaloadAllTools()— resolves all lazy modules for ListTools- CallTool handler routes by tool name to correct handler function
- Capabilities:
{ tools: {}, resources: {} }
package.json
{
"name": "@mcpengine/{platform-name}",
"version": "1.0.0",
"type": "module",
"main": "dist/main.js",
"bin": { "@mcpengine/{platform-name}": "dist/main.js" },
"scripts": {
"build": "tsc",
"start": "node dist/main.js",
"dev": "tsx watch src/main.ts"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.12.1",
"axios": "^1.7.0",
"zod": "^3.23.0"
},
"devDependencies": {
"typescript": "^5.6.0",
"tsx": "^4.19.0",
"@types/node": "^22.0.0"
}
}
tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"declaration": true,
"resolveJsonModule": true,
"forceConsistentCasingInFileNames": true,
"noUncheckedIndexedAccess": true,
"jsx": "react-jsx"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "src/apps", "src/ui", "src/**/react-app"]
}
NOTE: Always exclude apps/ui from TSC — they compile separately.
2. API Client (src/client/{platform}-client.ts or src/clients/{platform}.ts)
- Axios-based with
AxiosInstance - Constructor takes config object:
{ accessToken, baseUrl, timeout?, apiVersion? } - Request interceptor: Timestamp logging of method + URL
- Response interceptor: Rate limit header parsing, warning at 80% threshold, error normalization
- Rate limit tracking:
{ current, max }from response headers - Pagination helpers: Return
{ data: T[], pageInfo: { hasNextPage, nextPageUrl } } - Typed error handling with status code + message
- Default timeout: 30000ms
- Base URL constructed from env vars
Client Methods Pattern:
async get<T>(path: string, params?: Record<string, unknown>): Promise<T>
async create<T>(path: string, data: unknown): Promise<T>
async update<T>(path: string, data: unknown): Promise<T>
async delete(path: string): Promise<void>
3. Tool Files (src/tools/*.ts)
Structure:
import { z } from 'zod';
import { PlatformClient } from '../clients/platform.js';
const ListOrdersInput = z.object({
limit: z.number().min(1).max(250).default(50).describe('Results per page'),
page_info: z.string().optional().describe('Cursor for pagination'),
status: z.enum(['open', 'closed', 'any']).optional().describe('Filter by status'),
created_at_min: z.string().optional().describe('Filter by min creation date (ISO 8601)'),
});
// ... more Zod schemas ...
export default [
{
name: '{platform}_list_orders',
description: 'List orders with pagination and filtering by status, date range, and financial status. Use when the user wants to browse, search, or export their order history. Returns paginated results with cursor-based navigation.',
inputSchema: {
type: 'object' as const,
properties: { /* JSON Schema from Zod */ },
required: ['limit'],
},
handler: async (input: unknown, client: PlatformClient) => {
const validated = ListOrdersInput.parse(input);
const result = await client.get('/orders.json', validated);
return {
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
};
},
},
// ... more tools
];
Naming Conventions (MANDATORY):
{platform}_list_*— paginated collections{platform}_get_*— single resource by ID{platform}_create_*— create new resource{platform}_update_*— update existing resource{platform}_delete_*— delete resource{platform}_search_*— query-based lookup- Domain verbs:
{platform}_send_email,{platform}_cancel_order,{platform}_archive_card - ALL snake_case, ALL lowercase
- NEVER mix
fetch_*/get_*/retrieve_*— pick ONE (we useget_)
Description Requirements:
- BAD: "Lists contacts"
- GOOD: "Lists contacts from HubSpot with optional filtering by email, name, company, or lifecycle stage. Use when the user wants to browse, search, or export their contact database. Returns paginated results with cursor-based navigation. Supports up to 100 results per page."
- Every description must tell an AI agent WHEN and WHY to use the tool
- Include: what it returns, pagination details, filtering options, rate limit notes
Pagination (MANDATORY for all list_* tools):
limitparam with min/max/defaultpage_infoorcursororoffsetparam- Response includes
has_more/hasNextPageindicator - Response includes
next_cursor/next_page_urlfor follow-up
Handler Pattern:
- Validate input with Zod
.parse(input) - Call client method
- Return
{ content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] }
Tool Count Minimums:
| API Size | Minimum Tools |
|---|---|
| Small (<30 endpoints) | 15-20 |
| Medium (30-100 endpoints) | 30-50 |
| Large (100+ endpoints) | 50-80+ |
| 7-8 tools = a demo, NOT a product |
4. Types (src/types/index.ts)
- TypeScript interfaces for ALL API entities
- Export everything (interfaces, type aliases, enums)
- Include: request params, response types, pagination types, error types
- Example:
export interface Contact {
id: string;
firstName: string;
lastName: string;
email: string;
phone?: string;
company?: string;
createdAt: string;
updatedAt: string;
}
export interface PaginatedResponse<T> {
data: T[];
pageInfo: {
hasNextPage: boolean;
nextPageUrl?: string;
};
}
5. Apps (src/apps/{app-name}/)
Each app is a self-contained React application in its own subdirectory.
Files per app:
src/apps/{app-name}/
App.tsx — Main React component
main.tsx — Entry point with ErrorBoundary + Suspense
index.html — HTML shell
styles.css — Dark theme styling
main.tsx Pattern:
import { Suspense, lazy, Component, ErrorInfo, ReactNode } from 'react';
import { createRoot } from 'react-dom/client';
const App = lazy(() => import('./App'));
// ErrorBoundary class with getDerivedStateFromError + componentDidCatch
// LoadingSkeleton component with shimmer divs
const container = document.getElementById('root');
if (container) {
const root = createRoot(container);
root.render(
<ErrorBoundary>
<Suspense fallback={<LoadingSkeleton />}>
<App />
</Suspense>
</ErrorBoundary>
);
}
App.tsx Pattern:
- Self-contained with mock data (no live API calls)
useStatefor state managementuseDebouncehook for searchuseToasthook for notificationsuseTransitionfor non-blocking updates- Search/filter/sort functionality
- Responsive layout
- Mock data should be realistic and platform-relevant
index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{App Name} - {Platform} MCP</title>
<link rel="stylesheet" href="./styles.css">
</head>
<body>
<div id="root"></div>
<script type="module" src="./main.tsx"></script>
</body>
</html>
styles.css — Dark Theme:
:root {
--bg-primary: #0f172a;
--bg-secondary: #1e293b;
--bg-tertiary: #334155;
--text-primary: #f1f5f9;
--text-secondary: #cbd5e1;
--text-muted: #94a3b8;
--accent: #3b82f6;
--accent-hover: #2563eb;
--success: #10b981;
--error: #ef4444;
--warning: #f59e0b;
--border: #475569;
}
App Ideas per Platform Type:
- CRM: contact-manager, deal-pipeline, activity-feed, report-dashboard
- E-commerce: order-management, product-catalog, inventory-tracker, analytics-dashboard
- Project Mgmt: task-board, timeline-view, team-workload, sprint-dashboard
- Support: ticket-queue, knowledge-base, customer-timeline, satisfaction-dashboard
- HR: employee-directory, time-off-calendar, org-chart, payroll-dashboard
6. Landing Pages
Generation:
- Use
landing-pages/site-generator.jswith config object - Run:
node site-generator.js {platform-id} - Output: single HTML file
Config Structure:
{
name: 'Platform Name',
tagline: 'AI-Power Your {Domain} in 2 Clicks',
color: '#HEX', // Brand color
tools: '47', // Tool count string
description: 'The complete {Platform} MCP server. {What it does} with AI.',
features: [
{ title: 'Feature Name', desc: 'What it does in one sentence.' },
// 4 features
],
painPoints: [
{ bad: 'Manual way of doing X', good: 'AI does X automatically' },
// 3 pain points
]
}
Chat Demo Config (chatDemoData):
{
platformId: {
messages: [
{ type: 'user', text: 'Natural question about the platform' },
{
type: 'ai',
text: 'Response text',
widgetData: { type: 'metrics|tickets|board|deals|pnl|schedule', ... },
action: '✓ Summary of what was done'
},
{ type: 'user', text: 'Follow-up action request' },
{ type: 'ai', text: 'Confirmation', action: '✓ Action completed summary' }
]
}
}
6 Widget Types for Chat Demo:
- tickets — Table with ID, subject, priority badge, time. Has summary + alert.
- metrics — 3-column grid of metric cards with label, value, change (+/-). Has alert.
- board — Kanban-style columns with cards
- deals — Pipeline/deal cards with values
- pnl — Profit/loss bars with labels
- schedule — Time slots with bookings
Landing Page Sections:
- Hero with gradient text + glow effect
- Features grid (4 cards with hover glow)
- Animated chat demo — scroll-triggered GSAP, messages appear sequentially (1s delay)
- Pain points (bad → good comparisons)
- CTA section
- Footer
Animations:
animate-float/animate-float-delayed/animate-float-slow— floating elementscard-glow:hover— box-shadow + translateY(-4px)gradient-shift— animated gradient backgroundvideo-glow— pulsing box-shadow- GSAP ScrollTrigger for chat demo (plays once on viewport enter)
7. README.md
Every server MUST have a README with:
- Title + description
- Features list (bullet points of capabilities)
- Installation (
npm install && npm run build) - Environment Variables table — Variable, Required (✅/❌), Description, Example
- Getting Your Access Token — Step-by-step for that specific platform
- Required API Scopes — List of permissions needed
- Usage — Stdio mode + HTTP mode examples
- Coverage Manifest:
Total API endpoints: X Tools implemented: Y Intentionally skipped: Z (with reasons) Coverage: Y/X = NN%
8. File Tree Summary
servers/{platform}/
├── package.json # @mcpengine/{platform}, type:module, bin field
├── tsconfig.json # ES2022, Node16, strict, exclude apps
├── README.md # Full docs + coverage manifest
├── src/
│ ├── main.ts # Entry: env validation, graceful shutdown
│ ├── server.ts # Server CLASS with lazy-loaded tool modules
│ ├── clients/ # (or client/)
│ │ └── {platform}.ts # Axios client with interceptors + rate limiting
│ ├── types/
│ │ └── index.ts # All TypeScript interfaces
│ ├── tools/
│ │ ├── orders.ts # export default [...tools]
│ │ ├── customers.ts
│ │ └── ... # One file per domain area
│ └── apps/
│ ├── order-management/
│ │ ├── App.tsx
│ │ ├── main.tsx
│ │ ├── index.html
│ │ └── styles.css
│ └── ... # One dir per app
└── landing-pages/
└── sites/{platform}.html # Generated landing page
This is the gold standard. Every MCP server we build or fix must match this spec.