How to build an MCP server

Build agents
HoneyHive Engineering

Build an MCP server with an official SDK, tool definitions, a stdio or Streamable HTTP transport and a test in the Inspector, following the spec security rules.

To build an MCP server, pick an official SDK, register tools with a name, a description and a JSON Schema for the inputs, serve them over stdio or Streamable HTTP, and test with the Inspector before you connect a client. The MCP docs walk through a weather server in several languages. This page covers the choices around that tutorial and the rules the spec sets for a server. For what the protocol is, see what is the Model Context Protocol.

Decide what the server exposes

An MCP server can offer three kinds of capability, according to the docs: resources (file-like data a client can read), tools (functions the model can call, with user approval) and prompts (pre-written templates). The tutorial focuses on tools. If you are unsure whether you need a server at all, MCP vs API compares it with a direct API call.

Pick an SDK

The MCP docs list official SDKs in tiers based on “feature completeness, protocol support, and maintenance commitment.”

Tier SDKs
Tier 1 TypeScript, Python, C#, Go, Rust, Ruby
Tier 2 Java
Tier 3 Swift, PHP, Kotlin

Every SDK can create servers that expose tools, resources and prompts over local and remote transports. The docs also warn that SDKs adopt new protocol revisions on their own schedules, so check which revision your SDK supports. The quickstarts need Python 3.10 or higher for Python and Node.js 20 or higher for TypeScript.

Choose a transport

The spec defines two standard transports:

stdio Streamable HTTP
How it runs The client launches your server as a subprocess and exchanges newline-delimited messages over its standard streams Your server runs as its own process behind a single HTTP endpoint that accepts POST
Replies Over stdout A JSON object, or an SSE stream scoped to that request
Fits A local server a desktop client starts A remote server that serves many clients

The older HTTP+SSE transport from 2024-11-05 is deprecated, and new implementations should not adopt it. The 2026-07-28 revision also removed protocol-level sessions and the GET stream endpoint from Streamable HTTP, so a server that needs state across calls should return an explicit handle from a creation tool and take it as an argument later, as the tools spec suggests.

Define tools

A tool definition has a unique name, a description and an inputSchema that must be a JSON Schema object. It can also have a title, an outputSchema and annotations. The spec says names should be 1 to 128 characters of letters, digits, underscore, hyphen or dot, with no spaces. A server that supports tools must declare the tools capability, and the spec recommends returning tools in a deterministic order so clients can cache the list.

The Python SDK quickstart declares a tool by decorating a function with @mcp.tool(), and the docstring becomes the description (MCP docs):

@mcp.tool()
async def get_alerts(state: str) -> str:
    """Get weather alerts for a US state.

In TypeScript the quickstart calls server.registerTool with a name, a description, a Zod input schema and a handler.

Keep stdout clean

On a stdio server, stdout carries the protocol. The docs state: “Never write to stdout. Writing to stdout will corrupt the JSON-RPC messages and break your server.” In Python that rules out print(); use the logging module, which writes to stderr. In TypeScript, use console.error() instead of console.log(). The docs give the same warning for Java, Kotlin, C# and Ruby.

Return errors the model can use

The tools spec separates two kinds of error. Protocol errors (an unknown tool, a malformed request) come back as JSON-RPC errors. Tool execution errors (an API failure, a bad date, a business rule) go in the result with isError: true, so the model can read the message and retry with different arguments. Put the actionable detail in the second kind.

Follow the security rules

The tools spec says servers must validate all tool inputs, implement access controls, rate limit tool invocations and sanitize tool outputs. It also says there should always be a human in the loop with the ability to deny tool invocations.

For Streamable HTTP, the transport spec adds three rules:

  • Validate the Origin header on every connection and answer 403 if it is invalid, to prevent DNS rebinding.
  • When running locally, bind to 127.0.0.1 rather than 0.0.0.0.
  • Authenticate all connections.

The attacks behind these rules, such as tool poisoning and token passthrough, are in MCP security.

Test before you connect a client

The MCP Inspector is the reference tool for testing servers. It needs Node 22.19.0 or newer and runs through npx:

npx @modelcontextprotocol/inspector node path/to/server/index.js

That opens a web UI against a local stdio server. A --cli mode lists and calls tools from a script, and --server-url with --transport http points it at a remote server. One catch from the build guide: the Inspector defaults to the legacy protocol era, so to check that a server answers 2026-07-28 clients, run it with --protocol-era modern and then again with --protocol-era legacy.

In the TypeScript SDK, start the server with serveStdio rather than connecting a StdioServerTransport by hand. The docs say the hand-wired version only serves 2025-era clients.

Trace the server once it runs

A server that works in the Inspector can still fail in production on slow upstream calls or bad arguments from the model. Tracing MCP servers covers what to record on each call. HoneyHive’s docs show how to trace MCP servers and clients with OpenInference auto-tracing or the @trace decorator.

FAQ

How do I build an MCP server?

Pick an official SDK, define tools with names, descriptions and input schemas, serve them over stdio or Streamable HTTP, and test with the Inspector before connecting a client.

Which language should I use for an MCP server?

The MCP docs list Tier 1 SDKs for TypeScript, Python, C#, Go, Rust and Ruby. Java is Tier 2, and Swift, PHP and Kotlin are Tier 3.

Should I use stdio or HTTP for my MCP server?

Use stdio when a client launches your server as a local subprocess. Use Streamable HTTP for a server that runs elsewhere and serves many clients.

Why does my stdio MCP server break when I add a print statement?

Output on stdout is the JSON-RPC channel. The MCP docs say writing to stdout corrupts the messages and breaks the server, so log to stderr instead.

How do I test an MCP server?

Run the MCP Inspector against it, for example npx @modelcontextprotocol/inspector node path/to/server/index.js, and list and call its tools.

Share