Build Your Own MCP Server from Scratch A developer has published a guide for building a Model Context Protocol (MCP) server from scratch using the bare JSON-RPC 2.0 protocol, bypassing common frameworks to give engineers full control over the wire format. The protocol relies on just three core request types—`initialize`, `tools/list`, and `tools/call`—transported over stdio or HTTP, with each tool defined by a JSON Schema that agents use to construct valid calls. By understanding the protocol at this level, developers gain advantages in debugging, portability across any language or runtime, and the ability to evolve with future MCP capabilities without requiring full rewrites. Every AI agent ships with the same bottleneck: it can only reason over what it can reach. MCP servers dissolve that boundary. They expose tools, resources, and prompts to any compliant client over a JSON-RPC wire format so lean you can implement it in an afternoon. Yet most developers grab a framework, copy a template, and ship something they can barely debug. Forge starts differently. You will build an MCP server from the bare protocol up, understand every byte on the wire, and gain the mental model that makes every future server trivial. MCP is a JSON-RPC 2.0 protocol. A client sends a request. Your server returns a response. Three request types power the core loop: initialize , handshake. Client and server exchange capabilities. tools/list , discovery. Server returns every tool it offers, each with a JSON Schema describing its inputs. tools/call , execution. Client names a tool and passes arguments. Server runs the handler and returns structured content. Transport is either stdio JSON-RPC over stdin/stdout or HTTP Streamable HTTP . Stdio is the simplest place to start: read a line from stdin, parse it, dispatch, write a line to stdout. That is the entire architecture. Everything else is error handling, schema validation, and ergonomics. MCP servers are the new APIs. Where REST gave machines endpoints, MCP gives agents tools with typed inputs and structured outputs. Every integration layer from IDE assistants to autonomous workflows converges on this protocol. The standard is young. The primitives are stable. The surface area is small enough to hold in your head all at once. Knowing the wire format gives you three advantages frameworks obscure: Debugging , when a tool call fails, you can read the raw JSON-RPC message and pinpoint the fault in seconds. Portability , any language, any runtime, any transport. Write a server in Bash if you want. The protocol is the contract. Evolution , MCP will add capabilities. Understanding the base protocol lets you adopt new features by extension, always, sidestepping full rewrites. Forge articles build on this foundation. If you understand the three core requests and the JSON-RPC envelope, every subsequent pattern is just a new handler. ArchonHQ is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber. Every message shares the same shape: { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "get weather", "arguments": { "city": "Portland" } } } The response mirrors it: { "jsonrpc": "2.0", "id": 1, "result": { "content": { "type": "text", "text": "72°F, clear skies" } } } Errors swap result for error : { "jsonrpc": "2.0", "id": 1, "error": { "code": -32602, "message": "Invalid params: missing 'city'" } } Three fields matter: jsonrpc always "2.0" , id correlates request to response , and method the dispatch key . Each tool advertises itself through a JSON Schema object. A well-designed schema is the difference between a tool agents use and one they fumble. { "name": "get weather", "description": "Retrieve current weather for a given city", "inputSchema": { "type": "object", "properties": { "city": { "type": "string", "description": "City name, e.g. Portland" } }, "required": "city" } } Rules for effective schemas: Mark every required parameter in required . Agents rely on this to construct valid calls. Add description to each property. The agent reads descriptions to decide which tool to invoke and what values to pass. Use enum for constrained values. This prevents hallucinated inputs. Keep schemas flat. Nested objects are valid but harder for agents to populate correctly. Your server runs a loop: Step Action 1 Read a JSON-RPC line from stdin 2 Parse the method 3 Dispatch to the matching handler 4 Handler returns a result or raises an error 5 Serialize the response to JSON 6 Write it to stdout In Python with asyncio: python async def handle message message : method = message.get "method" if method == "initialize": return {"capabilities": {"tools": {}}} elif method == "tools/list": return {"tools": list tools } elif method == "tools/call": return await call tool message "params" else: return {"error": {"code": -32601, "message": f"Method {method} unseen"}} Start with the mcpbuild CLI. Run mcpbuild init my-server and you get a project scaffold: my-server/ pyproject.toml server.py tools/ init .py server.py contains the JSON-RPC read loop and dispatch table. Each tool is a function registered by name. The add-tool command generates a stub handler and appends the tool schema to the registry. The run command boots the server on stdio default or HTTP transport. The full CLI ships with this article. Download it, make it executable, and build your first MCP server in minutes. Feed this prompt a server concept. Get back a complete specification ready for implementation.