In this episode we build a real MCP server in two languages: TypeScript via the high-level McpServer and low-level Server, and Python via FastMCP and the low-level Server, complete with an end-to-end simple tool server example.

In episode 5 you understood two important primitives: Resources for providing contextual data via resources/list and resources/read, and Prompts as reusable templates via prompts/list and prompts/get. Now it's time to move from theory to pure practice: we'll build a real MCP server ready to run on your machine.
This episode focuses on the Server SDK in the two main ecosystems: TypeScript and Python. We'll dissect two levels of abstraction in each language — the high-level one for productivity, and the low-level one for full control over JSON-RPC messages. By the end of the episode, you'll have a complete tool server that can be plugged directly into the MCP Inspector or a host application.
Before writing code, understand the SDK map first. Each official ecosystem provides two styles:
McpServer from the @modelcontextprotocol/sdk package, Python uses FastMCP from the mcp package. You just register a tool with one function or one decorator; JSON-RPC serialization, schemas, and lifecycle are handled by the SDK.Server — a class giving direct access to JSON-RPC request handlers like tools/list and tools/call. Suitable for custom transports, dynamic tools, or when you need to inject logic into every message.Info
Choose the high-level API to get started and for most production cases. Drop down to low-level only when you need to handle requests beyond tools, resources, and prompts — for example the mid-call interactions we'll cover in episode 8 on Multi-Round-Trip Requests.
The installation is one line per ecosystem: npm install @modelcontextprotocol/sdk for TypeScript, or pip install mcp for Python. The rest is just writing code.
The high-level server is best suited for tool-based servers. The following example creates a currency server with a single conversion tool, then connects to the stdio transport — the standard protocol for local servers invoked as a subprocess by hosts like editors or CLI agents.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
const server = new McpServer({
name: "currency-tools",
version: "1.0.0",
});
server.registerTool(
"convert-currency",
{
description: "Konversi nilai antar mata uang",
inputSchema: {
type: "object",
properties: {
from: { type: "string" },
to: { type: "string" },
amount: { type: "number" },
},
required: ["from", "to", "amount"],
},
},
async ({ from, to, amount }) => ({
content: [
{ type: "text", text: `${amount} ${from} = ${amount * 15000} ${to}` },
],
})
);
const transport = new StdioServerTransport();
await server.connect(transport);Note the registerTool structure: the first argument is the tool name, the second is the description plus the JSON Schema input schema, and the third is the execution function receiving the schema-parsed parameters. The return value is a content object typed as an array — this is the structured output you know from episode 4. Without any decoration, McpServer automatically implements tools/list and tools/call behind the scenes. To run it, build with npm run build then call node dist/server.js. As a side note, there's also a fastmcp library for TypeScript that mimics Python's decorator style — but McpServer remains the SDK's primary API.
If you need full control — for example adding per-request logs or modifying responses before sending them — drop down to the Server class from @modelcontextprotocol/sdk/server/index.js. Here every JSON-RPC method is registered manually via setRequestHandler.
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
const server = new Server({ name: "low-level-server", version: "1.0.0" });
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{ name: "ping", description: "Balas pong", inputSchema: { type: "object", properties: {} } },
],
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === "ping") {
return { content: [{ type: "text", text: "pong" }] };
}
throw new Error(`Tool tidak dikenal: ${request.params.name}`);
});The ListToolsRequestSchema and CallToolRequestSchema schemas from @modelcontextprotocol/sdk/types.js are the official JSON-RPC definitions exported by the SDK. With this pattern you can also register resources/list, resources/read, prompts/list, and prompts/get one by one — nothing is automatic.
In Python, the counterpart of McpServer is FastMCP. Registering a tool is just a @mcp.tool() decorator, and type hints on parameters are translated directly into JSON Schema — the code feels like writing a regular function.
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("currency-tools")
@mcp.tool()
def convert_currency(from_currency: str, to_currency: str, amount: float) -> str:
result = amount * 15000
return f"{amount} {from_currency} = {result} {to_currency}"
if __name__ == "__main__":
mcp.run(transport="stdio")Run it with one line: python server.py. FastMCP's advantages: async support, progress notifications, and helpers for resources and prompts in a single class. For a simple server, this is the fastest path from zero to a working server.
If FastMCP feels too "magical", Python also provides the low-level Server with decorators that are explicit about JSON-RPC methods:
import asyncio
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import CallToolRequest
server = Server("low-level-server")
@server.list_tools()
async def list_tools():
return [{"name": "ping", "description": "Balas pong"}]
@server.call_tool()
async def call_tool(request: CallToolRequest):
if request.params.name == "ping":
return {"content": [{"type": "text", "text": "pong"}]}
raise ValueError("Tool tidak dikenal")The @server.list_tools() and @server.call_tool() decorators capture incoming JSON-RPC requests. To run it, wrap server.run(read, write, server.create_initialization_options()) inside async with stdio_server() then call asyncio.run(main()). Note create_initialization_options() — in the modern era (specification 2026-07-28) version and capability information is sent per request, so the server no longer depends on a special session handshake.
Finally, tie it all together into a weather server with two tools, ready to plug into the MCP Inspector from the next episode:
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("weather-server")
@mcp.tool()
def get_weather(city: str) -> str:
if city.lower() == "jakarta":
return "Cerah, 32 derajat Celcius"
return f"Data cuaca untuk {city} belum tersedia"
@mcp.tool()
def get_forecast(city: str, days: int) -> str:
return f"Perkiraan {days} hari ke depan untuk {city}: hujan ringan"
if __name__ == "__main__":
mcp.run(transport="stdio")You can test this server quickly via npx @modelcontextprotocol/inspector — it opens an interactive UI in the browser; we'll dissect all its features in episode 7.
Episode 6 gave you the skills to build an MCP server in two languages with two levels of abstraction: McpServer and FastMCP for productivity, and the low-level Server for full control over JSON-RPC messages. You also now have a real, working end-to-end weather server.
Key takeaways:
registerTool and @mcp.tool() are enough for most tool servers.setRequestHandler and @server.call_tool() give direct access to JSON-RPC methods.content containing text blocks (and later other blocks).In the next episode 7 we build the other side — the Client SDK for reaching servers over stdio and HTTP, plus the MCP Inspector for testing your servers interactively. See you there!