Guide for creating high-quality MCP (Model Context Protocol) servers that enable LLMs to interact with external services through well-designed tools. Use when building MCP servers to integrate external APIs or services, whether in Python (FastMCP) or Node/TypeScript (MCP SDK).
83
83%
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Advisory
Suggest reviewing before use
{service}_mcp (e.g., slack_mcp){service}-mcp-server (e.g., slack-mcp-server){service}_{action}_{resource}slack_send_message, github_create_issuelimit parameterhas_more, next_offset, total_countFollow these standardized naming patterns:
Python: Use format {service}_mcp (lowercase with underscores)
slack_mcp, github_mcp, jira_mcpNode/TypeScript: Use format {service}-mcp-server (lowercase with hyphens)
slack-mcp-server, github-mcp-server, jira-mcp-serverThe name should be general, descriptive of the service being integrated, easy to infer from the task description, and without version numbers.
search_users, create_project, get_channel_infoslack_send_message instead of just send_messagegithub_create_issue instead of just create_issueAll tools that return data should support multiple formats:
response_format="json")response_format="markdown", typically default)For tools that list resources:
limit parameteroffset or cursor-based paginationhas_more, next_offset/next_cursor, total_countExample pagination response:
{
"total": 150,
"count": 20,
"offset": 0,
"items": [...],
"has_more": true,
"next_offset": 20
}Best for: Remote servers, web services, multi-client scenarios
Characteristics:
Use when:
Best for: Local integrations, command-line tools
Characteristics:
Use when:
Note: stdio servers should NOT log to stdout (use stderr for logging)
| Criterion | stdio | Streamable HTTP |
|---|---|---|
| Deployment | Local | Remote |
| Clients | Single | Multiple |
| Complexity | Low | Medium |
| Real-time | No | Yes |
OAuth 2.1:
API Keys:
For streamable HTTP servers running locally:
Origin header on all incoming connections127.0.0.1 rather than 0.0.0.0Provide 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 |
Important: Annotations are hints, not security guarantees. Clients should not make security-critical decisions based solely on annotations.
Example error handling:
try {
const result = performOperation();
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.`
}]
};
}Comprehensive testing should cover: