filmorago pro会员解锁专业版
84.37M · 2025-10-04
在公司实习的时候,内部在大力推动 AI 编程,虽然市面上有很多 AI Coding 的产品,例如 Cursor、Trae 等。
对于从 0 开始的项目,或者一个小需求的修改,能很完美的达到我们预期的效果,但是如果我们只面对 LLM 的对话框,不打开浏览器,只让 AI 进行调试(对于前端项目,目的是让产品、后端能进行快速开发上线),就会有以下的问题:
光是这三点,是 AI Coding 遇到的痛点问题,为了解决目前的问题,最终采用 MCP 工具,来加强 AI 纠错、自动化的问题。
在学习 MCP 之前我们需要先了解一下什么是 Function Calling,它和 MCP 的关系。
在 LLM 领域,Function Calling(函数调用)
指的是模型根据用户需求,主动触发并调用外部工具(如 API、数据库、计算器、第三方服务等)的能力,本质是让 AI 从 “纯文本生成” 升级为 “能执行具体任务的工具使用者”,解决 LLM 自身无法处理的信息滞后、实时计算、数据查询等问题。
tools
参数)。大语言模型的核心能力是 “理解文本 + 生成文本”,但存在天然短板:
Function Calling
正是为弥补这些短板而生 —— 让 AI 成为 “任务协调者”:先理解用户需求,判断是否需要调用工具,再按规则生成工具调用指令,最后将工具返回的结果整理成自然语言回答。
完整的函数调用通常分为 4 个关键步骤,形成 “用户需求
→ AI 决策
→ 工具执行
→ 结果反馈
” 的闭环:
开发者需先明确 “AI 可调用的工具清单”,并为每个工具定义标准化的函数参数格式(告诉 AI “调用这个工具需要传哪些信息”)。
例:若要调用 “实时天气 API”,需定义函数结构:
// 函数名:getRealTimeWeather
// 参数说明:
// - city: 城市名称(字符串,必填)
// - date: 日期(格式YYYY-MM-DD,可选,默认当天)
function getRealTimeWeather(
city: string,
date?: string
): {
temperature: number
condition: string
wind: string
} {
// 调用外部天气API的逻辑
return { temperature: 25, condition: '晴天', wind: '3级' }
}
用户输入需求后,模型先分析:“这个需求能否仅用自身知识回答?还是需要调用工具?”
若需调用工具,模型会按提前定义的格式,生成结构化的调用指令(避免歧义,确保工具能解析)。
例:针对 “北京 2025 年 9 月 26 日的天气”,模型生成指令:
{
"name": "getRealTimeWeather", # 要调用的函数名
"parameters": {
"city": "北京",
"date": "2025-09-26"
}
}
MCP(Model Context Protocol),模型上下文协议) 是一种标准化的协议,用于在应用程序和 AI 模型之间建立通信标准。
它提供了一种统一的方式来共享上下文信息,使 AI 模型能够更好地理解和处理各种应用场景。有了 MCP,就可以扩展大模型的能力。
MCP 遵循客户端-服务器架构,其中 MCP Host(Claude Code 或 Claude Desktop)等 AI 应用程序,与一个或多个 MCP Server 建立连接。
MCP 主机(Host)通过为每个 MCP Server 创建一个 MCP Client 来实现这一目标。每个 MCP Client 都与相应的 MCP Server 保持专用的一对一连接。MCP 架构的主要组成者是:
例如:VSCode 充当 MCP 主机。当 VSCode 建立与 MCP 服务器(如 Sentry MCP 服务器)的连接时,VSCode 运行时实例化了维护与 Sentry MCP 服务器连接的 MCP 客户端对象。当 VSCode 随后连接到另一个 MCP 服务器时,例如本地文件系统服务器,VSCode 运行时实例化一个额外的 MCP 客户端对象来维护此连接,从而保持 MCP 客户端与 MCP 服务器的一对一关系。
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
。
该方案是客户端通过 HTTP GET 向服务端发请求,服务端通过 SSE 通道返回响应结果。
Streamable
并不是一个标准协议名,而是一个通用描述,指的是基于 HTTP 协议的“可流式传输”技术。它的核心思想是:在一个 HTTP 连接里,服务端可以持续不断地发送数据给客户端,客户端边接收边处理,类似“流”一样。与传统 HTTP 请求响应“一次性完成”不同,Streamable 保持连接不关闭,数据分片持续传输。
至于为什么要采用 Streamable,而不是 SSE,我们看看 SSE 的缺点就知道了:
Content-Type
支持任意格式,如 JSON、HTML、二进制等,更适合 AI 场景(可能要传 JSON + 音频 + 图片)下面这张图可以很好的解释 MCP 的作用:统一接口规范
分为远程和本地两种方式。
JSON-RPC 是一种轻量级的远程过程调用(RPC)协议,它使用 JSON 作为数据格式。
json-rpc 的消息类型:
{
// jsonrpc协议版本
"jsonrpc": "2.0",
// 调用方法名
"method": "methodName",
"params": {
"a": 1,
"b": 2
},
// 标识,与响应中id一致
"id": 1
}
{
"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.method
const { a, b } = req.params
const 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 的调用。
如何通信:
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 去按照规范调用它。
Tool 是 MCP 里的“执行者”,让 LLM 不只是进行语言输出,还能实际执行具体任务,本质上是服务器提供的一些函数或者 API。
用于执行具体的操作和计算,能够影响或修改外部系统的状态,比如进行数学计算、数据处理、API 调用、文件操作等。
服务器定义好工具后,客户端发现这些工具,LLM 可以根据需要调用,调用结果会返回给 LLM,以便其继续推理或者输出。
工具使模型能够与外部系统交互,例如查询数据库、调用 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"
}
}
要调用工具,客户端发送工具/调用请求
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°FnConditions: Partly cloudy"
}
],
"isError": false
}
}
文档:modelcontextprotocol.io/specificati…
工具结果也就是工具响应的类型,上面例子中是 text
,还有其他内容如下:
{
"type": "text",
"text": "Tool result text"
}
{
"type": "image",
"data": "base64-encoded-data",
"mimeType": "image/png"
"annotations": {
"audience": ["user"],
"priority": 0.9
}
}
{
"type": "audio",
"data": "base64-encoded-audio-data",
"mimeType": "audio/wav"
}
工具可以返回指向资源的链接,以提供额外的上下文或数据。在这种情况下,该工具将返回一个可以由客户端订阅或获取的 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 提供了对应语言的 SDK,用来开发的 MCP 工具,来构建符合 MCP 规范的 jsonrpc 数据。
下面采用 ts 开发。
pnpm init
这里需要安装的是 @modelcontextprotocol/sdk
、zod@3
pnpm add @modelcontextprotocol/sdk zod@3
# ts、node类型
pnpm add @types/node typescript -D
package.json
{
"type": "module",
"bin": {
"weather": "./build/index.js"
},
"scripts": {
"build": "tsc && chmod 755 build/index.js"
},
"files": ["build"]
}
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"]
}
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}:nn${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?: string
areaDesc?: string
severity?: string
status?: string
headline?: string
}
}
// 格式化警报数据
export function formatAlert(feature: AlertFeature): string {
const props = feature.properties
return [
`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?: string
temperature?: number
temperatureUnit?: string
windSpeed?: string
windDirection?: string
shortForecast?: string
}
export interface AlertsResponse {
features: AlertFeature[]
}
export interface PointsResponse {
properties: {
forecast?: string
}
}
export interface ForecastResponse {
properties: {
periods: ForecastPeriod[]
}
}
这里以 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!')
})
pnpm build
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"]
}
}
}
也可以将包发到 npm 上,然后通过 npx [包名]
来执行。
这里可以看到我们的 MCP 已经启动成功了:
我们只需要向它提问即可(一些支持 MCP 的客户端,就可以直接进行提问,它内部会去自动调用 MCP)
模型上下文协议 (MCP) 为服务器提供了一种向客户端公开提示模板的标准化方法。提示允许服务器提供结构化消息和与语言模型交互的指令。客户端可以发现可用的提示,检索其内容,并提供参数来自定义它们。
Prompt 是 MCP 的“模板大师”,提供预定义的交互模式或者推理指引,是 LLM 的“剧本”,告诉它怎么开口、怎么思考。
主要用于定义与 LLM 交互的模板,指导 LLM 的输出。它可以提供结构化的提示模板,包含参数,用于标准化和共享常见的 LLM 交互,例如生成文本模板、定义对话流程、创建特定任务的指令等。
服务器定义好一堆 Prompt 模板,客户端可以直接选择一个,填入参数,然后交给 LLM 执行。
通常,提示将通过用户界面中用户启动的命令触发,这允许用户自然地发现和调用可用的提示。
例如,作为斜杠命令:
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"
}
}
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')"
}
}
]
}
}
需求:编写一个代码 review 的 prompt
在之前的文件中,添加如下代码即可:
server.prompt(
'code-review',
'审查代码以进行改进和发现问题',
{
code: z.string()
},
({ code }) => ({
messages: [
{
role: 'user',
content: {
type: 'text',
text: `需要review的代码:nn${code}`
}
}
]
})
)
通过 /
命令,来测试:
MCP 为服务器提供了一种标准化的方式来公开 资源给客户。资源允许服务器共享数据,这些数据向 语言模型,例如文件、数据库模式或特定于应用程序的信息。 每个资源都有 URI。
Resource 是 MCP 的"知识库管理员",提供对各种数据源和信息资产的统一访问接口,是 LLM 的"外部记忆",让它能够访问和检索存储在外部系统中的信息。
例如,应用程序可以:
在平时使用 ai 的时候,有添加文件的作为上下文,这个就是 MCP Resource。
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"
}
}
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 来实现即可,就不具体演示了。
官网的例子是使用 Claude
的大模型来完成的,这里我们自己调用其他的大模型来测试。
所谓的 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 工具,来返回对应的结果。
这里采用 open router
+ openai sdk
来实现一个 mcp client:
pnpm init
package.json
{
"type": "module",
"scripts": {
"build": "tsc && chmod 755 build/index.js"
}
}
pnpm i @modelcontextprotocol/sdk openai dotenv
pnpm i @types/node typescript -D
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"]
}
.env
文件OPENROUTER_API_KEY="YOUR_OPENROUTER_API_KEY"
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: Client
private openai: OpenAI
private transport: StdioClientTransport | null = null
private 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 server
async 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') continue
const 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: Moderaten' +
'Status: Actualn' +
'Headline: 江苏省南京市高温预警,持续至晚上8点n' +
'---n' +
'Event: 洪水警报n' +
'Area: 浙江省沿海地区n' +
'Severity: Severen' +
'Status: Actualn' +
'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()
pnpm build
至于我们如何将我们的 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!')
})
node build/index.js 【mcp server文件路径 -> 要求绝对路径,用于mcp client启动mcp server的】
# 例如:
node build/index.js /Users/xxx/Desktop/mcp-demo/build/index.js
这也和我们模拟的数据是一致的:
在上面【编写一个 MCP Tool】中说了两种测试的方法,下面介绍一种官方文档中的测试方法。
运行如下命令:
npx @modelcontextprotocol/inspector
执行完成后会自动 http://localhost:6274/
,界面如下:
如图的:Command
就是启动 mcp 的命令,Arguments
就是启动 mcp 的参数。我这里是 node dist/index.js
,路径是当前执行 npx @modelcontextprotocol/inspector
的相对路径。
我们点击 Connect
按钮,会发送请求链接 mcp,然后我们在 History
中可以查看到请求的内容。
在前言中提到一些 AI Coding 的一些缺点,那我们如何利用 MCP 去解决呢?
流程是这样的:原型需求文档
-> UI设计
-> 任务确认
-> AI 生成代码
-> 如果需要登录态,通过 PlayWright 进行登录获取 Cookie
-> 错误捕获,重新“丢”给 AI
-> 自检通过,执行结束
原型需求文档:通过内部Cli
工具,在生成模版的时候去自动拉去需求文档,添加到项目的Prompt.md
中,MCP 让 AI 去读取该 md 文件,通过 cli 也提供了一些添加需求的命令,来减少手动修改 md 文件的步骤
错误捕获包括:终端运行时错误、浏览器错误
console
、pageerror
、requestfailed
这些事件console
:监听控制台消息(包括 log、warn、error 等类型)pageerror
:监听页面未捕获的 JavaScript 异常requestfailed
:监听网络请求失败事件让我们正式开始吧!
pnpm init
# playwright不能安装在开发依赖中的,打包会没有哦
pnpm i @modelcontextprotocol/sdk playwright axios
pnpm i -D @types/node typescript rimraf
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"]
}
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"
}
}
src/index.ts
目的是创建一个 MCP Server(new Server
)
#!/usr/bin/env node
import { 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: Server
constructor() {
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.params
logger.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)
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 = null
public 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变化时resolve
return 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 = false
for (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 = null
protected context: playwright.BrowserContext | null = null
protected page: Page | null = null
constructor() {
// 不在构造函数中启动,避免意外
}
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
}
}
src/tools/console.ts
文件作用是监控浏览器控制台的异常,并“丢给”AI。
/**
* 控制台日志
* @returns {使用 PlayWright 监控浏览器控制台日志, 返回错误日志内容}
*/
async function console(
params: {
url?: string
cookies?: string
includeConsoleLogs?: boolean
includeErrors?: 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()
// 添加cookie
if (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, // 会话cookie
httpOnly: false,
secure: false,
sameSite: 'Lax'
})
}
}
}
return cookies
}
src/tools/request.ts
文件该文件用于接口错误参数校验的工具,基于monitor_console_log
工具,当发现到网络请求的错误后,去重新请求,把请求的信息结果返回
async function sendRequest(params: {
url: string
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
headers?: Record<string, string>
body?: any
timeout?: number
}): Promise<ToolResponse> {
const { url, method = 'GET', headers = {}, body, timeout = 5000 } = params
try {
// 构建请求配置
const config: any = {
url,
method,
headers,
timeout,
validateStatus: () => true // 接受所有状态码,便于分析错误
}
// 如果有请求体且方法允许,则添加请求体
if (body && ['POST', 'PUT', 'PATCH'].includes(method.toUpperCase())) {
// 设置默认Content-Type
if (!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
)
需要在如果是本地的话可以使用node
或者npx
。
npx 的话必须要在package.json
中添加bin
字段:
{
"bin": {
"generate-llm-mcp": "dist/index.js"
}
}
如果要让别人使用你开发的 MCP 包,那就需要把包上传至 npm 仓库中即可,然后通过 npx 包名
运行使用。
也可以在 Cursor
这些 ide 中添加配置:
{
"mcpServers": {
"generate-llm-mcp": {
"command": "npx",
"args": ["-y", "generate-llm-mcp"]
}
}
}
npx 是什么?
npx 是一个命令行工具,它是 npm 5.2.0 版本中新增的功能。它允许用户在不安装全局包的情况下,运行已安装在本地项目中的包或者远程仓库中的包。
npx 的作用是在命令行中运行 node 包中的可执行文件,而不需要全局安装这些包。这可以使开发人员更轻松地管理包的依赖关系,并且可以避免全局污染的问题。
npx 的优势
npx
允许你执行 npm 包,而不需要你先全局安装它npx
会从 npm 的 package
仓库中下载并使用最新版npx
不仅可以执行在 package.json
的 scripts
部分定义的命令,还可以执行任何 npm 包npm 和 npx 区别
npm
侧重于安装或者卸载某个模块的。重在安装,并不具备执行某个模块的功能。npx
侧重于执行命令的,执行某个模块命令。虽然会自动安装模块,但是重在执行某个命令npx
的运行规则和 npm 是一样的 本地目录查找 .bin
看有没有 如果没有就去全局的 node_moduels
查找,如果还没有就去下载这个包然后运行命令,然后删除这个包
84.37M · 2025-10-04
97.24MB · 2025-10-04
132.36MB · 2025-10-04