MCP Server Implementation Patterns: 5-Minute Practical Guide with Claude Code¶
Target Audience
- Intermediate developers seeking concrete MCP Server implementation methods
Key Points¶
- Run a minimal MCP Server configuration
- Implement tool definitions and response handling
- Connect and test with Claude Code
Why This Matters Now¶
MCP (Model Context Protocol) is rapidly becoming the standard protocol for connecting AI agents with external systems. However, practical examples are scarce, causing many developers to struggle with initial implementation. This article provides immediately functional minimal code.
Solution Steps Overview¶
| Step | Content | Success Metric |
|---|---|---|
| 0 | MCP connection and management in Claude Code | Understanding config methods and scopes |
| 1 | Create minimal MCP Server | server.py launches |
| 2 | Add tool definitions | list_tools responds |
| 3 | Connect Claude Code | Successful invocation |
| 4 | Output limits and safety | Large output control |
| 5 | Tool design best practices | Production-quality tool implementation |
Step 0: MCP Connection, Management, and Limits in Claude Code¶
Before diving into implementation, understand how MCP connections, scopes, and management commands work in Claude Code.
Transport Types¶
MCP supports three transport types:
| Type | Use Case | Notes |
|---|---|---|
| HTTP (Streamable HTTP) | Remote servers (recommended) | Recommended for remote connections |
| SSE (Server-Sent Events) | Remote servers (legacy) | Deprecated. Requires explicit --transport sse flag |
| stdio | Local processes | Best for local development and testing |
MCP Scopes¶
MCP server configurations are managed across three scopes:
| Scope | Location | Use Case |
|---|---|---|
| local (default) | .claude/ directory (within project) | Personal use, recommended to gitignore |
| project | .mcp.json (project root) | Team sharing, committed to git |
| user | ~/.claude.json | Global settings across all projects |
Claude Desktop vs. Claude Code configuration paths
Claude Desktop uses ~/.config/claude/claude_desktop_config.json, but Claude Code does not use this file. For Claude Code, use the claude mcp add command or edit .mcp.json / ~/.claude.json.
MCP Management Commands¶
# Add a server (stdio transport)
claude mcp add my-server -s local -- node /path/to/server.js
# Add a server (HTTP transport - remote)
claude mcp add my-remote-server https://api.example.com/mcp
# Add via JSON (for complex configurations)
claude mcp add-json my-server '{"command":"node","args":["/path/to/server.js"]}'
# List all servers
claude mcp list
# Get server details
claude mcp get my-server
# Remove a server
claude mcp remove my-server
# Import settings from Claude Desktop
claude mcp add-from-claude-desktop
# Expose Claude Code itself as an MCP server
claude mcp serve
In-Session MCP Management¶
Use the /mcp command within a Claude Code interactive session to check connection status of MCP servers and initiate OAuth authentication flows.
Environment Variable Expansion in .mcp.json¶
Project-scoped .mcp.json supports environment variable expansion:
{
"mcpServers": {
"my-server": {
"command": "node",
"args": ["/path/to/server.js"],
"env": {
"API_KEY": "${API_KEY}",
"DB_HOST": "${DB_HOST:-localhost}"
}
}
}
}
${VAR}-- Expands to the value of environment variableVAR${VAR:-default}-- UsesdefaultifVARis not set
Step 1: Create Minimal MCP Server¶
Basic TypeScript implementation. Runs immediately in Node.js environment:
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
const server = new Server({
name: 'minimal-mcp',
version: '1.0.0',
}, {
capabilities: { tools: {} }
});
const transport = new StdioServerTransport();
await server.connect(transport);
Step 2: Tool Definition and Handler Implementation¶
Adding one practical tool. File system operation example:
server.setRequestHandler('tools/list', async () => ({
tools: [{
name: 'read_file',
description: 'Read file contents',
inputSchema: {
type: 'object',
properties: {
path: { type: 'string' }
},
required: ['path']
}
}]
}));
server.setRequestHandler('tools/call', async (req) => {
const { path } = req.params.arguments;
const content = await fs.readFile(path, 'utf-8');
return { content: [{ type: 'text', text: content }] };
});
Step 3: Claude Code Configuration and Connection¶
Method A: Add via CLI Command (Recommended)¶
# Add to local scope (default)
claude mcp add minimal-mcp -- node /path/to/server.js
# Add to project scope (for team sharing)
claude mcp add minimal-mcp -s project -- node /path/to/server.js
Method B: Edit .mcp.json Directly (Project Scope)¶
Create .mcp.json in the project root:
{
"mcpServers": {
"minimal-mcp": {
"command": "node",
"args": ["/path/to/server.js"]
}
}
}
Method C: Edit ~/.claude.json (User Scope)¶
Add to ~/.claude.json to make the server available across all projects:
{
"mcpServers": {
"minimal-mcp": {
"command": "node",
"args": ["/path/to/server.js"]
}
}
}
Common Mistake
~/.config/claude/claude_desktop_config.json is the configuration file for Claude Desktop, not Claude Code. When using Claude Code, use Methods A through C above.
After configuration, restart Claude Code to make the tool available.
Output Limits and Safety¶
When MCP tool output is too large, it consumes excessive context window space. Claude Code includes the following safety mechanisms:
| Threshold | Behavior |
|---|---|
| 10,000 tokens | Warning message displayed |
| 25,000 tokens (default maximum) | Output is truncated |
- The maximum can be configured via the
MAX_MCP_OUTPUT_TOKENSenvironment variable - When MCP tool definitions exceed 10% of context, the tool search feature (
ENABLE_TOOL_SEARCH) is automatically enabled
Handling Large Output
Tools that return large amounts of data should implement pagination and filtering to control output volume. See the next section "Tool Design Best Practices" for details.
Tool Design Best Practices¶
Guidelines for designing production-quality tools in MCP Servers:
Pagination¶
Break large result sets into pages:
server.setRequestHandler('tools/call', async (req) => {
const { query, page = 1, pageSize = 20 } = req.params.arguments;
const allResults = await searchDatabase(query);
const start = (page - 1) * pageSize;
const paged = allResults.slice(start, start + pageSize);
return {
content: [{
type: 'text',
text: JSON.stringify({
results: paged,
totalCount: allResults.length,
page,
totalPages: Math.ceil(allResults.length / pageSize)
})
}]
};
});
Filtering¶
Accept filter parameters to reduce output volume:
// Declare filter parameters in tool definition
{
name: 'list_issues',
description: 'List issues with optional filters',
inputSchema: {
type: 'object',
properties: {
status: { type: 'string', enum: ['open', 'closed', 'all'] },
assignee: { type: 'string' },
limit: { type: 'number', default: 10 }
}
}
}
Output Suppression¶
Return summaries instead of raw data for large datasets:
// Return summary instead of full raw data
return {
content: [{
type: 'text',
text: JSON.stringify({
summary: `Found ${results.length} items`,
topItems: results.slice(0, 5),
stats: { total: results.length, avgSize: avg }
})
}]
};
Error Handling¶
Return structured error messages:
server.setRequestHandler('tools/call', async (req) => {
try {
const result = await performAction(req.params.arguments);
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
} catch (error) {
return {
content: [{
type: 'text',
text: JSON.stringify({
error: true,
code: error.code || 'UNKNOWN_ERROR',
message: error.message,
suggestion: 'Check the input parameters and retry'
})
}],
isError: true
};
}
});
MCP Resources and Prompts¶
Beyond tools, MCP also provides Resources and Prompts capabilities.
Resources¶
MCP servers can expose resources (files, data, etc.) that can be referenced directly in prompts:
@server-name:protocol://resource/path
For example, to reference API documentation exposed by a docs-server:
@docs-server:file:///api/reference Please review the authentication endpoint specification
Prompts (Prompt Templates)¶
Prompt templates defined by MCP servers are available as slash commands in Claude Code:
/mcp__servername__promptname
For example, if a code-review server exposes a review prompt:
/mcp__code-review__review
OAuth Authentication (Remote MCP Servers)¶
Remote MCP servers support OAuth 2.0 authentication.
- Use the
/mcpcommand within a session to initiate the OAuth authentication flow - To pre-configure OAuth credentials, use the
--client-idand--client-secretflags:
claude mcp add my-oauth-server \
--transport http \
--client-id "your-client-id" \
--client-secret "your-client-secret" \
https://api.example.com/mcp
Managed MCP for Enterprise¶
A mechanism for organizations to centrally manage MCP servers.
Deploying managed-mcp.json¶
Place managed-mcp.json in system directories to centrally manage MCP server configurations applied to all users.
Policy Control¶
| Policy | Description |
|---|---|
allowedMcpServers | List of MCP servers permitted for use |
deniedMcpServers | List of MCP servers prohibited from use |
Denylist Takes Absolute Precedence
deniedMcpServers (denylist) takes absolute precedence. A server listed in both the allowed and denied lists will be denied.
Common Pitfalls and Solutions¶
| Symptom | Cause | Immediate Fix |
|---|---|---|
| Server not found | Path misconfiguration | Use absolute path |
| Permission denied | No execute permission | chmod +x server.js |
| Connection timeout | Port conflict | Use stdio transport |
| Config not applied | Wrong config file location | Claude Code uses .mcp.json or ~/.claude.json (not claude_desktop_config.json) |
| Output truncated | MCP output exceeds token limit | Adjust MAX_MCP_OUTPUT_TOKENS or implement pagination |
| Too many tools causing slowness | Tool definitions exceed 10% of context | ENABLE_TOOL_SEARCH auto-activates |
Advanced Configuration (Python Implementation)
Python MCP server implementation example:from mcp.server import Server, StdioServerTransport
import asyncio
server = Server("minimal-mcp")
@server.list_tools()
async def list_tools():
return [{"name": "read_file", "description": "Read file"}]
@server.call_tool()
async def call_tool(name: str, arguments: dict):
if name == "read_file":
with open(arguments["path"]) as f:
return {"content": f.read()}
async def main():
transport = StdioServerTransport()
await server.connect(transport)
asyncio.run(main())