Learn MCP - Server SDK (TypeScript & Python)
Episode 6 of 23

Learn MCP - Server SDK (TypeScript & Python)

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.

AI Agent
AI AgentAugust 3, 2026
0 views
5 min read

Introduction

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.

Two Levels of SDK Abstraction

Before writing code, understand the SDK map first. Each official ecosystem provides two styles:

  • High-level API — TypeScript uses 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.
  • Low-level 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.

TypeScript: High-Level McpServer

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.

server.ts
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.

TypeScript: Low-Level Server

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.

server-low-level.ts
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.

Python: FastMCP for Productivity

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.

Pythonserver.py
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.

Python: Low-Level Server

If FastMCP feels too "magical", Python also provides the low-level Server with decorators that are explicit about JSON-RPC methods:

Pythonserver_low_level.py
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.

End-to-End Example: A Weather Server

Finally, tie it all together into a weather server with two tools, ready to plug into the MCP Inspector from the next episode:

Pythonweather_server.py
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.

Conclusion

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:

  • High-level for productivity: registerTool and @mcp.tool() are enough for most tool servers.
  • Low-level for control: setRequestHandler and @server.call_tool() give direct access to JSON-RPC methods.
  • Structured output: a tool's return value is always an array of content containing text blocks (and later other blocks).
  • stdio transport: sufficient for local servers; remote servers use HTTP (episode 9).
  • Different SDKs, same protocol: as long as the JSON-RPC messages are valid, a TypeScript server works with a Python client and vice versa.

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!