Introduction to Custom MCP Servers for Claude
Model Context Protocol (MCP) servers are powerful extensions for Claude AI, enabling the model to interact with external data sources and tools via standardized HTTP endpoints. Unlike static tool definitions, MCP servers allow dynamic context injection and tool execution, making Claude more versatile for real-world applications.
In this tutorial, we'll build a custom MCP server using Node.js and Express that proxies GraphQL queries to a backend API. This setup lets Claude fetch live data from GraphQL endpoints (e.g., GitHub, Shopify, or your own Hasura instance) without hardcoding schemas in prompts. Perfect for developers integrating Claude into dashboards, agents, or enterprise workflows.
Why GraphQL with MCP?
- Flexibility: Query exactly what Claude needs, reducing token usage.
- Dynamic: No need to predefine every possible query in tool schemas.
- Scalable: Handles auth, caching, and rate limits server-side.
- Claude-native: Leverages Claude 3.5 Sonnet's advanced tool calling.
By the end, you'll have a running MCP server and Claude prompt example querying a public GraphQL API for country data.
Prerequisites
Before diving in:
- Node.js 18+ installed.
- Basic knowledge of JavaScript, Express, and GraphQL.
- Anthropic API key (get one at console.anthropic.com).
- Optional: ngrok for exposing your local server to remote Claude instances.
We'll use the free Countries GraphQL API for demos.
Step 1: Project Setup
Create a new directory and initialize your project:
mkdir claude-mcp-graphql
cd claude-mcp-graphql
npm init -y
npm install express graphql-request cors helmet dotenv
npm install -D nodemon
Create server.js and .env files:
# .env
PORT=3000
ANTHROPIC_API_KEY=your_key_here # Optional for server-side Claude calls
Update package.json scripts:
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
}
Step 2: MCP Server Basics
MCP servers follow a simple protocol:
- Base path:
/mcp - Endpoints:
POST /mcp/context: Inject dynamic context (e.g., user data).POST /mcp/tool/:name: Execute named tools with JSON params.GET /mcp/health: Liveness check.
Responses are JSON: { success: true, data: {...}, error: null }.
Start with a basic Express server in server.js:
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import { GraphQLClient } from 'graphql-request';
import dotenv from 'dotenv';
dotenv.config();
const app = express();
const PORT = process.env.PORT || 3000;
app.use(helmet());
app.use(cors());
app.use(express.json({ limit: '10mb' }));
// Health check
app.get('/mcp/health', (req, res) => {
res.json({ success: true, status: 'healthy' });
});
app.listen(PORT, () => {
console.log(`MCP Server running on http://localhost:${PORT}`);
});
Run npm run dev to test: Visit http://localhost:3000/mcp/health.
Step 3: Add GraphQL Tool Endpoint
Implement a graphql-query tool. Claude sends a GraphQL query string and variables; the server executes it safely.
Add to server.js:
const GRAPHQL_ENDPOINT = 'https://countries.trevorblades.com/';
const client = new GraphQLClient(GRAPHQL_ENDPOINT);
app.post('/mcp/tool/graphql-query', async (req, res) => {
try {
const { query, variables = {} } = req.body;
if (!query) {
return res.status(400).json({ success: false, error: 'Query required' });
}
// Sanitize: Limit query complexity (basic check)
if (query.length > 2000) {
return res.status(400).json({ success: false, error: 'Query too complex' });
}
const data = await client.request(query, variables);
res.json({
success: true,
data,
metadata: { source: GRAPHQL_ENDPOINT }
});
} catch (error) {
console.error('GraphQL error:', error);
res.status(500).json({ success: false, error: error.message });
}
});
This endpoint is secure: Server handles auth/errors, Claude just provides the query.
Example curl test:
curl -X POST http://localhost:3000/mcp/tool/graphql-query \
-H 'Content-Type: application/json' \
-d '{"query": "query { country(code: "US") { name capital languages { name } } }"}'
Response:
{
"success": true,
"data": {
"country": {
"name": "United States",
"capital": "Washington, D.C.",
"languages": [{ "name": "English" }]
}
}
}
Step 4: Advanced Features
Authentication
For private GraphQL (e.g., GitHub):
// In .env
GITHUB_TOKEN=your_github_token
// Updated endpoint
const client = new GraphQLClient('https://api.github.com/graphql', {
headers: { Authorization: `bearer ${process.env.GITHUB_TOKEN}` }
});
Caching
Use node-cache for perf:
npm install node-cache
import NodeCache from 'node-cache';
const cache = new NodeCache({ stdTTL: 300 }); // 5min
// In handler
const cacheKey = `graphql:${hash(query + JSON.stringify(variables))}`;
let data = cache.get(cacheKey);
if (!data) {
data = await client.request(query, variables);
cache.set(cacheKey, data);
}
Multiple Tools
Add a repo-search tool:
app.post('/mcp/tool/github-search', async (req, res) => {
// Similar, but fixed query with user vars
});
Step 5: Integrate with Claude API
Use Anthropic's SDK to define tools pointing to your MCP server.
Install SDK:
npm install @anthropic-ai/sdk
Example script claude-example.js:
import Anthropic from '@anthropic-ai/sdk';
const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
const MCP_URL = 'http://localhost:3000/mcp/tool/graphql-query';
const tools = [{
name: 'graphql_query',
description: 'Query a GraphQL API for data. Use for dynamic data retrieval.',
input_schema: {
type: 'object',
properties: {
query: { type: 'string', description: 'GraphQL query string' },
variables: { type: 'object', description: 'Optional variables' }
},
required: ['query']
}
}];
async function chat() {
const msg = await anthropic.messages.create({
model: 'claude-3-5-sonnet-20240620',
max_tokens: 1024,
tools,
messages: [{
role: 'user',
content: 'Find the capital of Japan and its official languages using GraphQL.'
}]
});
let response = msg;
while (response.stop_reason === 'tool_use') {
for (const tool of response.content) {
if (tool.type === 'tool_use') {
const toolCall = tool;
// Call MCP server
const toolResp = await fetch(MCP_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(toolCall.input)
});
const result = await toolResp.json();
response = await anthropic.messages.create({
model: 'claude-3-5-sonnet-20240620',
max_tokens: 1024,
tools,
messages: [
...msg.messages,
{ role: 'assistant', content: [{ type: 'tool_use', ...toolCall }] },
{
role: 'user',
content: [{
type: 'tool_result',
tool_use_id: tool.id,
content: result.success ? [{ type: 'text', text: JSON.stringify(result.data) }] : [{ type: 'text', text: result.error }]
}]
}
]
});
}
}
}
console.log(response.content[0].text);
}
chat();
Run with node claude-example.js. Claude will auto-generate a query like:
query { country(code: "JP") { name capital languages { name } } }
And respond: "Japan's capital is Tokyo. Official language: Japanese."
Step 6: Deployment and Best Practices
Local Testing
Use ngrok: ngrok http 3000 for remote access.
Production
- Deploy to Vercel, Render, or Fly.io.
- Use HTTPS.
- Add API keys to MCP endpoints.
- Monitor with Prometheus.
Security Tips:
- Validate inputs rigorously.
- Rate limit with
express-rate-limit. - Use HTTPS and CORS whitelisting.
Performance:
- Streaming responses for large data.
- Batch queries.
Claude Best Practices:
- Describe tools precisely in
description. - Use Sonnet for complex reasoning.
- Combine with RAG for hybrid setups.
Real-World Use Cases
- E-commerce: Query Shopify GraphQL for inventory.
- DevOps: Fetch GitHub issues/PRs.
- Analytics: Integrate Hasura/Postgres.
- Agents: Chain multiple MCP tools.
Extend further: Add WebSocket for real-time context.
Conclusion
You've now built a production-ready MCP server bridging Claude to GraphQL! This unlocks dynamic, efficient tooling for your apps. Experiment with your APIs, contribute to Claude Directory, and share your builds.
Next Steps:
- Explore Claude Code CLI for local MCP management.
- Build agents with n8n + MCP.
- Compare with OpenAI tools.
Word count: ~1450. Questions? Comment below!
Stay ahead of the AI curve
The most important updates, news, and content — delivered in one weekly newsletter.