Claude_Code-Gemini-MCP A developer published a hands-on guide for building a custom Model Context Protocol (MCP) server in Python that extends Claude Code with external tools and APIs. The guide walks through Claude Code's three configuration scopes—project, local, and user—and warns that servers registered without `--scope user` only work in the current directory, recommending permanent storage under `~/.claude-mcp-servers/`. It includes a working JSON-RPC server template implementing initialize, tools/list, and tools/call handlers. This guide will walk you through creating a custom MCP Model Context Protocol server that integrates with Claude Code, allowing you to extend Claude's capabilities with external tools, APIs, or even other AI models. MCP Model Context Protocol is a protocol that allows Claude to communicate with external servers to access tools and capabilities beyond its built-in features. Think of it as a plugin system for Claude. - Python 3.8 or higher - Claude Code CLI installed npm install -g @anthropic-ai/claude-code - Basic understanding of JSON-RPC protocol Before building your MCP server, understand Claude Code's configuration hierarchy to avoid common issues: Claude Code supports three configuration scopes in order of priority : 1. Project Scope .vscode/mcp.json - Highest priority, overrides everything 2. Local Scope claude mcp add default - Works only in current directory 3. User Scope claude mcp add --scope user - Global configuration ❌ WRONG Local scope - only works in current directory : claude mcp add my-server python3 /path/to/server.py ✅ CORRECT User scope - works globally : claude mcp add --scope user my-server python3 /path/to/server.py 1. Always use --scope user for global MCP servers 2. Store servers in permanent location : ~/.claude-mcp-servers/ 3. Avoid project-local configs unless specifically needed 4. Remove conflicting .vscode/mcp.json files If your MCP only works in one directory: Check current configuration claude mcp list Remove local config claude mcp remove your-server Re-add with user scope claude mcp add --scope user your-server python3 /path/to/server.py Remove any project-local configs rm .vscode/mcp.json if exists mkdir my-mcp-server cd my-mcp-server Create a file named server.py : python /usr/bin/env python3 """ Basic MCP Server Template """ import json import sys import os from typing import Dict, Any, Optional Ensure unbuffered output for proper communication sys.stdout = os.fdopen sys.stdout.fileno , 'w', 1 sys.stderr = os.fdopen sys.stderr.fileno , 'w', 1 def send response response: Dict str, Any : """Send a JSON-RPC response""" print json.dumps response , flush=True def handle initialize request id: Any - Dict str, Any : """Handle initialization request""" return { "jsonrpc": "2.0", "id": request id, "result": { "protocolVersion": "2024-11-05", "capabilities": { "tools": {} }, "serverInfo": { "name": "my-mcp-server", "version": "1.0.0" } } } def handle tools list request id: Any - Dict str, Any : """List available tools""" tools = { "name": "hello world", "description": "A simple hello world tool", "inputSchema": { "type": "object", "properties": { "name": { "type": "string", "description": "Name to greet" } }, "required": "name" } } return { "jsonrpc": "2.0", "id": request id, "result": { "tools": tools } } def handle tool call request id: Any, params: Dict str, Any - Dict str, Any : """Handle tool execution""" tool name = params.get "name" arguments = params.get "arguments", {} try: if tool name == "hello world": name = arguments.get "name", "World" result = f"Hello, {name} This is a response from your MCP server." else: raise ValueError f"Unknown tool: {tool name}" return { "jsonrpc": "2.0", "id": request id, "result": { "content": { "type": "text", "text": result } } } except Exception as e: return { "jsonrpc": "2.0", "id": request id, "error": { "code": -32603, "message": str e } } def main : """Main server loop""" while True: try: line = sys.stdin.readline if not line: break request = json.loads line.strip method = request.get "method" request id = request.get "id" params = request.get "params", {} if method == "initialize": response = handle initialize request id elif method == "tools/list": response = handle tools list request id elif method == "tools/call": response = handle tool call request id, params else: response = { "jsonrpc": "2.0", "id": request id, "error": { "code": -32601, "message": f"Method not found: {method}" } } send response response except json.JSONDecodeError: continue except EOFError: break except Exception as e: if 'request id' in locals : send response { "jsonrpc": "2.0", "id": request id, "error": { "code": -32603, "message": f"Internal error: {str e }" } } if name == " main ": main chmod +x server.py claude mcp add my-server python3 /path/to/your/server.py Your MCP server must handle these JSON-RPC methods: 1. initialize - Called when Claude connects to your server - Must return protocol version and capabilities 2. tools/list - Lists all available tools - Returns array of tool definitions with schemas 3. tools/call - Executes a specific tool - Receives tool name and arguments - Returns results that Claude can use All communication uses JSON-RPC 2.0 over standard input/output: Request from Claude: { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "hello world", "arguments": {"name": "Claude"} } } Response from your server: { "jsonrpc": "2.0", "id": 1, "result": { "content": { "type": "text", "text": "Hello, Claude " } } } Here's a more practical example that fetches weather data: python /usr/bin/env python3 import json import sys import os import requests from typing import Dict, Any sys.stdout = os.fdopen sys.stdout.fileno , 'w', 1 sys.stderr = os.fdopen sys.stderr.fileno , 'w', 1 Your API key store securely in production WEATHER API KEY = "your-api-key-here" def get weather city: str - str: """Fetch weather data from API""" try: url = f"http://api.openweathermap.org/data/2.5/weather" params = { "q": city, "appid": WEATHER API KEY, "units": "metric" } response = requests.get url, params=params data = response.json if response.status code == 200: temp = data "main" "temp" desc = data "weather" 0 "description" return f"Weather in {city}: {temp}°C, {desc}" else: return f"Error: {data.get 'message', 'Unknown error' }" except Exception as e: return f"Error fetching weather: {str e }" ... include the same boilerplate as before def handle tools list request id: Any - Dict str, Any : tools = { "name": "get weather", "description": "Get current weather for a city", "inputSchema": { "type": "object", "properties": { "city": { "type": "string", "description": "City name" } }, "required": "city" } } ... rest of implementation Always wrap tool execution in try-except blocks and return proper JSON-RPC errors: try: Your tool logic result = do something except Exception as e: return { "jsonrpc": "2.0", "id": request id, "error": { "code": -32603, "message": str e } } Validate all inputs from the arguments: python def validate arguments arguments: Dict str, Any , required: List str : for field in required: if field not in arguments: raise ValueError f"Missing required field: {field}" Use stderr for logging to avoid interfering with JSON-RPC: python import logging logging.basicConfig level=logging.INFO, stream=sys.stderr Create a requirements.txt file: requests =2.28.0 Add other dependencies Install with: pip install -r requirements.txt Test individual methods: Test initialize echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' | python3 server.py Test tools/list echo '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' | python3 server.py Test tool call echo '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"hello world","arguments":{"name":"Test"}}}' | python3 server.py After adding to Claude Code: List all MCP servers claude mcp list In a Claude Code session, your tools will appear as: mcp