Why Custom MCP Servers?
Claude AI excels at reasoning, coding, and creative tasks, but its native tool capabilities are limited to predefined functions. For enterprise teams or developers needing domain-specific extensions—like querying internal databases, analyzing proprietary data, or integrating niche APIs—custom tools are essential.
Enter MCP (Model Context Protocol) servers: lightweight HTTP servers that extend Claude's context with remote, stateful tools. Unlike local function calls, MCP servers run independently, scale horizontally, and maintain session state across interactions. They're perfect for Claude Code workflows, API integrations, and agentic systems.
Common pain points MCP solves:
- Scalability: Handle high-volume requests without bloating your Claude prompt.
- Security: Deno's permissions model isolates tools from your main app.
- Reusability: Share tools across multiple Claude projects or teams.
- Claude-specific: Optimized for Anthropic's tool-use XML format, enabling seamless integration.
In this guide, we'll build a production-ready MCP server in Deno/TypeScript for a real-world use case: a GitHub repo analyzer that fetches repo stats, suggests improvements, and integrates directly with Claude Opus/Sonnet.
What is the Model Context Protocol (MCP)?
MCP is an open protocol for Claude-compatible tool servers. It defines two core endpoints:
GET /tools: Lists available tools with schemas (name, description, parameters).POST /call: Executes a tool call, returning structured JSON.
Claude clients (like Claude Code CLI or custom SDK apps) discover and invoke these automatically when you include the MCP URL in your system prompt or config.
Protocol Flow:
- User prompt mentions MCP server URL (e.g.,
Use tools from http://localhost:8000). - Claude generates a
<tool_use>XML block with tool name/args. - Host app (Claude Code/API) calls
/callon MCP server. - Server responds; Claude incorporates result into next response.
This mirrors Anthropic's tool use but offloads execution to remote servers, enabling complex logic without prompt bloat.
Why Deno for MCP Servers?
Deno is a secure TypeScript runtime that's ideal for MCP:
- Native TypeScript: No transpilation—write TS directly.
- Secure by default: Permissions like
--allow-net,--allow-envprevent exploits. - Tiny footprint: Single executable, no Node.js deps or
package.jsonhell. - Built-in std lib: Fetch, testing, CLI flags out-of-the-box.
- Hot reload & deploy: Easy to
deno runlocally or deploy to Deno Deploy (free tier).
Compared to Node/Express:
| Feature | Deno | Node |
|---|---|---|
| TS Support | Native | Babel/ts-node |
| Security | Granular perms | Manual |
| Size | ~50MB | 200MB+ w/ deps |
| Startup | Instant | Slower |
Prerequisites
- Deno 1.40+ (
curl -fsSL https://deno.land/install.sh | sh) - GitHub Personal Access Token (for our example)
- Basic TypeScript knowledge
- Claude API key (optional, for testing)
Step 1: Project Setup
Create a new directory:
deno init mcp-github-analyzer
cd mcp-github-analyzer
No package.json needed. Deno uses import maps or URLs.
Step 2: Define Your MCP Server
Create server.ts:
import { serve } from 'https://deno.land/std@0.224.0/http/server.ts';
interface Tool {
name: string;
description: string;
parameters: Record<string, any>;
}
const TOOLS: Tool[] = [
{
name: 'analyze_repo',
description: 'Analyze a GitHub repo: stars, forks, languages, recent commits, and AI-suggested improvements.',
parameters: {
type: 'object',
properties: {
owner: { type: 'string', description: 'Repo owner' },
repo: { type: 'string', description: 'Repo name' },
},
required: ['owner', 'repo'],
},
},
];
serve(async (req: Request) => {
const url = new URL(req.url);
if (url.pathname === '/tools') {
return new Response(JSON.stringify(TOOLS), {
headers: { 'Content-Type': 'application/json' },
});
}
if (url.pathname === '/call' && req.method === 'POST') {
const { name, arguments: args } = await req.json();
if (name === 'analyze_repo') {
return handleAnalyzeRepo(args);
}
return new Response(JSON.stringify({ error: 'Unknown tool' }), { status: 400 });
}
return new Response('Not Found', { status: 404 });
});
function handleAnalyzeRepo({ owner, repo }: { owner: string; repo: string }) {
// Simulate GitHub API call (use real fetch with token in prod)
// deno-lint-ignore no-explicit-any
const data: any = {
stars: 1500,
forks: 300,
languages: ['TypeScript', 'Python'],
commits: ['feat: add MCP support'],
suggestions: 'Optimize Deno deps; add tests.',
};
return new Response(JSON.stringify(data));
}
console.log('MCP Server running on http://localhost:8000');
Run with permissions:
den o run --allow-net --allow-env server.ts
Test /tools:
curl http://localhost:8000/tools
Step 3: Implement Real GitHub Integration
Replace handleAnalyzeRepo with actual API calls. Add GitHub token via env:
async function handleAnalyzeRepo({ owner, repo }: { owner: string; repo: string }) {
const token = Deno.env.get('GITHUB_TOKEN');
if (!token) throw new Error('Missing GITHUB_TOKEN');
const repoRes = await fetch(
`https://api.github.com/repos/${owner}/${repo}`,
{ headers: { Authorization: `token ${token}` } },
);
const repoData = await repoRes.json();
const langsRes = await fetch(
`https://api.github.com/repos/${owner}/${repo}/languages`,
{ headers: { Authorization: `token ${token}` } },
);
const languages = await langsRes.json();
const commitsRes = await fetch(
`https://api.github.com/repos/${owner}/${repo}/commits?per_page=5`,
{ headers: { Authorization: `token ${token}` } },
);
const commits = await commitsRes.json();
// Claude-powered suggestions (call Anthropic API here if needed)
const suggestions = 'Consider adding Deno support for better security.';
return {
stars: repoData.stargazers_count as number,
forks: repoData.forks_count as number,
languages: Object.keys(languages),
recent_commits: commits.map((c: any) => c.commit.message),
suggestions,
};
}
Export GITHUB_TOKEN=your_token and restart.
Step 4: Integrate with Claude
Using Claude Code CLI:
- Install Claude Code:
pip install claude-code(hypothetical; check docs). - Config: Add MCP URL to
~/.claude-code/config.json:
{
"mcp_servers": ["http://localhost:8000"]
}
- Prompt Claude: "Analyze the repo anthropic/claude-code using available tools."
Claude will auto-discover analyze_repo and call it!
Custom API Integration (Advanced):
In your Node/Python app using Anthropic SDK:
// Pseudo-code
import { Anthropic } from '@anthropic-ai/sdk';
const client = new Anthropic();
async function chatWithMCP(prompt: string, mcpUrl: string) {
let message = await client.messages.create({
model: 'claude-3-5-sonnet-20240620',
max_tokens: 1024,
messages: [{ role: 'user', content: `${prompt}\
Use tools from ${mcpUrl}` }],
tools: await fetchTools(mcpUrl), // Fetch /tools
});
while (message.content.some((c: any) => c.type === 'tool_use')) {
const toolCall = message.content.find((c: any) => c.type === 'tool_use');
const result = await callMCP(mcpUrl, toolCall);
message = await client.messages.create({
// Append tool result
});
}
}
function fetchTools(mcpUrl: string) {
// Implement GET /tools
}
async function callMCP(mcpUrl: string, toolCall: any) {
const res = await fetch(`${mcpUrl}/call`, {
method: 'POST',
body: JSON.stringify(toolCall),
});
return res.json();
}
Step 5: Security Best Practices
- Permissions: Use minimal Deno flags:
--allow-net=localhost:443,api.github.com. - CORS: Add headers:
res.headers.set('Access-Control-Allow-Origin', '*');(restrict in prod). - Auth: JWT or API keys on
/call. - Rate Limiting: Use Deno std/future for middleware.
- Validation: Zod for args:
import { z } from 'https://deno.land/x/zod@3.22.4/mod.ts'; - HTTPS: Deno Deploy auto-TLS.
Example Zod validation:
const AnalyzeSchema = z.object({
owner: z.string().min(1),
repo: z.string().min(1),
});
// In handleAnalyzeRepo
const validated = AnalyzeSchema.parse(args);
Step 6: Deployment
Deno Deploy (Free): Push to GitHub, connect repo at deploy.deno.com.
deploy.ts entrypoint:
// Add to server.ts for Deploy
const handler = async (req: Request) => { /* existing serve logic */ };
Deno.serve(handler);
Scaling: Multiple MCP servers for different domains (e.g., HR DB query, Sales CRM).
Advanced Topics
- Stateful Sessions: Redis for multi-turn context.
- Multiple Tools: Add
query_prs,suggest_contribs. - AI-Augmented Tools: Call Claude API inside MCP for meta-reasoning.
- Agents: Chain MCPs in Claude agents via n8n/Zapier.
- Monitoring: Deno's
console.metrics()+ Prometheus.
Pro Tip: For enterprise, integrate with Slack: Claude bot calls MCP for team repo insights.
Conclusion
Custom MCP servers in Deno/TypeScript transform Claude from a generalist into a domain expert. Our GitHub analyzer example clocks in at <100 lines, deploys instantly, and scales effortlessly.
Next Steps:
- Fork this repo: github.com/your/mcp-github.
- Build your tool: Weather, CRM, Legal DB?
- Share on Claude Directory forums!
Word count: ~1450. Questions? Comment below.
Stay ahead of the AI curve
The most important updates, news, and content — delivered in one weekly newsletter.