Skip to content

MCP Server Implementation Patterns: 5-Minute Practical Guide with Claude Code

Target Audience

  • Intermediate developers seeking concrete MCP Server implementation methods

Key Points

  1. Run a minimal MCP Server configuration
  2. Implement tool definitions and response handling
  3. 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

StepContentSuccess Metric
0MCP connection and management in Claude CodeUnderstanding config methods and scopes
1Create minimal MCP Serverserver.py launches
2Add tool definitionslist_tools responds
3Connect Claude CodeSuccessful invocation
4Output limits and safetyLarge output control
5Tool design best practicesProduction-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:

TypeUse CaseNotes
HTTP (Streamable HTTP)Remote servers (recommended)Recommended for remote connections
SSE (Server-Sent Events)Remote servers (legacy)Deprecated. Requires explicit --transport sse flag
stdioLocal processesBest for local development and testing

MCP Scopes

MCP server configurations are managed across three scopes:

ScopeLocationUse Case
local (default).claude/ directory (within project)Personal use, recommended to gitignore
project.mcp.json (project root)Team sharing, committed to git
user~/.claude.jsonGlobal 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 variable VAR
  • ${VAR:-default} -- Uses default if VAR is 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

# 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:

ThresholdBehavior
10,000 tokensWarning message displayed
25,000 tokens (default maximum)Output is truncated
  • The maximum can be configured via the MAX_MCP_OUTPUT_TOKENS environment 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 /mcp command within a session to initiate the OAuth authentication flow
  • To pre-configure OAuth credentials, use the --client-id and --client-secret flags:
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

PolicyDescription
allowedMcpServersList of MCP servers permitted for use
deniedMcpServersList 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

SymptomCauseImmediate Fix
Server not foundPath misconfigurationUse absolute path
Permission deniedNo execute permissionchmod +x server.js
Connection timeoutPort conflictUse stdio transport
Config not appliedWrong config file locationClaude Code uses .mcp.json or ~/.claude.json (not claude_desktop_config.json)
Output truncatedMCP output exceeds token limitAdjust MAX_MCP_OUTPUT_TOKENS or implement pagination
Too many tools causing slownessTool definitions exceed 10% of contextENABLE_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())