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:
- Launching and managing MCP Server processes (or connecting to remote Servers)
- Executing the protocol handshake and negotiating capabilities
- Forwarding the AI model's tool-call requests to the Server
- Formatting Server responses and returning them to the AI model
- Managing Server lifecycle (restarts, timeout handling)
The primary MCP Client host applications are:
- Claude Desktop: graphical application, simplest configuration
- Claude Code: CLI tool, supports project-level and global configuration
- Custom applications: integrate directly in code using the MCP SDK
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:
command: the executable to launch the Server (e.g.,python,node,npx)args: array of arguments passed to the commandenv: additional environment variables (merged with the system environment)disabled: settrueto disable without removing the configurationalwaysAllow: tools in this list bypass user confirmation dialogs (use carefully)
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
- Save the configuration file
- Fully quit and restart Claude Desktop
- In a new conversation, click the tools icon (hammer icon) in the bottom right
- If your configured server name appears in the tools list, the connection succeeded
- 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:
- Server not appearing in list: usually an incorrect command path or missing dependencies
- Tool calls failing: check API keys and connection strings in
env - Permission errors: verify the Server has read/write access to configured paths
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:
- Writing or modifying files
- Executing shell commands
- Sending requests to external services
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:
- Claude Desktop configuration is simple but limited; Claude Code supports project-level configuration
- When integrating multiple Servers, namespace tools (
server__tool) to avoid name conflicts - Custom Python/TypeScript integration requires manually implementing the tool-call loop
- Permission controls should be implemented server-side; client-side confirmation is supplementary
- 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.