Chapter 36

Building Your Own MCP Server: Complete Implementation with Python/TypeScript SDK and Debug Deployment

Chapter 36: MCP Client Integration: Connecting MCP in Claude Code and Desktop Applications

36.1 The MCP Client Role

The previous chapter covered building MCP Servers — the capability-providing side. This chapter turns to the other side: MCP Clients, and how to connect and use MCP Server capabilities in actual applications.

An MCP Client is responsible for:

  1. Launching and managing MCP Server processes (or connecting to remote Servers)
  2. Executing the protocol handshake and negotiating capabilities
  3. Forwarding the AI model's tool-call requests to the Server
  4. Formatting Server responses and returning them to the AI model
  5. Managing Server lifecycle (restarts, timeout handling)

The primary MCP Client host applications are:

36.2 Claude Desktop Integration

36.2.1 Configuration File Location

Claude Desktop uses a single JSON configuration file to manage all MCP Server connections:

OS Config File Path
macOS ~/Library/Application Support/Claude/claude_desktop_config.json
Windows %APPDATA%\Claude\claude_desktop_config.json
Linux ~/.config/Claude/claude_desktop_config.json

36.2.2 Configuration Format

{
  "mcpServers": {
    "server-name": {
      "command": "launch-command",
      "args": ["arg1", "arg2"],
      "env": {
        "ENV_VAR": "value"
      },
      "disabled": false,
      "alwaysAllow": ["tool_name_1"]
    }
  }
}

Fields:

36.2.3 Common Server Configuration Examples

{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": [
        "-y",
        "@modelcontextprotocol/server-filesystem",
        "/Users/alice/Documents",
        "/Users/alice/Projects"
      ]
    },
    "github": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-github"],
      "env": {
        "GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_your_token_here"
      }
    },
    "postgres": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-postgres"],
      "env": {
        "POSTGRES_CONNECTION_STRING": "postgresql://user:pass@localhost:5432/mydb"
      }
    },
    "custom-python-server": {
      "command": "python",
      "args": ["/Users/alice/mcp-servers/my_server.py"],
      "env": {
        "WORKSPACE": "/Users/alice/Projects"
      }
    }
  }
}

36.2.4 Verifying the Connection

  1. Save the configuration file
  2. Fully quit and restart Claude Desktop
  3. In a new conversation, click the tools icon (hammer icon) in the bottom right
  4. If your configured server name appears in the tools list, the connection succeeded
  5. If connection fails, check the logs at: ~/Library/Logs/Claude/mcp-server-{name}.log

36.2.5 Troubleshooting

# Manually test if the server can start
python /path/to/server.py

# Test if npx can find the package
npx -y @modelcontextprotocol/server-filesystem /tmp

# Verify environment variables
echo $GITHUB_PERSONAL_ACCESS_TOKEN

Common issues:

36.3 Claude Code MCP Integration

Claude Code is Anthropic's command-line AI coding tool with more flexible MCP support, including project-level configuration and dynamic Server management.

36.3.1 Managing Servers via CLI

# List all configured MCP servers
claude mcp list

# Add a stdio server (global scope)
claude mcp add my-server python /path/to/server.py --scope global

# Add a server with arguments
claude mcp add filesystem npx -- -y @modelcontextprotocol/server-filesystem /home/user

# Add a server with environment variables
claude mcp add github npx -- -y @modelcontextprotocol/server-github \
  --env GITHUB_PERSONAL_ACCESS_TOKEN=ghp_xxxxx

# Add a remote HTTP server
claude mcp add remote-server --transport http https://my-mcp-server.example.com

# Remove a server
claude mcp remove my-server

# View details for a specific server
claude mcp get my-server

36.3.2 Project-Level MCP Configuration

Create .claude/mcp.json in your project root — all Claude Code sessions within the project automatically load these servers:

{
  "mcpServers": {
    "project-tools": {
      "command": "python",
      "args": ["./scripts/mcp_server.py"],
      "env": {
        "PROJECT_ROOT": "."
      }
    },
    "database": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-postgres"],
      "env": {
        "POSTGRES_CONNECTION_STRING": "${DATABASE_URL}"
      }
    }
  }
}

The ${DATABASE_URL} syntax reads from system environment variables, avoiding sensitive data in version control.

36.3.3 CLAUDE.md MCP Documentation

Document MCP Server usage in your project's CLAUDE.md so Claude Code knows when and how to use each tool:

# Project MCP Configuration

## Available MCP Tools

### project-tools Server
- `read_schema`: Read database schema — call before any database-related tasks
- `run_migration`: Execute database migrations — always backup first
- `deploy_preview`: Deploy to preview environment

### Usage Guidelines
1. Before modifying the database, call `read_schema` to confirm the table structure
2. After generating code, call `run_tests` to confirm tests pass
3. Only call `deploy_preview` when the user explicitly requests it

36.4 Custom Python Application Integration

36.4.1 Using the MCP Python SDK as a Client

import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
import anthropic

async def ai_agent_with_mcp(user_request: str):
    """AI Agent with MCP tool capabilities."""
    
    server_params = StdioServerParameters(
        command="python",
        args=["server.py"],
        env={"WORKSPACE": "/home/user/project"}
    )
    
    async with stdio_client(server_params) as (read_stream, write_stream):
        async with ClientSession(read_stream, write_stream) as session:
            await session.initialize()
            
            # Get available tools and convert to Anthropic format
            tools_response = await session.list_tools()
            anthropic_tools = [
                {
                    "name": tool.name,
                    "description": tool.description,
                    "input_schema": tool.inputSchema
                }
                for tool in tools_response.tools
            ]
            
            claude_client = anthropic.Anthropic()
            messages = [{"role": "user", "content": user_request}]
            
            while True:
                response = claude_client.messages.create(
                    model="claude-opus-4-5",
                    max_tokens=4096,
                    tools=anthropic_tools,
                    messages=messages
                )
                
                if response.stop_reason == "end_turn":
                    return next(
                        (b.text for b in response.content if b.type == "text"), ""
                    )
                
                elif response.stop_reason == "tool_use":
                    messages.append({"role": "assistant", "content": response.content})
                    
                    tool_results = []
                    for block in response.content:
                        if block.type == "tool_use":
                            mcp_result = await session.call_tool(block.name, block.input)
                            
                            result_text = "\n".join([
                                c.text for c in mcp_result.content
                                if hasattr(c, "text")
                            ])
                            
                            tool_results.append({
                                "type": "tool_result",
                                "tool_use_id": block.id,
                                "content": result_text,
                                "is_error": mcp_result.isError
                            })
                    
                    messages.append({"role": "user", "content": tool_results})

36.4.2 Connecting Multiple MCP Servers

from contextlib import AsyncExitStack
from typing import Dict, List

class MultiMCPClient:
    """Client managing multiple MCP Server connections."""
    
    def __init__(self):
        self.sessions: Dict[str, ClientSession] = {}
        self.tools: List[dict] = []
        self._exit_stack = AsyncExitStack()
    
    async def connect(self, name: str, params: StdioServerParameters):
        read, write = await self._exit_stack.enter_async_context(stdio_client(params))
        session = await self._exit_stack.enter_async_context(ClientSession(read, write))
        await session.initialize()
        self.sessions[name] = session
        
        tools_response = await session.list_tools()
        for tool in tools_response.tools:
            self.tools.append({
                "name": f"{name}__{tool.name}",  # Namespace to avoid conflicts
                "description": f"[{name}] {tool.description}",
                "input_schema": tool.inputSchema,
                "_server": name,
                "_original_name": tool.name
            })
    
    async def call_tool(self, namespaced_name: str, arguments: dict) -> str:
        tool_info = next((t for t in self.tools if t["name"] == namespaced_name), None)
        if not tool_info:
            raise ValueError(f"Unknown tool: {namespaced_name}")
        
        session = self.sessions[tool_info["_server"]]
        result = await session.call_tool(tool_info["_original_name"], arguments)
        return "\n".join([c.text for c in result.content if hasattr(c, "text")])
    
    async def close(self):
        await self._exit_stack.aclose()

36.5 TypeScript Application Integration

36.5.1 TypeScript MCP Client

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import Anthropic from "@anthropic-ai/sdk";

async function createMCPAgent(serverCommand: string, serverArgs: string[]) {
  const transport = new StdioClientTransport({ command: serverCommand, args: serverArgs });
  const mcpClient = new Client({ name: "my-agent", version: "1.0.0" }, { capabilities: {} });
  await mcpClient.connect(transport);

  const toolsResponse = await mcpClient.listTools();
  const anthropicTools = toolsResponse.tools.map((tool) => ({
    name: tool.name,
    description: tool.description ?? "",
    input_schema: tool.inputSchema,
  }));

  const claude = new Anthropic();
  const messages: Anthropic.MessageParam[] = [
    { role: "user", content: "List all Python files in the workspace" },
  ];

  let keepGoing = true;
  while (keepGoing) {
    const response = await claude.messages.create({
      model: "claude-opus-4-5",
      max_tokens: 4096,
      tools: anthropicTools as Anthropic.Tool[],
      messages,
    });

    if (response.stop_reason === "end_turn") {
      const text = response.content
        .filter((b): b is Anthropic.TextBlock => b.type === "text")
        .map((b) => b.text)
        .join("\n");
      console.log("Final answer:", text);
      keepGoing = false;
    } else if (response.stop_reason === "tool_use") {
      messages.push({ role: "assistant", content: response.content });

      const toolResults: Anthropic.ToolResultBlockParam[] = [];
      for (const block of response.content) {
        if (block.type !== "tool_use") continue;
        const mcpResult = await mcpClient.callTool({
          name: block.name,
          arguments: block.input as Record<string, unknown>,
        });
        const resultText = mcpResult.content
          .filter((c): c is { type: "text"; text: string } => c.type === "text")
          .map((c) => c.text)
          .join("\n");
        toolResults.push({
          type: "tool_result",
          tool_use_id: block.id,
          content: resultText,
          is_error: mcpResult.isError ?? false,
        });
      }
      messages.push({ role: "user", content: toolResults });
    } else {
      keepGoing = false;
    }
  }

  await mcpClient.close();
}

36.5.2 Connecting to Remote HTTP+SSE Servers

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";

async function connectRemote(serverUrl: string): Promise<Client> {
  const transport = new SSEClientTransport(new URL(serverUrl));
  const client = new Client(
    { name: "remote-client", version: "1.0.0" },
    { capabilities: {} }
  );
  await client.connect(transport);
  return client;
}

36.6 Access Control and Permissions

36.6.1 Claude Desktop's Confirmation Mechanism

Claude Desktop requests user confirmation by default for:

You can bypass confirmation for specific safe tools:

{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp/safe-dir"],
      "alwaysAllow": ["read_file", "list_directory"]
    }
  }
}

36.6.2 Server-Side Permission Enforcement

Best practice: implement permission controls on the Server side rather than relying on Client-side confirmations:

ALLOWED_PATHS = [Path("/home/user/projects"), Path("/tmp/workspace")]

def is_path_allowed(path: Path) -> bool:
    resolved = path.resolve()
    return any(
        resolved == allowed or allowed in resolved.parents
        for allowed in ALLOWED_PATHS
    )

@app.call_tool()
async def call_tool(name: str, arguments: dict):
    if name == "write_file":
        target = Path(arguments["path"])
        if not is_path_allowed(target):
            return [types.TextContent(
                type="text",
                text=f"Permission denied: path {arguments['path']} is outside allowed directories"
            )]
        # ... normal processing

36.7 Monitoring and Observability

Log MCP interactions for debugging and audit purposes:

import logging, json

mcp_logger = logging.getLogger("mcp.interactions")

# Log before tool call (strip sensitive keys)
mcp_logger.info(json.dumps({
    "event": "tool_call",
    "tool": name,
    "arguments": {k: v for k, v in arguments.items()
                  if k not in ["password", "api_key", "token"]}
}))

Summary

This chapter covered MCP Client integration at three levels: Claude Desktop's JSON configuration, Claude Code's CLI management, and programmatic integration in custom applications using the SDK.

Key takeaways:

  1. Claude Desktop configuration is simple but limited; Claude Code supports project-level configuration
  2. When integrating multiple Servers, namespace tools (server__tool) to avoid name conflicts
  3. Custom Python/TypeScript integration requires manually implementing the tool-call loop
  4. Permission controls should be implemented server-side; client-side confirmation is supplementary
  5. Sensitive configuration (API keys, etc.) should be passed via environment variables, never hardcoded

The next chapter surveys the official MCP Server ecosystem, covering ready-to-use Servers for file systems, databases, and browser control.

Rate this chapter
4.6  / 5  (3 ratings)

💬 Comments