以 ECharts 为例入手 MCP 服务器开发
随着人工智能应用的普及,越来越多的开发者开始关注如何构建自己的 AI 应用。Model Context Protocol(MCP)作为一种新的协议标准,为 AI 应用的开发提供了更高效、更通用的方式。本文将以 Apache ECharts
+ TypeScript
为例,快速入手 MCP 服务器开发。
MCP 是什么
MCP (Model Context Protocol) 是一种用于 LLM 与应用程序之间交互的协议。它尽可能统一了 LLM 与外部应用(工具)之间的交互方式,让开发者可以注重于业务开发,而不是费心于与不同种类的 LLM 交互的细节。
本文将注重于应用的开发上手实操,而不是 MCP 的具体实现细节。MCP 的详细介绍、技术参数、架构设计等可以参考 MCP 官方文档。
开始开发
准备工作
我们假定你读到这时,已经配备好了你熟悉的 TypeScript 开发环境,一个支持 MCP 服务的 LLM 客户端(Claude Desktop、Cherry Studio,甚至是 VSCode)。
接下来请你创建一个新的 TypeScript 项目,并安装下面的依赖:
npm i \
@modelcontextprotocol/sdk \ # MCP TS SDK
canvas \ # SSR 画布支持
echarts # Apache ECharts
npm i \
express \ # 你也可以选用其他 HTTP 框架
zod # Schema 定义时的验证使用 zod 会很方便
创建一个 tsconfig 文件,这是我所使用的示例,你也可以自行修改:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"outDir": "./build",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
代码!
首先,构建一个初始的服务器:
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
const server = new Server(
{
name: "ECharts",
version: "0.0.1",
},
{
capabilities: {
tools: {},
},
}
);
// 你还可以在这一步配置一些监听,比如:
// server.onerror = (error) => {};
// process.on("SIGINT", () => {});
更多的代码!
接着我们创建一些模块来处理实际的业务,并将它们统一导出:
/**
* 在这个模块里,我们定义一些导出,分别是
* schema: 提供给模型的输入参数蓝图
* tool: 工具导出
* create(): 实际的业务函数,也就是图表的创建逻辑
*
* 在其他的任何实际业务模块里,都沿用一样的导出结构。
*/
import { z } from "zod";
import { Tool } from "@modelcontextprotocol/sdk/types.js";
import { zodToJsonSchema } from "zod-to-json-schema";
import { createCanvas } from "canvas";
import * as echarts from "echarts";
import { ToolSchema } from "@modelcontextprotocol/sdk/types.js";
type ToolInput = z.infer<typeof ToolSchema.shape.inputSchema>;
const schema = z.object({
width: z.number().describe("图表宽度"),
height: z.number().describe("图表高度"),
options: z.object({
title: z
.object({
text: z.string().describe("图表标题"),
})
.optional(),
xAxis: z.object({
type: z.literal("category").describe("X 轴固定为类目轴"),
data: z.array(z.string()).describe("X 轴数据"),
name: z.string().describe("X 轴名称"),
}),
yAxis: z.object({
type: z.literal("value").describe("Y 轴固定为数值轴"),
name: z.string().describe("Y 轴名称"),
}),
series: z.array(
z.object({
type: z.literal("bar").describe("系列类型固定为柱状图"),
data: z.array(z.number()).describe("系列数据"),
})
),
}),
});
const tool: Tool = {
name: "barOnGrid",
description: "创建直角坐标系柱状图",
inputSchema: zodToJsonSchema(schema) as ToolInput,
};
async function create(input: ToolInput) {
const { width, height, options } = input;
const canvas = createCanvas(width, height);
const chart = echarts.init(canvas as unknown as HTMLElement);
chart.setOption(options);
const buffer = canvas.toBuffer("image/png");
return buffer;
}
export const barOnGrid = {
schema,
tool,
create,
};
让我来解释一下上面的代码:
- 首先,我们定义了一个 type
ToolInput
,用于转换我们的输入 schema 的类型。 - 接着,我们使用
zod
定义了一个输入 schema,它的结构定义比较简单,将宽度、高度这两个与 ECharts Option 结构无关的参数放置在了外层,而options
则是一个和 ECharts Option 结构定义完全相符的简单柱状图参数。zod
的使用能让结构更清晰,同时也能在后续的代码中使用zod
的验证功能。zod
的describe
方法可以为每个字段添加描述信息,这些信息模型是能够看到的,增强了模型对参数的理解。
- 然后,我们定义了一个用于注册在服务中的
Tool
,它包含了工具的名称、描述和输入 schema。模型在与服务器交互时能够看到这些信息。 - 最后,我们定义了一个
create
函数,它接收输入参数,创建一个画布,并使用 ECharts 绘制图表。绘制完成后,将画布转换为 PNG 格式的 Buffer 返回。
接下来,你可以创建更多这样结构的模块,来处理不同类型的图表。我们假设你这样创建了另外两个模块,分别是 lineOnGrid.ts
和 pie.ts
,它们的结构与 barOnGrid.ts
类似,只是输入 schema 有所差别。
为了减少工具注册的重复工作,我们可以这样做:
// echarts/index.js
export { barOnGrid as "barOnGrid" } from "./barOnGrid.js";
export { lineOnGrid as "lineOnGrid" } from "./lineOnGrid.js";
export { pie as "pie" } from "./pie.js";
// 在另一个文件里导出这个常量作为图表映射
export const ChartTypes = {
barOnGrid: "barOnGrid",
lineOnGrid: "lineOnGrid",
pie: "pie",
} as const;
// 接着上面我们创建 Server 的代码继续写
import {
CallToolRequestSchema,
ErrorCode,
ListToolsRequestSchema,
McpError,
} from "@modelcontextprotocol/sdk/types.js";
// 导入所需的图表、图表类型映射
import * as Charts from "../echarts/index.js";
import { ChartTypes } from "schema.js";
/**
* 通过工具类型映射在一句话内注册所有工具
* 这个 handler 实际是在客户端请求时列出所有可用工具的
*/
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: Object.values(Charts).map((chart) => chart.tool),
}));
/**
* 处理实际的工具调用请求
* 使用一个通用的处理函数来处理所有图表类型的请求
* 同样是通过映射实现动态传入
*/
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const chartType = ChartTypes[request.params.name as keyof typeof ChartTypes];
// 在这里开始实现各种处理逻辑,这些代码就省去了
// 比如输入验证这样的检查方法
// 一切通过就去调用你实现的create()
// 最终返回一个链接?或者是 base64?都取决于你
// 但最后的最后,你的返回必须是这样的:
return {
content: [
{
type: "text",
text: text, // 这里放置你的实际返回内容
},
],
};
});
快要完成了!但…还有代码!
我们已经完成了大部分业务工作,接着就只需要将这个已经创建并配置好工具的服务器连接到传输层了:
// 同样是接着服务器创建和工具注册的代码继续写
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
const transport = new StdioServerTransport();
await server.connect(transport);
提示
在这里我将只演示最简单的 STDIO 传输演示,对于 SSE(已弃用) 和 Streamable HTTP,你可以前往 这个仓库 看看。在那里是这篇文章的一个更完整的示例。
就这样,我们算是已经完成了这个简单的 MCP 服务器的开发。
运行和调试
现在,将 TypeScript 代码编译并且运行你的服务器。调试上,你有两个选择:
- 使用支持 MCP 的客户端直接试用,比如 Claude Desktop 或者 Cherry Studio。
- 使用官方的调试工具 inspector,它可以帮助你调试 MCP 服务器(但是你人工调试,而不是模型调试)。
两者的使用都十分简单,因此本篇文章算是到此为止了 (毕竟我们假定了你熟悉 LLM)。