An MCP server provides tools, resources, and prompts via the standardized protocol. In this lesson you'll build your own MCP server — with TypeScript and Python.
npm install @modelcontextprotocol/sdk
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({
name: "my-tools",
version: "1.0.0"
});
// Define tool
server.tool(
"get_weather",
"Get current weather for a city",
{ city: z.string().describe("City name") },
async ({ city }) => {
const data = await fetchWeather(city);
return {
content: [{ type: "text", text: JSON.stringify(data) }]
};
}
);
// Start server
const transport = new StdioServerTransport();
await server.connect(transport);
pip install mcp
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent
server = Server("my-tools")
@server.list_tools()
async def list_tools() -> list[Tool]:
return [
Tool(
name="get_weather",
description="Current weather for a city",
inputSchema={
"type": "object",
"properties": {
"city": {"type": "string", "description": "City name"}
},
"required": ["city"]
}
)
]
@server.call_tool()
async def call_tool(name: str, arguments: dict):
if name == "get_weather":
data = await fetch_weather(arguments["city"])
return [TextContent(type="text", text=str(data))]
async def main():
async with stdio_server() as (read, write):
await server.run(read, write)
Good tool definitions are crucial — the LLM decides when to use which tool based on name and description:
| Property | Description | Best Practice |
|---|---|---|
| name | Unique identifier | Snake_case, descriptive |
| description | What the tool does | Clear, precise, when to use |
| inputSchema | JSON Schema for parameters | Document all fields |
Resources are data the model can read:
server.resource(
"config://app",
"Current application configuration",
async () => ({
contents: [{
uri: "config://app",
text: JSON.stringify(appConfig),
mimeType: "application/json"
}]
})
);
Predefined prompts the client can request:
server.prompt(
"analyze_data",
"Analyze data and generate insights",
{ dataset: z.string().describe("Name of the dataset") },
async ({ dataset }) => ({
messages: [{
role: "user",
content: {
type: "text",
text: `Analyze the dataset "${dataset}" and identify the most important trends.`
}
}]
})
);
Communication via standard input/output. Ideal for local servers:
{
"mcpServers": {
"my-tools": {
"command": "node",
"args": ["./build/index.js"]
}
}
}
For remote servers over HTTP:
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
// HTTP server with SSE endpoint
Practical tip: Start with stdio — it's the simplest transport. Switch to SSE when your server runs remotely. Test your server with the MCP Inspector before using it in an application.