使用说明
MCP计算器(TypeScript)
现场演示 --不安装任何东西就尝试一下
为什么存在
大多数AI教程都用幻灯片或伪代码解释概念。这个项目采取了不同的方法: 你通过阅读一个可用的应用程序来学习.
该领域故意简单(计算器),因此您可以完全专注于AI模式。每个函数都以它所教授的概念命名。这段代码读起来像教科书一样自上而下。
我构建这个是因为当我开始构建人工智能应用程序时,我想要一个小的代码库,展示所有部分是如何组合在一起的——MCP、工具调用、代理循环、思维链、即时工程——用真正的HTTP请求、真正的LLM调用和真正的工具执行端到端地连接起来。这就是代码库。
你将学到什么
五个AI工程概念,都在代码中可见:
| # | 概念 | 代码中的位置 |
|---|---|---|
| 1 | MCP(模型上下文协议) --LLM如何发现和调用外部工具 | calculator-server.ts, initializeMcp() |
| 2 | 工具调用 --LLM选择一个函数和参数,代码运行它 | convertMcpToolsToOpenAiFormat(), executeSingleToolCall() |
| 3 | 代理循环 --LLM是一个循环:思考、行动、观察、重复 | runAgenticLoop() |
| 4 | 思维链 --法学硕士在选择工具之前“大声思考” runAgenticLoop(),寻找 llm_chain_of_thought |
|
| 5 | 提示工程 --系统提示如何塑造LLM行为 | buildConversation() |
这是给谁的
- 学习MCP的工程师 --查看握手、发现和工具执行端到端工作
- 人们正在构建他们的第一个人工智能代理 --这里的代理循环与生产代理使用的模式相同,只是更小
- 任何对工具调用感兴趣的人 --观察LLM决定调用哪个函数以及使用什么参数
- TypeScript开发人员 --严格的类型,每个边界的Zod验证,干净的项目结构
不需要AI经验。如果你能阅读TypeScript并理解HTTP请求,你就可以遵循这段代码。
5分钟快速入门
先决条件: Node.js>=20,一个OpenAI API密钥
# 1. Clone
git clone https://github.com/madhusudankapoor/mcpcal.git
cd mcpcal
# 2. Install and build
npm install
npm run build
# 3. Add your OpenAI key
echo 'OPENAI_API_KEY=sk-...' > .env
# 4. Start (backend auto-spawns MCP server)
npm run start:client
试试这三件事:
- 点击
5 + 3 =--观看学习控制台显示MCP阶段(握手→ 发现→ 执行) - 类型
what is 5 + 3?在聊天中——单轮代理循环(LLM调用一个工具,得到答案) - 类型
what is 4 * 4 * 4?在聊天中——多轮代理循环(LLM调用在3轮中加倍)
底部的学习控制台显示了每一步:发送到LLM的内容、返回的内容、选择的工具以及循环如何收敛到答案。
或者完全跳过设置,使用 现场演示.
建筑
┌─────────────────────────────────────────────────────────────────┐
│ YOUR MACHINE │
│ │
│ ┌──────────────┐ HTTP ┌───────────────────────────┐ │
│ │ Browser UI │ ────────────> │ Express Backend │ │
│ │ │ │ (client-backend.ts) │ │
│ │ Calculator │ POST /chat │ │ │
│ │ buttons │ ────────────> │ 1. Receives user message │ │
│ │ │ │ 2. Talks to OpenAI │ │
│ │ Chat input │ │ 3. Executes MCP tools │ │
│ │ │ JSON response│ 4. Talks to OpenAI again │ │
│ │ Chat output │ <──────────── │ 5. Returns final answer │ │
│ └──────────────┘ └──────┬──────────┬─────────┘ │
│ │ │ │
│ MCP/stdio HTTPS API │
│ │ │ │
│ ▼ │ │
│ ┌───────────────┐ │ │
│ │ MCP Server │ │ │
│ │ (separate │ │ │
│ │ process) │ │ │
│ │ │ │ │
│ │ Tools: │ │ │
│ │ - add │ │ │
│ │ - subtract │ │ │
│ │ - multiply │ │ │
│ │ - divide │ │ │
│ └───────────────┘ │ │
│ │ │
└───────────────────────────────────────────────────┼─────────────┘
│
▼
┌───────────────┐
│ OpenAI API │
│ (cloud) │
│ │
│ gpt-4o-mini │
│ │
│ Receives: │
│ - message │
│ - tool menu │
│ │
│ Returns: │
│ - which tool │
│ to call │
│ - or a text │
│ response │
└───────────────┘
核心思想: OpenAI从不与您的MCP服务器通信。OpenAI是一个无法移动双手的大脑。它查看工具菜单,然后说“我想调用add(15,27)”。您的后端实际上通过MCP运行该工具并反馈结果。
工作原理:一步一步
当你键入“15加27等于多少?”时会发生什么
Step 1: Browser sends message to backend
POST /chat { message: "What is 15 plus 27?" }
Step 2: Backend sends message + tool menu to OpenAI
"Here is a user question. Here are tools you can use:
add(a,b), subtract(a,b), multiply(a,b), divide(a,b)"
Step 3: OpenAI decides (does NOT execute)
"I want to call add(a=15, b=27)"
Step 4: Backend executes via MCP
callTool("add", {a:15, b:27}) → { ok: true, result: 42 }
Step 5: Backend feeds result back to OpenAI
"The add tool returned 42. Give the user a friendly answer."
Step 6: OpenAI responds
"15 plus 27 equals 42."
Step 7: Backend returns answer to browser
代理循环
简单的问题在一轮中完成。但是“什么是4*4*4?”需要多轮,因为LLM一次只能乘两个数字:
┌──────────────────────────────────────────────────┐
│ AGENTIC LOOP │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ OBSERVE │───▶│ THINK │───▶│ ACT │ │
│ │ (tool │ │ (LLM │ │ (execute │ │
│ │ results) │ │ reasons) │ │ tools) │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ ▲ │ │
│ └───────────────────────────────┘ │
│ loop until done │
└──────────────────────────────────────────────────┘
Round 1: User asks "what is 4 * 4 * 4?"
LLM thinks → calls multiply(4, 4)
Tool returns 16
Round 2: LLM sees 16
LLM thinks → calls multiply(16, 4)
Tool returns 64
Round 3: LLM sees 64
No more tools needed → returns "4 * 4 * 4 equals 64."
谈话每一轮都在增加。LLM可以看到所有以前的工具调用和结果,这就是它知道如何再次乘以16 x 4而不是4 x 4的原因。
循环以以下两种方式之一结束:
- 天然: LLM返回没有工具调用的文本,它有自己的答案。
- 安全: 循环达到最大循环次数限制(10),以防止无限循环。
思维链
有时LLM在调用工具之前会大声思考。例如,当被问到“什么是(2+3)乘以(4+5)?”:
LLM is thinking: "I need to compute 2+3 and 4+5 first, then multiply."
LLM wants to call 2 tool(s):
→ add(a=2, b=3)
→ add(a=4, b=5)
此推理文本作为思维链事件记录在学习控制台中。
两条路径,同一MCP服务器
┌─────────────────────────┐
Button click: │ │
User picks tool │ MCP Server │
─────────────────>│ (same server, │
│ same tools, │
Chat message: │ same result) │
LLM picks tool │ │
─────────────────>│ │
└─────────────────────────┘
MCP生命周期
第一阶段:启动握手
启动时,后端将MCP服务器作为子进程生成,并通过stdio连接:
Backend MCP Server
│ spawn process + connect │
│ ────────────────────────────> │
│ handshake + capabilities │
│ <──────────────────────────── │
第二阶段:工具发现
后端询问MCP服务器它有什么工具。此列表被缓存并用于按钮UI和OpenAI工具菜单:
Backend MCP Server
│ listTools() │
│ ────────────────────────────> │
│ [add, subtract, multiply, │
│ divide] with JSON Schemas │
│ <──────────────────────────── │
MCP工具格式被转换为OpenAI格式,以便LLM理解:
MCP format: OpenAI format:
{ {
name: "add", --> type: "function",
description: "Add function: {
two numbers", name: "add",
inputSchema: { description: "Add two numbers",
properties: { parameters: {
a: {type:"number"}, properties: {
b: {type:"number"} a: {type:"number"},
} b: {type:"number"}
} }
} }
}
如果MCP服务器明天添加了一个新工具,LLM将自动访问它。无需更改代码。
第3阶段:工具执行
对于按钮点击:
Browser Backend MCP Server
│ POST /calculate │ │
│ {tool:"add", │ │
│ args:{a:5, b:3}} │ │
│ ──────────────────────> │ callTool("add",{a:5,b:3})│
│ │ ──────────────────────> │
│ │ {ok:true, result:8} │
│ │ <────────────────────── │
│ {result:8, │ │
│ expression:"5 + 3"} │ │
│ <────────────────────── │ │
聊天,同样 callTool() 发生在代理循环内部。MCP服务器不知道或不关心是谁给它打电话的。
聊天端点——5个步骤
聊天路由是一个简短的编排器,按顺序调用AI助手函数:
| 步骤 | 发生了什么 | AI概念 |
|---|---|---|
| 1 | 使用Zod验证请求 | 运行时类型安全 |
| 2 | buildConversation(message) |
提示工程 |
| 3 | convertMcpToolsToOpenAiFormat() |
工具调用 |
| 4 | runAgenticLoop(messages, tools, mcp, 10) |
代理循环 |
| 5 | 返回响应 | HTTP响应 |
在代理循环中,还有两个助手处理工具执行:
| 助手 | 它做什么 |
|---|---|
executeSingleToolCall(toolCall, mcp) |
解析参数→ 使用Zod进行验证→ MCP调用工具→ 解析结果 |
executeToolCallsFromLlm(functionCalls, mcp) |
从一个LLM响应运行一批工具调用 |
学习控制台
浏览器中的实时面板会解释发生的每一步。使用服务器发送事件(SSE)从后端发送事件流。
聊天时你会看到什么:
| 事件 | 它显示了什么 |
|---|---|
llm_sending_messages → |
完整对话正在发送给法学硕士 |
llm_received_response ← |
LLM返回的内容(文本、工具调用或两者兼而有之) |
llm_chain_of_thought |
LLM推理文本,当它大声思考时 |
llm_tool_selection |
LLM选择了哪些具有确切参数的工具 |
llm_tool_results_feeding_back ↻ |
正在为下一轮添加工具结果 |
llm_response |
最终答案+循环如何结束 |
终点:
GET /mcp-events--历史快照(JSON)GET /mcp-events/stream--SSE直播流
项目结构
mcp-calculator/
├── src/
│ ├── server/
│ │ └── calculator-server.ts # MCP server (tools live here)
│ ├── client/
│ │ ├── client-backend.ts # Express + MCP client + AI helpers (the main story)
│ │ ├── trace.ts # Learning console trace/logging system
│ │ ├── formatters.ts # Human-readable message summarizers
│ │ ├── mcp-payload.ts # MCP response parsing and tool normalization
│ │ ├── utils.ts # Generic runtime helpers (isRecord, getString, etc.)
│ │ └── ui/
│ │ └── app.ts # Browser-side UI logic
│ └── shared/
│ └── types.ts # Shared types and Zod schemas
├── public/
│ ├── index.html # Calculator + chat + learning console HTML
│ ├── app.js # Compiled UI (generated)
│ └── styles.css
├── package.json
├── tsconfig.json
├── tsconfig.server.json
├── tsconfig.client.json
└── README.md
每个文件的作用
client-backend.ts 是主文件。从上到下阅读-它告诉了聊天消息如何成为工具调用并返回答案的故事。它包含:
- AI助手功能(
buildConversation,convertMcpToolsToOpenAiFormat,executeSingleToolCall,executeToolCallsFromLlm,runAgenticLoop) - 快速路线(
/calculate,/chat,/tools,/mcp-events) - MCP客户端设置和生命周期
calculator-server.ts 是MCP服务器。它注册了四个工具(加、减、乘、除),用Zod验证参数,计算结果,并将其作为MCP文本内容中键入的JSON返回。它是无状态的——每个呼叫都是独立的。
types.ts 保存服务器、后端和UI之间共享的类型和Zod模式。这就是我们如何在所有三个层中保持验证的一致性。
trace.ts 处理日志和SSE系统。每个重要事件都被格式化为人类可读的解释,并广播到连接的浏览器。
formatters.ts 将原始消息数组和LLM响应转换为学习控制台的可读文本。
mcp-payload.ts 解析MCP响应并规范工具定义。这是管道——它处理解析的混乱部分,使主文件保持干净。
app.ts (浏览器)管理计算器UI、聊天面板和学习控制台。它在启动时从后端发现工具,并使用类型保护程序验证每个响应。
使用Zod进行运行时验证
TypeScript类型在运行时消失。跨越边界的数据(HTTP、MCP、LLM参数)以如下方式到达 unknown.Zod在三个地方验证它:
- HTTP边界 --浏览器向后端发送POST
- MCP边界 --后端向MCP服务器发送工具调用
- LLM边界 --OpenAI返回工具名称和参数
这可以防止坏数据成为隐藏的错误。
环境变量
| 变量 | 必填 | 默认 | 描述 |
|---|---|---|---|
OPENAI_API_KEY |
是(用于聊天) | - | 您的OpenAI API密钥 |
PORT |
没有 | 3000 |
HTTP服务器端口 |
MCP_SERVER_ENTRY |
否 | 自动检测到 | MCP服务器入口文件的路径 |
创建一个 .env 项目根目录中的文件(已在 .gitignore):
echo 'OPENAI_API_KEY=sk-...' > .env
API终点
GET /tools
返回发现的工具:
{
"tools": [
{
"name": "add",
"description": "Add two numbers together",
"inputSchema": {
"type": "object",
"properties": {
"a": { "type": "number", "description": "First number" },
"b": { "type": "number", "description": "Second number" }
},
"required": ["a", "b"],
"additionalProperties": false
}
}
]
}
POST /calculate
{ "tool": "divide", "args": { "a": 5, "b": 2 } }
成功: { "result": 2.5, "expression": "5 / 2" }
错误: { "error": "Division by zero is not allowed.", "details": "Provide a non-zero divisor." }
POST /chat
{ "message": "what is 10 times 5?" }
成功:
{
"response": "10 times 5 equals 50.",
"toolCalls": [
{
"tool": "multiply",
"args": { "a": 10, "b": 5 },
"result": { "ok": true, "result": 50 }
}
]
}
GET /healthz
退货 { "status": "ok" }.
GET /mcp-events
返回跟踪历史快照。
GET /mcp-events/stream
实时服务器发送的跟踪事件流。
贡献
欢迎捐款。这是一个教育项目,所以清晰比聪明更重要。
如何做出贡献:
- 分叉回购
- 创建分支(
git checkout -b my-change) - 进行更改
- 确保
npm run build通过 - 打开拉取请求
良好的首次贡献:
- 添加新的MCP工具(例如。,
modulo,power)--服务器、类型和UI都需要更新 - 改进学习控制台解释
- 添加测试
- 修复代码注释或README中的拼写错误或措辞不清楚
指导方针:
- 保持代码简单易读——这是一个教学工具
- 每个函数都应该以它的作用命名
- 对JSDoc长篇论文的简短评论
- 如果您添加了功能,请更新README
许可证
麻省理工学院
为什么选择MCP作为计算器?
对于真正的数学,MCP是多余的。这个项目具有教育意义。
当工具是远程的、异构的,或者需要跨多个客户端和服务器进行标准化发现时,MCP变得很有用。这个计算器显示了一个足够小的代码库中的所有模式(握手、发现、执行、代理循环),可以一次读取。