Building an MCP Server
4.1 Development Environment Setup
TypeScript (Recommended)
mkdir my-mcp-server && cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk
npm install -D typescript @types/node
npx tsc --init
Python
mkdir my-mcp-server && cd my-mcp-server
pip install mcp
4.2 Complete TypeScript Example
An MCP Server providing a weather query tool:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
// Create Server instance
const server = new McpServer({
name: "weather-server",
version: "1.0.0"
});
// Define a tool
server.tool(
"get_weather", // Tool name
"Get current weather for a specified city", // Description
{ // Input parameter schema
city: {
type: "string",
description: "City name"
}
},
async ({ city }) => { // Execution function
// Call your weather API here
const weather = await fetchWeather(city);
return {
content: [{
type: "text",
text: `Current weather in ${city}: ${weather.temp} C, ${weather.condition}`
}]
};
}
);
// Start (stdio transport)
const transport = new StdioServerTransport();
await server.connect(transport);
Multi-Tool Server (with outputSchema and structuredContent)
Protocol version 2025-11-25 supports outputSchema for defining structured output format, with structuredContent for returning structured data:
const server = new McpServer({
name: "commerce-server",
version: "1.0.0"
});
// Tool 1: Search products (with outputSchema)
server.tool(
"search_products",
"Search the product catalog",
{
query: { type: "string", description: "Search keyword" },
category: { type: "string", description: "Product category (optional)" },
limit: { type: "number", description: "Number of results, default 10" }
},
async ({ query, category, limit = 10 }) => {
const results = await db.products.search(query, { category, limit });
const data = {
products: results.map(p => ({
name: p.name, sku: p.sku, price: p.price, inStock: p.inventory > 0
})),
total: results.totalCount
};
return {
content: [{
type: "text",
text: JSON.stringify(data, null, 2)
}],
// structuredContent: for programmatic processing (when outputSchema is defined)
structuredContent: data
};
}
);
// Tool 2: Check inventory
server.tool(
"check_inventory",
"Query real-time product inventory",
{
sku: { type: "string", description: "Product SKU" }
},
async ({ sku }) => {
const inventory = await db.inventory.getBySku(sku);
if (!inventory) {
return {
content: [{ type: "text", text: `SKU ${sku} does not exist` }],
isError: true
};
}
return {
content: [{
type: "text",
text: `SKU ${sku}: ${inventory.quantity} units (${inventory.warehouse} warehouse)`
}]
};
}
);
// Tool 3: Get order
server.tool(
"get_order",
"Query order status",
{
orderId: { type: "string", description: "Order ID" }
},
async ({ orderId }) => {
const order = await db.orders.getById(orderId);
if (!order) {
return {
content: [{ type: "text", text: `Order ${orderId} not found` }],
isError: true
};
}
return {
content: [{
type: "text",
text: JSON.stringify({
id: order.id,
status: order.status,
items: order.items.length,
total: order.total,
tracking: order.trackingNumber
}, null, 2)
}]
};
}
);
4.3 Complete Python Example
from mcp.server import Server
from mcp.server.stdio import stdio_server
import mcp.types as types
server = Server("commerce-server")
@server.list_tools()
async def list_tools() -> list[types.Tool]:
return [
types.Tool(
name="search_products",
description="Search the product catalog",
inputSchema={
"type": "object",
"properties": {
"query": {"type": "string", "description": "Search keyword"},
"limit": {"type": "number", "description": "Number of results"}
},
"required": ["query"]
}
),
types.Tool(
name="check_inventory",
description="Query real-time product inventory",
inputSchema={
"type": "object",
"properties": {
"sku": {"type": "string", "description": "Product SKU"}
},
"required": ["sku"]
}
)
]
@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
if name == "search_products":
results = await search_products(
arguments["query"],
arguments.get("limit", 10)
)
return [types.TextContent(
type="text",
text=json.dumps(results, ensure_ascii=False)
)]
elif name == "check_inventory":
inventory = await get_inventory(arguments["sku"])
return [types.TextContent(
type="text",
text=f"SKU {arguments['sku']}: {inventory['quantity']} units"
)]
async def main():
async with stdio_server() as (read_stream, write_stream):
await server.run(read_stream, write_stream)
if __name__ == "__main__":
import asyncio
asyncio.run(main())
4.4 Exposing Resources
In addition to tools, Servers can also expose data resources:
// Expose product categories as a resource
server.resource(
"commerce://catalog/categories",
"Product Categories",
"application/json",
async () => {
const categories = await db.categories.getAll();
return JSON.stringify(categories, null, 2);
}
);
// Expose company information as a resource
server.resource(
"commerce://company/info",
"Company Information",
"text/markdown",
async () => {
return `# Company Name\n\n> One-line description\n\n## Main Business\n...`;
}
);
4.5 Streamable HTTP Server
Use Streamable HTTP transport for remote deployment:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import express from "express";
const app = express();
app.use(express.json());
const server = new McpServer({
name: "my-remote-server",
version: "1.0.0"
});
// Register tools (same as above)...
// Create Streamable HTTP transport
const transport = new StreamableHTTPServerTransport({
endpoint: "/mcp"
});
// Mount to Express
app.post("/mcp", transport.handlePost.bind(transport));
app.get("/mcp", transport.handleGet.bind(transport));
await server.connect(transport);
app.listen(3000, () => {
console.error("MCP Server (Streamable HTTP) running on port 3000");
});
4.6 Testing and Debugging
MCP Inspector
The official interactive debugging tool:
npx @modelcontextprotocol/inspector node dist/index.js
The Inspector opens a web interface where you can:
- View all tools, resources, and prompts exposed by the Server
- Manually enter parameters and execute tools
- Inspect the raw JSON-RPC requests and responses
- Validate inputSchema and outputSchema
Testing in Claude Desktop
Edit the Claude Desktop configuration file:
macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
Windows: %APPDATA%\Claude\claude_desktop_config.json
{
"mcpServers": {
"my-server": {
"command": "node",
"args": ["D:/path/to/my-server/dist/index.js"]
}
}
}
Restart Claude Desktop and try using your tools in a conversation.
4.7 Common Errors
| Error | Cause | Fix |
|---|
| Server starts but does not respond | console.log output to stdout interferes with the protocol | Send logs to stderr instead |
| Tools do not appear | tools/list returns empty | Check tool registration code |
| Parameter validation fails | inputSchema and actual parameters mismatch | Check JSON Schema definition |
| Connection timeout | Server process did not start correctly | Check command and args configuration |
structuredContent is ignored | No outputSchema defined | Add outputSchema or use content only |
| Initialization fails | Protocol version mismatch | Confirm Client and Server use compatible versions |
stdio Servers must never use console.log. stdout is the protocol channel; any non-JSON-RPC output will break communication. Use console.error or write to a log file instead.
Next Chapter: Commerce MCP Server — MCP Server design patterns for e-commerce scenarios