# 以 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 文件,这是我所使用的示例,你也可以自行修改:

点击查看示例
tsconfig.json
{
"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,
}

让我来解释一下上面的代码:

  1. 首先,我们定义了一个 type ToolInput,用于转换我们的输入 schema 的类型。
  2. 接着,我们使用 zod 定义了一个输入 schema,它的结构定义比较简单,将宽度、高度这两个与 ECharts Option 结构无关的参数放置在了外层,而 options 则是一个和 ECharts Option 结构定义完全相符的简单柱状图参数。
    • zod 的使用能让结构更清晰,同时也能在后续的代码中使用 zod 的验证功能。
    • zoddescribe 方法可以为每个字段添加描述信息,这些信息模型是能够看到的,增强了模型对参数的理解。
  3. 然后,我们定义了一个用于注册在服务中的 Tool,它包含了工具的名称、描述和输入 schema。模型在与服务器交互时能够看到这些信息。
  4. 最后,我们定义了一个 create 函数,它接收输入参数,创建一个画布,并使用 ECharts 绘制图表。绘制完成后,将画布转换为 PNG 格式的 Buffer 返回。

接下来,你可以创建更多这样结构的模块,来处理不同类型的图表。我们假设你这样创建了另外两个模块,分别是 lineOnGrid.tspie.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)

就这样,我们算是已经完成了这个简单的 MCP 服务器的开发。

运行和调试

现在,将 TypeScript 代码编译并且运行你的服务器。调试上,你有两个选择:

  1. 使用支持 MCP 的客户端直接试用,比如 Claude Desktop 或者 Cherry Studio。
  2. 使用官方的调试工具 inspector,它可以帮助你调试 MCP 服务器(但是你人工调试,而不是模型调试)。

两者的使用都十分简单,因此本篇文章算是到此为止了 (毕竟我们假定了你熟悉 LLM)


参考:
Model Context Protocol Doc
Inspector Doc
MCP Github
本文项目代码

写下此篇时暂时不是懒狗的星语

这是开发的责任感和前瞻性的问题。不兼容的改变不应该轻易被加入到有许多依赖代码的软件中。升级所付出的代价可能是巨大的。
—— 《语义化版本》


更多文章