盒子里的MCP📦
在30分钟内构建您的第一个自定义MCP服务器
一个动手实验室,通过构建一个连接到OpenAI API的工作天气仪表板服务器来学习模型上下文协议(MCP)。
______________________________________________________________________
你将建造什么
功能齐全的MCP服务器:
- 暴露a
get_weather返回模拟天气数据的工具 - 使用OpenAI的响应API
- 可以扩展以连接到真实的API
最后,您将了解MCP服务器的工作原理以及如何构建自己的服务器。
______________________________________________________________________
先决条件
知识需求
- 基础JavaScript/Node.js(变量、函数、异步/等待)
- 使用终端/命令行舒适
- 对API的基本了解(它们是什么,请求是如何工作的)
所需工具
检查您的设置
运行以下命令以验证您是否准备就绪:
node --version # Should show v18.x.x or higher
npm --version # Should show 9.x.x or higher______________________________________________________________________
实验室概述
| 步骤 | 你要做什么 | 时间 |
|---|---|---|
| 1 | 设置项目 | 5分钟 |
| 2 | 构建MCP服务器 | 10分钟 |
| 3 | 本地测试 | 5分钟 |
| 4 | 连接到OpenAI | 10分钟 |
总时间:~30分钟
______________________________________________________________________
步骤1:设置项目
1.1创建项目目录
mkdir mcp-weather-server
cd mcp-weather-server1.2初始化项目
npm init -y1.3 ES模块配置
打开 package.json 并将其内容替换为:
{
"name": "mcp-weather-server",
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.20.2",
"zod": "^3.25.76"
}
}1.4安装依赖项
npm install您应该看到软件包安装成功。
______________________________________________________________________
步骤2:构建MCP服务器
2.1理解架构
在我们编码之前,以下是我们正在构建的内容:
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ OpenAI API │────▶│ Your MCP Server │────▶│ Weather Data │
│ (or ChatGPT) │◀────│ (Node.js) │◀────│ (mock/real) │
└─────────────────┘ └──────────────────┘ └─────────────────┘关键概念:
- 工具:服务器公开的函数(如
get_weather) - 输入架构:该工具接受哪些参数(用Zod验证)
- 运输:请求/响应如何流动(我们使用Streamable HTTP)
2.2创建服务器文件
创建一个名为的新文件 server.js 并粘贴以下代码:
// server.js - MCP Weather Server
// A simple MCP server that provides weather information
import { createServer } from "node:http";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { z } from "zod";
// ============================================
// WEATHER DATA (Mock data for the lab)
// In production, replace with real API calls
// ============================================
const weatherDatabase = {
"new york": { temp: 72, condition: "Partly Cloudy", humidity: 65, wind: 8 },
"los angeles": { temp: 78, condition: "Sunny", humidity: 45, wind: 5 },
"chicago": { temp: 68, condition: "Windy", humidity: 55, wind: 15 },
"miami": { temp: 85, condition: "Humid", humidity: 80, wind: 10 },
"seattle": { temp: 62, condition: "Rainy", humidity: 75, wind: 7 },
"denver": { temp: 70, condition: "Clear", humidity: 30, wind: 6 },
"boston": { temp: 65, condition: "Cloudy", humidity: 60, wind: 12 },
"san francisco": { temp: 64, condition: "Foggy", humidity: 70, wind: 9 },
};
// Helper function to get weather data
function getWeatherData(city) {
const normalizedCity = city.toLowerCase().trim();
const weather = weatherDatabase[normalizedCity];
if (!weather) {
return {
found: false,
city: city,
message: `Weather data not available for "${city}". Available cities: ${Object.keys(weatherDatabase).join(", ")}`,
};
}
return {
found: true,
city: city,
temperature: weather.temp,
temperatureUnit: "°F",
condition: weather.condition,
humidity: weather.humidity,
humidityUnit: "%",
windSpeed: weather.wind,
windUnit: "mph",
timestamp: new Date().toISOString(),
};
}
// ============================================
// MCP SERVER SETUP
// ============================================
function createWeatherServer() {
// Initialize the MCP server with name and version
const server = new McpServer({
name: "weather-server",
version: "1.0.0",
});
// ----------------------------------------
// TOOL: get_weather
// This is the main tool our server exposes
// ----------------------------------------
server.registerTool(
"get_weather", // Tool name (what the model calls)
{
title: "Get Weather",
description: "Get current weather information for a city. Returns temperature, conditions, humidity, and wind speed.",
// Input schema using Zod for validation
inputSchema: {
city: z.string().min(1).describe("The city name to get weather for"),
},
// Metadata for OpenAI integration
_meta: {
"openai/toolInvocation/invoking": "Checking weather...",
"openai/toolInvocation/invoked": "Weather retrieved!",
},
},
// The handler function - runs when the tool is called
async ({ city }) => {
console.log(`[get_weather] Request for city: ${city}`);
const weatherData = getWeatherData(city);
if (!weatherData.found) {
return {
structuredContent: weatherData,
content: [{ type: "text", text: weatherData.message }],
};
}
// Return structured data for the model to use
return {
structuredContent: weatherData,
content: [
{
type: "text",
text: `Weather in ${city}: ${weatherData.temperature}${weatherData.temperatureUnit}, ${weatherData.condition}`,
},
],
};
}
);
// ----------------------------------------
// TOOL: list_cities
// Bonus tool to show available cities
// ----------------------------------------
server.registerTool(
"list_cities",
{
title: "List Available Cities",
description: "Get a list of all cities that have weather data available.",
inputSchema: {}, // No input required
},
async () => {
const cities = Object.keys(weatherDatabase);
return {
structuredContent: { cities, count: cities.length },
content: [
{
type: "text",
text: `Available cities: ${cities.join(", ")}`,
},
],
};
}
);
return server;
}
// ============================================
// HTTP SERVER
// Handles incoming requests and routes to MCP
// ============================================
const PORT = process.env.PORT || 3000;
const MCP_PATH = "/mcp";
const httpServer = createServer(async (req, res) => {
const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
// Log all incoming requests
console.log(`[${new Date().toISOString()}] ${req.method} ${url.pathname}`);
// Handle CORS preflight requests
if (req.method === "OPTIONS") {
res.writeHead(204, {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "POST, GET, DELETE, OPTIONS",
"Access-Control-Allow-Headers": "content-type, mcp-session-id",
"Access-Control-Expose-Headers": "Mcp-Session-Id",
});
res.end();
return;
}
// Health check endpoint
if (req.method === "GET" && url.pathname === "/") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({
status: "ok",
server: "MCP Weather Server",
version: "1.0.0",
mcpEndpoint: MCP_PATH,
}));
return;
}
// MCP endpoint - handle all MCP requests
const MCP_METHODS = new Set(["POST", "GET", "DELETE"]);
if (url.pathname === MCP_PATH && MCP_METHODS.has(req.method || "")) {
// Set CORS headers
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Expose-Headers", "Mcp-Session-Id");
// Create a new server instance for this request
const server = createWeatherServer();
// Create the transport layer
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined, // Stateless mode
enableJsonResponse: true,
});
// Clean up when connection closes
res.on("close", () => {
transport.close();
server.close();
});
try {
await server.connect(transport);
await transport.handleRequest(req, res);
} catch (error) {
console.error("[MCP Error]", error);
if (!res.headersSent) {
res.writeHead(500, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Internal server error" }));
}
}
return;
}
// 404 for unknown routes
res.writeHead(404, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Not found" }));
});
// Start the server
httpServer.listen(PORT, () => {
console.log(`
╔════════════════════════════════════════════════╗
║ MCP Weather Server is Running! 🌤️ ║
╠════════════════════════════════════════════════╣
║ Local: http://localhost:${PORT} ║
║ MCP: http://localhost:${PORT}${MCP_PATH} ║
╠════════════════════════════════════════════════╣
║ Next steps: ║
║ 1. Test: curl http://localhost:${PORT} ║
║ 2. Tunnel: ngrok http ${PORT} ║
║ 3. Connect to OpenAI Responses API ║
╚════════════════════════════════════════════════╝
`);
});2.3理解准则
让我们分解一下关键部分:
1.工具登记
server.registerTool(
"get_weather", // Name the model uses to call it
{
title: "Get Weather", // Human-readable title
description: "...", // Helps the model know when to use it
inputSchema: { ... }, // Zod schema for validation
},
async ({ city }) => { } // Handler function
);2.响应结构
return {
structuredContent: { ... }, // JSON data the model can reason about
content: [{ type: "text", text: "..." }], // Human-readable response
};3.HTTP传输 服务器使用 StreamableHTTPServerTransport 这是生产MCP服务器的推荐传输方式。
______________________________________________________________________
步骤3:本地测试
3.1启动服务器
npm start您应该看到:
╔════════════════════════════════════════════════╗
║ MCP Weather Server is Running! 🌤️ ║
...3.2测试健康终点
打开一个新终端并运行:
curl http://localhost:3000预期响应:
{"status":"ok","server":"MCP Weather Server","version":"1.0.0","mcpEndpoint":"/mcp"}3.3使用MCP检查员进行测试(可选但推荐)
MCP检查器允许您以交互方式测试工具:
npx @modelcontextprotocol/inspector@latest http://localhost:3000/mcp这将打开一个浏览器窗口,您可以在其中:
- 查看您注册的工具
- 呼叫
get_weather不同的城市 - 查看回复
______________________________________________________________________
步骤4:连接到OpenAI
4.1使用ngrok暴露您的服务器
在新终端中(保持服务器运行):
ngrok http 3000ngrok将向您显示一个公共URL,例如:
Forwarding https://abc123.ngrok.app -> http://localhost:3000复制HTTPS URL (例如。, https://abc123.ngrok.app)
4.2通过OpenAI响应API进行测试
创建一个名为的文件 test-openai.sh:
#!/bin/bash
# Replace with your values
OPENAI_API_KEY="your-api-key-here"
MCP_SERVER_URL="https://your-ngrok-url.ngrok.app/mcp"
curl https://api.openai.com/v1/responses \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-d '{
"model": "gpt-4.1",
"tools": [
{
"type": "mcp",
"server_label": "weather",
"server_url": "'"$MCP_SERVER_URL"'",
"require_approval": "never"
}
],
"input": "What is the weather like in Seattle?"
}'运行它:
chmod +x test-openai.sh
./test-openai.sh4.3预期响应
API将:
- 连接到您的MCP服务器
- 发现
get_weather工具 - 称之为
city: "Seattle" - 在响应中返回天气数据
您应该看到您的服务器日志:
[get_weather] Request for city: Seattle______________________________________________________________________
挑战:扩展服务器
现在您已经有了一个可用的MCP服务器,请尝试以下挑战:
挑战1:添加预测工具(简单)
添加a get_forecast 返回3天预测的工具。
Hint
创建模拟预测数据并注册新工具:
server.registerTool("get_forecast", { ... }, async ({ city }) => { ... });挑战2:连接到真实天气API(中等)
将模拟数据替换为对真实天气API的调用,如 OpenWeatherMap.
Hint
- 注册免费API密钥
- 使用
fetch()调用API - 将响应转换为结构化格式
挑战3:添加温度单位转换(简单)
添加一个参数以返回摄氏度或华氏度的温度。
Hint
更新输入架构:
inputSchema: {
city: z.string(),
unit: z.enum(["celsius", "fahrenheit"]).optional().default("fahrenheit"),
}______________________________________________________________________
故障排除
服务器无法启动
错误: Cannot find module
- 跑
npm install安装依赖项 - 检查
package.json有"type": "module"
错误: Port already in use
- 更改端口:
PORT=3001 npm start - 或者使用端口3000终止进程
韩国问题
未找到ngrok
- 安装ngrok:
npm install -g ngrok或从ngrok.com下载 - 跑
ngrok authtoken YOUR_TOKEN如果需要
隧道断开连接
- 免费隧道到期。只需重新启动ngrok。
OpenAI API错误
401未经授权
- 检查您的API密钥是否正确
- 确保您拥有Responses API访问权限
MCP连接失败
- 验证ngrok是否正在运行
- 检查MCP端点URL是否以结尾
/mcp - 查看服务器日志中的错误
______________________________________________________________________
你学到了什么
✅ MCP服务器的结构\ ✅ 如何使用输入模式注册工具\ ✅ 如何返回结构化内容\ ✅ 如何公开服务器以供外部访问\ ✅ 如何连接到OpenAI的Responses API
______________________________________________________________________
后续步骤
- 阅读OpenAI的MCP文档: https://developers.openai.com/apps-sdk/build/mcp-server/
- 探索MCP规范: https://modelcontextprotocol.io/
- 构建ChatGPT应用程序:使用Apps SDK添加UI组件
- 添加身份验证:为用户特定的数据实现OAuth
______________________________________________________________________
资源
______________________________________________________________________
祝贺 🎉 您已经构建了第一个MCP服务器。现在,去创造一些令人惊叹的东西吧!
