当前位置: 首页 > news >正文

探究Function Calling 和 MCP 的奥秘

前言

在公司实习的时候,内部在大力推动 AI 编程,虽然市面上有很多 AI Coding 的产品,例如 Cursor、Trae 等。

对于从 0 开始的项目,或者一个小需求的修改,能很完美的达到我们预期的效果,但是如果我们只面对 LLM 的对话框,不打开浏览器,只让 AI 进行调试(对于前端项目,目的是让产品、后端能进行快速开发上线),就会有以下的问题:

叠 buff:这些问题对于我们前端工程师来说,可以自己手动调试,不是什么难事,但是对于产品、后端这些对前端不是那么熟悉的,更何况项目采用了 vue、react 这样的框架,对他们的上手程度就会变高。

  1. 登录态问题:目前公司内的项目几乎是采用 Cookie 的形式,AI 没办法感知,需要自己手动去登录页获得校验信息;
  2. 接口问题:对于某些接口,可能因为状态码的不同、响应的数据格式不同,导致 AI 无法进行识别,需要自己手动去调试接口,获取正确的数据格式;
  3. 页面异常问题:对于浏览器控制台、终端的错误,调试变得困难,只能不断去复制错误,再次丢给 AI,这样会大大浪费时间。

光是这三点,是 AI Coding 遇到的痛点问题,为了解决目前的问题,最终采用 MCP 工具,来加强 AI 纠错、自动化的问题。

文章后面也有对加强 AI Coding 的思路和具体代码。

在学习 MCP 之前我们需要先了解一下什么是 Function Calling,它和 MCP 的关系。

Function Calling

什么是 Function Calling

在 LLM 领域,Function Calling(函数调用) 指的是模型根据用户需求,主动触发并调用外部工具(如 API、数据库、计算器、第三方服务等)的能力,本质是让 AI 从 “纯文本生成” 升级为 “能执行具体任务的工具使用者”,解决 LLM 自身无法处理的信息滞后、实时计算、数据查询等问题。

  • 广义的 Function Calling:是指让大模型能够调用外部工具的一种技术实现:先向大模型提供可用函数的列表及说明,由大模型在对话过程中智能判断是否需要调用函数,并自动生成调用所需的参数,最终用文字返回符合约定格式的函数调用请求
  • 狭义的 Function Calling:特指大模型提供商在模型内部与 API 层面做了支持的一种能力,它最早由 OpenAI 引入:
    • 在模型层面:模型提供商需对大模型进行特别优化,使其具备根据上下文正确选择合适函数、生成有效参数的能力(比如有监督微调、强化学习)。
    • 在 API 层面:模型提供商需额外开放对 Function Calling 的支持(比如 API 中提供了一个 tools 参数)。

思考:为什么需要 Function Calling?

大语言模型的核心能力是 “理解文本 + 生成文本”,但存在天然短板:

  • 信息滞后:训练数据有截止日期,无法获取实时信息;
  • 缺乏外部数据访问权:无法直接连接企业内部数据库、CRM 系统、物联网设备(如控制智能家居);
  • 无法执行操作:不能主动发送邮件、预订机票、调用地图 API 获取路线等。

Function Calling 正是为弥补这些短板而生 —— 让 AI 成为 “任务协调者”:先理解用户需求,判断是否需要调用工具,再按规则生成工具调用指令,最后将工具返回的结果整理成自然语言回答。

Function Calling 的工作流程

完整的函数调用通常分为 4 个关键步骤,形成 “用户需求AI 决策工具执行结果反馈” 的闭环:

  1. 步骤 1:定义函数(提前配置)

开发者需先明确 “AI 可调用的工具清单”,并为每个工具定义标准化的函数参数格式(告诉 AI “调用这个工具需要传哪些信息”)。

例:若要调用 “实时天气 API”,需定义函数结构:

// 函数名:getRealTimeWeather
// 参数说明:
// - city: 城市名称(字符串,必填)
// - date: 日期(格式YYYY-MM-DD,可选,默认当天)
function getRealTimeWeather(city: string,date?: string
): {temperature: numbercondition: stringwind: string
} {// 调用外部天气API的逻辑return { temperature: 25, condition: '晴天', wind: '3级' }
}
  1. 步骤 2:AI 判断是否调用函数

用户输入需求后,模型先分析:“这个需求能否仅用自身知识回答?还是需要调用工具?”

  • 无需调用:如 “解释什么是人工智能”,模型直接生成文本回答;
  • 需调用:如 “北京 2025 年 9 月 30 日的天气”,模型判断 “自身无 2025 年数据,需调用天气 API”。
  1. 步骤 3:AI 生成函数调用指令

若需调用工具,模型会按提前定义的格式,生成结构化的调用指令(避免歧义,确保工具能解析)。

例:针对 “北京 2025 年 9 月 26 日的天气”,模型生成指令:

{"name": "getRealTimeWeather",  # 要调用的函数名"parameters": {"city": "北京","date": "2025-09-26"}
}
  1. 步骤 4:工具执行 + 结果整理
  • 外部工具(如天气 API)接收指令,执行并返回原始结果(通常是 JSON 格式);
  • 模型再将原始结果 “翻译” 成自然语言,反馈给用户:“北京 2025 年 9 月 26 日的天气为晴天,气温 25℃,风力 3 级。”

AI 执行工具的前提,必须是 AI 支持 Function Calling 才行。

MCP 是什么

MCP(Model Context Protocol),模型上下文协议) 是一种标准化的协议,用于在应用程序和 AI 模型之间建立通信标准。

它提供了一种统一的方式来共享上下文信息,使 AI 模型能够更好地理解和处理各种应用场景。有了 MCP,就可以扩展大模型的能力。

上下文:指的是模型在决策时可访问的所有信息,如当前用户输入、历史对话信息、外部工具(tool)信息、外部数据源(resource)信息、提示词(prompt)信息等等。

MCP 解决了什么问题(相较于 Function Calling)

  • 代码冗余与开发效率问题:Function Call 在开发时存在代码冗余的问题,例如单个天气功能可能需编写 150 多行代码,包括 Schema 定义、提示词模板等。而 MCP 通过 SDK 封装通用逻辑,开发者只需专注业务逻辑,将工具开发时间从周级压缩至小时级,大大提高了开发效率。
  • 厂商壁垒与标准化问题:Function Call 通常是大模型厂商提供的特有接口特性,如 OpenAI 的 JSON Schema 与 Anthropic 的 YAML 格式互不兼容,存在厂商壁垒。MCP 则是一个开放标准,支持 8 种语言 SDK,能让不同大模型与不同工具统一对接,解决了“一对多”的对接难题,降低了集成成本。
  • 性能瓶颈问题:Function Call 采用同步调用方式,会导致每秒请求量下降,存在性能瓶颈。MCP 则是异步非阻塞的执行模式,支持长时任务后台执行,例如 30 分钟渲染视频等任务,其平均响应时间更短,并发处理能力更强。
  • 状态管理与多轮推理问题:Function Call 是无状态的,单轮执行,在多轮推理中,函数状态不透明,模型难以追踪执行路径,上下文容易丢失。MCP 是有状态的,任务上下文可追踪,支持能力协商、上下文感知、任务复合执行等,让模型在多轮推理中能够更好地管理状态和追踪执行路径。
  • 资源管理与调度问题:Function Call 没有资源管理能力,每增加一个函数需要重构 schema,调度能力也受限,主要是模型侧编排。MCP 支持工具、提示、资源目录,工具和资源注册后即可复用,服务端可调度、可代理模型行为,能够更好地进行资源管理和任务调度。
  • 权限控制与生态建设问题:Function Call 很难做到细粒度的权限管理,且通常限定在特定语言生态内,不支持远程调用,不利于生态建设。MCP 可以为不同工具设置不同的权限和访问控制,工具可以用任何语言实现,还支持网络调用,为生态建设提供了统一的发布、发现、集成标准。

MCP 核心概念

MCP 遵循客户端-服务器架构,其中 MCP Host(Claude Code 或 Claude Desktop)等 AI 应用程序,与一个或多个 MCP Server 建立连接。

MCP 主机(Host)通过为每个 MCP Server 创建一个 MCP Client 来实现这一目标。每个 MCP Client 都与相应的 MCP Server 保持专用的一对一连接。MCP 架构的主要组成者是:

  • MCP Host:往往指代 A 应用本身,用于发现 MCP Server 以及其中的工具列表
  • MCP Client:用于和 MCP Server 通信的客户端,往往在 Host 内部开启,通常情况下,每启动一个 MCP Server,就会开启一个 MCP Client。从 MCP 服务器获取上下文,供 MCP 主机使用。
  • MCP Server:一个为 MCP Client 提供上下文的程序

例如:VSCode 充当 MCP 主机。当 VSCode 建立与 MCP 服务器(如 Sentry MCP 服务器)的连接时,VSCode 运行时实例化了维护与 Sentry MCP 服务器连接的 MCP 客户端对象。当 VSCode 随后连接到另一个 MCP 服务器时,例如本地文件系统服务器,VSCode 运行时实例化一个额外的 MCP 客户端对象来维护此连接,从而保持 MCP 客户端与 MCP 服务器的一对一关系。

MCP 传输协议(通信方式)

官网:https://modelcontextprotocol.io/docs/getting-started/intro

Stdio

stdio:标准输入输出,这里可以理解为进程的两个接口:标准输入接口标准输出接口

在这里插入图片描述

这里我们拿 nodejs 举例:

  • 新建一个 server.js 文件
process.stdout.write('Hello World!\n')

输出Hello World到终端中,和console.log效果一样,在 node 环境下,console.log 内部是调用了 stdout

我们可以再输出一个进程 id:

process.stdout.write(process.pid + '\n')

当我们执行 node server.js 的时候,输出的 process.pid 是 node 进程的 id,和终端的 id 不是同一个,但是我们在终端中看到了 node 进程的内容,说明终端和 node 进程一定完成了通信。

通信过程如下:

在这里插入图片描述

可以用下面的代码监听终端的输入:

process.stdout.on('data', function (data) {console.log(data)
})

这里的父进程输入内容充当client端,子进程收到后响应充当server端。

再看下面的例子:

  • client.js
import { spawn } from 'child_process'// 启动server.js子进程,也就是执行node server.js
// serverProcess实例和process是一样的
const serverProcess = spawn('node', ['server.js'])// 监听子进程(server,也就是server.js)的输出
serverProcess.stdout.on('data', (data) => {console.log('收到server数据: ' + data.toString())
})setTimeout(() => {// 向子进程发送消息serverProcess.stdin.write('hello')
}, 1000)
  • server.js
process.stdin.setEncoding('utf8')
// 监听client的数据
process.stdin.on('data', (chunk) => {// 回复client数据process.stdout.write('res' + chunk)
})

当我们运行 node client.js 时,会启动一个 server.js 进程,然后 client.js 进程向 server.js 进程发送数据,server.js 进程会回复数据给 client.js 进程。

所以 父进程(client) 不一定是终端,只要能创建子进程,都能作为 client

stdio 通信简洁高效,但仅适用于本地进程间通信。

SSE 传输(旧方案,2024.10)

该方案是客户端通过 HTTP GET 向服务端发请求,服务端通过 SSE 通道返回响应结果。

SSE(Server-Sent Events 服务器发送事件),是一种服务器单向推送数据给客户端的技术,基于 HTTP 协议。

Streamable 传输(新方案,2025.03)

Streamable 是 SSE 传输方案的升级版,目前正在逐步取代原有的 SSE 传输方案

Streamable 并不是一个标准协议名,而是一个通用描述,指的是基于 HTTP 协议的“可流式传输”技术。它的核心思想是:在一个 HTTP 连接里,服务端可以持续不断地发送数据给客户端,客户端边接收边处理,类似“流”一样。与传统 HTTP 请求响应“一次性完成”不同,Streamable 保持连接不关闭,数据分片持续传输。

至于为什么要采用 Streamable,而不是 SSE,我们看看 SSE 的缺点就知道了:

  1. SSE 数据类型只支持文本格式,而 Streamable 的Content-Type支持任意格式,如 JSON、HTML、二进制等,更适合 AI 场景(可能要传 JSON + 音频 + 图片)
  2. 只支持 GET 请求,参数在 URL 中,相对来说不安全
  3. 兼容性:SSE 现代浏览器为主,IE 不支持;而 Streamable 兼容性更好,支持所有浏览器
  4. 性能问题:SSE 是基于 HTTP/1.1 长连接,Streamable HTTP 可以基于 HTTP/2/3

下面这张图可以很好的解释 MCP 的作用:统一接口规范

在这里插入图片描述

分为远程和本地两种方式。

MCP 通信格式 json-rpc

JSON-RPC 是一种轻量级的远程过程调用(RPC)协议,它使用 JSON 作为数据格式。

json-rpc 的消息类型:

  • 请求(request)
{// jsonrpc协议版本"jsonrpc": "2.0",// 调用方法名"method": "methodName","params": {"a": 1,"b": 2},// 标识,与响应中id一致"id": 1
}
  • 响应(response)
{"jsonrpc": "2.0",// 调用结果,任何类型"result": {"c": 3},"id": 1
}

我们来看一个例子:

  • server.js
import util from './util.js'process.stdin.on('data', (data) => {const req = JSON.parse(data)const methodName = req.methodconst { a, b } = req.paramsconst result = util[methodName](a, b)const res = {jsonrpc: '2.0',result,id: req.id}process.stdout.write(JSON.stringify(res) + '\n')
})// util.js
export default {sum(a, b) {return a + b}
}

当我们执行 node server.js 的时候,在终端输入如下内容:

{ "jsonrpc": "2.0", "method": "sum", "params": { "a": 1, "b": 2 }, "id": 1 }

此时会返回 {"jsonrpc":"2.0","result":3,"id":1} 这个数据,这就完成了一次 JSON-RPC 的调用。

MCP 基本规范

如何通信

  • 通信方式:
    • stdio:推荐,优点是高效、简单、本地
    • http:可远程
  • 通信格式:基于 jsonrpc 的进一步规范

request 请求

{"jsonrpc": "2.0","id": 1,"method": "initialize", // 固定为initialize"params": {"protocolVersion": "2025-06-18", // mcp 协议版本"capabilities": {"roots": {"listChanged": true},"sampling": {},"elicitation": {}},"clientInfo": {// 告诉服务器,客户端的信息"name": "ExampleClient","title": "Example Client Title","version": "1.0.0"}}
}

response 响应

{"jsonrpc": "2.0","id": 1,"result": {"protocolVersion": "2025-06-18","capabilities": {// 服务器具有哪些功能"logging": {},"prompts": {"listChanged": true},"resources": {"subscribe": true,"listChanged": true},"tools": {"listChanged": true}},"serverInfo": {// 服务端信息"name": "ExampleServer","title": "Example Server Title","version": "1.0.0"},"instructions": "..."}
}

只要 MCP 的请求/响应格式是上面的 json 规范格式,就可以让 AI 去按照规范调用它。

MCP Server

MCP Tool

Tool 是 MCP 里的“执行者”,让 LLM 不只是进行语言输出,还能实际执行具体任务,本质上是服务器提供的一些函数或者 API。

用于执行具体的操作和计算,能够影响或修改外部系统的状态,比如进行数学计算、数据处理、API 调用、文件操作等。

服务器定义好工具后,客户端发现这些工具,LLM 可以根据需要调用,调用结果会返回给 LLM,以便其继续推理或者输出。

工具发现 tools/list

文档:https://modelcontextprotocol.io/specification/2025-06-18/server/tools

中文文档:https://docs.mcpcn.org/introduction

工具使模型能够与外部系统交互,例如查询数据库、调用 API 或执行计算。每个工具都由名称唯一标识,并包含描述其模式的元数据。

若要发现可用工具,客户端会发送工具/列表请求。

例如:

request 请求

{"jsonrpc": "2.0","id": 1,"method": "tools/list", // method固定"params": {"cursor": "optional-cursor-value"}
}

response 响应

{"jsonrpc": "2.0","id": 1,"result": {"tools": [{"name": "get_weather","title": "Weather Information Provider","description": "Get current weather information for a location","inputSchema": {"type": "object","properties": {"location": {"type": "string","description": "City name or zip code"}},"required": ["location"]}}],"nextCursor": "next-page-cursor"}
}

其实在 result.tools 中就是我们后续定义的 mcp 工具了。

工具调用 tools/call

要调用工具,客户端发送工具/调用请求

request 请求

{"jsonrpc": "2.0","id": 2,"method": "tools/call", // method 固定"params": {"name": "get_weather", // 请求的方法名,需要和工具列表中的result.tools的name一致"arguments": {// 工具参数"location": "New York"}}
}

response 响应

{"jsonrpc": "2.0","id": 2,"result": {"content": [{"type": "text", // 响应类型"text": "Current weather in New York:\nTemperature: 72°F\nConditions: Partly cloudy"}],"isError": false}
}

工具结果 Tool Result

文档:https://modelcontextprotocol.io/specification/2025-06-18/server/tools#tool-result

工具结果也就是工具响应的类型,上面例子中是 text,还有其他内容如下:

  • Text Content 文本内容
{"type": "text","text": "Tool result text"
}
  • Image Content 图片内容
{"type": "image","data": "base64-encoded-data","mimeType": "image/png""annotations": {"audience": ["user"],"priority": 0.9}}
  • Audio Content 音频内容
{"type": "audio","data": "base64-encoded-audio-data","mimeType": "audio/wav"
}
  • Resource Links 资源链接

工具可以返回指向资源的链接,以提供额外的上下文或数据。在这种情况下,该工具将返回一个可以由客户端订阅或获取的 URI:

{"type": "resource_link","uri": "file:///project/src/main.rs","name": "main.rs","description": "Primary application entry point","mimeType": "text/x-rust","annotations": {"audience": ["assistant"],"priority": 0.9}
}

还有一些别的返回类型结果,可以自行查阅文档。

编写一个 MCP Tool

MCP 提供了对应语言的 SDK,用来开发的 MCP 工具,来构建符合 MCP 规范的 jsonrpc 数据。

下面采用 ts 开发。

文档:https://modelcontextprotocol.io/docs/develop/build-server#node

github:https://github.com/modelcontextprotocol/typescript-sdk

  1. 初始化项目
pnpm init
  1. 安装依赖

这里需要安装的是 @modelcontextprotocol/sdkzod@3

pnpm add @modelcontextprotocol/sdk zod@3
# ts、node类型
pnpm add @types/node typescript -D
  1. 修改 package.json
{"type": "module","bin": {"weather": "./build/index.js"},"scripts": {"build": "tsc && chmod 755 build/index.js"},"files": ["build"]
}
  1. 根目录创建 tsconfig.json
{"compilerOptions": {"target": "ES2022","module": "Node16","moduleResolution": "Node16","outDir": "./build","rootDir": "./src","strict": true,"esModuleInterop": true,"skipLibCheck": true,"forceConsistentCasingInFileNames": true},"include": ["src/**/*"],"exclude": ["node_modules"]
}
  1. 新建 src/index.ts

这里以官网文档中的例子为例:需求是写一个天气查询的 MCP 工具

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import { z } from 'zod'
import {AlertsResponse,formatAlert,getWeatherRequest
} from './utils/index.js'// 创建服务器实例
const server = new McpServer({name: 'weather',version: '1.0.0'
})// 注册天气工具
server.tool('get-weather','获取某个城市的天气警报',{state: z.string().length(2).describe('中国某城市的名字(例如 合肥,南京)')},async ({ state }) => {const WEATHER_API_BASE = 'http://127.0.0.1:4000'const stateCode = state.toUpperCase()const alertsUrl = `${WEATHER_API_BASE}/alerts?area=${stateCode}`// 用于发送http请求,获取天气信息const alertsData = await getWeatherRequest<AlertsResponse>(alertsUrl)// 下面就是根据不同情况返回对应MCP格式的json数据了if (!alertsData) {return {content: [{type: 'text',text: '无法检索城市警报数据'}]}}const features = alertsData.features || []if (features.length === 0) {return {content: [{type: 'text',text: `没有 ${stateCode} 的活动警报`}]}}const formattedAlerts = features.map(formatAlert)const alertsText = `活动警报 ${stateCode}:\n\n${formattedAlerts.join('\n')}`return {content: [{type: 'text',text: alertsText}]}}
)async function main() {// 连接传输const transport = new StdioServerTransport()await server.connect(transport)console.error('Weather MCP Server running on stdio')
}main().catch((error) => {console.error('Fatal error in main():', error)process.exit(1)
})

utils/index.ts 文件:

const USER_AGENT = 'weather-app/1.0'// 用于发出天气查询API请求的辅助函数
export async function getWeatherRequest<T>(url: string): Promise<T | null> {const headers = {'User-Agent': USER_AGENT,Accept: 'application/geo+json'}try {const response = await fetch(url, { headers })if (!response.ok) {throw new Error(`HTTP error! status: ${response.status}`)}return (await response.json()) as T} catch (error) {console.error('Error making NWS request:', error)return null}
}export interface AlertFeature {properties: {event?: stringareaDesc?: stringseverity?: stringstatus?: stringheadline?: string}
}// 格式化警报数据
export function formatAlert(feature: AlertFeature): string {const props = feature.propertiesreturn [`Event: ${props.event || 'Unknown'}`,`Area: ${props.areaDesc || 'Unknown'}`,`Severity: ${props.severity || 'Unknown'}`,`Status: ${props.status || 'Unknown'}`,`Headline: ${props.headline || 'No headline'}`,'---'].join('\n')
}export interface ForecastPeriod {name?: stringtemperature?: numbertemperatureUnit?: stringwindSpeed?: stringwindDirection?: stringshortForecast?: string
}export interface AlertsResponse {features: AlertFeature[]
}export interface PointsResponse {properties: {forecast?: string}
}export interface ForecastResponse {properties: {periods: ForecastPeriod[]}
}
  1. 创建后端 server 来响应请求

这里以 express 为例,mock 一些数据:

const mockAlertsData = {features: [{properties: {event: '高温预警',areaDesc: '江苏省南京市',severity: 'Moderate',status: 'Actual',headline: '江苏省南京市高温预警,持续至晚上8点'}},{properties: {event: '洪水警报',areaDesc: '浙江省沿海地区',severity: 'Severe',status: 'Actual',headline: '因强降雨影响,浙江省沿海地区发布洪水警报'}}]
}const express = require('express')
const app = express()app.get('/alerts', (req, res) => {res.json(mockAlertsData)
})app.listen(4000, () => {console.log('Weather API listening on port 4000!')
})
  1. 对 MCP 进行打包
pnpm build
  1. 测试 MCP

MCP 通过 node build/index.js 来启动

测试方法 1:

package.json 中添加如下脚本:

{"scripts": {// 查看MCP工具列表"test": "echo '测试工具列表...' && echo '{\"jsonrpc\": \"2.0\", \"id\": 1, \"method\": \"tools/list\", \"params\": {}}' | node build/index.js",// 查询城市天气"test:weather": "echo '测试城市天气信息...' && echo '{\"jsonrpc\": \"2.0\", \"id\": 4, \"method\": \"tools/call\", \"params\": {\"name\": \"get-weather\", \"arguments\": {\"state\": \"南京\"}}}' | node build/index.js"}
}

测试结果:

在这里插入图片描述

测试方法 2:

用一个支持 MCP 的客户端开进行测试,例如:VSCode、Cursor、通义灵码(VSCode 插件)…

这里拿灵码测试,找到对应的 MCP 配置文件,有可视化地方进行选择的,打开的一般是 mcp.json 文件。

{"mcpServers": {"weather": {"command": "node","args": ["/Users/xxx/Desktop/demo/build/index.js"]}}
}

⚠️ 注意:args 参数需要填入上面 build 后的文件路径。

也可以将包发到 npm 上,然后通过 npx [包名] 来执行。

这里可以看到我们的 MCP 已经启动成功了:

在这里插入图片描述

我们只需要向它提问即可(一些支持 MCP 的客户端,就可以直接进行提问,它内部会去自动调用 MCP)

在这里插入图片描述

MCP Prompt

文档:https://modelcontextprotocol.io/specification/2025-06-18/server/prompts

模型上下文协议 (MCP) 为服务器提供了一种向客户端公开提示模板的标准化方法。提示允许服务器提供结构化消息和与语言模型交互的指令。客户端可以发现可用的提示,检索其内容,并提供参数来自定义它们。

Prompt 是 MCP 的“模板大师”,提供预定义的交互模式或者推理指引,是 LLM 的“剧本”,告诉它怎么开口、怎么思考。

主要用于定义与 LLM 交互的模板,指导 LLM 的输出。它可以提供结构化的提示模板,包含参数,用于标准化和共享常见的 LLM 交互,例如生成文本模板、定义对话流程、创建特定任务的指令等。

服务器定义好一堆 Prompt 模板,客户端可以直接选择一个,填入参数,然后交给 LLM 执行。

通常,提示将通过用户界面中用户启动的命令触发,这允许用户自然地发现和调用可用的提示。

例如,作为斜杠命令:

在这里插入图片描述

提示列表 prompts/list

request 请求

{"jsonrpc": "2.0","id": 1,"method": "prompts/list","params": {"cursor": "optional-cursor-value"}
}

response 响应

{"jsonrpc": "2.0","id": 1,"result": {"prompts": [{"name": "code-review","description": "审查代码以进行改进和发现问题","arguments": [{"name": "code","required": true}]}],"nextCursor": "next-page-cursor"}
}

获取提示 prompts/get

request 请求

{"jsonrpc": "2.0","id": 2,"method": "prompts/get","params": {"name": "code_review","arguments": {"code": "def hello():\n    print('world')"}}
}

response 响应

{"jsonrpc": "2.0","id": 2,"result": {"description": "Code review prompt","messages": [{"role": "user","content": {"type": "text","text": "Please review this Python code:\ndef hello():\n    print('world')"}}]}
}

编写一个 MCP Prompt

需求:编写一个代码 review 的 prompt

在之前的文件中,添加如下代码即可:

server.prompt('code-review','审查代码以进行改进和发现问题',{code: z.string()},({ code }) => ({messages: [{role: 'user',content: {type: 'text',text: `需要review的代码:\n\n${code}`}}]})
)

通过 / 命令,来测试:

在这里插入图片描述

MCP Resource

MCP 为服务器提供了一种标准化的方式来公开 资源给客户。资源允许服务器共享数据,这些数据向 语言模型,例如文件、数据库模式或特定于应用程序的信息。 每个资源都有 URI。

Resource 是 MCP 的"知识库管理员",提供对各种数据源和信息资产的统一访问接口,是 LLM 的"外部记忆",让它能够访问和检索存储在外部系统中的信息。

例如,应用程序可以:

  • 通过 UI 元素在树视图或列表视图中公开资源以供显式选择
  • 允许用户搜索和筛选可用资源
  • 根据启发式或 AI 模型的选择实现自动上下文包含

在平时使用 ai 的时候,有添加文件的作为上下文,这个就是 MCP Resource。

在这里插入图片描述

资源列表 resources/list

request 请求

{"jsonrpc": "2.0","id": 1,"method": "resources/list","params": {"cursor": "optional-cursor-value"}
}

response 响应

{"jsonrpc": "2.0","id": 1,"result": {"resources": [{"uri": "file:///project/src/main.rs","name": "main.rs","title": "Rust Software Application Main File","description": "Primary application entry point","mimeType": "text/x-rust"}],"nextCursor": "next-page-cursor"}
}

读取资源 resources/read

request 请求

{"jsonrpc": "2.0","id": 2,"method": "resources/read","params": {"uri": "file:///project/src/main.rs"}
}

response 响应

{"jsonrpc": "2.0","id": 2,"result": {"contents": [{"uri": "file:///project/src/main.rs","name": "main.rs","title": "Rust Software Application Main File","mimeType": "text/x-rust","text": "fn main() {\n    println!(\"Hello world!\");\n}"}]}
}

这部分也是调用 sdk 来实现即可,就不具体演示了。

MCP Client

官网的例子是使用 Claude 的大模型来完成的,这里我们自己调用其他的大模型来测试。

只要大模型支持 MCP 即可,这里使用的是 open routerqwen3(免费 😂)

所谓的 mcp client,可以想象是一个 ai 应用,服务端会先和 mcp server 进行连接(这里是 mcp client 通过命令启动一个 mcp server,具体代码有演示),获取到所有的 mcp 工具,将这些工具组装成 function call,这是准备工作。

当我们每次对话,将我们的消息,和调用的工具一起发给大模型:

const res = this.openai.chat.completions.create({model,messages,tools
})// [
//   {
//     id: 'chatcmpl-tool-13b3970424f24a2fb175b36ba6f1bd6b',
//     type: 'function',
//     index: 0,
//     function: { name: 'get-weather', arguments: '{"state": "南京"}' }
//   }
// ]

此时大模型会返回当前调用的工具,例如:

const mcpCallResult = await this.mcp.callTool({name: toolName,arguments: toolArgs
})

调用后的结果,一并同上下文 message 一起再次发给大模型,此时大模型就会根据这个 mcp 工具,来返回对应的结果。

对于大模型是如何能够根据我们的提示找到对应的 mcp 工具,其中一点是需要给 mcp 工具写好对应的描述,然后就是靠大模型自身了,内部是如何运转的这里就不深究了。

这里采用 open router + openai sdk 来实现一个 mcp client:

  1. 初始化
pnpm init
  1. 修改 package.json
{"type": "module","scripts": {"build": "tsc && chmod 755 build/index.js"}
}
  1. 安装依赖
pnpm i @modelcontextprotocol/sdk openai dotenv
pnpm i @types/node typescript -D
  1. 新增 tsconfig.json
{"compilerOptions": {"target": "ES2022","module": "Node16","moduleResolution": "Node16","outDir": "./build","rootDir": "./","strict": true,"esModuleInterop": true,"skipLibCheck": true,"forceConsistentCasingInFileNames": true},"include": ["index.ts"],"exclude": ["node_modules"]
}
  1. 新增 .env 文件

这个需要在 https://openrouter.ai/ 官网去注册

OPENROUTER_API_KEY="YOUR_OPENROUTER_API_KEY"
  1. 新增 index.ts

这个文件的注释也比较完善,流程就和上面说的一样,这部分就略过了。

import OpenAI from 'openai'
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
import readline from 'readline/promises'
import dotenv from 'dotenv'dotenv.config()const OPENROUTER_API_KEY = process.env.OPENROUTER_API_KEY
const model = 'qwen/qwen3-235b-a22b:free'class MCPClient {private mcp: Clientprivate openai: OpenAIprivate transport: StdioClientTransport | null = nullprivate tools: OpenAI.Chat.Completions.ChatCompletionTool[] = []constructor() {this.openai = new OpenAI({baseURL: 'https://openrouter.ai/api/v1',apiKey: OPENROUTER_API_KEY})this.mcp = new Client({ name: 'mcp-client-cli', version: '1.0.0' })}// 连接mcp serverasync connectToServer(serverScriptPath: string) {try {const command = process.execPath// client创建一个新的mcp server进程,来进行mcp server调用this.transport = new StdioClientTransport({command, // command: "node",args: [serverScriptPath] // args: ["server.js"]})await this.mcp.connect(this.transport)// 获取到mcp server所有工具const toolsResult = await this.mcp.listTools()// 在openai的标准中,调用工具是采用的是function calling// mcp也就是在function calling上套了一层协议this.tools = toolsResult.tools.map((tool) => {return {type: 'function',function: {name: tool.name,description: tool.description,parameters: tool.inputSchema}}})// 输出mcp工具console.log('Connected to server with tools:',this.tools.map((tool) => (tool as any).function?.name || '') // [ 'get-weather' ])} catch (e) {console.log('Failed to connect to MCP server: ', e)throw e}}// 处理查询逻辑async processQuery(query: string) {const messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [{role: 'user',content: query}]// 将我们的参数组合成消息发送给大模型/*** response.choices[0].message中包含大模型返回的数据信息* {*      "role": "assistant",*      "content": "The weather in Beijing is 25 degrees Celsius and sunny.",*      "tool_calls": [{id: 'chatcmpl-tool-13b3970424f24a2fb175b36ba6f1bd6b',type: 'function',index: 0,function: { name: 'get-weather', arguments: '{"state": "南京"}' }}],...* }*/const toolResponse = await this.openai.chat.completions.create({model,messages,tools: this.tools,tool_choice: 'auto' // 判断是否需要调用工具})/*** toolCalls会展示出工具调用* [{id: 'chatcmpl-tool-13b3970424f24a2fb175b36ba6f1bd6b',type: 'function',index: 0,function: { name: 'get-weather', arguments: '{"state": "南京"}' }}]*/const toolCalls = toolResponse.choices[0].message.tool_calls// 收集结果const finalText = []if (toolCalls && toolCalls.length > 0) {for (const toolCall of toolCalls) {// 这里只是做ts类型判断if (toolCall.type !== 'function') continueconst toolName = toolCall.function.name// 参数解析const toolArgs = JSON.parse(toolCall.function.arguments)finalText.push(`[🚀 Calling tool ${toolName} with args ${toolCall.function.arguments}]`)// mcp工具调用结果/*** {content: [{type: 'text',text: '活动警报 南京:\n' +'\n' +'Event: 高温预警\n' +'Area: 江苏省南京市\n' +'Severity: Moderate\n' +'Status: Actual\n' +'Headline: 江苏省南京市高温预警,持续至晚上8点\n' +'---\n' +'Event: 洪水警报\n' +'Area: 浙江省沿海地区\n' +'Severity: Severe\n' +'Status: Actual\n' +'Headline: 因强降雨影响,浙江省沿海地区发布洪水警报\n' +'---'}]}*/const mcpCallResult = await this.mcp.callTool({name: toolName,arguments: toolArgs})messages.push(toolResponse.choices[0].message)messages.push({role: 'tool',content:typeof mcpCallResult.content === 'string'? mcpCallResult.content: JSON.stringify(mcpCallResult.content),tool_call_id: toolCall.id})// 将mcp工具调用结果,和上下文信息交给大模型,重新调用提问const response = await this.openai.chat.completions.create({model,messages})finalText.push(response.choices[0].message.content || '')}}return finalText.join('\n')}// 交互式聊天循环,负责处理用户与mcp客户端之间的持续对话async chatLoop() {// 获取输入信息const rl = readline.createInterface({input: process.stdin,output: process.stdout})try {while (true) {const message = await rl.question('\nQuery: ')if (message.toLowerCase() === 'quit') {break}const responseMessage = await this.processQuery(message)console.log('\n' + responseMessage)}} finally {rl.close()}}async cleanup() {await this.mcp.close()}
}async function main() {if (process.argv.length < 3) {console.log('Usage: node build/index.js <path_to_server_script>')return}const mcpClient = new MCPClient()try {await mcpClient.connectToServer(process.argv[2])await mcpClient.chatLoop()} catch (error) {console.error('Error in main:', error)} finally {await mcpClient.cleanup()process.exit(0)}
}main()
  1. 构建 ts
pnpm build
  1. 此时我们还需要一个 server 端,来为 mcp server 提供数据

这里的 mcp server 采用的是上面的城市天气查询的例子

至于我们如何将我们的 mcp client 与 mcp server 进行连接对应呢,是通过后续的启动命令来的。(因为 mcp client 会通过命令脚本来启动一个 mcp server)

这里还是采用 express 来实现一个 api 接口,【城市天气查询】例子也有说,可以略过。

const mockAlertsData = {features: [{properties: {event: '高温预警',areaDesc: '江苏省南京市',severity: 'Moderate',status: 'Actual',headline: '江苏省南京市高温预警,持续至晚上8点'}},{properties: {event: '洪水警报',areaDesc: '浙江省沿海地区',severity: 'Severe',status: 'Actual',headline: '因强降雨影响,浙江省沿海地区发布洪水警报'}}]
}const express = require('express')
const app = express()app.get('/alerts', (req, res) => {console.log(mockAlertsData)res.json(mockAlertsData)
})app.listen(4000, () => {console.log('Weather API listening on port 4000!')
})
  1. 启动项目
node build/index.js 【mcp server文件路径 -> 要求绝对路径,用于mcp client启动mcp server的】
# 例如:
node build/index.js /Users/xxx/Desktop/mcp-demo/build/index.js

在这里插入图片描述

这也和我们模拟的数据是一致的:

在这里插入图片描述

MCP 调试

在上面【编写一个 MCP Tool】中说了两种测试的方法,下面介绍一种官方文档中的测试方法。

运行如下命令:

npx @modelcontextprotocol/inspector

要求:node 大于 22.7.5

npm:https://www.npmjs.com/package/@modelcontextprotocol/inspector

连接检测

执行完成后会自动 http://localhost:6274/,界面如下:

在这里插入图片描述

如图的:Command 就是启动 mcp 的命令,Arguments 就是启动 mcp 的参数。我这里是 node dist/index.js,路径是当前执行 npx @modelcontextprotocol/inspector 的相对路径。

我们点击 Connect 按钮,会发送请求链接 mcp,然后我们在 History 中可以查看到请求的内容。

工具发现检测

在这里插入图片描述

工具调用检测

在这里插入图片描述

MCP 实战

在前言中提到一些 AI Coding 的一些缺点,那我们如何利用 MCP 去解决呢?

流程是这样的:原型需求文档 -> UI设计 -> 任务确认 -> AI 生成代码 -> 如果需要登录态,通过 PlayWright 进行登录获取 Cookie -> 错误捕获,重新“丢”给 AI -> 自检通过,执行结束

  • 原型需求文档:通过内部Cli工具,在生成模版的时候去自动拉去需求文档,添加到项目的Prompt.md 中,MCP 让 AI 去读取该 md 文件,通过 cli 也提供了一些添加需求的命令,来减少手动修改 md 文件的步骤

  • 错误捕获包括:终端运行时错误、浏览器错误

    • 浏览器错误可以用 PlayWright 监听consolepageerrorrequestfailed这些事件
    • console:监听控制台消息(包括 log、warn、error 等类型)
    • pageerror:监听页面未捕获的 JavaScript 异常
    • requestfailed:监听网络请求失败事件

目前 AI 编辑器功能越来越强,像 Trae 就能进行流程自检

让我们正式开始吧!

  1. 初始化项目
pnpm init
  1. 安装依赖
# playwright不能安装在开发依赖中的,打包会没有哦
pnpm i @modelcontextprotocol/sdk playwright axios
pnpm i -D @types/node typescript rimraf
  1. 项目根目录创建 tsconfig.json
{"compilerOptions": {"target": "ES2022","module": "ESNext","moduleResolution": "node","allowSyntheticDefaultImports": true,"esModuleInterop": true,"allowJs": true,"outDir": "./dist","rootDir": "./src","strict": true,"noImplicitAny": true,"strictNullChecks": true,"strictFunctionTypes": true,"noImplicitReturns": true,"noFallthroughCasesInSwitch": true,"noUncheckedIndexedAccess": true,"noImplicitOverride": true,"exactOptionalPropertyTypes": true,"noPropertyAccessFromIndexSignature": true,"noImplicitThis": true,"alwaysStrict": true,"skipLibCheck": true,"forceConsistentCasingInFileNames": true,"declaration": true,"declarationMap": true,"sourceMap": true,"removeComments": false,"importHelpers": true,"experimentalDecorators": true,"emitDecoratorMetadata": true,"resolveJsonModule": true,"isolatedModules": true,"verbatimModuleSyntax": false,"baseUrl": ".","paths": {"src/*": ["./src/*"]}},"include": ["src/**/*"],"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
}
  1. 修改 package.json
{"main": "dist/index.js","type": "module","bin": {"generate-llm-mcp": "dist/index.js"},"scripts": {"build": "npm run clean && tsc","start": "node dist/index.js","test": "echo '测试工具列表...' && echo '{\"jsonrpc\": \"2.0\", \"id\": 1, \"method\": \"tools/list\", \"params\": {}}' | node dist/index.js","clean": "rimraf dist"}
}
  1. 创建 src/index.ts

目的是创建一个 MCP Server(new Server

#!/usr/bin/env nodeimport { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import {CallToolRequestSchema,ListToolsRequestSchema,CallToolRequest,ListToolsRequest
} from '@modelcontextprotocol/sdk/types.js'
import { tools } from './tools/index.js'
import { logger } from './utils/index.js'class GenerateLLMServer {private server: Serverconstructor() {this.server = new Server({name: 'generate-llm-mcp',version: '1.0.0'})this.setupToolHandlers()logger.info('GenerateLLM MCP 服务器初始化完成')}private setupToolHandlers(): void {// 工具列表处理器this.server.setRequestHandler(ListToolsRequestSchema,async (_request: ListToolsRequest) => {logger.debug(`列出可用工具,共 ${tools.length}`)return {tools: tools.map((tool) => tool.config)}})// 工具调用处理器this.server.setRequestHandler(CallToolRequestSchema,async (request: CallToolRequest): Promise<any> => {const { name, arguments: args } = request.paramslogger.info(`调用工具: ${name}`)logger.debug(`工具参数: ${JSON.stringify(args)}`)try {const tool = tools.find((tool) => tool.config.name === name)if (!tool) {throw new Error(`未知工具: ${name}`)}const result = await tool.fn(args)logger.info(`工具 ${name} 执行成功`)return result} catch (error) {const errorMessage =error instanceof Error ? error.message : String(error)logger.error(`工具 ${name} 执行失败: ${errorMessage}`)return {content: [{type: 'text',text: `错误: ${errorMessage}`}]}}})}public async run(): Promise<void> {try {const transport = new StdioServerTransport()await this.server.connect(transport)logger.info('GenerateLLM MCP 服务器已启动并监听连接')} catch (error) {logger.error('服务器启动失败', error as Error)process.exit(1)}}
}const server = new GenerateLLMServer()
server.run().catch(console.error)
  1. 创建 src/tools/auth.ts

目的是创建一个获取登录态的 MCP Tool。

思路是借助 PlayWright 打开登录态 URL,把 Cookie 塞入上下文。

async function checkAuth(params: { url?: string } = {}): Promise<ToolResponse> {// 创建浏览器实例并启动const browserAuth = new BrowserAuthorization()try {await browserAuth.start()if (params.url) {// 运行浏览器并等待cookie变化const cookies = await browserAuth.run(params.url)// 成功获取到cookies后返回结果/*** 直接返回cookies对象的JSON字符串,这样其他工具可以直接使用* 格式:{"cookieName":{"name":"cookieName","value":"cookieValue","domain":"..."},...}*/return createSuccessResponse(JSON.stringify(cookies))} else {return createErrorResponse('请提供要检查的URL')}} catch (error) {return createErrorResponse(error instanceof Error ? error.message : '未知错误')} finally {await browserAuth.stop()}
}export default defineTool({name: 'check_auth',description:'该工具的作用是检查用户是否已经授权,用户的登录信息(登录态),项目的登录信息都存储在cookie中,该mcp工具会自动打开浏览器,进入项目登录页,当用户登录成功后,该mcp工具会自动获取cookie,将内容返回,并做全局存储。当发现接口返回的请求响应码中包含未登录信息,或者返回的内容是未登录、登录状态失效等内容,也需要调用该工具进行重新登录。',inputSchema: {type: 'object',properties: {url: {type: 'string',description: '登录态的网页URL',format: 'uri'},cookies: {type: 'string',description: '暴露的登录态cookie信息'}},required: ['url', 'cookies']}},checkAuth
)

BrowserAuthorization 类的实现:

封装了常用的方法,例如 start启动、run运行等。

import * as playwright from 'playwright'class BrowserAuthorization extends BaseBrowser {currentCookies: { [name: string]: playwright.Cookie } = {}private cookieChangedCallback:| ((cookies: { [name: string]: playwright.Cookie }) => void)| null = nullpublic override async start(): Promise<void> {this.browser = await playwright.chromium.launch({headless: false, // 有头模式,方便用户登录channel: 'chrome'})this.context = await this.browser.newContext()this.page = await this.context.newPage()}public async run(url: string): Promise<any> {// 让用户登录await this.page?.goto(url)// await this.initCookie();// 设置cookie变化监听器this.setupCookieListeners()// 返回一个Promise,当cookie变化时resolvereturn new Promise((resolve) => {// 设置cookie变化的回调函数this.cookieChangedCallback = (cookies) => {resolve(cookies)}})}private setupCookieListeners() {// 监听响应,可能会有cookie的变化this.page?.on('response', async (response) => {if (response.headers()['set-cookie']) {await this.checkChanges()}})// 监听跳转的load事件,也可能会有cookie的变化this.page?.on('load', async () => {await this.checkChanges()})// 监听页面的请求完成事件this.page?.on('requestfinished', async () => {await this.checkChanges()})}/*** 初始化清空cookie*/private async initCookie() {await this.page?.context().clearCookies()}/*** 检查cookie变化*/private async checkChanges() {const newCookies = await this.page?.context().cookies()const newCookiesMap: { [name: string]: playwright.Cookie } = {}newCookies?.forEach((cookie) => {newCookiesMap[cookie.name] = { ...cookie }})// 检测变化let hasNewOrChangedCookie = falsefor (const [name, newCookie] of Object.entries(newCookiesMap)) {const oldCookie = this.currentCookies[name]if (!oldCookie) {console.log(`[Cookie 新增] ${name}=${newCookie.value}`)hasNewOrChangedCookie = true} else if (oldCookie.value !== newCookie.value) {console.log(`[Cookie 修改] ${name}: ${oldCookie.value}${newCookie.value}`)hasNewOrChangedCookie = true}}// 检测删除for (const [name, _oldCookie] of Object.entries(this.currentCookies)) {if (!newCookiesMap[name]) {console.log(`[Cookie 删除] ${name}`)}}this.currentCookies = newCookiesMap// 如果检测到新的或变化的cookie,并且回调函数存在,则调用回调函数if (hasNewOrChangedCookie && this.cookieChangedCallback) {this.cookieChangedCallback(newCookiesMap)// 清空回调函数,防止重复调用this.cookieChangedCallback = null}}public getCookies() {return this.currentCookies}
}

BaseBrowser 基类的实现:

export class BaseBrowser {protected browser: Browser | null = nullprotected context: playwright.BrowserContext | null = nullprotected page: Page | null = nullconstructor() {// 不在构造函数中启动,避免意外}public async start() {if (!this.browser) {this.browser = await chromium.launch({headless: false,channel: 'chrome'})}if (!this.page) {this.context = await this.browser.newContext()// 通过context创建的page会保留上下文this.page = await this.context.newPage()}}public async stop() {try {if (this.page) {await this.page.close()this.page = null}} catch (error) {process.stderr.write(`Error closing page: ${error}\n`)}try {if (this.browser) {await this.browser.close()this.browser = null}} catch (error) {process.stderr.write(`Error closing browser: ${error}\n`)}}public getPageInstance() {return this.page}public getBrowserInstance() {return this.browser}
}
  1. 创建 src/tools/console.ts 文件

作用是监控浏览器控制台的异常,并“丢给”AI。

/*** 控制台日志* @returns {使用 PlayWright 监控浏览器控制台日志, 返回错误日志内容}*/
async function console(params: {url?: stringcookies?: stringincludeConsoleLogs?: booleanincludeErrors?: boolean} = {}
): Promise<ToolResponse> {const monitor = new ConsoleMonitor()try {// 处理cookies参数,支持多种格式let cookiesArray: playwright.Cookie[] = []if (params.cookies) {try {cookiesArray = parseCookieString(params.cookies, params.url)} catch (error) {return createErrorResponse(`Cookie格式解析错误: ${error}`)}}await monitor.start(cookiesArray)await monitor.run(params.includeConsoleLogs || false,params.includeErrors !== false)if (params.url) {try {const page = monitor.getPageInstance()await page?.goto(params.url)// 等待一段时间以确保所有资源加载完成await new Promise((resolve) => setTimeout(resolve, 2000))} catch (error) {return createErrorResponse(`错误: ${error}`)}}const text = monitor.getLogsToString()return createSuccessResponse(text)} finally {await monitor.stop()}
}export default defineTool({name: 'monitor_console_log',description:'打开页面,监控浏览器控制台日志,需要监控错误的日志,捕获页面的错误信息,返回对应的错误内容。如果上个步骤,或者上文有cookie,需要写入到该mcp的参数中',inputSchema: {type: 'object',properties: {url: {type: 'string',description: '要监控的网页URL',format: 'uri'},cookies: {type: 'string',description: '网站登录的Cookie信息'},includeConsoleLogs: {type: 'boolean',description: '是否包含控制台日志(console.log)',default: false},includeErrors: {type: 'boolean',description: '是否包含错误信息(console.error 和 pageerror)',default: true}},required: ['url', 'cookies']}},console
)

ConsoleMonitor 类的实现:

作用是使用 Playwright 向项目的页面注入 Cookie,并监听控制台日志和错误信息,如果有错误信息,则返回给 AI。

class ConsoleMonitor extends BaseBrowser {private consoleLogs: ConsoleMessage[] = []constructor() {// 不在构造函数中启动,避免意外super()}public override async start(cookies: playwright.Cookie[] = []) {this.browser = await playwright.chromium.launch({headless: false, // 有头模式,方便用户登录channel: 'chrome'})this.context = await this.browser.newContext()this.page = await this.context.newPage()// 添加cookieif (this.context && cookies.length > 0) {await this.context.addCookies(cookies)}}/*** 页面监控* 该函数负责设置页面事件监听器,用于捕获和处理页面中的控制台消息* 和JavaScript错误。通过监听'console'和'pageerror'事件,将捕获到的* 消息转换为统一的格式并进行处理。** 监听的事件类型:* - console: 监听控制台消息(包括log、warn、error等类型)* - pageerror: 监听页面未捕获的JavaScript异常* - requestfailed: 监听网络请求失败事件** @param includeConsoleLogs - 是否包含控制台日志* @param includeErrors - 是否包含错误信息* @returns {Promise<void>} - 返回一个Promise,表示异步操作完成*/public async run(includeConsoleLogs: boolean = false,includeErrors: boolean = true): Promise<void> {// 监听控制台消息this.page?.on('console', async (event) => {const messageType = event.type()if ((includeErrors && messageType === 'error') ||(includeConsoleLogs &&['log', 'info', 'warn', 'debug'].includes(messageType))) {this.handleConsoleMessage(this.messageToConsoleMessage(event))}})// 捕获并记录页面抛出的JavaScript错误if (includeErrors) {this.page?.on('pageerror', async (error) =>this.handleConsoleMessage(this.pageErrorToConsoleMessage(error)))}// 监听网络请求失败事件if (includeErrors) {this.page?.on('requestfailed', async (request) => {const failure = request.failure()if (failure) {const message: ConsoleMessage = {type: 'error',text: `网络请求失败: ${request.url()} - ${failure.errorText}`,toString: () =>`[NETWORK] 网络请求失败: ${request.url()} - ${failure.errorText}`}this.handleConsoleMessage(message)}})}}/*** 处理控制台消息* @param message - 要处理的控制台消息对象*/private handleConsoleMessage(message: ConsoleMessage) {this.consoleLogs?.push(message)}/*** 将Playwright的控制台消息转换为内部ConsoleMessage格式* @param message - Playwright的控制台消息对象* @returns 转换后的ConsoleMessage对象,包含类型、文本和格式化字符串表示*/private messageToConsoleMessage(message: playwright.ConsoleMessage): ConsoleMessage {return {type: message.type(),text: message.text(),// 使用统一的格式:[类型] 文本 @ URL:行号toString: () =>`[${message.type().toUpperCase()}] ${message.text()} @ ${message.location().url}:${message.location().lineNumber}`}}/*** 将页面错误转换为控制台消息对象* @param error - 发生的错误对象,可以是Error实例或任何其他类型的值* @returns 返回包含错误信息的控制台消息对象*/private pageErrorToConsoleMessage(error: Error | any): ConsoleMessage {if (error instanceof Error) {return {type: 'error',text: error.message,toString: () => error.stack || error.message}}return {type: 'error',text: String(error),toString: () => String(error)}}public getLogs() {return this.consoleLogs || []}public getLogsToString() {return this.consoleLogs.join('\n') || ''}
}

parseCookieString 方法:用于解析 Cookie 字符串(因为上下文信息的内容是字符串,所以需要解析后才能塞入 Cookie)

/*** 解析cookie字符串,支持多种格式* @param cookieString - cookie字符串,可能是JSON格式或浏览器cookie格式* @param url - 目标URL,用于推断域名* @returns 返回Playwright所需的cookie对象数组*/
function parseCookieString(cookieString: string,url?: string
): playwright.Cookie[] {// 首先尝试解析为JSON格式try {const parsed = JSON.parse(cookieString)// 如果解析后的结果有cookies属性,说明是包装格式 {"cookies": {...}}if (parsed.cookies) {return Object.values(parsed.cookies) as playwright.Cookie[]} else if (typeof parsed === 'object' && parsed !== null) {// 直接是cookie对象格式 {"cookieName": {...}, ...}return Object.values(parsed) as playwright.Cookie[]}} catch (error) {// JSON解析失败,尝试解析为浏览器cookie字符串格式}// 从URL推断域名let domain = '.haiziwang.com' // 默认域名if (url) {try {const urlObj = new URL(url)const hostname = urlObj.hostname// 如果是子域名,使用顶级域名if (hostname.includes('.')) {const parts = hostname.split('.')if (parts.length >= 2) {domain = '.' + parts.slice(-2).join('.')}} else {domain = hostname}} catch (error) {// URL解析失败,使用默认域名}}// 解析浏览器cookie字符串格式: "name1=value1; name2=value2; ..."const cookies: playwright.Cookie[] = []const cookiePairs = cookieString.split(';')for (const pair of cookiePairs) {const trimmedPair = pair.trim()if (trimmedPair) {const [name, value] = trimmedPair.split('=')if (name && value !== undefined) {cookies.push({name: name.trim(),value: value.trim(),domain: domain,path: '/',expires: -1, // 会话cookiehttpOnly: false,secure: false,sameSite: 'Lax'})}}}return cookies
}
  1. 创建 src/tools/request.ts 文件

该文件用于接口错误参数校验的工具,基于monitor_console_log工具,当发现到网络请求的错误后,去重新请求,把请求的信息结果返回

async function sendRequest(params: {url: stringmethod?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'headers?: Record<string, string>body?: anytimeout?: number
}): Promise<ToolResponse> {const { url, method = 'GET', headers = {}, body, timeout = 5000 } = paramstry {// 构建请求配置const config: any = {url,method,headers,timeout,validateStatus: () => true // 接受所有状态码,便于分析错误}// 如果有请求体且方法允许,则添加请求体if (body && ['POST', 'PUT', 'PATCH'].includes(method.toUpperCase())) {// 设置默认Content-Typeif (!headers['Content-Type']) {headers['Content-Type'] = 'application/json'}config.data = body}// 发送请求const response = await axios(config)// 返回详细响应信息const result = {status: response.status,statusText: response.statusText,headers: response.headers,data: response.data,url: response.config?.url,method: response.config?.method?.toUpperCase()}return createSuccessResponse(JSON.stringify(result, null, 2))} catch (error: any) {// 处理请求错误if (error.isAxiosError) {const errorInfo = {message: error.message,code: error.code,url: error.config?.url,method: error.config?.method?.toUpperCase(),response: error.response? {status: error.response.status,statusText: error.response.statusText,headers: error.response.headers,data: error.response.data}: null}return createSuccessResponse(`请求失败:
${JSON.stringify(errorInfo, null, 2)}`)}// 处理其他错误return createErrorResponse(error instanceof Error ? error : new Error(String(error)))}
}export default defineTool({name: 'validate_api_error',description:'验证API接口错误返回信息。当通过monitor_console_log工具捕获到失败的接口请求时,使用此工具重新发送请求以验证错误详情,帮助识别由于不同错误代码导致的问题。',inputSchema: {type: 'object',properties: {url: {type: 'string',description: '要请求的API地址',format: 'uri'},method: {type: 'string',description: 'HTTP请求方法',enum: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],default: 'GET'},headers: {type: 'object',description: '请求头信息',additionalProperties: { type: 'string' }},body: {type: 'object',description: '请求体内容(仅适用于POST、PUT、PATCH请求)'},timeout: {type: 'number',description: '请求超时时间(毫秒)',default: 5000}},required: ['url']}},sendRequest
)
  1. 最后进行打包

需要在如果是本地的话可以使用node或者npx

npx 的话必须要在package.json中添加bin字段:

{"bin": {"generate-llm-mcp": "dist/index.js"}
}

npx 的主要作用是临时执行 npm 包中的可执行文件

如果要让别人使用你开发的 MCP 包,那就需要把包上传至 npm 仓库中即可,然后通过 npx 包名 运行使用。

也可以在 Cursor 这些 ide 中添加配置:

{"mcpServers": {"generate-llm-mcp": {"command": "npx","args": ["-y", "generate-llm-mcp"]}}
}

补充:npx

npx 是什么?

npx 是一个命令行工具,它是 npm 5.2.0 版本中新增的功能。它允许用户在不安装全局包的情况下,运行已安装在本地项目中的包或者远程仓库中的包。

npx 的作用是在命令行中运行 node 包中的可执行文件,而不需要全局安装这些包。这可以使开发人员更轻松地管理包的依赖关系,并且可以避免全局污染的问题。

npx 的优势

  • 避免全局安装:npx 允许你执行 npm 包,而不需要你先全局安装它
  • 总是使用最新版本:如果你没有在本地安装相应的 npm 包,npx 会从 npm 的 package 仓库中下载并使用最新版
  • 执行任意 npm 包:npx 不仅可以执行在 package.jsonscripts 部分定义的命令,还可以执行任何 npm 包

npm 和 npx 区别

  • npm 侧重于安装或者卸载某个模块的。重在安装,并不具备执行某个模块的功能。
  • npx 侧重于执行命令的,执行某个模块命令。虽然会自动安装模块,但是重在执行某个命令

npx 的运行规则和 npm 是一样的 本地目录查找 .bin 看有没有 如果没有就去全局的 node_moduels 查找,如果还没有就去下载这个包然后运行命令,然后删除这个包

所以用 npx 安装的模块,安装完就会删除,是一次性的

http://www.dtcms.com/a/435361.html

相关文章:

  • 网站如何做前后端分离网络营销课程学什么
  • 自己手机怎么免费做网站安徽四建东方建设有限公司网站
  • 域名后缀cn做网站家装设计师需要考证吗
  • 广州网站vi设计报价网站建设 图片问题
  • 数据安全能力成熟度模型 (DSMM) 核心要点
  • 飞算JavaAI 使用指南
  • 打鱼跟电子游戏网站怎么做深圳网站制作公司兴田德润信任高
  • 企业级桌面软件如何通过UI/UE设计提升生产效率?
  • 企业级网站内容管理系统学用mvc4做网站
  • 4-7〔O҉S҉C҉P҉ ◈ 研记〕❘ WEB应用攻击▸文件上传漏洞-B
  • 大模型PEFT参数高效微调-详解
  • 设置网站关键词怎么做济南网站优化网站
  • 网站建设属于IT吗备案网站ip
  • 网站营销成功的案例温州 建网站
  • 使用python的围棋落子策略
  • 做网站的职位叫什么wordpress页面无法更新
  • Eclipse 创建 Java 包
  • K8s学习笔记(十) Deployment 副本控制器
  • daily notes[68]
  • SPI Flash基础知识点 (chatgpt版本)
  • 昆明城乡和住房建设局网站做网站公司的收费多少
  • LLM 笔记 —— 02 大语言模型能力评定
  • 做网站视频用哪个视频编辑软件网站设计相似侵权吗
  • [特殊字符]灵感补给站 | pinterest 设计灵感分享 UI版面设计3
  • 网站主机一般选哪种的远程访问群晖wordpress
  • Edit Starts找不到Mybatis Framwork依赖
  • 【C++算法】类与对象通用题目解析分享
  • C语言中原子操作(简要)
  • 珠宝行业做网站的好处网站建设平台有哪些 谢谢平台建站
  • OpenOCD 终端使用指令大全