☰
Dillinger MCP 服务器构建实战:基于 Model Context Protocol 的工具设计、资源模式与安全最佳实践
2026/9/26 15:44:49 网站建设 项目流程
  • 前端
  • 开发工具

【免费下载链接】dillinger

The last Markdown editor, ever.

项目地址:https://gitcode.com/gh_mirrors/di/dillinger
点击查看免费下载

本指南以 Dillinger 仓库中的 MCP Builder 技能文档(.agent/skills/mcp-builder/SKILL.md)为核心骨架,结合仓库内真实实现的 MCP 服务器(packages/mcp/)与后端 API(app/api/v1/)展开讲解。读完本文,你将掌握 MCP 服务器的完整构建方法论——从工具(Tools)输入模式设计、资源(Resources)URI 规划、错误处理、多模态编码,到环境变量配置与测试策略,并能对照 Dillinger 的开源实现,亲手构建一个可被 Claude Desktop 等客户端直接加载的 Stdio 型 MCP 服务器。


1. MCP 概览:让 AI 系统连接外部工具与数据

1.1 什么是 MCP

Model Context Protocol(模型上下文协议,简称 MCP)是连接 AI 系统与外部工具、数据源的标准协议。它定义了一套统一的通信范式,使 LLM 客户端(如 Claude Desktop、各类 Agent)无需针对每个集成方单独适配,即可调用外部能力。

Dillinger 对 MCP 的定位非常清晰——在其packages/mcp/README.md中写道:

MCP (Model Context Protocol) server for Dillinger. Lets LLMs use Dillinger as a native markdown tool.

也就是说,Dillinger 通过 MCP 服务器把自己的 Markdown 渲染、PDF/HTML 导出、HTML 转 Markdown 等能力开放给 LLM,让大模型把 Dillinger 当作一个"原生 Markdown 工具"来使用。这正是 MCP 的典型应用场景:把 AI 无法直接完成的确定性计算(渲染、转换、导出)交给外部服务完成。

1.2 三大核心概念

概念用途Dillinger 中的实例
Tools(工具)AI 可以调用的函数,有明确的名称、输入模式与返回结构render_markdown、export_pdf、export_html、convert_html_to_markdown
Resources(资源)AI 可以读取的数据,通过 URI 寻址本仓库 MCP 服务器暂未暴露资源,但资源 URI 设计范式见第 4 节
Prompts(提示模板)预定义的提示词模板,复用常见工作流可按需扩展,如"将这段 Markdown 转成会议纪要 PDF"

MCP 协议的价值在于:工具是函数(可执行),资源是数据(可读),提示是模板(可复用)——三者边界清晰,客户端(AI)与服务器(能力提供方)各司其职。


2. 服务器架构:从目录结构到传输方式

2.1 项目结构

MCP Builder 文档给出的最小项目结构如下:

my-mcp-server/ ├── src/ │ └── index.ts # Main entry ├── package.json └── tsconfig.json

Dillinger 仓库中的packages/mcp/完全遵循这一结构,且每个文件都有明确职责:

  • packages/mcp/src/index.ts—— 唯一入口,负责创建 Server、注册工具、连接传输层;
  • packages/mcp/package.json—— 声明@dillinger/mcp包、bin(dillinger-mcp命令)、build/start脚本与依赖@modelcontextprotocol/sdk;
  • packages/mcp/tsconfig.json—— 以strict: true、target: ES2022、module: Node16编译到dist/,并开启declaration: true生成类型声明。

编译产出与运行命令:

# 在 packages/mcp 目录下 npm install npm run build # tsc 编译到 dist/ npm start # node dist/index.js

2.2 传输类型(Transport)

类型适用场景
Stdio本地、基于 CLI 的标准输入输出,客户端以子进程方式启动服务器
SSE(Server-Sent Events)基于 Web 的服务端事件流,适合远程部署与流式推送
WebSocket实时、双向通信,适合需要持续双向交互的场景

Dillinger 的 MCP 服务器采用Stdio传输,实现于packages/mcp/src/index.ts:

async function main() { const transport = new StdioServerTransport(); await server.connect(transport); }

选择 Stdio 的工程考量:本地 MCP 客户端(如 Claude Desktop)可以零配置地以子进程方式拉起node dist/index.js,无需端口监听、无需处理网络鉴权,天然满足"本地、CLI 化"的使用场景。若未来需要把 Dillinger MCP 部署为远程服务,则可替换为 SSE 或 WebSocket 传输,业务逻辑(工具实现)无需改动。


3. 工具设计原则:让 AI 用得对、用得好

3.1 优秀工具的四个原则

原则说明正面示例 / 反面示例
名称清晰动作导向,动词开头,见名知义get_weather✅ /w❌
单一职责一个工具只做好一件事render_markdown只负责渲染
输入校验用带类型和描述的 Schema 约束参数见 3.2 节
结构化输出返回格式可预测,便于 AI 解析统一返回{ content: [...] }

Dillinger 的四个工具全部采用"动词_名词"命名法,职责单一:

工具名职责
render_markdown用完整插件管线把 Markdown 渲染为 HTML
export_pdf把 Markdown 转换为 PDF(返回 base64)
export_html把 Markdown 转换为可直接发布的样式化 HTML 文档
convert_html_to_markdown把 HTML 内容转换为干净的 Markdown(可用于抓取网页内容)

3.2 输入模式(Input Schema)设计

MCP 工具通过 JSON Schema 声明输入,Builder 文档要求以下字段:

字段是否必填说明
type是顶层必须为object
properties是逐个定义每个参数的类型与描述
required是列出必填参数数组
description是人类可读的参数说明,供 AI 理解用途

以 Dillinger 的export_html为例(packages/mcp/src/index.ts):

{ name: "export_html", description: "Convert markdown to a complete, styled HTML document ready for publishing or sharing.", inputSchema: { type: "object", properties: { markdown: { type: "string", description: "Markdown content to convert" }, title: { type: "string", description: "Document title" }, styled: { type: "boolean", description: "Include CSS styling in the HTML document (default: true)" }, }, required: ["markdown"], }, }

设计要点:只把真正必需的参数(markdown)放入required,可选参数(title、styled)给出默认值与语义化描述。这让 AI 在调用时既能拿到最小可用约束,又不会因未知的可选参数而困惑。description的价值不可低估——MCP 工具文档最后强调:"The AI relies on descriptions to use them correctly"(AI 依赖描述来正确使用工具),描述写得越精确,AI 的调用成功率越高。

3.3 工具注册与调用处理

MCP SDK 要求服务器实现两个核心请求处理器:

  • ListToolsRequestSchema:客户端询问"你有哪些工具",服务器返回工具清单(含 Schema);
  • CallToolRequestSchema:客户端发起实际调用,服务器按工具名分发并返回结果。

Dillinger 的实现结构(packages/mcp/src/index.ts):

const server = new Server( { name: "dillinger", version: "0.1.0" }, { capabilities: { tools: {} } } ); server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ /* 工具清单 */ ] })); server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; switch (name) { case "render_markdown": { /* ... */ } case "export_pdf": { /* ... */ } case "export_html": { /* ... */ } case "convert_html_to_markdown": { /* ... */ } default: return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true }; } });

注意capabilities: { tools: {} }声明了该服务器仅提供工具能力(无 resources/prompts),这是对协议能力的显式声明。未知工具返回isError: true,符合"结构化错误"的要求。


4. 资源模式:数据读取的 URI 设计

4.1 资源类型

类型用途
静态(Static)固定数据,如配置文件、文档
动态(Dynamic)按请求即时生成的数据
模板(Template)带参数的 URI,参数化寻址

4.2 URI 模式

模式示例
固定docs://readme
参数化users://{userId}
集合files://project/*

设计建议:资源的 URI 就是它的"身份证",应具备确定性(同一 URI 永远指向同一资源)与可读性(scheme 表达来源域)。Dillinger 当前 MCP 服务器以工具为中心,未暴露资源端点;但如果你要扩展,一个自然的做法是暴露dillinger://documents/*之类模板资源,让 AI 直接"读取"已保存的 Markdown 文档。


5. 错误处理:结构化、可操作、不泄露

5.1 错误类型与响应策略

场景响应策略
参数无效返回校验错误信息(Validation error)
资源/工具不存在明确返回 "not found"
服务器内部错误返回通用错误,细节写入日志

5.2 最佳实践清单

  • 返回结构化错误(结构化 JSON 而非裸字符串异常);
  • 不向客户端暴露内部实现细节(堆栈、内部变量);
  • 记录日志以便调试;
  • 提供可操作的错误信息(告诉 AI 该怎么修正)。

Dillinger 的实现非常典型(packages/mcp/src/index.ts):

server.setRequestHandler(CallToolRequestSchema, async (request) => { try { switch (name) { /* ... */ } } catch (error) { return { content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true, }; } });

同时,其上游 API 层也遵循同样的分层错误策略:参数缺失返回400与明确的error文案(如markdown field is required),未配置密钥返回503,密钥无效返回403(见lib/api-auth.ts)。MCP 服务器只透传"可操作"的错误信息(如API error 403: Invalid API key),原始堆栈被吞掉,内部细节不会暴露给 AI。


6. 多模态处理:文本、图片与文件的编码

MCP 工具返回内容支持多模态,Builder 文档列出的编码方式:

类型编码方式
文本纯文本
图片Base64 + MIME 类型
文件Base64 + MIME 类型

Dillinger 的export_pdf是"文件以 Base64 返回"的实例(packages/mcp/src/index.ts):

case "export_pdf": { const response = await apiCall("/export/pdf", { markdown, title: title || "document" }); const buffer = await response.arrayBuffer(); const base64 = Buffer.from(buffer).toString("base64"); return { content: [{ type: "text", text: `PDF generated successfully (${buffer.byteLength} bytes). Base64-encoded content follows:\n${base64}`, }], }; }

这里有一个工程上的务实取舍:MCP 的content块统一以text形式承载,PDF 二进制被转成 Base64 字符串放入text,并在前面附加"PDF generated successfully (N bytes)"的说明文本。这样既绕开了对图片/文件 content 类型的额外协商,又让 AI 明确知道返回的是什么、有多大。若返回图片,则应使用{ type: "image", data: "<base64>", mimeType: "image/png" }结构。


7. 安全原则:输入校验、密钥与最小权限

7.1 输入校验

  • 校验所有工具输入(类型、必填、边界);
  • 清洗用户提供的数据(防止注入类问题);
  • 限制资源访问范围(最小权限)。

Dillinger 在后端 API 层做了双重校验:MCP 服务器层校验参数存在性(缺markdown时由上游返回 400),后端路由层再校验typeof markdown !== "string" || !markdown.trim()(见app/api/v1/render/route.ts)。校验应该分层冗余,靠近边界的每一层都假设自己是唯一防线。

7.2 API 密钥管理

  • 使用环境变量,不硬编码;
  • 不记录密钥到日志;
  • 校验权限(最小权限原则)。

Dillinger 的密钥管理是"环境变量贯穿全链路"的教科书示例:

MCP 服务器侧(packages/mcp/src/index.ts):

const BASE_URL = process.env.DILLINGER_URL || "https://dillinger.io"; const API_KEY = process.env.DILLINGER_API_KEY || "";

后端鉴权侧(lib/api-auth.ts)逐层检查:未配置密钥(503)→ 缺少Authorization: Bearer头(401)→ 密钥不匹配(403)。每个分支都返回不同的状态码与可操作信息,便于 AI 与运维人员区分"配置问题"与"凭证问题"。此外,DILLINGER_URL提供默认值(https://dillinger.io),DILLINGER_API_KEY无默认值,强制显式配置——这正是"安全默认"原则的体现。


8. 配置:以 Claude Desktop 为例

8.1 配置文件字段

Claude Desktop 的 MCP 服务器配置位于~/Library/Application Support/Claude/claude_desktop_config.json,其mcpServers条目字段如下:

字段用途
command要执行的可执行文件
args命令行参数
env传递给子进程的环境变量

8.2 Dillinger MCP 的完整配置示例

Dillinger 官方 README(packages/mcp/README.md)给出了可直接照用的配置:

{ "mcpServers": { "dillinger": { "command": "node", "args": ["/path/to/packages/mcp/dist/index.js"], "env": { "DILLINGER_API_KEY": "your-api-key", "DILLINGER_URL": "https://dillinger.io" } } } }

要点说明:

  • args指向编译产物dist/index.js(而非src/index.ts),因此配置前必须先执行npm run build;
  • DILLINGER_URL指向后端 API 的根地址,DILLINGER_API_KEY必须与后端部署时设置的环境变量一致,否则调用会收到 401/403;
  • 若将@dillinger/mcp作为 npm 包安装,package.json中的bin字段提供了dillinger-mcp命令,可直接把command换成"dillinger-mcp"。

9. 测试策略:单元、集成与契约测试

类型关注点
单元测试单个工具的逻辑(参数校验、输出格式)
集成测试完整服务器(启动 → 连接 → 调用 → 返回)
契约测试Schema 校验(输入输出是否符合声明)

Dillinger 仓库虽然尚未为 MCP 服务器编写独立测试,但其后端 API 的测试模式可作为同构参考:仓库的tests/routes/目录中,如tests/routes/export-pdf.route.test.ts、tests/routes/import-html-to-markdown.route.test.ts等,对每个 API 路由的鉴权、参数校验、成功/失败分支做了覆盖。为 MCP 服务器编写测试时,可沿袭同样的思路:为apiCall注入 mock 的fetch,验证四种工具的分发逻辑、未知工具分支与错误兜底分支。


10. 最佳实践检查清单

按照 Builder 文档,交付一个 MCP 服务器前应逐项确认:

  • 工具命名清晰、动作导向(动词开头);
  • 输入 Schema 完整,每个参数都有描述;
  • 输出为结构化 JSON;
  • 所有错误场景都有处理(无效参数、未找到、服务器错误);
  • 输入经过校验;
  • 配置基于环境变量;
  • 记录日志以便调试。

Dillinger 的 MCP 服务器逐条对照:

检查项落点
动作导向命名render_markdown、export_pdf等四个工具
完整 Schema每个工具均声明properties与required(packages/mcp/src/index.ts)
结构化 JSON 输出统一{ content: [{ type: "text", text }] }
全场景错误处理未知工具分支 + try/catch 兜底 +isError: true
输入校验MCP 层 + 后端路由层双重校验
环境变量配置DILLINGER_URL/DILLINGER_API_KEY
日志main().catch(console.error)兜底记录启动失败

附:Dillinger MCP 的调用链路全景

为了让前面各节的知识形成闭环,这里给出 Dillinger MCP 服务器一次完整调用的真实链路(均有源码依据):

  1. 客户端(Claude Desktop)依据claude_desktop_config.json以 Stdio 方式启动node dist/index.js;
  2. 握手与清单:客户端先通过ListToolsRequestSchema拿到四个工具的 Schema(packages/mcp/src/index.ts);
  3. 发起调用:AI 选择某个工具,通过CallToolRequestSchema传入参数;
  4. 代理转发:MCP 服务器调用apiCall(),向${BASE_URL}/api/v1${path}发起带Authorization: Bearer ${API_KEY}的 POST 请求(packages/mcp/src/index.ts);
  5. 后端处理:对应路由(如app/api/v1/render/route.ts)先过validateApiKey鉴权,再执行真实逻辑——renderMarkdown会加载 markdown-it 及 11 个插件(abbr、checkbox、deflist、footnote、ins、mark、sub、sup、texmath+KaTeX、toc)与 highlight.js 高亮(见lib/markdown.ts);
  6. 结果回传:后端返回 HTML/PDF/文本,MCP 服务器封装为 MCP content 块返回给 AI,其中 PDF 以 Base64 编码(packages/mcp/src/index.ts)。

这条链路清晰地展示了 MCP 服务器的定位:它不是业务实现者,而是把后端能力"翻译"成 AI 可理解、可调用的协议接口。理解了这一点,再回看 Builder 文档中的每一条原则——清晰命名、完整 Schema、结构化输出、分层错误、环境变量密钥——就都能找到它们在真实工程中的落点。

  • 前端
  • 开发工具

【免费下载链接】dillinger

The last Markdown editor, ever.

项目地址:https://gitcode.com/gh_mirrors/di/dillinger
点击查看免费下载

相关推荐

上一篇:如何快速搭建开源电子签名平台OpenSign:完整安装与使用指南
下一篇:Qwen3-4B-Instruct-2507震撼发布:40亿参数模型实现超长上下文与多维度能力跃升

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询