cd /news/ai-agents/claude-code-gemini-mcp Β· home β€Ί topics β€Ί ai-agents β€Ί article
[ARTICLE Β· art-169307] src=gist.github.com β†— pub= topic=ai-agents verified=true sentiment=↑ positive

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.

by read10 min views3 publishedNov 13, 2026

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:

claude mcp list

claude mcp remove your-server

claude mcp add --scope user your-server python3 /path/to/server.py

rm .vscode/mcp.json  # if exists
mkdir my-mcp-server
cd my-mcp-server

Create a file named server.py:

#!/usr/bin/env python3
"""
Basic MCP Server Template
"""

import json
import sys
import os
from typing import Dict, Any, Optional

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:

initialize - Called when Claude connects to your server

  • Must return protocol version and capabilities

tools/list - Lists all available tools

  • Returns array of tool definitions with schemas

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:

#!/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)

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)}"


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"]
            }
        }
    ]

Always wrap tool execution in try-except blocks and return proper JSON-RPC errors:

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

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:

import logging
logging.basicConfig(level=logging.INFO, stream=sys.stderr)

Create a requirements.txt file:

requests>=2.28.0

Install with: pip install -r requirements.txt

Test individual methods:

echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' | python3 server.py

echo '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' | python3 server.py

echo '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"hello_world","arguments":{"name":"Test"}}}' | python3 server.py

After adding to Claude Code:

claude mcp list

Check configuration scope first :

claude mcp list

ls .vscode/mcp.json

cd ~ && claude mcp list

Check logs :

ls ~/Library/Caches/claude-cli-nodejs/*/mcp-logs-<server-name>/

Run with debug mode :

claude --debug

Common issues :

  • MCP only works in one directory : Wrong scope, use--scope user
  • MCP not found : Check if.vscode/mcp.json exists and conflicts
  • Import errors: Ensure all dependencies are installed
  • Connection closed: Check for syntax errors or crashes
  • Tools not appearing: Verify tools/list returns valid schema

Configuration conflicts :

rm .vscode/mcp.json

claude mcp remove server-name

claude mcp add --scope user server-name python3 /path/to/server.py

Store conversation context:

class MCPServer:
    def __init__(self):
        self.conversation_history = []
    
    def add_to_history(self, role: str, content: str):
        self.conversation_history.append({
            "role": role,
            "content": content
        })

Return different content types:

{
    "type": "text",
    "text": "Your response"
}

{
    "type": "image",
    "data": base64_encoded_image,
    "mimeType": "image/png"
}

For long-running tasks, consider implementing progress updates or background processing.

Store your MCP servers in a permanent location:

~/.claude-mcp-servers/
β”œβ”€β”€ your-server-name/
β”‚   β”œβ”€β”€ server.py
β”‚   β”œβ”€β”€ requirements.txt
β”‚   β”œβ”€β”€ setup.py
β”‚   └── README.md
└── backup.sh

Create setup.py for easy installation:

#!/usr/bin/env python3
"""Setup script for MCP server"""
import subprocess
import sys
import os

def check_python_version():
    if sys.version_info < (3, 8):
        print("❌ Python 3.8+ required")
        sys.exit(1)

def install_dependencies():
    print("πŸ“¦ Installing dependencies...")
    subprocess.check_call([sys.executable, "-m", "pip", "install", "-r", "requirements.txt"])

def add_to_claude():
    server_path = os.path.join(os.path.dirname(__file__), "server.py")
    print(f"πŸ”§ Adding to Claude MCP with global scope...")
    subprocess.run(["claude", "mcp", "add", "--scope", "user", "your-server", "python3", server_path])

if __name__ == "__main__":
    check_python_version()
    install_dependencies()
    add_to_claude()
    print("βœ… Setup complete!")

Add version tracking to your server:

__version__ = "1.0.0"
__updated__ = "2025-06-11"

{
    "name": "server_info",
    "description": "Get server version and status",
    "inputSchema": {"type": "object", "properties": {}}
}

if tool_name == "server_info":
    return f"Server v{__version__} (updated {__updated__})"

Add automatic dependency checking:

def check_dependencies():
    """Check if all required packages are installed"""
    required = ["requests", "other-package"]
    missing = []
    
    for package in required:
        try:
            __import__(package)
        except ImportError:
            missing.append(package)
    
    if missing:
        return f"Missing packages: {', '.join(missing)}"
    return "All dependencies installed!"

Create an update mechanism for your server:

def check_for_updates():
    """Check if updates are available"""
    try:
        import requests
        response = requests.get("https://api.github.com/repos/YOUR_REPO/releases/latest")
        latest_version = response.json()["tag_name"]
        if latest_version > __version__:
            return f"Update available: {latest_version}"
        return "Server is up to date"
    except:
        return "Could not check for updates"

{
    "name": "update_server",
    "description": "Update the MCP server to latest version",
    "inputSchema": {"type": "object", "properties": {}}
}

Support environment variables for configuration:

import os

API_KEY = os.environ.get("YOUR_API_KEY", "default-key-if-any")

DEBUG = os.environ.get("MCP_DEBUG", "false").lower() == "true"
LOG_LEVEL = os.environ.get("MCP_LOG_LEVEL", "ERROR")

Package your server :

my-mcp-server/
β”œβ”€β”€ server.py
β”œβ”€β”€ requirements.txt
β”œβ”€β”€ setup.py
β”œβ”€β”€ README.md
└── LICENSE

Create one-line installer :

curl -sSL https://your-repo/install.sh | bash

Share on GitHub with clear documentation

  1. Database Query Tool : Allow Claude to query your database

  2. API Integration : Connect to any REST API

  3. System Monitoring : Check system stats, logs, etc.

  4. Custom AI Models : Integrate other AI models (like we did with Gemini)

  5. Development Tools : Linters, formatters, test runners

  6. Communication Tools : Send emails, Slack messages, etc.

API Keys : Never hardcode sensitive keys

API_KEY = os.environ.get("MY_API_KEY")

Input Sanitization : Always validate and sanitize inputs 3. Access Control : Limit what your MCP server can access 4. Rate Limiting : Implement rate limits for API calls

Here's a complete working example that enables Claude Code to collaborate with Google's Gemini AI:

mkdir -p ~/.claude-mcp-servers/gemini-collab

pip install google-generativeai

curl -o ~/.claude-mcp-servers/gemini-collab/server.py https://your-repo/server.py

claude mcp add --scope user gemini-collab python3 ~/.claude-mcp-servers/gemini-collab/server.py

claude
/mcp  # Should show gemini-collab connected

Once installed, you'll have these tools globally:

  • mcp__gemini-collab__ask_gemini - Ask Gemini questions
  • mcp__gemini-collab__gemini_code_review - Code reviews
  • mcp__gemini-collab__gemini_brainstorm - Collaborative brainstorming
claude

mcp__gemini-collab__gemini_code_review
  code: "function authenticate(user) { return user.password === 'admin'; }"
  focus: "security"

  1. Always use --scope user for global access
  2. Store in ~/.claude-mcp-servers/ for permanence
  3. Remove conflicting local configs like.vscode/mcp.json
  4. Test in multiple directories to verify global access
  5. Environment variables for API keys when possible

MCP servers extend Claude Code's capabilities infinitely. You can integrate any API, tool, or service by following this protocol. The key points for success:

  1. Use proper configuration scope (--scope user for global)
  2. Handle errors gracefully with try-except blocks
  3. Provide clear tool descriptions so Claude knows how to use them
  4. Test thoroughly in multiple directories
  5. Store servers permanently in~/.claude-mcp-servers/

Remember: Configuration scope is the #1 source of MCP issues. When in doubt, use --scope user!

Happy building! πŸš€

── more in #ai-agents 4 stories Β· sorted by recency
── more on @claude code 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/claude-code-gemini-m…] indexed:0 read:10min 2026-11-13 Β· β€”