format: apply repo prettier (3.8.1) to the folded skills tree
963 markdown files reformatted with the repository's pinned prettier so pnpm format:check covers the folded tree like every other repo file. The formatter's embedded-language pass also normalized code fences (TS semicolons, closed HTML tags in examples, lowercased CSS hex colors, one renumbered list that skipped an index). Alphanumeric token deltas vs the fold commit were audited file-by-file; all are formatter-equivalent markup normalizations plus the four sanitized skills.
This commit is contained in:
@@ -43,6 +43,7 @@ Start with the sitemap to find relevant pages: `https://modelcontextprotocol.io/
|
||||
Then fetch specific pages with `.md` suffix for markdown format (e.g., `https://modelcontextprotocol.io/specification/draft.md`).
|
||||
|
||||
Key pages to review:
|
||||
|
||||
- Specification overview and architecture
|
||||
- Transport mechanisms (streamable HTTP, stdio)
|
||||
- Tool, resource, and prompt definitions
|
||||
@@ -50,6 +51,7 @@ Key pages to review:
|
||||
#### 1.3 Study Framework Documentation
|
||||
|
||||
**Recommended stack:**
|
||||
|
||||
- **Language**: TypeScript (high-quality SDK support and good compatibility in many execution environments e.g. MCPB. Plus AI models are good at generating TypeScript code, benefiting from its broad usage, static typing and good linting tools)
|
||||
- **Transport**: Streamable HTTP for remote servers, using stateless JSON (simpler to scale and maintain, as opposed to stateful sessions and streaming responses). stdio for local servers.
|
||||
|
||||
@@ -58,10 +60,12 @@ Key pages to review:
|
||||
- **MCP Best Practices**: [📋 View Best Practices](./reference/mcp_best_practices.md) - Core guidelines
|
||||
|
||||
**For TypeScript (recommended):**
|
||||
|
||||
- **TypeScript SDK**: Use WebFetch to load `https://raw.githubusercontent.com/modelcontextprotocol/typescript-sdk/main/README.md`
|
||||
- [⚡ TypeScript Guide](./reference/node_mcp_server.md) - TypeScript patterns and examples
|
||||
|
||||
**For Python:**
|
||||
|
||||
- **Python SDK**: Use WebFetch to load `https://raw.githubusercontent.com/modelcontextprotocol/python-sdk/main/README.md`
|
||||
- [🐍 Python Guide](./reference/python_mcp_server.md) - Python patterns and examples
|
||||
|
||||
@@ -80,12 +84,14 @@ Prioritize comprehensive API coverage. List endpoints to implement, starting wit
|
||||
#### 2.1 Set Up Project Structure
|
||||
|
||||
See language-specific guides for project setup:
|
||||
|
||||
- [⚡ TypeScript Guide](./reference/node_mcp_server.md) - Project structure, package.json, tsconfig.json
|
||||
- [🐍 Python Guide](./reference/python_mcp_server.md) - Module organization, dependencies
|
||||
|
||||
#### 2.2 Implement Core Infrastructure
|
||||
|
||||
Create shared utilities:
|
||||
|
||||
- API client with authentication
|
||||
- Error handling helpers
|
||||
- Response formatting (JSON/Markdown)
|
||||
@@ -96,27 +102,32 @@ Create shared utilities:
|
||||
For each tool:
|
||||
|
||||
**Input Schema:**
|
||||
|
||||
- Use Zod (TypeScript) or Pydantic (Python)
|
||||
- Include constraints and clear descriptions
|
||||
- Add examples in field descriptions
|
||||
|
||||
**Output Schema:**
|
||||
|
||||
- Define `outputSchema` where possible for structured data
|
||||
- Use `structuredContent` in tool responses (TypeScript SDK feature)
|
||||
- Helps clients understand and process tool outputs
|
||||
|
||||
**Tool Description:**
|
||||
|
||||
- Concise summary of functionality
|
||||
- Parameter descriptions
|
||||
- Return type schema
|
||||
|
||||
**Implementation:**
|
||||
|
||||
- Async/await for I/O operations
|
||||
- Proper error handling with actionable messages
|
||||
- Support pagination where applicable
|
||||
- Return both text content and structured data when using modern SDKs
|
||||
|
||||
**Annotations:**
|
||||
|
||||
- `readOnlyHint`: true/false
|
||||
- `destructiveHint`: true/false
|
||||
- `idempotentHint`: true/false
|
||||
@@ -129,6 +140,7 @@ For each tool:
|
||||
#### 3.1 Code Quality
|
||||
|
||||
Review for:
|
||||
|
||||
- No duplicated code (DRY principle)
|
||||
- Consistent error handling
|
||||
- Full type coverage
|
||||
@@ -137,10 +149,12 @@ Review for:
|
||||
#### 3.2 Build and Test
|
||||
|
||||
**TypeScript:**
|
||||
|
||||
- Run `npm run build` to verify compilation
|
||||
- Test with MCP Inspector: `npx @modelcontextprotocol/inspector`
|
||||
|
||||
**Python:**
|
||||
|
||||
- Verify syntax: `python -m py_compile your_server.py`
|
||||
- Test with MCP Inspector
|
||||
|
||||
@@ -170,6 +184,7 @@ To create effective evaluations, follow the process outlined in the evaluation g
|
||||
#### 4.3 Evaluation Requirements
|
||||
|
||||
Ensure each question is:
|
||||
|
||||
- **Independent**: Not dependent on other questions
|
||||
- **Read-only**: Only non-destructive operations required
|
||||
- **Complex**: Requiring multiple tool calls and deep exploration
|
||||
@@ -200,6 +215,7 @@ Create an XML file with this structure:
|
||||
Load these resources as needed during development:
|
||||
|
||||
### Core MCP Documentation (Load First)
|
||||
|
||||
- **MCP Protocol**: Start with sitemap at `https://modelcontextprotocol.io/sitemap.xml`, then fetch specific pages with `.md` suffix
|
||||
- [📋 MCP Best Practices](./reference/mcp_best_practices.md) - Universal MCP guidelines including:
|
||||
- Server and tool naming conventions
|
||||
@@ -209,10 +225,12 @@ Load these resources as needed during development:
|
||||
- Security and error handling standards
|
||||
|
||||
### SDK Documentation (Load During Phase 1/2)
|
||||
|
||||
- **Python SDK**: Fetch from `https://raw.githubusercontent.com/modelcontextprotocol/python-sdk/main/README.md`
|
||||
- **TypeScript SDK**: Fetch from `https://raw.githubusercontent.com/modelcontextprotocol/typescript-sdk/main/README.md`
|
||||
|
||||
### Language-Specific Implementation Guides (Load During Phase 2)
|
||||
|
||||
- [🐍 Python Implementation Guide](./reference/python_mcp_server.md) - Complete Python/FastMCP guide with:
|
||||
- Server initialization patterns
|
||||
- Pydantic model examples
|
||||
@@ -228,6 +246,7 @@ Load these resources as needed during development:
|
||||
- Quality checklist
|
||||
|
||||
### Evaluation Guide (Load During Phase 4)
|
||||
|
||||
- [✅ Evaluation Guide](./reference/evaluation.md) - Complete evaluation creation guide with:
|
||||
- Question creation guidelines
|
||||
- Answer verification strategies
|
||||
|
||||
@@ -9,6 +9,7 @@ This document provides guidance on creating comprehensive evaluations for MCP se
|
||||
## Quick Reference
|
||||
|
||||
### Evaluation Requirements
|
||||
|
||||
- Create 10 human-readable questions
|
||||
- Questions must be READ-ONLY, INDEPENDENT, NON-DESTRUCTIVE
|
||||
- Each question requires multiple tool calls (potentially dozens)
|
||||
@@ -16,6 +17,7 @@ This document provides guidance on creating comprehensive evaluations for MCP se
|
||||
- Answers must be STABLE (won't change over time)
|
||||
|
||||
### Output Format
|
||||
|
||||
```xml
|
||||
<evaluation>
|
||||
<qa_pair>
|
||||
@@ -34,6 +36,7 @@ The measure of quality of an MCP server is NOT how well or comprehensively the s
|
||||
## Evaluation Overview
|
||||
|
||||
Create 10 human-readable questions requiring ONLY READ-ONLY, INDEPENDENT, NON-DESTRUCTIVE, and IDEMPOTENT operations to answer. Each question should be:
|
||||
|
||||
- Realistic
|
||||
- Clear and concise
|
||||
- Unambiguous
|
||||
@@ -176,6 +179,7 @@ Create 10 human-readable questions requiring ONLY READ-ONLY, INDEPENDENT, NON-DE
|
||||
### Step 1: Documentation Inspection
|
||||
|
||||
Read the documentation of the target API to understand:
|
||||
|
||||
- Available endpoints and functionality
|
||||
- If ambiguity exists, fetch additional information from the web
|
||||
- Parallelize this step AS MUCH AS POSSIBLE
|
||||
@@ -184,6 +188,7 @@ Read the documentation of the target API to understand:
|
||||
### Step 2: Tool Inspection
|
||||
|
||||
List the tools available in the MCP server:
|
||||
|
||||
- Inspect the MCP server directly
|
||||
- Understand input/output schemas, docstrings, and descriptions
|
||||
- WITHOUT calling the tools themselves at this stage
|
||||
@@ -191,6 +196,7 @@ List the tools available in the MCP server:
|
||||
### Step 3: Developing Understanding
|
||||
|
||||
Repeat steps 1 & 2 until you have a good understanding:
|
||||
|
||||
- Iterate multiple times
|
||||
- Think about the kinds of tasks you want to create
|
||||
- Refine your understanding
|
||||
@@ -200,6 +206,7 @@ Repeat steps 1 & 2 until you have a good understanding:
|
||||
### Step 4: Read-Only Content Inspection
|
||||
|
||||
After understanding the API and tools, USE the MCP server tools:
|
||||
|
||||
- Inspect content using READ-ONLY and NON-DESTRUCTIVE operations ONLY
|
||||
- Goal: identify specific content (e.g., users, channels, messages, projects, tasks) for creating realistic questions
|
||||
- Should NOT call any tools that modify state
|
||||
@@ -214,6 +221,7 @@ After understanding the API and tools, USE the MCP server tools:
|
||||
### Step 5: Task Generation
|
||||
|
||||
After inspecting the content, create 10 human-readable questions:
|
||||
|
||||
- An LLM should be able to answer these with the MCP server
|
||||
- Follow all question and answer guidelines above
|
||||
|
||||
@@ -247,6 +255,7 @@ Each QA pair consists of a question and an answer. The output should be an XML f
|
||||
### Good Questions
|
||||
|
||||
**Example 1: Multi-hop question requiring deep exploration (GitHub MCP)**
|
||||
|
||||
```xml
|
||||
<qa_pair>
|
||||
<question>Find the repository that was archived in Q3 2023 and had previously been the most forked project in the organization. What was the primary programming language used in that repository?</question>
|
||||
@@ -255,6 +264,7 @@ Each QA pair consists of a question and an answer. The output should be an XML f
|
||||
```
|
||||
|
||||
This question is good because:
|
||||
|
||||
- Requires multiple searches to find archived repositories
|
||||
- Needs to identify which had the most forks before archival
|
||||
- Requires examining repository details for the language
|
||||
@@ -262,6 +272,7 @@ This question is good because:
|
||||
- Based on historical (closed) data that won't change
|
||||
|
||||
**Example 2: Requires understanding context without keyword matching (Project Management MCP)**
|
||||
|
||||
```xml
|
||||
<qa_pair>
|
||||
<question>Locate the initiative focused on improving customer onboarding that was completed in late 2023. The project lead created a retrospective document after completion. What was the lead's role title at that time?</question>
|
||||
@@ -270,6 +281,7 @@ This question is good because:
|
||||
```
|
||||
|
||||
This question is good because:
|
||||
|
||||
- Doesn't use specific project name ("initiative focused on improving customer onboarding")
|
||||
- Requires finding completed projects from specific timeframe
|
||||
- Needs to identify the project lead and their role
|
||||
@@ -278,6 +290,7 @@ This question is good because:
|
||||
- Based on completed work (won't change)
|
||||
|
||||
**Example 3: Complex aggregation requiring multiple steps (Issue Tracker MCP)**
|
||||
|
||||
```xml
|
||||
<qa_pair>
|
||||
<question>Among all bugs reported in January 2024 that were marked as critical priority, which assignee resolved the highest percentage of their assigned bugs within 48 hours? Provide the assignee's username.</question>
|
||||
@@ -286,6 +299,7 @@ This question is good because:
|
||||
```
|
||||
|
||||
This question is good because:
|
||||
|
||||
- Requires filtering bugs by date, priority, and status
|
||||
- Needs to group by assignee and calculate resolution rates
|
||||
- Requires understanding timestamps to determine 48-hour windows
|
||||
@@ -294,6 +308,7 @@ This question is good because:
|
||||
- Based on historical data from specific time period
|
||||
|
||||
**Example 4: Requires synthesis across multiple data types (CRM MCP)**
|
||||
|
||||
```xml
|
||||
<qa_pair>
|
||||
<question>Find the account that upgraded from the Starter to Enterprise plan in Q4 2023 and had the highest annual contract value. What industry does this account operate in?</question>
|
||||
@@ -302,6 +317,7 @@ This question is good because:
|
||||
```
|
||||
|
||||
This question is good because:
|
||||
|
||||
- Requires understanding subscription tier changes
|
||||
- Needs to identify upgrade events in specific timeframe
|
||||
- Requires comparing contract values
|
||||
@@ -312,6 +328,7 @@ This question is good because:
|
||||
### Poor Questions
|
||||
|
||||
**Example 1: Answer changes over time**
|
||||
|
||||
```xml
|
||||
<qa_pair>
|
||||
<question>How many open issues are currently assigned to the engineering team?</question>
|
||||
@@ -320,11 +337,13 @@ This question is good because:
|
||||
```
|
||||
|
||||
This question is poor because:
|
||||
|
||||
- The answer will change as issues are created, closed, or reassigned
|
||||
- Not based on stable/stationary data
|
||||
- Relies on "current state" which is dynamic
|
||||
|
||||
**Example 2: Too easy with keyword search**
|
||||
|
||||
```xml
|
||||
<qa_pair>
|
||||
<question>Find the pull request with title "Add authentication feature" and tell me who created it.</question>
|
||||
@@ -333,11 +352,13 @@ This question is poor because:
|
||||
```
|
||||
|
||||
This question is poor because:
|
||||
|
||||
- Can be solved with a straightforward keyword search for exact title
|
||||
- Doesn't require deep exploration or understanding
|
||||
- No synthesis or analysis needed
|
||||
|
||||
**Example 3: Ambiguous answer format**
|
||||
|
||||
```xml
|
||||
<qa_pair>
|
||||
<question>List all the repositories that have Python as their primary language.</question>
|
||||
@@ -346,6 +367,7 @@ This question is poor because:
|
||||
```
|
||||
|
||||
This question is poor because:
|
||||
|
||||
- Answer is a list that could be returned in any order
|
||||
- Difficult to verify with direct string comparison
|
||||
- LLM might format differently (JSON array, comma-separated, newline-separated)
|
||||
@@ -388,6 +410,7 @@ After creating your evaluation file, you can use the provided evaluation harness
|
||||
```
|
||||
|
||||
Or install manually:
|
||||
|
||||
```bash
|
||||
pip install anthropic mcp
|
||||
```
|
||||
@@ -420,6 +443,7 @@ Evaluation files use XML format with `<qa_pair>` elements:
|
||||
The evaluation script (`scripts/evaluation.py`) supports three transport types:
|
||||
|
||||
**Important:**
|
||||
|
||||
- **stdio transport**: The evaluation script automatically launches and manages the MCP server process for you. Do not run the server manually.
|
||||
- **sse/http transports**: You must start the MCP server separately before running the evaluation. The script connects to the already-running server at the specified URL.
|
||||
|
||||
@@ -436,6 +460,7 @@ python scripts/evaluation.py \
|
||||
```
|
||||
|
||||
With environment variables:
|
||||
|
||||
```bash
|
||||
python scripts/evaluation.py \
|
||||
-t stdio \
|
||||
@@ -580,6 +605,7 @@ python scripts/evaluation.py \
|
||||
### Connection Errors
|
||||
|
||||
If you get connection errors:
|
||||
|
||||
- **STDIO**: Verify the command and arguments are correct
|
||||
- **SSE/HTTP**: Check the URL is accessible and headers are correct
|
||||
- Ensure any required API keys are set in environment variables or headers
|
||||
@@ -587,6 +613,7 @@ If you get connection errors:
|
||||
### Low Accuracy
|
||||
|
||||
If many evaluations fail:
|
||||
|
||||
- Review the agent's feedback for each task
|
||||
- Check if tool descriptions are clear and comprehensive
|
||||
- Verify input parameters are well-documented
|
||||
@@ -596,7 +623,8 @@ If many evaluations fail:
|
||||
### Timeout Issues
|
||||
|
||||
If tasks are timing out:
|
||||
|
||||
- Use a more capable model (e.g., `claude-3-7-sonnet-20250219`)
|
||||
- Check if tools are returning too much data
|
||||
- Verify pagination is working correctly
|
||||
- Consider simplifying complex questions
|
||||
- Consider simplifying complex questions
|
||||
|
||||
@@ -3,25 +3,30 @@
|
||||
## Quick Reference
|
||||
|
||||
### Server Naming
|
||||
|
||||
- **Python**: `{service}_mcp` (e.g., `slack_mcp`)
|
||||
- **Node/TypeScript**: `{service}-mcp-server` (e.g., `slack-mcp-server`)
|
||||
|
||||
### Tool Naming
|
||||
|
||||
- Use snake_case with service prefix
|
||||
- Format: `{service}_{action}_{resource}`
|
||||
- Example: `slack_send_message`, `github_create_issue`
|
||||
|
||||
### Response Formats
|
||||
|
||||
- Support both JSON and Markdown formats
|
||||
- JSON for programmatic processing
|
||||
- Markdown for human readability
|
||||
|
||||
### Pagination
|
||||
|
||||
- Always respect `limit` parameter
|
||||
- Return `has_more`, `next_offset`, `total_count`
|
||||
- Default to 20-50 items
|
||||
|
||||
### Transport
|
||||
|
||||
- **Streamable HTTP**: For remote servers, multi-client scenarios
|
||||
- **stdio**: For local integrations, command-line tools
|
||||
- Avoid SSE (deprecated in favor of streamable HTTP)
|
||||
@@ -33,9 +38,11 @@
|
||||
Follow these standardized naming patterns:
|
||||
|
||||
**Python**: Use format `{service}_mcp` (lowercase with underscores)
|
||||
|
||||
- Examples: `slack_mcp`, `github_mcp`, `jira_mcp`
|
||||
|
||||
**Node/TypeScript**: Use format `{service}-mcp-server` (lowercase with hyphens)
|
||||
|
||||
- Examples: `slack-mcp-server`, `github-mcp-server`, `jira-mcp-server`
|
||||
|
||||
The name should be general, descriptive of the service being integrated, easy to infer from the task description, and without version numbers.
|
||||
@@ -67,12 +74,14 @@ The name should be general, descriptive of the service being integrated, easy to
|
||||
All tools that return data should support multiple formats:
|
||||
|
||||
### JSON Format (`response_format="json"`)
|
||||
|
||||
- Machine-readable structured data
|
||||
- Include all available fields and metadata
|
||||
- Consistent field names and types
|
||||
- Use for programmatic processing
|
||||
|
||||
### Markdown Format (`response_format="markdown"`, typically default)
|
||||
|
||||
- Human-readable formatted text
|
||||
- Use headers, lists, and formatting for clarity
|
||||
- Convert timestamps to human-readable format
|
||||
@@ -92,6 +101,7 @@ For tools that list resources:
|
||||
- **Default to reasonable limits**: 20-50 items is typical
|
||||
|
||||
Example pagination response:
|
||||
|
||||
```json
|
||||
{
|
||||
"total": 150,
|
||||
@@ -112,12 +122,14 @@ Example pagination response:
|
||||
**Best for**: Remote servers, web services, multi-client scenarios
|
||||
|
||||
**Characteristics**:
|
||||
|
||||
- Bidirectional communication over HTTP
|
||||
- Supports multiple simultaneous clients
|
||||
- Can be deployed as a web service
|
||||
- Enables server-to-client notifications
|
||||
|
||||
**Use when**:
|
||||
|
||||
- Serving multiple clients simultaneously
|
||||
- Deploying as a cloud service
|
||||
- Integration with web applications
|
||||
@@ -127,11 +139,13 @@ Example pagination response:
|
||||
**Best for**: Local integrations, command-line tools
|
||||
|
||||
**Characteristics**:
|
||||
|
||||
- Standard input/output stream communication
|
||||
- Simple setup, no network configuration needed
|
||||
- Runs as a subprocess of the client
|
||||
|
||||
**Use when**:
|
||||
|
||||
- Building tools for local development environments
|
||||
- Integrating with desktop applications
|
||||
- Single-user, single-session scenarios
|
||||
@@ -140,12 +154,12 @@ Example pagination response:
|
||||
|
||||
### Transport Selection
|
||||
|
||||
| Criterion | stdio | Streamable HTTP |
|
||||
|-----------|-------|-----------------|
|
||||
| **Deployment** | Local | Remote |
|
||||
| **Clients** | Single | Multiple |
|
||||
| **Complexity** | Low | Medium |
|
||||
| **Real-time** | No | Yes |
|
||||
| Criterion | stdio | Streamable HTTP |
|
||||
| -------------- | ------ | --------------- |
|
||||
| **Deployment** | Local | Remote |
|
||||
| **Clients** | Single | Multiple |
|
||||
| **Complexity** | Low | Medium |
|
||||
| **Real-time** | No | Yes |
|
||||
|
||||
---
|
||||
|
||||
@@ -154,11 +168,13 @@ Example pagination response:
|
||||
### Authentication and Authorization
|
||||
|
||||
**OAuth 2.1**:
|
||||
|
||||
- Use secure OAuth 2.1 with certificates from recognized authorities
|
||||
- Validate access tokens before processing requests
|
||||
- Only accept tokens specifically intended for your server
|
||||
|
||||
**API Keys**:
|
||||
|
||||
- Store API keys in environment variables, never in code
|
||||
- Validate keys on server startup
|
||||
- Provide clear error messages when authentication fails
|
||||
@@ -181,6 +197,7 @@ Example pagination response:
|
||||
### DNS Rebinding Protection
|
||||
|
||||
For streamable HTTP servers running locally:
|
||||
|
||||
- Enable DNS rebinding protection
|
||||
- Validate the `Origin` header on all incoming connections
|
||||
- Bind to `127.0.0.1` rather than `0.0.0.0`
|
||||
@@ -191,12 +208,12 @@ For streamable HTTP servers running locally:
|
||||
|
||||
Provide annotations to help clients understand tool behavior:
|
||||
|
||||
| Annotation | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `readOnlyHint` | boolean | false | Tool does not modify its environment |
|
||||
| `destructiveHint` | boolean | true | Tool may perform destructive updates |
|
||||
| `idempotentHint` | boolean | false | Repeated calls with same args have no additional effect |
|
||||
| `openWorldHint` | boolean | true | Tool interacts with external entities |
|
||||
| Annotation | Type | Default | Description |
|
||||
| ----------------- | ------- | ------- | ------------------------------------------------------- |
|
||||
| `readOnlyHint` | boolean | false | Tool does not modify its environment |
|
||||
| `destructiveHint` | boolean | true | Tool may perform destructive updates |
|
||||
| `idempotentHint` | boolean | false | Repeated calls with same args have no additional effect |
|
||||
| `openWorldHint` | boolean | true | Tool interacts with external entities |
|
||||
|
||||
**Important**: Annotations are hints, not security guarantees. Clients should not make security-critical decisions based solely on annotations.
|
||||
|
||||
@@ -211,17 +228,20 @@ Provide annotations to help clients understand tool behavior:
|
||||
- Clean up resources properly on errors
|
||||
|
||||
Example error handling:
|
||||
|
||||
```typescript
|
||||
try {
|
||||
const result = performOperation();
|
||||
return { content: [{ type: "text", text: result }] };
|
||||
return { content: [{ type: 'text', text: result }] };
|
||||
} catch (error) {
|
||||
return {
|
||||
isError: true,
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `Error: ${error.message}. Try using filter='active_only' to reduce results.`
|
||||
}]
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Error: ${error.message}. Try using filter='active_only' to reduce results.`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
@@ -9,39 +9,42 @@ This document provides Node/TypeScript-specific best practices and examples for
|
||||
## Quick Reference
|
||||
|
||||
### Key Imports
|
||||
|
||||
```typescript
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import express from "express";
|
||||
import { z } from "zod";
|
||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
import express from 'express';
|
||||
import { z } from 'zod';
|
||||
```
|
||||
|
||||
### Server Initialization
|
||||
|
||||
```typescript
|
||||
const server = new McpServer({
|
||||
name: "service-mcp-server",
|
||||
version: "1.0.0"
|
||||
name: 'service-mcp-server',
|
||||
version: '1.0.0',
|
||||
});
|
||||
```
|
||||
|
||||
### Tool Registration Pattern
|
||||
|
||||
```typescript
|
||||
server.registerTool(
|
||||
"tool_name",
|
||||
'tool_name',
|
||||
{
|
||||
title: "Tool Display Name",
|
||||
description: "What the tool does",
|
||||
title: 'Tool Display Name',
|
||||
description: 'What the tool does',
|
||||
inputSchema: { param: z.string() },
|
||||
outputSchema: { result: z.string() }
|
||||
outputSchema: { result: z.string() },
|
||||
},
|
||||
async ({ param }) => {
|
||||
const output = { result: `Processed: ${param}` };
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(output) }],
|
||||
structuredContent: output // Modern pattern for structured data
|
||||
content: [{ type: 'text', text: JSON.stringify(output) }],
|
||||
structuredContent: output, // Modern pattern for structured data
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
@@ -50,12 +53,14 @@ server.registerTool(
|
||||
## MCP TypeScript SDK
|
||||
|
||||
The official MCP TypeScript SDK provides:
|
||||
|
||||
- `McpServer` class for server initialization
|
||||
- `registerTool` method for tool registration
|
||||
- Zod schema integration for runtime input validation
|
||||
- Type-safe tool handler implementations
|
||||
|
||||
**IMPORTANT - Use Modern APIs Only:**
|
||||
|
||||
- **DO use**: `server.registerTool()`, `server.registerResource()`, `server.registerPrompt()`
|
||||
- **DO NOT use**: Old deprecated APIs such as `server.tool()`, `server.setRequestHandler(ListToolsRequestSchema, ...)`, or manual handler registration
|
||||
- The `register*` methods provide better type safety, automatic schema handling, and are the recommended approach
|
||||
@@ -65,10 +70,12 @@ See the MCP SDK documentation in the references for complete details.
|
||||
## Server Naming Convention
|
||||
|
||||
Node/TypeScript MCP servers must follow this naming pattern:
|
||||
|
||||
- **Format**: `{service}-mcp-server` (lowercase with hyphens)
|
||||
- **Examples**: `github-mcp-server`, `jira-mcp-server`, `stripe-mcp-server`
|
||||
|
||||
The name should be:
|
||||
|
||||
- General (not tied to specific features)
|
||||
- Descriptive of the service/API being integrated
|
||||
- Easy to infer from the task description
|
||||
@@ -100,6 +107,7 @@ Create the following structure for Node/TypeScript MCP servers:
|
||||
Use snake_case for tool names (e.g., "search_users", "create_project", "get_channel_info") with clear, action-oriented names.
|
||||
|
||||
**Avoid Naming Conflicts**: Include the service context to prevent overlaps:
|
||||
|
||||
- Use "slack_send_message" instead of just "send_message"
|
||||
- Use "github_create_issue" instead of just "create_issue"
|
||||
- Use "asana_list_tasks" instead of just "list_tasks"
|
||||
@@ -107,6 +115,7 @@ Use snake_case for tool names (e.g., "search_users", "create_project", "get_chan
|
||||
### Tool Structure
|
||||
|
||||
Tools are registered using the `registerTool` method with the following requirements:
|
||||
|
||||
- Use Zod schemas for runtime input validation and type safety
|
||||
- The `description` field must be explicitly provided - JSDoc comments are NOT automatically extracted
|
||||
- Explicitly provide `title`, `description`, `inputSchema`, and `annotations`
|
||||
@@ -114,43 +123,38 @@ Tools are registered using the `registerTool` method with the following requirem
|
||||
- Type all parameters and return values explicitly
|
||||
|
||||
```typescript
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { z } from "zod";
|
||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { z } from 'zod';
|
||||
|
||||
const server = new McpServer({
|
||||
name: "example-mcp",
|
||||
version: "1.0.0"
|
||||
name: 'example-mcp',
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
// Zod schema for input validation
|
||||
const UserSearchInputSchema = z.object({
|
||||
query: z.string()
|
||||
.min(2, "Query must be at least 2 characters")
|
||||
.max(200, "Query must not exceed 200 characters")
|
||||
.describe("Search string to match against names/emails"),
|
||||
limit: z.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.max(100)
|
||||
.default(20)
|
||||
.describe("Maximum results to return"),
|
||||
offset: z.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.default(0)
|
||||
.describe("Number of results to skip for pagination"),
|
||||
response_format: z.nativeEnum(ResponseFormat)
|
||||
.default(ResponseFormat.MARKDOWN)
|
||||
.describe("Output format: 'markdown' for human-readable or 'json' for machine-readable")
|
||||
}).strict();
|
||||
const UserSearchInputSchema = z
|
||||
.object({
|
||||
query: z
|
||||
.string()
|
||||
.min(2, 'Query must be at least 2 characters')
|
||||
.max(200, 'Query must not exceed 200 characters')
|
||||
.describe('Search string to match against names/emails'),
|
||||
limit: z.number().int().min(1).max(100).default(20).describe('Maximum results to return'),
|
||||
offset: z.number().int().min(0).default(0).describe('Number of results to skip for pagination'),
|
||||
response_format: z
|
||||
.nativeEnum(ResponseFormat)
|
||||
.default(ResponseFormat.MARKDOWN)
|
||||
.describe("Output format: 'markdown' for human-readable or 'json' for machine-readable"),
|
||||
})
|
||||
.strict();
|
||||
|
||||
// Type definition from Zod schema
|
||||
type UserSearchInput = z.infer<typeof UserSearchInputSchema>;
|
||||
|
||||
server.registerTool(
|
||||
"example_search_users",
|
||||
'example_search_users',
|
||||
{
|
||||
title: "Search Example Users",
|
||||
title: 'Search Example Users',
|
||||
description: `Search for users in the Example system by name, email, or team.
|
||||
|
||||
This tool searches across all user profiles in the Example platform, supporting partial matches and various search filters. It does NOT create or modify users, only searches existing ones.
|
||||
@@ -193,33 +197,30 @@ Error Handling:
|
||||
readOnlyHint: true,
|
||||
destructiveHint: false,
|
||||
idempotentHint: true,
|
||||
openWorldHint: true
|
||||
}
|
||||
openWorldHint: true,
|
||||
},
|
||||
},
|
||||
async (params: UserSearchInput) => {
|
||||
try {
|
||||
// Input validation is handled by Zod schema
|
||||
// Make API request using validated parameters
|
||||
const data = await makeApiRequest<any>(
|
||||
"users/search",
|
||||
"GET",
|
||||
undefined,
|
||||
{
|
||||
q: params.query,
|
||||
limit: params.limit,
|
||||
offset: params.offset
|
||||
}
|
||||
);
|
||||
const data = await makeApiRequest<any>('users/search', 'GET', undefined, {
|
||||
q: params.query,
|
||||
limit: params.limit,
|
||||
offset: params.offset,
|
||||
});
|
||||
|
||||
const users = data.users || [];
|
||||
const total = data.total || 0;
|
||||
|
||||
if (!users.length) {
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `No users found matching '${params.query}'`
|
||||
}]
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `No users found matching '${params.query}'`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -233,43 +234,51 @@ Error Handling:
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
...(user.team ? { team: user.team } : {}),
|
||||
active: user.active ?? true
|
||||
active: user.active ?? true,
|
||||
})),
|
||||
has_more: total > params.offset + users.length,
|
||||
...(total > params.offset + users.length ? {
|
||||
next_offset: params.offset + users.length
|
||||
} : {})
|
||||
...(total > params.offset + users.length
|
||||
? {
|
||||
next_offset: params.offset + users.length,
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
|
||||
// Format text representation based on requested format
|
||||
let textContent: string;
|
||||
if (params.response_format === ResponseFormat.MARKDOWN) {
|
||||
const lines = [`# User Search Results: '${params.query}'`, "",
|
||||
`Found ${total} users (showing ${users.length})`, ""];
|
||||
const lines = [
|
||||
`# User Search Results: '${params.query}'`,
|
||||
'',
|
||||
`Found ${total} users (showing ${users.length})`,
|
||||
'',
|
||||
];
|
||||
for (const user of users) {
|
||||
lines.push(`## ${user.name} (${user.id})`);
|
||||
lines.push(`- **Email**: ${user.email}`);
|
||||
if (user.team) lines.push(`- **Team**: ${user.team}`);
|
||||
lines.push("");
|
||||
lines.push('');
|
||||
}
|
||||
textContent = lines.join("\n");
|
||||
textContent = lines.join('\n');
|
||||
} else {
|
||||
textContent = JSON.stringify(output, null, 2);
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{ type: "text", text: textContent }],
|
||||
structuredContent: output // Modern pattern for structured data
|
||||
content: [{ type: 'text', text: textContent }],
|
||||
structuredContent: output, // Modern pattern for structured data
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: handleApiError(error)
|
||||
}]
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: handleApiError(error),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
@@ -278,46 +287,38 @@ Error Handling:
|
||||
Zod provides runtime type validation:
|
||||
|
||||
```typescript
|
||||
import { z } from "zod";
|
||||
import { z } from 'zod';
|
||||
|
||||
// Basic schema with validation
|
||||
const CreateUserSchema = z.object({
|
||||
name: z.string()
|
||||
.min(1, "Name is required")
|
||||
.max(100, "Name must not exceed 100 characters"),
|
||||
email: z.string()
|
||||
.email("Invalid email format"),
|
||||
age: z.number()
|
||||
.int("Age must be a whole number")
|
||||
.min(0, "Age cannot be negative")
|
||||
.max(150, "Age cannot be greater than 150")
|
||||
}).strict(); // Use .strict() to forbid extra fields
|
||||
const CreateUserSchema = z
|
||||
.object({
|
||||
name: z.string().min(1, 'Name is required').max(100, 'Name must not exceed 100 characters'),
|
||||
email: z.string().email('Invalid email format'),
|
||||
age: z
|
||||
.number()
|
||||
.int('Age must be a whole number')
|
||||
.min(0, 'Age cannot be negative')
|
||||
.max(150, 'Age cannot be greater than 150'),
|
||||
})
|
||||
.strict(); // Use .strict() to forbid extra fields
|
||||
|
||||
// Enums
|
||||
enum ResponseFormat {
|
||||
MARKDOWN = "markdown",
|
||||
JSON = "json"
|
||||
MARKDOWN = 'markdown',
|
||||
JSON = 'json',
|
||||
}
|
||||
|
||||
const SearchSchema = z.object({
|
||||
response_format: z.nativeEnum(ResponseFormat)
|
||||
response_format: z
|
||||
.nativeEnum(ResponseFormat)
|
||||
.default(ResponseFormat.MARKDOWN)
|
||||
.describe("Output format")
|
||||
.describe('Output format'),
|
||||
});
|
||||
|
||||
// Optional fields with defaults
|
||||
const PaginationSchema = z.object({
|
||||
limit: z.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.max(100)
|
||||
.default(20)
|
||||
.describe("Maximum results to return"),
|
||||
offset: z.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.default(0)
|
||||
.describe("Number of results to skip")
|
||||
limit: z.number().int().min(1).max(100).default(20).describe('Maximum results to return'),
|
||||
offset: z.number().int().min(0).default(0).describe('Number of results to skip'),
|
||||
});
|
||||
```
|
||||
|
||||
@@ -327,19 +328,21 @@ Support multiple output formats for flexibility:
|
||||
|
||||
```typescript
|
||||
enum ResponseFormat {
|
||||
MARKDOWN = "markdown",
|
||||
JSON = "json"
|
||||
MARKDOWN = 'markdown',
|
||||
JSON = 'json',
|
||||
}
|
||||
|
||||
const inputSchema = z.object({
|
||||
query: z.string(),
|
||||
response_format: z.nativeEnum(ResponseFormat)
|
||||
response_format: z
|
||||
.nativeEnum(ResponseFormat)
|
||||
.default(ResponseFormat.MARKDOWN)
|
||||
.describe("Output format: 'markdown' for human-readable or 'json' for machine-readable")
|
||||
.describe("Output format: 'markdown' for human-readable or 'json' for machine-readable"),
|
||||
});
|
||||
```
|
||||
|
||||
**Markdown format**:
|
||||
|
||||
- Use headers, lists, and formatting for clarity
|
||||
- Convert timestamps to human-readable format
|
||||
- Show display names with IDs in parentheses
|
||||
@@ -347,6 +350,7 @@ const inputSchema = z.object({
|
||||
- Group related information logically
|
||||
|
||||
**JSON format**:
|
||||
|
||||
- Return complete, structured data suitable for programmatic processing
|
||||
- Include all available fields and metadata
|
||||
- Use consistent field names and types
|
||||
@@ -358,7 +362,7 @@ For tools that list resources:
|
||||
```typescript
|
||||
const ListSchema = z.object({
|
||||
limit: z.number().int().min(1).max(100).default(20),
|
||||
offset: z.number().int().min(0).default(0)
|
||||
offset: z.number().int().min(0).default(0),
|
||||
});
|
||||
|
||||
async function listItems(params: z.infer<typeof ListSchema>) {
|
||||
@@ -370,9 +374,10 @@ async function listItems(params: z.infer<typeof ListSchema>) {
|
||||
offset: params.offset,
|
||||
items: data.items,
|
||||
has_more: data.total > params.offset + data.items.length,
|
||||
next_offset: data.total > params.offset + data.items.length
|
||||
? params.offset + data.items.length
|
||||
: undefined
|
||||
next_offset:
|
||||
data.total > params.offset + data.items.length
|
||||
? params.offset + data.items.length
|
||||
: undefined,
|
||||
};
|
||||
|
||||
return JSON.stringify(response, null, 2);
|
||||
@@ -385,7 +390,7 @@ Add a CHARACTER_LIMIT constant to prevent overwhelming responses:
|
||||
|
||||
```typescript
|
||||
// At module level in constants.ts
|
||||
export const CHARACTER_LIMIT = 25000; // Maximum response size in characters
|
||||
export const CHARACTER_LIMIT = 25000; // Maximum response size in characters
|
||||
|
||||
async function searchTool(params: SearchInput) {
|
||||
let result = generateResponse(data);
|
||||
@@ -410,23 +415,23 @@ async function searchTool(params: SearchInput) {
|
||||
Provide clear, actionable error messages:
|
||||
|
||||
```typescript
|
||||
import axios, { AxiosError } from "axios";
|
||||
import axios, { AxiosError } from 'axios';
|
||||
|
||||
function handleApiError(error: unknown): string {
|
||||
if (error instanceof AxiosError) {
|
||||
if (error.response) {
|
||||
switch (error.response.status) {
|
||||
case 404:
|
||||
return "Error: Resource not found. Please check the ID is correct.";
|
||||
return 'Error: Resource not found. Please check the ID is correct.';
|
||||
case 403:
|
||||
return "Error: Permission denied. You don't have access to this resource.";
|
||||
case 429:
|
||||
return "Error: Rate limit exceeded. Please wait before making more requests.";
|
||||
return 'Error: Rate limit exceeded. Please wait before making more requests.';
|
||||
default:
|
||||
return `Error: API request failed with status ${error.response.status}`;
|
||||
}
|
||||
} else if (error.code === "ECONNABORTED") {
|
||||
return "Error: Request timed out. Please try again.";
|
||||
} else if (error.code === 'ECONNABORTED') {
|
||||
return 'Error: Request timed out. Please try again.';
|
||||
}
|
||||
}
|
||||
return `Error: Unexpected error occurred: ${error instanceof Error ? error.message : String(error)}`;
|
||||
@@ -441,9 +446,9 @@ Extract common functionality into reusable functions:
|
||||
// Shared API request function
|
||||
async function makeApiRequest<T>(
|
||||
endpoint: string,
|
||||
method: "GET" | "POST" | "PUT" | "DELETE" = "GET",
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET',
|
||||
data?: any,
|
||||
params?: any
|
||||
params?: any,
|
||||
): Promise<T> {
|
||||
try {
|
||||
const response = await axios({
|
||||
@@ -453,9 +458,9 @@ async function makeApiRequest<T>(
|
||||
params,
|
||||
timeout: 30000,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json"
|
||||
}
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
@@ -477,8 +482,7 @@ async function fetchData(resourceId: string): Promise<ResourceData> {
|
||||
|
||||
// Bad: Promise chains
|
||||
function fetchData(resourceId: string): Promise<ResourceData> {
|
||||
return axios.get(`${API_URL}/resource/${resourceId}`)
|
||||
.then(response => response.data); // Harder to read and maintain
|
||||
return axios.get(`${API_URL}/resource/${resourceId}`).then((response) => response.data); // Harder to read and maintain
|
||||
}
|
||||
```
|
||||
|
||||
@@ -507,19 +511,19 @@ const UserSchema = z.object({
|
||||
name: z.string(),
|
||||
email: z.string().email(),
|
||||
team: z.string().optional(),
|
||||
active: z.boolean()
|
||||
active: z.boolean(),
|
||||
});
|
||||
|
||||
type User = z.infer<typeof UserSchema>;
|
||||
|
||||
async function getUser(id: string): Promise<User> {
|
||||
const data = await apiCall(`/users/${id}`);
|
||||
return UserSchema.parse(data); // Runtime validation
|
||||
return UserSchema.parse(data); // Runtime validation
|
||||
}
|
||||
|
||||
// Bad: Using any
|
||||
async function getUser(id: string): Promise<any> {
|
||||
return await apiCall(`/users/${id}`); // No type safety
|
||||
return await apiCall(`/users/${id}`); // No type safety
|
||||
}
|
||||
```
|
||||
|
||||
@@ -592,51 +596,46 @@ async function getUser(id: string): Promise<any> {
|
||||
* project management, and data export capabilities.
|
||||
*/
|
||||
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import { z } from "zod";
|
||||
import axios, { AxiosError } from "axios";
|
||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
import { z } from 'zod';
|
||||
import axios, { AxiosError } from 'axios';
|
||||
|
||||
// Constants
|
||||
const API_BASE_URL = "https://api.example.com/v1";
|
||||
const API_BASE_URL = 'https://api.example.com/v1';
|
||||
const CHARACTER_LIMIT = 25000;
|
||||
|
||||
// Enums
|
||||
enum ResponseFormat {
|
||||
MARKDOWN = "markdown",
|
||||
JSON = "json"
|
||||
MARKDOWN = 'markdown',
|
||||
JSON = 'json',
|
||||
}
|
||||
|
||||
// Zod schemas
|
||||
const UserSearchInputSchema = z.object({
|
||||
query: z.string()
|
||||
.min(2, "Query must be at least 2 characters")
|
||||
.max(200, "Query must not exceed 200 characters")
|
||||
.describe("Search string to match against names/emails"),
|
||||
limit: z.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.max(100)
|
||||
.default(20)
|
||||
.describe("Maximum results to return"),
|
||||
offset: z.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.default(0)
|
||||
.describe("Number of results to skip for pagination"),
|
||||
response_format: z.nativeEnum(ResponseFormat)
|
||||
.default(ResponseFormat.MARKDOWN)
|
||||
.describe("Output format: 'markdown' for human-readable or 'json' for machine-readable")
|
||||
}).strict();
|
||||
const UserSearchInputSchema = z
|
||||
.object({
|
||||
query: z
|
||||
.string()
|
||||
.min(2, 'Query must be at least 2 characters')
|
||||
.max(200, 'Query must not exceed 200 characters')
|
||||
.describe('Search string to match against names/emails'),
|
||||
limit: z.number().int().min(1).max(100).default(20).describe('Maximum results to return'),
|
||||
offset: z.number().int().min(0).default(0).describe('Number of results to skip for pagination'),
|
||||
response_format: z
|
||||
.nativeEnum(ResponseFormat)
|
||||
.default(ResponseFormat.MARKDOWN)
|
||||
.describe("Output format: 'markdown' for human-readable or 'json' for machine-readable"),
|
||||
})
|
||||
.strict();
|
||||
|
||||
type UserSearchInput = z.infer<typeof UserSearchInputSchema>;
|
||||
|
||||
// Shared utility functions
|
||||
async function makeApiRequest<T>(
|
||||
endpoint: string,
|
||||
method: "GET" | "POST" | "PUT" | "DELETE" = "GET",
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET',
|
||||
data?: any,
|
||||
params?: any
|
||||
params?: any,
|
||||
): Promise<T> {
|
||||
try {
|
||||
const response = await axios({
|
||||
@@ -646,9 +645,9 @@ async function makeApiRequest<T>(
|
||||
params,
|
||||
timeout: 30000,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json"
|
||||
}
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
@@ -661,16 +660,16 @@ function handleApiError(error: unknown): string {
|
||||
if (error.response) {
|
||||
switch (error.response.status) {
|
||||
case 404:
|
||||
return "Error: Resource not found. Please check the ID is correct.";
|
||||
return 'Error: Resource not found. Please check the ID is correct.';
|
||||
case 403:
|
||||
return "Error: Permission denied. You don't have access to this resource.";
|
||||
case 429:
|
||||
return "Error: Rate limit exceeded. Please wait before making more requests.";
|
||||
return 'Error: Rate limit exceeded. Please wait before making more requests.';
|
||||
default:
|
||||
return `Error: API request failed with status ${error.response.status}`;
|
||||
}
|
||||
} else if (error.code === "ECONNABORTED") {
|
||||
return "Error: Request timed out. Please try again.";
|
||||
} else if (error.code === 'ECONNABORTED') {
|
||||
return 'Error: Request timed out. Please try again.';
|
||||
}
|
||||
}
|
||||
return `Error: Unexpected error occurred: ${error instanceof Error ? error.message : String(error)}`;
|
||||
@@ -678,46 +677,46 @@ function handleApiError(error: unknown): string {
|
||||
|
||||
// Create MCP server instance
|
||||
const server = new McpServer({
|
||||
name: "example-mcp",
|
||||
version: "1.0.0"
|
||||
name: 'example-mcp',
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
// Register tools
|
||||
server.registerTool(
|
||||
"example_search_users",
|
||||
'example_search_users',
|
||||
{
|
||||
title: "Search Example Users",
|
||||
title: 'Search Example Users',
|
||||
description: `[Full description as shown above]`,
|
||||
inputSchema: UserSearchInputSchema,
|
||||
annotations: {
|
||||
readOnlyHint: true,
|
||||
destructiveHint: false,
|
||||
idempotentHint: true,
|
||||
openWorldHint: true
|
||||
}
|
||||
openWorldHint: true,
|
||||
},
|
||||
},
|
||||
async (params: UserSearchInput) => {
|
||||
// Implementation as shown above
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Main function
|
||||
// For stdio (local):
|
||||
async function runStdio() {
|
||||
if (!process.env.EXAMPLE_API_KEY) {
|
||||
console.error("ERROR: EXAMPLE_API_KEY environment variable is required");
|
||||
console.error('ERROR: EXAMPLE_API_KEY environment variable is required');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
console.error("MCP server running via stdio");
|
||||
console.error('MCP server running via stdio');
|
||||
}
|
||||
|
||||
// For streamable HTTP (remote):
|
||||
async function runHTTP() {
|
||||
if (!process.env.EXAMPLE_API_KEY) {
|
||||
console.error("ERROR: EXAMPLE_API_KEY environment variable is required");
|
||||
console.error('ERROR: EXAMPLE_API_KEY environment variable is required');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -727,7 +726,7 @@ async function runHTTP() {
|
||||
app.post('/mcp', async (req, res) => {
|
||||
const transport = new StreamableHTTPServerTransport({
|
||||
sessionIdGenerator: undefined,
|
||||
enableJsonResponse: true
|
||||
enableJsonResponse: true,
|
||||
});
|
||||
res.on('close', () => transport.close());
|
||||
await server.connect(transport);
|
||||
@@ -743,13 +742,13 @@ async function runHTTP() {
|
||||
// Choose transport based on environment
|
||||
const transport = process.env.TRANSPORT || 'stdio';
|
||||
if (transport === 'http') {
|
||||
runHTTP().catch(error => {
|
||||
console.error("Server error:", error);
|
||||
runHTTP().catch((error) => {
|
||||
console.error('Server error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
} else {
|
||||
runStdio().catch(error => {
|
||||
console.error("Server error:", error);
|
||||
runStdio().catch((error) => {
|
||||
console.error('Server error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
@@ -764,51 +763,54 @@ if (transport === 'http') {
|
||||
Expose data as resources for efficient, URI-based access:
|
||||
|
||||
```typescript
|
||||
import { ResourceTemplate } from "@modelcontextprotocol/sdk/types.js";
|
||||
import { ResourceTemplate } from '@modelcontextprotocol/sdk/types.js';
|
||||
|
||||
// Register a resource with URI template
|
||||
server.registerResource(
|
||||
{
|
||||
uri: "file://documents/{name}",
|
||||
name: "Document Resource",
|
||||
description: "Access documents by name",
|
||||
mimeType: "text/plain"
|
||||
uri: 'file://documents/{name}',
|
||||
name: 'Document Resource',
|
||||
description: 'Access documents by name',
|
||||
mimeType: 'text/plain',
|
||||
},
|
||||
async (uri: string) => {
|
||||
// Extract parameter from URI
|
||||
const match = uri.match(/^file:\/\/documents\/(.+)$/);
|
||||
if (!match) {
|
||||
throw new Error("Invalid URI format");
|
||||
throw new Error('Invalid URI format');
|
||||
}
|
||||
|
||||
const documentName = match[1];
|
||||
const content = await loadDocument(documentName);
|
||||
|
||||
return {
|
||||
contents: [{
|
||||
uri,
|
||||
mimeType: "text/plain",
|
||||
text: content
|
||||
}]
|
||||
contents: [
|
||||
{
|
||||
uri,
|
||||
mimeType: 'text/plain',
|
||||
text: content,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// List available resources dynamically
|
||||
server.registerResourceList(async () => {
|
||||
const documents = await getAvailableDocuments();
|
||||
return {
|
||||
resources: documents.map(doc => ({
|
||||
resources: documents.map((doc) => ({
|
||||
uri: `file://documents/${doc.name}`,
|
||||
name: doc.name,
|
||||
mimeType: "text/plain",
|
||||
description: doc.description
|
||||
}))
|
||||
mimeType: 'text/plain',
|
||||
description: doc.description,
|
||||
})),
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
**When to use Resources vs Tools:**
|
||||
|
||||
- **Resources**: For data access with simple URI-based parameters
|
||||
- **Tools**: For complex operations requiring validation and business logic
|
||||
- **Resources**: When data is relatively static or template-based
|
||||
@@ -821,8 +823,8 @@ The TypeScript SDK supports two main transport mechanisms:
|
||||
#### Streamable HTTP (Recommended for Remote Servers)
|
||||
|
||||
```typescript
|
||||
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
||||
import express from "express";
|
||||
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
||||
import express from 'express';
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
@@ -831,7 +833,7 @@ app.post('/mcp', async (req, res) => {
|
||||
// Create new transport for each request (stateless, prevents request ID collisions)
|
||||
const transport = new StreamableHTTPServerTransport({
|
||||
sessionIdGenerator: undefined,
|
||||
enableJsonResponse: true
|
||||
enableJsonResponse: true,
|
||||
});
|
||||
|
||||
res.on('close', () => transport.close());
|
||||
@@ -846,13 +848,14 @@ app.listen(3000);
|
||||
#### stdio (For Local Integrations)
|
||||
|
||||
```typescript
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
```
|
||||
|
||||
**Transport selection:**
|
||||
|
||||
- **Streamable HTTP**: Web services, remote access, multiple clients
|
||||
- **stdio**: Command-line tools, local development, subprocess integration
|
||||
|
||||
@@ -863,12 +866,12 @@ Notify clients when server state changes:
|
||||
```typescript
|
||||
// Notify when tools list changes
|
||||
server.notification({
|
||||
method: "notifications/tools/list_changed"
|
||||
method: 'notifications/tools/list_changed',
|
||||
});
|
||||
|
||||
// Notify when resources change
|
||||
server.notification({
|
||||
method: "notifications/resources/list_changed"
|
||||
method: 'notifications/resources/list_changed',
|
||||
});
|
||||
```
|
||||
|
||||
@@ -917,6 +920,7 @@ Always ensure `npm run build` completes successfully before considering the impl
|
||||
Before finalizing your Node/TypeScript MCP server implementation, ensure:
|
||||
|
||||
### Strategic Design
|
||||
|
||||
- [ ] Tools enable complete workflows, not just API endpoint wrappers
|
||||
- [ ] Tool names reflect natural task subdivisions
|
||||
- [ ] Response formats optimize for agent context efficiency
|
||||
@@ -924,6 +928,7 @@ Before finalizing your Node/TypeScript MCP server implementation, ensure:
|
||||
- [ ] Error messages guide agents toward correct usage
|
||||
|
||||
### Implementation Quality
|
||||
|
||||
- [ ] FOCUSED IMPLEMENTATION: Most important and valuable tools implemented
|
||||
- [ ] All tools registered using `registerTool` with complete configuration
|
||||
- [ ] All tools include `title`, `description`, `inputSchema`, and `annotations`
|
||||
@@ -935,6 +940,7 @@ Before finalizing your Node/TypeScript MCP server implementation, ensure:
|
||||
- [ ] Error messages are clear, actionable, and educational
|
||||
|
||||
### TypeScript Quality
|
||||
|
||||
- [ ] TypeScript interfaces are defined for all data structures
|
||||
- [ ] Strict TypeScript is enabled in tsconfig.json
|
||||
- [ ] No use of `any` type - use `unknown` or proper types instead
|
||||
@@ -942,12 +948,14 @@ Before finalizing your Node/TypeScript MCP server implementation, ensure:
|
||||
- [ ] Error handling uses proper type guards (e.g., `axios.isAxiosError`, `z.ZodError`)
|
||||
|
||||
### Advanced Features (where applicable)
|
||||
|
||||
- [ ] Resources registered for appropriate data endpoints
|
||||
- [ ] Appropriate transport configured (stdio or streamable HTTP)
|
||||
- [ ] Notifications implemented for dynamic server capabilities
|
||||
- [ ] Type-safe with SDK interfaces
|
||||
|
||||
### Project Configuration
|
||||
|
||||
- [ ] Package.json includes all necessary dependencies
|
||||
- [ ] Build script produces working JavaScript in dist/ directory
|
||||
- [ ] Main entry point is properly configured as dist/index.js
|
||||
@@ -955,6 +963,7 @@ Before finalizing your Node/TypeScript MCP server implementation, ensure:
|
||||
- [ ] tsconfig.json properly configured with strict mode
|
||||
|
||||
### Code Quality
|
||||
|
||||
- [ ] Pagination is properly implemented where applicable
|
||||
- [ ] Large responses check CHARACTER_LIMIT constant and truncate with clear messages
|
||||
- [ ] Filtering options are provided for potentially large result sets
|
||||
@@ -963,8 +972,9 @@ Before finalizing your Node/TypeScript MCP server implementation, ensure:
|
||||
- [ ] Return types are consistent across similar operations
|
||||
|
||||
### Testing and Build
|
||||
|
||||
- [ ] `npm run build` completes successfully without errors
|
||||
- [ ] dist/index.js created and executable
|
||||
- [ ] Server runs: `node dist/index.js --help`
|
||||
- [ ] All imports resolve correctly
|
||||
- [ ] Sample tool calls work as expected
|
||||
- [ ] Sample tool calls work as expected
|
||||
|
||||
@@ -9,6 +9,7 @@ This document provides Python-specific best practices and examples for implement
|
||||
## Quick Reference
|
||||
|
||||
### Key Imports
|
||||
|
||||
```python
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
from pydantic import BaseModel, Field, field_validator, ConfigDict
|
||||
@@ -18,11 +19,13 @@ import httpx
|
||||
```
|
||||
|
||||
### Server Initialization
|
||||
|
||||
```python
|
||||
mcp = FastMCP("service_mcp")
|
||||
```
|
||||
|
||||
### Tool Registration Pattern
|
||||
|
||||
```python
|
||||
@mcp.tool(name="tool_name", annotations={...})
|
||||
async def tool_function(params: InputModel) -> str:
|
||||
@@ -35,6 +38,7 @@ async def tool_function(params: InputModel) -> str:
|
||||
## MCP Python SDK and FastMCP
|
||||
|
||||
The official MCP Python SDK provides FastMCP, a high-level framework for building MCP servers. It provides:
|
||||
|
||||
- Automatic description and inputSchema generation from function signatures and docstrings
|
||||
- Pydantic model integration for input validation
|
||||
- Decorator-based tool registration with `@mcp.tool`
|
||||
@@ -45,10 +49,12 @@ The official MCP Python SDK provides FastMCP, a high-level framework for buildin
|
||||
## Server Naming Convention
|
||||
|
||||
Python MCP servers must follow this naming pattern:
|
||||
|
||||
- **Format**: `{service}_mcp` (lowercase with underscores)
|
||||
- **Examples**: `github_mcp`, `jira_mcp`, `stripe_mcp`
|
||||
|
||||
The name should be:
|
||||
|
||||
- General (not tied to specific features)
|
||||
- Descriptive of the service/API being integrated
|
||||
- Easy to infer from the task description
|
||||
@@ -61,6 +67,7 @@ The name should be:
|
||||
Use snake_case for tool names (e.g., "search_users", "create_project", "get_channel_info") with clear, action-oriented names.
|
||||
|
||||
**Avoid Naming Conflicts**: Include the service context to prevent overlaps:
|
||||
|
||||
- Use "slack_send_message" instead of just "send_message"
|
||||
- Use "github_create_issue" instead of just "create_issue"
|
||||
- Use "asana_list_tasks" instead of just "list_tasks"
|
||||
@@ -168,6 +175,7 @@ class UserSearchInput(BaseModel):
|
||||
```
|
||||
|
||||
**Markdown format**:
|
||||
|
||||
- Use headers, lists, and formatting for clarity
|
||||
- Convert timestamps to human-readable format (e.g., "2024-01-15 10:30:00 UTC" instead of epoch)
|
||||
- Show display names with IDs in parentheses (e.g., "@john.doe (U123456)")
|
||||
@@ -175,6 +183,7 @@ class UserSearchInput(BaseModel):
|
||||
- Group related information logically
|
||||
|
||||
**JSON format**:
|
||||
|
||||
- Return complete, structured data suitable for programmatic processing
|
||||
- Include all available fields and metadata
|
||||
- Use consistent field names and types
|
||||
@@ -518,6 +527,7 @@ async def interactive_tool(resource_id: str, ctx: Context) -> str:
|
||||
```
|
||||
|
||||
**Context capabilities:**
|
||||
|
||||
- `ctx.report_progress(progress, message)` - Report progress for long operations
|
||||
- `ctx.log_info(message, data)` / `ctx.log_error()` / `ctx.log_debug()` - Logging
|
||||
- `ctx.elicit(prompt, input_type)` - Request input from users
|
||||
@@ -548,6 +558,7 @@ async def get_setting(key: str, ctx: Context) -> str:
|
||||
```
|
||||
|
||||
**When to use Resources vs Tools:**
|
||||
|
||||
- **Resources**: For data access with simple parameters (URI templates)
|
||||
- **Tools**: For complex operations with validation and business logic
|
||||
|
||||
@@ -631,6 +642,7 @@ if __name__ == "__main__":
|
||||
```
|
||||
|
||||
**Transport selection:**
|
||||
|
||||
- **stdio**: Command-line tools, local integrations, subprocess execution
|
||||
- **Streamable HTTP**: Web services, remote access, multiple clients
|
||||
|
||||
@@ -670,6 +682,7 @@ Your implementation MUST prioritize composability and code reuse:
|
||||
Before finalizing your Python MCP server implementation, ensure:
|
||||
|
||||
### Strategic Design
|
||||
|
||||
- [ ] Tools enable complete workflows, not just API endpoint wrappers
|
||||
- [ ] Tool names reflect natural task subdivisions
|
||||
- [ ] Response formats optimize for agent context efficiency
|
||||
@@ -677,6 +690,7 @@ Before finalizing your Python MCP server implementation, ensure:
|
||||
- [ ] Error messages guide agents toward correct usage
|
||||
|
||||
### Implementation Quality
|
||||
|
||||
- [ ] FOCUSED IMPLEMENTATION: Most important and valuable tools implemented
|
||||
- [ ] All tools have descriptive names and documentation
|
||||
- [ ] Return types are consistent across similar operations
|
||||
@@ -688,6 +702,7 @@ Before finalizing your Python MCP server implementation, ensure:
|
||||
- [ ] Outputs are properly validated and formatted
|
||||
|
||||
### Tool Configuration
|
||||
|
||||
- [ ] All tools implement 'name' and 'annotations' in the decorator
|
||||
- [ ] Annotations correctly set (readOnlyHint, destructiveHint, idempotentHint, openWorldHint)
|
||||
- [ ] All tools use Pydantic BaseModel for input validation with Field() definitions
|
||||
@@ -697,6 +712,7 @@ Before finalizing your Python MCP server implementation, ensure:
|
||||
- [ ] Pydantic models handle input validation (no manual validation needed)
|
||||
|
||||
### Advanced Features (where applicable)
|
||||
|
||||
- [ ] Context injection used for logging, progress, or elicitation
|
||||
- [ ] Resources registered for appropriate data endpoints
|
||||
- [ ] Lifespan management implemented for persistent connections
|
||||
@@ -704,6 +720,7 @@ Before finalizing your Python MCP server implementation, ensure:
|
||||
- [ ] Appropriate transport configured (stdio or streamable HTTP)
|
||||
|
||||
### Code Quality
|
||||
|
||||
- [ ] File includes proper imports including Pydantic imports
|
||||
- [ ] Pagination is properly implemented where applicable
|
||||
- [ ] Filtering options are provided for potentially large result sets
|
||||
@@ -713,7 +730,8 @@ Before finalizing your Python MCP server implementation, ensure:
|
||||
- [ ] Constants are defined at module level in UPPER_CASE
|
||||
|
||||
### Testing
|
||||
|
||||
- [ ] Server runs successfully: `python your_server.py --help`
|
||||
- [ ] All imports resolve correctly
|
||||
- [ ] Sample tool calls work as expected
|
||||
- [ ] Error scenarios handled gracefully
|
||||
- [ ] Error scenarios handled gracefully
|
||||
|
||||
Reference in New Issue
Block a user