{"slug": "claude-code-gemini-mcp", "title": "Claude_Code-Gemini-MCP", "summary": "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.", "body_md": "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.\n\nMCP (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.\n\n- Python 3.8 or higher\n- Claude Code CLI installed (`npm install -g @anthropic-ai/claude-code` )\n- Basic understanding of JSON-RPC protocol\n\nBefore building your MCP server, understand Claude Code's configuration hierarchy to avoid common issues:\n\nClaude Code supports three configuration scopes (in order of priority):\n\n1. **Project Scope** (`.vscode/mcp.json` ) - Highest priority, overrides everything\n2. **Local Scope** (`claude mcp add` default) - Works only in current directory\n3. **User Scope** (`claude mcp add --scope user` ) - Global configuration\n\n**❌ WRONG (Local scope - only works in current directory):**\n\n```\nclaude mcp add my-server python3 /path/to/server.py\n```\n\n**✅ CORRECT (User scope - works globally):**\n\n```\nclaude mcp add --scope user my-server python3 /path/to/server.py\n```\n\n1. **Always use `--scope user`** for global MCP servers\n2. **Store servers in permanent location** :`~/.claude-mcp-servers/`\n3. **Avoid project-local configs** unless specifically needed\n4. **Remove conflicting `.vscode/mcp.json`** files\n\nIf your MCP only works in one directory:\n\n```\n# Check current configuration\nclaude mcp list\n\n# Remove local config\nclaude mcp remove your-server\n\n# Re-add with user scope\nclaude mcp add --scope user your-server python3 /path/to/server.py\n\n# Remove any project-local configs\nrm .vscode/mcp.json  # if exists\nmkdir my-mcp-server\ncd my-mcp-server\n```\n\nCreate a file named `server.py`:\n\n``` python\n#!/usr/bin/env python3\n\"\"\"\nBasic MCP Server Template\n\"\"\"\n\nimport json\nimport sys\nimport os\nfrom typing import Dict, Any, Optional\n\n# Ensure unbuffered output for proper communication\nsys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 1)\nsys.stderr = os.fdopen(sys.stderr.fileno(), 'w', 1)\n\ndef send_response(response: Dict[str, Any]):\n    \"\"\"Send a JSON-RPC response\"\"\"\n    print(json.dumps(response), flush=True)\n\ndef handle_initialize(request_id: Any) -> Dict[str, Any]:\n    \"\"\"Handle initialization request\"\"\"\n    return {\n        \"jsonrpc\": \"2.0\",\n        \"id\": request_id,\n        \"result\": {\n            \"protocolVersion\": \"2024-11-05\",\n            \"capabilities\": {\n                \"tools\": {}\n            },\n            \"serverInfo\": {\n                \"name\": \"my-mcp-server\",\n                \"version\": \"1.0.0\"\n            }\n        }\n    }\n\ndef handle_tools_list(request_id: Any) -> Dict[str, Any]:\n    \"\"\"List available tools\"\"\"\n    tools = [\n        {\n            \"name\": \"hello_world\",\n            \"description\": \"A simple hello world tool\",\n            \"inputSchema\": {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"name\": {\n                        \"type\": \"string\",\n                        \"description\": \"Name to greet\"\n                    }\n                },\n                \"required\": [\"name\"]\n            }\n        }\n    ]\n    \n    return {\n        \"jsonrpc\": \"2.0\",\n        \"id\": request_id,\n        \"result\": {\n            \"tools\": tools\n        }\n    }\n\ndef handle_tool_call(request_id: Any, params: Dict[str, Any]) -> Dict[str, Any]:\n    \"\"\"Handle tool execution\"\"\"\n    tool_name = params.get(\"name\")\n    arguments = params.get(\"arguments\", {})\n    \n    try:\n        if tool_name == \"hello_world\":\n            name = arguments.get(\"name\", \"World\")\n            result = f\"Hello, {name}! This is a response from your MCP server.\"\n        else:\n            raise ValueError(f\"Unknown tool: {tool_name}\")\n        \n        return {\n            \"jsonrpc\": \"2.0\",\n            \"id\": request_id,\n            \"result\": {\n                \"content\": [\n                    {\n                        \"type\": \"text\",\n                        \"text\": result\n                    }\n                ]\n            }\n        }\n    except Exception as e:\n        return {\n            \"jsonrpc\": \"2.0\",\n            \"id\": request_id,\n            \"error\": {\n                \"code\": -32603,\n                \"message\": str(e)\n            }\n        }\n\ndef main():\n    \"\"\"Main server loop\"\"\"\n    while True:\n        try:\n            line = sys.stdin.readline()\n            if not line:\n                break\n            \n            request = json.loads(line.strip())\n            method = request.get(\"method\")\n            request_id = request.get(\"id\")\n            params = request.get(\"params\", {})\n            \n            if method == \"initialize\":\n                response = handle_initialize(request_id)\n            elif method == \"tools/list\":\n                response = handle_tools_list(request_id)\n            elif method == \"tools/call\":\n                response = handle_tool_call(request_id, params)\n            else:\n                response = {\n                    \"jsonrpc\": \"2.0\",\n                    \"id\": request_id,\n                    \"error\": {\n                        \"code\": -32601,\n                        \"message\": f\"Method not found: {method}\"\n                    }\n                }\n            \n            send_response(response)\n            \n        except json.JSONDecodeError:\n            continue\n        except EOFError:\n            break\n        except Exception as e:\n            if 'request_id' in locals():\n                send_response({\n                    \"jsonrpc\": \"2.0\",\n                    \"id\": request_id,\n                    \"error\": {\n                        \"code\": -32603,\n                        \"message\": f\"Internal error: {str(e)}\"\n                    }\n                })\n\nif __name__ == \"__main__\":\n    main()\nchmod +x server.py\nclaude mcp add my-server python3 /path/to/your/server.py\n```\n\nYour MCP server must handle these JSON-RPC methods:\n\n1. \n**`initialize`** - Called when Claude connects to your server\n  - Must return protocol version and capabilities\n2. \n**`tools/list`** - Lists all available tools\n  - Returns array of tool definitions with schemas\n3. \n**`tools/call`** - Executes a specific tool\n  - Receives tool name and arguments\n  - Returns results that Claude can use\n\nAll communication uses JSON-RPC 2.0 over standard input/output:\n\n**Request from Claude:**\n\n```\n{\n  \"jsonrpc\": \"2.0\",\n  \"id\": 1,\n  \"method\": \"tools/call\",\n  \"params\": {\n    \"name\": \"hello_world\",\n    \"arguments\": {\"name\": \"Claude\"}\n  }\n}\n```\n\n**Response from your server:**\n\n```\n{\n  \"jsonrpc\": \"2.0\",\n  \"id\": 1,\n  \"result\": {\n    \"content\": [\n      {\n        \"type\": \"text\",\n        \"text\": \"Hello, Claude!\"\n      }\n    ]\n  }\n}\n```\n\nHere's a more practical example that fetches weather data:\n\n``` python\n#!/usr/bin/env python3\nimport json\nimport sys\nimport os\nimport requests\nfrom typing import Dict, Any\n\nsys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 1)\nsys.stderr = os.fdopen(sys.stderr.fileno(), 'w', 1)\n\n# Your API key (store securely in production)\nWEATHER_API_KEY = \"your-api-key-here\"\n\ndef get_weather(city: str) -> str:\n    \"\"\"Fetch weather data from API\"\"\"\n    try:\n        url = f\"http://api.openweathermap.org/data/2.5/weather\"\n        params = {\n            \"q\": city,\n            \"appid\": WEATHER_API_KEY,\n            \"units\": \"metric\"\n        }\n        response = requests.get(url, params=params)\n        data = response.json()\n        \n        if response.status_code == 200:\n            temp = data[\"main\"][\"temp\"]\n            desc = data[\"weather\"][0][\"description\"]\n            return f\"Weather in {city}: {temp}°C, {desc}\"\n        else:\n            return f\"Error: {data.get('message', 'Unknown error')}\"\n    except Exception as e:\n        return f\"Error fetching weather: {str(e)}\"\n\n# ... (include the same boilerplate as before)\n\ndef handle_tools_list(request_id: Any) -> Dict[str, Any]:\n    tools = [\n        {\n            \"name\": \"get_weather\",\n            \"description\": \"Get current weather for a city\",\n            \"inputSchema\": {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"city\": {\n                        \"type\": \"string\",\n                        \"description\": \"City name\"\n                    }\n                },\n                \"required\": [\"city\"]\n            }\n        }\n    ]\n    # ... rest of implementation\n```\n\nAlways wrap tool execution in try-except blocks and return proper JSON-RPC errors:\n\n```\ntry:\n    # Your tool logic\n    result = do_something()\nexcept Exception as e:\n    return {\n        \"jsonrpc\": \"2.0\",\n        \"id\": request_id,\n        \"error\": {\n            \"code\": -32603,\n            \"message\": str(e)\n        }\n    }\n```\n\nValidate all inputs from the arguments:\n\n``` python\ndef validate_arguments(arguments: Dict[str, Any], required: List[str]):\n    for field in required:\n        if field not in arguments:\n            raise ValueError(f\"Missing required field: {field}\")\n```\n\nUse stderr for logging to avoid interfering with JSON-RPC:\n\n``` python\nimport logging\nlogging.basicConfig(level=logging.INFO, stream=sys.stderr)\n```\n\nCreate a `requirements.txt` file:\n\n```\nrequests>=2.28.0\n# Add other dependencies\n```\n\nInstall with: `pip install -r requirements.txt`\n\nTest individual methods:\n\n```\n# Test initialize\necho '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{}}' | python3 server.py\n\n# Test tools/list\necho '{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/list\",\"params\":{}}' | python3 server.py\n\n# Test tool call\necho '{\"jsonrpc\":\"2.0\",\"id\":3,\"method\":\"tools/call\",\"params\":{\"name\":\"hello_world\",\"arguments\":{\"name\":\"Test\"}}}' | python3 server.py\n```\n\nAfter adding to Claude Code:\n\n```\n# List all MCP servers\nclaude mcp list\n\n# In a Claude Code session, your tools will appear as:\n# mcp__<server-name>__<tool-name>\n```\n\n1. \n**Check configuration scope first** :\n\n```\n# Check what's configured\nclaude mcp list\n\n# Check if you're in a directory with local config\nls .vscode/mcp.json\n\n# Test in different directories\ncd ~ && claude mcp list\n```\n\n2. \n**Check logs** :\n\n```\nls ~/Library/Caches/claude-cli-nodejs/*/mcp-logs-<server-name>/\n```\n\n3. \n**Run with debug mode** :\n\n```\nclaude --debug\n```\n\n4. \n**Common issues** :\n  - **MCP only works in one directory** : Wrong scope, use`--scope user`\n  - **MCP not found** : Check if`.vscode/mcp.json` exists and conflicts\n  - Import errors: Ensure all dependencies are installed\n  - Connection closed: Check for syntax errors or crashes\n  - Tools not appearing: Verify tools/list returns valid schema\n5. \n**Configuration conflicts** :\n\n```\n# Remove project-local config\nrm .vscode/mcp.json\n\n# Remove local scope config\nclaude mcp remove server-name\n\n# Re-add with user scope\nclaude mcp add --scope user server-name python3 /path/to/server.py\n```\n\nStore conversation context:\n\n``` python\nclass MCPServer:\n    def __init__(self):\n        self.conversation_history = []\n    \n    def add_to_history(self, role: str, content: str):\n        self.conversation_history.append({\n            \"role\": role,\n            \"content\": content\n        })\n```\n\nReturn different content types:\n\n```\n# Text content\n{\n    \"type\": \"text\",\n    \"text\": \"Your response\"\n}\n\n# Image content (base64)\n{\n    \"type\": \"image\",\n    \"data\": base64_encoded_image,\n    \"mimeType\": \"image/png\"\n}\n```\n\nFor long-running tasks, consider implementing progress updates or background processing.\n\nStore your MCP servers in a permanent location:\n\n```\n~/.claude-mcp-servers/\n├── your-server-name/\n│   ├── server.py\n│   ├── requirements.txt\n│   ├── setup.py\n│   └── README.md\n└── backup.sh\n```\n\nCreate `setup.py` for easy installation:\n\n``` python\n#!/usr/bin/env python3\n\"\"\"Setup script for MCP server\"\"\"\nimport subprocess\nimport sys\nimport os\n\ndef check_python_version():\n    if sys.version_info < (3, 8):\n        print(\"❌ Python 3.8+ required\")\n        sys.exit(1)\n\ndef install_dependencies():\n    print(\"📦 Installing dependencies...\")\n    subprocess.check_call([sys.executable, \"-m\", \"pip\", \"install\", \"-r\", \"requirements.txt\"])\n\ndef add_to_claude():\n    server_path = os.path.join(os.path.dirname(__file__), \"server.py\")\n    print(f\"🔧 Adding to Claude MCP with global scope...\")\n    # IMPORTANT: Use --scope user for global access!\n    subprocess.run([\"claude\", \"mcp\", \"add\", \"--scope\", \"user\", \"your-server\", \"python3\", server_path])\n\nif __name__ == \"__main__\":\n    check_python_version()\n    install_dependencies()\n    add_to_claude()\n    print(\"✅ Setup complete!\")\n```\n\nAdd version tracking to your server:\n\n```\n__version__ = \"1.0.0\"\n__updated__ = \"2025-06-11\"\n\n# In your tools list, add:\n{\n    \"name\": \"server_info\",\n    \"description\": \"Get server version and status\",\n    \"inputSchema\": {\"type\": \"object\", \"properties\": {}}\n}\n\n# In tool handler:\nif tool_name == \"server_info\":\n    return f\"Server v{__version__} (updated {__updated__})\"\n```\n\nAdd automatic dependency checking:\n\n``` python\ndef check_dependencies():\n    \"\"\"Check if all required packages are installed\"\"\"\n    required = [\"requests\", \"other-package\"]\n    missing = []\n    \n    for package in required:\n        try:\n            __import__(package)\n        except ImportError:\n            missing.append(package)\n    \n    if missing:\n        return f\"Missing packages: {', '.join(missing)}\"\n    return \"All dependencies installed!\"\n```\n\nCreate an update mechanism for your server:\n\n``` python\n# In your server.py\ndef check_for_updates():\n    \"\"\"Check if updates are available\"\"\"\n    try:\n        import requests\n        response = requests.get(\"https://api.github.com/repos/YOUR_REPO/releases/latest\")\n        latest_version = response.json()[\"tag_name\"]\n        if latest_version > __version__:\n            return f\"Update available: {latest_version}\"\n        return \"Server is up to date\"\n    except:\n        return \"Could not check for updates\"\n\n# Add update tool to your tools list\n{\n    \"name\": \"update_server\",\n    \"description\": \"Update the MCP server to latest version\",\n    \"inputSchema\": {\"type\": \"object\", \"properties\": {}}\n}\n```\n\nSupport environment variables for configuration:\n\n``` python\nimport os\n\n# API keys and sensitive data\nAPI_KEY = os.environ.get(\"YOUR_API_KEY\", \"default-key-if-any\")\n\n# Configuration\nDEBUG = os.environ.get(\"MCP_DEBUG\", \"false\").lower() == \"true\"\nLOG_LEVEL = os.environ.get(\"MCP_LOG_LEVEL\", \"ERROR\")\n```\n\n1. \n**Package your server** :\n\n```\nmy-mcp-server/\n├── server.py\n├── requirements.txt\n├── setup.py\n├── README.md\n└── LICENSE\n```\n\n2. \n**Create one-line installer** :\n\n```\n# In your README:\ncurl -sSL https://your-repo/install.sh | bash\n```\n\n3. \n**Share on GitHub** with clear documentation\n\n1. **Database Query Tool** : Allow Claude to query your database\n2. **API Integration** : Connect to any REST API\n3. **System Monitoring** : Check system stats, logs, etc.\n4. **Custom AI Models** : Integrate other AI models (like we did with Gemini)\n5. **Development Tools** : Linters, formatters, test runners\n6. **Communication Tools** : Send emails, Slack messages, etc.\n\n1. \n**API Keys** : Never hardcode sensitive keys\n\n```\nAPI_KEY = os.environ.get(\"MY_API_KEY\")\n```\n\n2. \n**Input Sanitization** : Always validate and sanitize inputs\n3. \n**Access Control** : Limit what your MCP server can access\n4. \n**Rate Limiting** : Implement rate limits for API calls\n\nHere's a complete working example that enables Claude Code to collaborate with Google's Gemini AI:\n\n```\n# 1. Create permanent directory\nmkdir -p ~/.claude-mcp-servers/gemini-collab\n\n# 2. Install Gemini SDK\npip install google-generativeai\n\n# 3. Download server (simplified version)\ncurl -o ~/.claude-mcp-servers/gemini-collab/server.py https://your-repo/server.py\n\n# 4. Add to Claude with USER SCOPE (crucial!)\nclaude mcp add --scope user gemini-collab python3 ~/.claude-mcp-servers/gemini-collab/server.py\n\n# 5. Test from any directory\nclaude\n/mcp  # Should show gemini-collab connected\n```\n\nOnce installed, you'll have these tools globally:\n\n- `mcp__gemini-collab__ask_gemini` - Ask Gemini questions\n- `mcp__gemini-collab__gemini_code_review` - Code reviews\n- `mcp__gemini-collab__gemini_brainstorm` - Collaborative brainstorming\n\n```\n# In any directory, start Claude Code:\nclaude\n\n# Use Gemini for code review:\nmcp__gemini-collab__gemini_code_review\n  code: \"function authenticate(user) { return user.password === 'admin'; }\"\n  focus: \"security\"\n\n# Gemini's response appears directly in Claude's context!\n```\n\n1. **Always use `--scope user`** for global access\n2. **Store in `~/.claude-mcp-servers/`** for permanence\n3. **Remove conflicting local configs** like`.vscode/mcp.json`\n4. **Test in multiple directories** to verify global access\n5. **Environment variables** for API keys when possible\n\nMCP servers extend Claude Code's capabilities infinitely. You can integrate any API, tool, or service by following this protocol. The key points for success:\n\n1. **Use proper configuration scope** (`--scope user` for global)\n2. **Handle errors gracefully** with try-except blocks\n3. **Provide clear tool descriptions** so Claude knows how to use them\n4. **Test thoroughly** in multiple directories\n5. **Store servers permanently** in`~/.claude-mcp-servers/`\n\n**Remember**: Configuration scope is the #1 source of MCP issues. When in doubt, use `--scope user`!\n\nHappy building! 🚀", "url": "https://wpnews.pro/news/claude-code-gemini-mcp", "canonical_source": "https://gist.github.com/SomosFeClub/83dc049e087d567422738bea2c3b87fd", "published_at": "2026-11-13 23:08:33+00:00", "updated_at": "2026-11-13 23:20:30.350431+00:00", "lang": "en", "topics": ["ai-agents", "agent-protocols", "developer-tools", "ai-tools"], "entities": ["Claude Code", "Anthropic", "Model Context Protocol", "Python"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/claude-code-gemini-mcp", "markdown": "https://wpnews.pro/news/claude-code-gemini-mcp.md", "text": "https://wpnews.pro/news/claude-code-gemini-mcp.txt", "jsonld": "https://wpnews.pro/news/claude-code-gemini-mcp.jsonld"}}