How to Build MCP Servers That Extend Claude Code Capabilities
Problem
Claude Code was getting in my way. Every time I asked it to analyze code, it would spawn multiple tool calls, burn through tokens, and take forever to do simple tasks. The inefficiency was eating my budget.
I tried configuring it through CLAUDE.md rules, but the improvements were marginal. The core problem was that Claude Code’s built-in tools weren’t optimized for my workflow.
What I needed was a custom tool — something that could do specific analysis tasks without burning through API calls. But how do you extend Claude Code’s capabilities?
What I discovered
Turns out, Claude Code supports MCP (Model Context Protocol) servers natively. The key insight from a Reddit discussion:
“The whole thing runs as an MCP server, so Claude Code picks it up natively.”
This means I could build a standalone binary that Claude Code would automatically discover and use as a tool. No plugins, no extensions, no configuration gymnastics.
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐│ Claude Code │─────▶│ MCP Protocol │─────▶│ Custom Server ││ (Client) │ │ (stdio) │ │ (My Tools) │└─────────────────┘ └─────────────────┘ └─────────────────┘ │ │ │ │ │ │ ▼ ▼ ▼ "Run analyze" ────▶ JSON-RPC Message ────▶ Execute Tool Return ResultThe beauty of MCP is that it’s just a protocol. I could write my server in any language — C, Python, TypeScript, Rust, Go — as long as it speaks MCP over stdio.
How I built it
I decided to build my MCP server in C. Why C? A few reasons:
- Zero runtime dependencies — single binary, no node_modules, no pip installs
- Deterministic behavior — no garbage collection pauses, predictable memory usage
- Small binary size — the whole thing compiled to under 2MB
- Maximum performance — for CPU-intensive code analysis
Here’s the basic structure I started with:
#include <stdio.h>#include <stdlib.h>#include <string.h>#include <json-c/json.h>
// MCP uses JSON-RPC 2.0 over stdio// Every message is a JSON object on a single line
void send_response(json_object *response) { const char *json_str = json_object_to_json_string(response); fprintf(stdout, "Content-Length: %zu\r\n\r\n%s", strlen(json_str), json_str); fflush(stdout);}
void handle_initialize(json_object *params) { json_object *response = json_object_new_object(); json_object_object_add(response, "jsonrpc", json_object_new_string("2.0"));
json_object *result = json_object_new_object(); json_object *capabilities = json_object_new_object(); json_object_object_add(capabilities, "tools", json_object_new_object()); json_object_object_add(result, "capabilities", capabilities); json_object_object_add(result, "serverInfo", json_object_new_object()); json_object_object_add(response, "result", result);
send_response(response); json_object_put(response);}
int main(int argc, char *argv[]) { char buffer[65536];
while (fgets(buffer, sizeof(buffer), stdin)) { json_object *request = json_tokener_parse(buffer); json_object *method_obj;
if (json_object_object_get_ex(request, "method", &method_obj)) { const char *method = json_object_get_string(method_obj);
if (strcmp(method, "initialize") == 0) { json_object *params; json_object_object_get_ex(request, "params", ¶ms); handle_initialize(params); } // ... handle other methods } json_object_put(request); }
return 0;}I compiled it:
gcc -o mcp-server mcp-server.c -ljson-c -staticBut then I hit my first issue.
Issue 1: Protocol handshake
My server wasn’t responding. Claude Code would try to connect, but nothing happened.
I added some debug logging:
// Log to stderr (stdout is used for MCP communication)#define DEBUG_LOG(msg) fprintf(stderr, "[DEBUG] %s\n", msg)
int main(int argc, char *argv[]) { DEBUG_LOG("Server starting...");
while (fgets(buffer, sizeof(buffer), stdin)) { DEBUG_LOG("Received message"); // ... }}The logs showed the server was receiving messages but not responding correctly. The problem was the Content-Length header format. MCP expects HTTP-like headers:
Content-Length: 123
{"jsonrpc":"2.0","method":"initialize","params":{...}}I fixed my send function:
void send_response(json_object *response) { const char *json_str = json_object_to_json_string(response); size_t len = strlen(json_str);
// MCP requires Content-Length header followed by blank line fprintf(stdout, "Content-Length: %zu\r\n", len); fprintf(stdout, "\r\n"); fwrite(json_str, 1, len, stdout); fflush(stdout);}Issue 2: Tool registration
After fixing the protocol, my server connected but Claude Code couldn’t see my tools.
I was missing the tools/list handler. MCP requires explicit tool registration:
void handle_list_tools() { json_object *response = json_object_new_object(); json_object_object_add(response, "jsonrpc", json_object_new_string("2.0")); json_object_object_add(response, "id", json_object_new_int(2));
json_object *result = json_object_new_object(); json_object *tools = json_object_new_array();
// Tool 1: Code analyzer json_object *tool1 = json_object_new_object(); json_object_object_add(tool1, "name", json_object_new_string("analyze_code")); json_object_object_add(tool1, "description", json_object_new_string("Analyze code structure and identify patterns"));
json_object *input_schema = json_object_new_object(); json_object_object_add(input_schema, "type", json_object_new_string("object")); json_object *properties = json_object_new_object(); json_object *code_prop = json_object_new_object(); json_object_object_add(code_prop, "type", json_object_new_string("string")); json_object_object_add(properties, "code", code_prop); json_object_object_add(input_schema, "properties", properties); json_object_object_add(tool1, "inputSchema", input_schema);
json_object_array_add(tools, tool1); json_object_object_add(result, "tools", tools); json_object_object_add(response, "result", result);
send_response(response);}Now Claude Code could see my tools.
Issue 3: Configuration
I needed to tell Claude Code about my server. There are two ways:
Option A: Manual configuration
{ "mcpServers": { "my-code-analyzer": { "command": "/path/to/mcp-server", "args": [] } }}Option B: Agentic integration (what I chose)
According to the Reddit discussion:
“Tell Claude Code to read the agentic integration docs and it will install and configure everything autonomously.”
I placed my binary in a discoverable location and let Claude Code find it automatically.
The real build: 22K lines of C
After the basic prototype worked, I built the real thing. Over several weeks, I pair-programmed with Claude Opus to write approximately 22,000 lines of C code.
The architecture looks like this:
┌────────────────────────────────────────────────────────────────┐│ MCP Server (C Binary) │├────────────────────────────────────────────────────────────────┤│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ││ │ Code Parser │ │ Pattern │ │ Dependency │ ││ │ │ │ Matcher │ │ Analyzer │ ││ └──────────────┘ └──────────────┘ └──────────────┘ ││ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ││ │ Dead Code │ │ Complexity │ │ Import │ ││ │ Detector │ │ Calculator │ │ Optimizer │ ││ └──────────────┘ └──────────────┘ └──────────────┘ │├────────────────────────────────────────────────────────────────┤│ MCP Protocol Layer ││ (JSON-RPC 2.0 over stdio) │└────────────────────────────────────────────────────────────────┘Key design decisions:
Deterministic behavior The code is pure C, deterministic, no LLM at runtime. This means:
Input: "analyze this function"Output: SAME result every time (given same input)
vs. LLM-based tools:Input: "analyze this function"Output: Varies between runs, depends on temperature, model version, etc.Stateless operation Each tool call is independent. No state persists between calls. This makes debugging easier and prevents memory leaks.
Fast startup Claude Code calls tools frequently. A slow startup kills the user experience. My binary starts in under 10ms.
Python alternative (for faster development)
If you don’t need C-level performance, Python is much faster to develop:
#!/usr/bin/env python3from mcp.server import Serverfrom mcp.server.stdio import stdio_serverfrom mcp.types import Tool, TextContent
server = Server("my-analyzer")
@server.list_tools()async def list_tools() -> list[Tool]: return [ Tool( name="analyze_code", description="Analyze code structure and patterns", inputSchema={ "type": "object", "properties": { "code": {"type": "string"} }, "required": ["code"] } ) ]
@server.call_tool()async def call_tool(name: str, arguments: dict) -> list[TextContent]: if name == "analyze_code": code = arguments["code"] # Your analysis logic here result = perform_analysis(code) return [TextContent(type="text", text=result)] raise ValueError(f"Unknown tool: {name}")
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())The Python version is ~50 lines vs. ~500 lines in C. But the C version:
- Starts 10x faster
- Uses 100x less memory
- Has zero runtime dependencies
- Produces a single 2MB binary
Choose based on your needs.
Best practices I learned
DO: Use single-binary deployment
cp mcp-server /usr/local/bin/chmod +x /usr/local/bin/mcp-server# Requires Python virtualenvsource venv/bin/activatepython mcp_server.py
# Requires node_modulesnode dist/server.jsDO: Validate all inputs
json_object *code_obj;if (!json_object_object_get_ex(params, "code", &code_obj)) { // Return error - missing required parameter return error_response("Missing required parameter: code");}
if (!json_object_is_type(code_obj, json_type_string)) { return error_response("Parameter 'code' must be a string");}
const char *code = json_object_get_string(code_obj);size_t code_len = strlen(code);if (code_len > MAX_CODE_SIZE) { return error_response("Code exceeds maximum size limit");}DO: Handle errors gracefully
json_object *error_response(const char *message) { json_object *response = json_object_new_object(); json_object_object_add(response, "jsonrpc", json_object_new_string("2.0"));
json_object *error = json_object_new_object(); json_object_object_add(error, "code", json_object_new_int(-32603)); json_object_object_add(error, "message", json_object_new_string(message)); json_object_object_add(response, "error", error);
return response;}DON’T: Skip tool descriptions
json_object_object_add(tool, "description", json_object_new_string("Analyze code"));json_object_object_add(tool, "description", json_object_new_string( "Analyze code structure, detect dead code, calculate cyclomatic complexity, " "and identify refactoring opportunities. Returns structured analysis with " "line numbers and severity levels." ));DON’T: Forget resource cleanup
json_object *request = json_tokener_parse(buffer);// ... process request ...// Forgot to free! Memory leak!json_object *request = json_tokener_parse(buffer);// ... process request ...json_object_put(request); // Always freeDebugging tips
Enable MCP logging
# Claude Code logs MCP communicationexport MCP_DEBUG=1claudeTest your server standalone
# Send a test initialize messageecho '{"jsonrpc":"2.0","method":"initialize","params":{},"id":1}' | ./mcp-server
# Expected output:# Content-Length: XXX## {"jsonrpc":"2.0","result":{...},"id":1}Use MCP Inspector
npx @modelcontextprotocol/inspector ./mcp-serverThis opens a web UI where you can test each tool interactively.
Summary
I built an MCP server in C to extend Claude Code’s capabilities. The key lessons:
- MCP is just a protocol — JSON-RPC 2.0 over stdio, nothing magical
- Claude Code discovers servers automatically — place your binary, it works
- Deterministic is better — no LLM at runtime means predictable behavior
- Single-binary deployment wins — zero dependencies, fast startup
- Input validation is critical — your server will receive malformed data
The Reddit discussion was right: MCP servers are the cleanest way to extend Claude Code. Build a binary, implement the protocol, and Claude Code picks it up natively.
For my use case (code analysis), the C implementation was worth the extra development time. For simpler tools, Python or TypeScript would be faster to ship. The protocol is the same either way.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments