Claude MCP Server Setup Guide: Build Custom Tool Integrations for Claude Code and Claude Desktop
What Is MCP and Why It Matters for Claude Users
The Model Context Protocol (MCP) is Anthropic’s open standard for connecting Claude to external tools, data sources, and services. Think of it as a USB port for AI — a universal interface that lets Claude interact with databases, APIs, file systems, and custom tools through a standardized protocol.
Before MCP, integrating Claude with external services meant building custom API wrappers, managing authentication, and writing glue code for every integration. MCP standardizes this: you build an MCP server once, and it works with Claude Code, Claude Desktop, and any other MCP-compatible client.
For developers, MCP unlocks three capabilities:
- Tools: functions Claude can call to perform actions (query a database, create a file, send a message)
- Resources: data Claude can read (documentation, configuration files, database schemas)
- Prompts: reusable prompt templates that Claude can access
This guide focuses on the practical setup — getting MCP servers running with Claude Code and Claude Desktop.
MCP Architecture: How the Pieces Fit Together
Client-Server Model
Claude Code (client) <--> MCP Server <--> External Service
Claude Desktop (client) <--> MCP Server <--> Database
<--> API
<--> File System
The MCP client (Claude Code or Claude Desktop) communicates with MCP servers over stdio (standard input/output) or HTTP with server-sent events. Each server exposes tools, resources, and prompts that Claude can use during a conversation.
Transport Types
stdio (recommended for local development): The MCP server runs as a child process. Claude Code launches it and communicates via stdin/stdout. Simple, fast, no network configuration.
HTTP + SSE (for remote servers): The MCP server runs as a web service. Claude connects via HTTP. Use this for shared team servers or cloud-hosted tools.
Setting Up Your First MCP Server (TypeScript)
Step 1: Initialize the Project
mkdir my-mcp-server cd my-mcp-server npm init -y npm install @modelcontextprotocol/sdk npm install -D typescript @types/node npx tsc --init
Step 2: Create the Server
Create src/index.ts:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({
name: "my-tools",
version: "1.0.0",
});
// Define a tool
server.tool(
"get-weather",
"Get current weather for a city",
{
city: z.string().describe("City name, e.g. 'San Francisco'"),
units: z.enum(["celsius", "fahrenheit"]).default("celsius")
.describe("Temperature units"),
},
async ({ city, units }) => {
// Your implementation here
const temp = units === "celsius" ? "22°C" : "72°F";
return {
content: [
{
type: "text",
text: `Weather in ${city}: ${temp}, partly cloudy`,
},
],
};
}
);
// Start the server
const transport = new StdioServerTransport();
await server.connect(transport);
Step 3: Build and Test
# Add to package.json scripts "build": "tsc", "start": "node dist/index.js" # Build npm run build
Step 4: Configure in Claude Code
Add to your project’s .claude/mcp.json:
{
"mcpServers": {
"my-tools": {
"command": "node",
"args": ["path/to/my-mcp-server/dist/index.js"],
"env": {
"API_KEY": "your-api-key-here"
}
}
}
}
Or add to global settings at ~/.claude/mcp.json for all projects.
Step 5: Verify
Launch Claude Code and check:
claude # Claude should show your MCP server as connected # Ask Claude to use your tool: # "What's the weather in San Francisco?"
Claude will call the get-weather tool and display the result.
Setting Up an MCP Server (Python)
Step 1: Initialize
mkdir my-mcp-server-python cd my-mcp-server-python pip install mcp
Step 2: Create the Server
Create server.py:
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent
import json
server = Server("my-python-tools")
@server.list_tools()
async def list_tools():
return [
Tool(
name="query-database",
description="Execute a read-only SQL query against the project database",
inputSchema={
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "SQL SELECT query to execute"
}
},
"required": ["query"]
}
)
]
@server.call_tool()
async def call_tool(name: str, arguments: dict):
if name == "query-database":
query = arguments["query"]
# Validate it's a SELECT query
if not query.strip().upper().startswith("SELECT"):
return [TextContent(
type="text",
text="Error: Only SELECT queries are allowed"
)]
# Execute query (replace with your database logic)
results = execute_query(query)
return [TextContent(
type="text",
text=json.dumps(results, indent=2)
)]
async def main():
async with stdio_server() as (read_stream, write_stream):
await server.run(read_stream, write_stream)
if __name__ == "__main__":
import asyncio
asyncio.run(main())
Step 3: Configure
In .claude/mcp.json:
{
"mcpServers": {
"db-tools": {
"command": "python",
"args": ["path/to/server.py"],
"env": {
"DATABASE_URL": "postgresql://user:pass@localhost:5432/mydb"
}
}
}
}
Common MCP Server Patterns
Database Query Tool
A read-only database query tool that lets Claude explore your data:
server.tool(
"query-db",
"Execute a read-only SQL query against the project database",
{
query: z.string().describe("SQL SELECT query"),
},
async ({ query }) => {
// Safety: only allow SELECT
if (!query.trim().toUpperCase().startsWith("SELECT")) {
return { content: [{ type: "text", text: "Only SELECT queries allowed" }] };
}
const results = await db.query(query);
return {
content: [{ type: "text", text: JSON.stringify(results.rows, null, 2) }],
};
}
);
API Integration Tool
Connect Claude to an external API:
server.tool(
"search-jira",
"Search Jira issues by JQL query",
{
jql: z.string().describe("JQL query string"),
maxResults: z.number().default(10).describe("Maximum results to return"),
},
async ({ jql, maxResults }) => {
const response = await fetch(
`${JIRA_URL}/rest/api/3/search?jql=${encodeURIComponent(jql)}&maxResults=${maxResults}`,
{ headers: { Authorization: `Basic ${JIRA_TOKEN}` } }
);
const data = await response.json();
const summary = data.issues.map(i => ({
key: i.key,
summary: i.fields.summary,
status: i.fields.status.name,
assignee: i.fields.assignee?.displayName || "Unassigned",
}));
return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] };
}
);
File System Tool
Give Claude controlled access to specific directories:
server.tool(
"read-config",
"Read a configuration file from the config directory",
{
filename: z.string().describe("Config filename (e.g., 'database.yml')"),
},
async ({ filename }) => {
// Safety: prevent path traversal
const safeName = path.basename(filename);
const filepath = path.join(CONFIG_DIR, safeName);
if (!fs.existsSync(filepath)) {
return { content: [{ type: "text", text: `Config file '${safeName}' not found` }] };
}
const content = fs.readFileSync(filepath, "utf-8");
return { content: [{ type: "text", text: content }] };
}
);
Configuring Claude Desktop
Claude Desktop uses a different configuration file:
macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
Windows: %APPDATA%\Claude\claude_desktop_config.json
{
"mcpServers": {
"my-tools": {
"command": "node",
"args": ["/absolute/path/to/dist/index.js"],
"env": {
"API_KEY": "your-key"
}
}
}
}
After saving, restart Claude Desktop. Your tools appear in the tool list when you start a new conversation.
Security Best Practices
Input Validation
Always validate inputs before executing:
// Validate SQL queries
if (query.includes(";") && query.split(";").length > 2) {
throw new Error("Multiple statements not allowed");
}
// Validate file paths
const resolved = path.resolve(basePath, userInput);
if (!resolved.startsWith(basePath)) {
throw new Error("Path traversal detected");
}
// Validate URLs
const url = new URL(userInput);
if (!ALLOWED_HOSTS.includes(url.hostname)) {
throw new Error("Host not in allowlist");
}
Principle of Least Privilege
- Database tools should use read-only database credentials
- File system tools should be scoped to specific directories
- API tools should use tokens with minimal required permissions
- Never expose admin endpoints through MCP
Environment Variable Management
- Store secrets in environment variables, never in code
- Use
.envfiles for local development (add to.gitignore) - Document required environment variables in README
- Use the
envfield in mcp.json to pass variables securely
Debugging MCP Servers
Common Issues
Server does not appear in Claude:
- Check that the path in mcp.json is correct (use absolute paths)
- Verify the server builds and runs without errors:
node dist/index.js - Check Claude Code logs for MCP connection errors
Tool calls fail silently:
- Add console.error logging to your tool handlers (stderr does not interfere with stdio transport)
- Check that input schema matches what Claude sends
- Verify environment variables are set correctly
Server crashes after first call:
- Ensure async handlers do not throw unhandled exceptions
- Wrap tool handlers in try-catch blocks
- Check for missing dependencies
Logging
Use stderr for debug logging (stdout is reserved for MCP protocol):
server.tool("my-tool", "...", schema, async (args) => {
console.error(`[DEBUG] my-tool called with:`, JSON.stringify(args));
try {
const result = await doWork(args);
console.error(`[DEBUG] my-tool result:`, result);
return { content: [{ type: "text", text: result }] };
} catch (error) {
console.error(`[ERROR] my-tool failed:`, error);
return { content: [{ type: "text", text: `Error: ${error.message}` }] };
}
});
Distributing MCP Servers to Your Team
npm Package Distribution
Package your MCP server as an npm package:
{
"name": "@mycompany/mcp-db-tools",
"version": "1.0.0",
"bin": { "mcp-db-tools": "dist/index.js" },
"files": ["dist/"]
}
Team members install and configure:
npm install -g @mycompany/mcp-db-tools
{
"mcpServers": {
"db-tools": {
"command": "mcp-db-tools",
"env": { "DATABASE_URL": "..." }
}
}
}
Docker Distribution
For complex servers with dependencies:
FROM node:20-slim WORKDIR /app COPY package*.json ./ RUN npm ci --production COPY dist/ ./dist/ ENTRYPOINT ["node", "dist/index.js"]
Frequently Asked Questions
Can I use multiple MCP servers simultaneously?
Yes. Add multiple entries to your mcp.json. Each server provides its own set of tools, and Claude can use tools from any connected server in a single conversation.
Do MCP servers work offline?
stdio-based MCP servers run locally and work offline. However, if your server calls external APIs or databases, those connections need network access.
What happens if an MCP server crashes mid-conversation?
Claude Code detects the disconnection and notifies you. The conversation continues but tools from that server become unavailable until it reconnects. Fix the issue and restart the server.
Can MCP servers maintain state between tool calls?
Yes. The server process persists for the duration of the Claude session. You can maintain in-memory state, database connections, and caches across multiple tool calls.
Is there a marketplace for MCP servers?
The MCP ecosystem is growing. Community servers are available on GitHub and npm. Anthropic maintains a list of reference implementations. Check the MCP documentation for the latest directory.
How do I update an MCP server without restarting Claude?
For Claude Code, restart the session or use the MCP management commands. For Claude Desktop, restart the application after updating the server binary.