MCP计算器工具

一个基于MCP(模型上下文协议)的AI教育工具,通过实际代码演示工具调用、代理循环和链式思考等AI工程概念。

  • by madhusudankapoor
  • TypeScript
  • 本地部署
  • 更新于
在 GitHub 查看 madhusudankapoor/mcpcal

使用说明

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

打开 http://localhost:3000.

试试这三件事:

  1. 点击 5 + 3 = --观看学习控制台显示MCP阶段(握手→ 发现→ 执行)
  2. 类型 what is 5 + 3? 在聊天中——单轮代理循环(LLM调用一个工具,得到答案)
  3. 类型 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在三个地方验证它:

  1. HTTP边界 --浏览器向后端发送POST
  2. MCP边界 --后端向MCP服务器发送工具调用
  3. 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

实时服务器发送的跟踪事件流。

贡献

欢迎捐款。这是一个教育项目,所以清晰比聪明更重要。

如何做出贡献:

  1. 分叉回购
  2. 创建分支(git checkout -b my-change)
  3. 进行更改
  4. 确保 npm run build 通过
  5. 打开拉取请求

良好的首次贡献:

  • 添加新的MCP工具(例如。, modulo, power)--服务器、类型和UI都需要更新
  • 改进学习控制台解释
  • 添加测试
  • 修复代码注释或README中的拼写错误或措辞不清楚

指导方针:

  • 保持代码简单易读——这是一个教学工具
  • 每个函数都应该以它的作用命名
  • 对JSDoc长篇论文的简短评论
  • 如果您添加了功能,请更新README

许可证

麻省理工学院

为什么选择MCP作为计算器?

对于真正的数学,MCP是多余的。这个项目具有教育意义。

当工具是远程的、异构的,或者需要跨多个客户端和服务器进行标准化发现时,MCP变得很有用。这个计算器显示了一个足够小的代码库中的所有模式(握手、发现、执行、代理循环),可以一次读取。