AI

从零开发 MCP Server 到发布:完整实战指南

Posted by NekouTarou on 02-09,2026

以 kingbase-mcp-server 为例,涵盖 MCP 协议原理、架构设计、工具注册、传输层实现、测试、CI/CD 和发布全流程。


目录


一、MCP 协议概述

1.1 什么是 MCP

MCP(Model Context Protocol,模型上下文协议)是 Anthropic 提出的开放标准,用于在 AI 助手(如 Claude)和外部数据源/工具之间建立标准化的通信桥梁。

MCP 的核心定位:让 LLM 以安全、标准化的方式调用外部工具和获取上下文信息

┌──────────────┐     MCP 协议       ┌──────────────┐
│              │  ◄──────────────►  │              │
│  MCP Client  │    JSON-RPC 2.0    │  MCP Server  │
│  (AI 助手)    │                    │  (你的服务)   │
│              │  工具调用 / 结果     │              │
└──────────────┘                    └──────────────┘
                                          │
                                    ┌─────┴─────┐
                                    │ 数据库/API │
                                    │ 文件系统等  │
                                    └───────────┘

1.2 核心概念

MCP 定义了三种服务端向客户端暴露的能力:

概念说明控制方
Tools(工具)可被 LLM 调用的函数,带有输入 schema 和返回值LLM 决定何时调用
Resources(资源)向 LLM 暴露数据/文件内容(类似 GET)应用程序控制
Prompts(提示词模板)预定义的提示词模板,可由用户选择触发用户控制

本项目主要使用 Tools 能力,将数据库操作封装为 11 个工具暴露给 LLM。

1.3 通信模型

MCP 基于 JSON-RPC 2.0 协议,支持两种传输方式:

1. stdio(标准输入输出)

  • 客户端启动 MCP Server 进程
  • 通过 stdin/stdout 交换 JSON-RPC 消息
  • 适用于本地使用,一对一的进程关系
Client ──stdin──► Server Process ──stdout──► Client

2. Streamable HTTP

  • 使用 HTTP POST/GET/DELETE 与 Server-Sent Events (SSE)
  • 支持远程部署,多客户端共享一个服务端
  • 通过 mcp-session-id Header 区分会话
Client ──HTTP POST /mcp──► Express Server ──JSON-RPC Response──► Client
Client ──HTTP GET  /mcp──► Express Server ──SSE Stream──────────► Client
Client ──HTTP DELETE /mcp─► Express Server ──Session Terminated──► Client

1.4 协议版本

当前使用的协议版本为 2025-03-26。客户端在 initialize 请求中发送协议版本号,服务端在响应中确认。这个握手过程由 SDK 自动处理。


二、项目初始化

2.1 技术栈选型

组件选型说明
语言TypeScript 5.7+类型安全,MCP SDK 官方支持
运行时Node.js >= 20长期支持版本
MCP SDK@modelcontextprotocol/sdk ^1.6.1官方 TypeScript SDK
Schema 校验zod ^3.23MCP SDK 原生集成 Zod
HTTP 框架express ^5.2HTTP 传输模式使用
数据库驱动pg ^8.13PostgreSQL 协议驱动
构建工具tsc(TypeScript Compiler)简单直接,无需 bundler
测试框架vitest ^4.0快速、兼容 ESM
Schema 转换zod-to-json-schema ^3.25将 Zod schema 转为 JSON Schema(用于 HTTP 工具发现端点)

2.2 项目结构

kingbase-mcp-server/
├── src/
│   ├── index.ts          # 主文件:Server、工具注册、传输层、HTTP 端点
│   └── utils.ts           # 纯工具函数(可独立测试)
├── tests/
│   └── utils.test.ts      # 单元测试
├── scripts/
│   └── generate-docs.ts   # 文档自动生成脚本
├── dist/                   # 编译输出(gitignore)
├── docs/                   # 文档目录
├── tsconfig.json
├── package.json
├── .env.example
├── TOOLS.md               # 自动生成的工具参考文档
├── CLAUDE.md              # AI 助手的代码导航文件
└── .github/workflows/
    ├── ci.yml             # CI 流水线
    └── publish.yml        # npm 发布流水线

设计原则:尽量简单。整个 Server 在一个文件(src/index.ts)中实现,便于理解和维护。只有纯工具函数被提取到 src/utils.ts,原因是 index.ts 在 import 时就会启动 Server,无法直接用于单元测试。

整体框架:

MCP-Server-Architecture.png

2.3 TypeScript 配置

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "lib": ["ES2022"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "declaration": true,
    "sourceMap": true,
    "allowSyntheticDefaultImports": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

关键点:

  • module: "Node16" + moduleResolution: "Node16":使用 Node.js 原生 ESM 模块系统
  • target: "ES2022":支持 top-level await 等现代语法
  • strict: true:开启所有严格类型检查
  • declaration: true:生成 .d.ts 类型声明(npm 包需要)

2.4 package.json 关键配置

{
  "name": "kingbase-mcp-server",
  "type": "module",
  "main": "dist/index.js",
  "bin": {
    "kingbase-mcp-server": "dist/index.js"
  },
  "files": ["dist", "README.md", "LICENSE", "TOOLS.md"],
  "engines": { "node": ">=20" },
  "scripts": {
    "start": "node dist/index.js",
    "start:http": "TRANSPORT=http node dist/index.js",
    "dev": "npx tsx watch src/index.ts",
    "build": "npm run docs:generate && tsc",
    "test": "vitest run",
    "prepublishOnly": "npm run build"
  }
}

关键点:

  • "type": "module":声明为 ESM 包,import 路径必须带 .js 扩展名
  • "bin":注册 CLI 命令,安装后可直接 kingbase-mcp-server 执行
  • "files":控制 npm 发布内容,只包含编译输出
  • "prepublishOnly":发布前自动构建
  • src/index.ts 首行的 #!/usr/bin/env node shebang 确保可作为可执行文件运行

三、架构设计

3.1 整体架构

                              ┌─────────────────────────────────────────┐
                              │             src/index.ts                 │
                              │                                         │
 ┌─────────────┐   stdio      │  ┌──────────────┐   ┌───────────────┐  │
 │ MCP Client  │◄────────────►│  │ McpServer    │   │ Tool Registry │  │
 │ (Claude等)  │              │  │ (SDK 实例)    │   │ (元数据存储)   │  │
 └─────────────┘              │  └──────┬───────┘   └───────────────┘  │
                              │         │                               │
 ┌─────────────┐   HTTP       │  ┌──────┴───────┐                      │
 │ MCP Client  │◄────────────►│  │ 11 Tools     │                      │
 │ (远程)      │  /mcp        │  │ (工具实现)    │                      │
 └─────────────┘              │  └──────┬───────┘                      │
                              │         │                               │
 ┌─────────────┐   REST       │  ┌──────┴───────┐   ┌──────────────┐  │
 │ HTTP Client │◄────────────►│  │ Express App  │   │ Session Mgmt │  │
 │ (浏览器等)  │  /tools      │  │ (HTTP 端点)  │   │ (会话管理)    │  │
 └─────────────┘  /health     │  └──────────────┘   └──────────────┘  │
                              └──────────┬──────────────────────────────┘
                                         │
                              ┌──────────┴──────────┐
                              │     src/utils.ts     │
                              │  (纯工具函数 + 常量)  │
                              └──────────┬──────────┘
                                         │
                              ┌──────────┴──────────┐
                              │    pg.Pool           │
                              │  (数据库连接池)       │
                              └──────────┬──────────┘
                                         │
                              ┌──────────┴──────────┐
                              │   KingBase / PG      │
                              │   (目标数据库)        │
                              └─────────────────────┘

3.2 代码分层

src/index.ts 按从上到下的顺序组织:

 1. Imports & Constants          — 导入依赖、定义常量
 2. Tool Registry                — 工具注册表(用于 HTTP 端点)
 3. Database Connection          — createPool() 数据库连接池
 4. Helpers                      — executeQuery() 查询执行封装
 5. Server Factory               — createServer() + registerTools()
 6. Tool Definitions (×11)       — 每个工具:Schema → 注册 → Handler
 7. Main                         — 环境校验 → 连接验证 → 传输分发
 8. Session Management           — HTTP 模式的会话管理
 9. HTTP Server                  — Express 应用 + 各端点定义

3.3 双传输模式设计

通过环境变量 TRANSPORT 在运行时选择模式:

const transportMode = (process.env.TRANSPORT || "stdio").toLowerCase();

if (transportMode === "http") {
  await startHttpServer(); // HTTP 模式
} else {
  // stdio 模式
  const server = createServer();
  const transport = new StdioServerTransport();
  await server.connect(transport);
}

stdio 模式:创建单个 McpServer 实例 + StdioServerTransport,一对一服务。

HTTP 模式:创建单个共享的 McpServer + 单个共享的 StreamableHTTPServerTransport,由 SDK 的 transport 内部通过 mcp-session-id 路由不同客户端的请求。activeSessions Map 仅用于监控和超时清理,不参与请求路由。


四、核心开发

4.1 Server 创建与工厂模式

使用工厂函数封装 Server 创建和工具注册:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

function createServer(): McpServer {
  const server = new McpServer({
    name: "kingbase-mcp-server", // 服务名称(协议握手时返回)
    version: "1.0.0", // 服务版本
  });

  registerTools(server); // 注册所有工具
  return server;
}

McpServer 是 MCP SDK 提供的高层封装,内部处理了:

  • JSON-RPC 消息的序列化/反序列化
  • initialize 握手与能力协商
  • 工具列表响应(tools/list
  • 工具调用分发(tools/call

4.2 工具注册(Tool Registration)

这是开发 MCP Server 的核心环节。每个 Tool 由三部分组成:

server.registerTool(
  name, // 1. 工具名称(唯一标识符)
  config, // 2. 配置对象(title, description, inputSchema, annotations)
  handler, // 3. 异步处理函数
);

完整示例:

registerToolWithMetadata(
  server,
  "kb_query",
  {
    title: "Execute Query", // 人类可读标题
    description: `Execute a read-only SQL...`, // 详细描述(LLM 据此决定何时调用)
    inputSchema: QueryInputSchema, // Zod schema 定义输入参数
    annotations: {
      // 工具行为标注
      readOnlyHint: true,
      destructiveHint: false,
      idempotentHint: true,
      openWorldHint: false,
    },
  },
  async (params) => {
    // params 已经过 Zod 校验,类型安全
    const result = await executeQuery(params.sql, params.params);
    return {
      content: [{ type: "text", text: formatRows(result.rows) }],
    };
  },
);

重点description 字段极其重要!LLM 完全依赖这个描述来判断何时以及如何调用工具。需要包含:

  • 工具的功能说明
  • 参数含义与示例
  • 返回值说明
  • 使用限制和注意事项

4.3 输入校验(Zod Schema)

MCP SDK 原生集成 Zod,用于定义工具的输入参数:

import { z } from "zod";

const QueryInputSchema = z
  .object({
    sql: z
      .string()
      .min(1, "SQL query is required")
      .describe("SELECT query to execute"), // describe() 会出现在 JSON Schema 中
    params: z
      .array(z.union([z.string(), z.number(), z.boolean(), z.null()]))
      .optional()
      .describe("Optional parameterized query values ($1, $2, ...)"),
    schema: z
      .string()
      .optional()
      .describe("Schema name for unqualified table names"),
  })
  .strict(); // .strict() 拒绝未定义的额外字段

开发要点

  1. 每个字段都要加 .describe() — 这是 LLM 理解参数含义的关键
  2. 使用 .strict() 防止 LLM 传入未定义的字段
  3. .default() 为可选参数提供合理默认值
  4. .min() / .max() 添加业务约束

SDK 内部会将 Zod schema 自动转换为 JSON Schema,在 tools/list 响应中返回给客户端。

4.4 MCP Annotations(工具元数据注解)

Annotations 是 MCP 协议定义的工具行为标注,帮助客户端理解工具特性:

annotations: {
  readOnlyHint: boolean,      // true = 工具不修改任何状态
  destructiveHint: boolean,   // true = 工具可能执行不可逆操作
  idempotentHint: boolean,    // true = 相同输入多次调用结果相同
  openWorldHint: boolean,     // true = 工具与外部世界交互(如发邮件)
}
工具类型readOnlydestructiveidempotentopenWorld
只读查询truefalsetruefalse
INSERT/UPDATE/DELETEfalsetruefalsefalse
DDL (CREATE/ALTER/DROP)falsetruefalsefalse
EXPLAINfalsefalsetruefalse

这些注解帮助 MCP 客户端做出安全决策,例如 Claude Desktop 可能会在调用 destructiveHint: true 的工具前额外提示用户确认。

4.5 工具处理函数的返回格式

工具处理函数必须返回 MCP 规定的格式:

// 成功响应
return {
  content: [{ type: "text", text: "查询结果..." }],
};

// 错误响应
return {
  isError: true,
  content: [{ type: "text", text: "Error: syntax error at position 42" }],
};

content 是一个数组,支持多种类型:

  • { type: "text", text: "..." } — 纯文本
  • { type: "image", data: "base64...", mimeType: "image/png" } — 图片
  • { type: "resource", resource: { uri: "...", text: "..." } } — 资源引用

本项目所有工具统一返回 text 类型,使用 Markdown 格式化的表格。

4.6 数据库连接层

使用 pg.Pool 连接池管理数据库连接:

import pg from "pg"; // ESM 默认导入
const { Pool } = pg; // 解构获取 Pool 类

function createPool(): pg.Pool {
  return new Pool({
    host: process.env.DB_HOST || "localhost",
    port: parseInt(process.env.DB_PORT || "54321", 10),
    user: process.env.DB_USER || "system",
    password: process.env.DB_PASSWORD || "",
    database: process.env.DB_NAME || "kingbase",
    max: 5, // 最大连接数
    idleTimeoutMillis: 30000, // 空闲连接超时 30s
    connectionTimeoutMillis: 10000, // 连接超时 10s
  });
}

ESM/CJS 互操作注意pg 是 CommonJS 包,在 ESM 项目中必须用 import pg from "pg" 默认导入后再解构,不能直接 import { Pool } from "pg"

查询执行封装确保连接正确释放:

async function executeQuery(
  sql: string,
  params?: unknown[],
): Promise<pg.QueryResult> {
  const client = await pool.connect();
  try {
    return await client.query(sql, params);
  } finally {
    client.release(); // 无论成功失败都释放连接回池
  }
}

五、传输层实现

5.1 stdio 传输

这是最简单的模式,5 行代码即可启动:

import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";

const server = createServer();
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("KingBase MCP Server running via stdio");

注意:

  • 诊断输出必须走 console.error(stderr),因为 stdout 是 MCP 通信通道
  • 这也是 src/index.ts 首行 #!/usr/bin/env node 的原因 —— 让它可作为可执行脚本运行

5.2 Streamable HTTP 传输

HTTP 模式需要配合 Express 构建完整的 HTTP 服务:

import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
import express from "express";

const app = express();
app.use(express.json());

// 创建共享的 Transport 和 Server(单实例架构)
const transport = new StreamableHTTPServerTransport({
  sessionIdGenerator: () => randomUUID(),
  onsessionclosed: (sessionId) => {
    activeSessions.delete(sessionId);
  },
});

const server = createServer();
await server.connect(transport);

MCP Streamable HTTP 需要三个端点:

// POST /mcp — 接收 JSON-RPC 请求(包括 initialize 和普通调用)
app.post("/mcp", async (req, res) => {
  await transport.handleRequest(req, res, req.body);
});

// GET /mcp — SSE 流,用于服务端主动推送消息
app.get("/mcp", async (req, res) => {
  await transport.handleRequest(req, res);
});

// DELETE /mcp — 终止会话
app.delete("/mcp", async (req, res) => {
  await transport.handleRequest(req, res);
});

架构选择说明:本项目采用单实例架构(一个 McpServer + 一个 Transport),而非"每个会话一个 Server 实例"的模式。SDK 的 StreamableHTTPServerTransport 内部通过 mcp-session-id Header 路由请求到对应会话,无需开发者手动管理。这种架构更简单、资源占用更少。

5.3 会话管理

会话追踪仅用于运维监控和超时清理:

interface SessionInfo {
  transport: StreamableHTTPServerTransport;
  server: McpServer;
  createdAt: Date;
  lastActivityAt: Date;
  requestCount: number;
}

const activeSessions = new Map<string, SessionInfo>();

超时清理:每分钟扫描一次,清理超过 30 分钟无活动的会话:

const cleanupTimer = setInterval(() => {
  cleanupStaleSessions(activeSessions);
}, SESSION_CLEANUP_INTERVAL_MS);

容量控制:限制最大并发会话数,超过时返回 503:

if (activeSessions.size >= MAX_SESSIONS) {
  res.status(503).json({
    jsonrpc: "2.0",
    error: { code: -32000, message: "Server capacity reached" },
    id: null,
  });
}

优雅关闭:SIGINT 时依次关闭 transport → 清理会话 → 关闭 HTTP server → 关闭数据库连接池:

process.on("SIGINT", async () => {
  clearInterval(cleanupTimer);
  await transport.close();
  activeSessions.clear();
  httpServer.close(() => {
    pool.end().then(() => process.exit(0));
  });
});

六、安全设计

6.1 权限分级

通过 ACCESS_MODE 环境变量实现 4 级递增权限:

type AccessMode = "readonly" | "readwrite" | "full" | "admin";

const ACCESS_MODE_LEVELS: Record<AccessMode, number> = {
  readonly: 0, // SELECT / 查看 schema 结构
  readwrite: 1, // + INSERT / UPDATE
  full: 2, // + DELETE
  admin: 3, // + DDL (CREATE / ALTER / DROP / TRUNCATE)
};

function hasAccess(required: AccessMode): boolean {
  return ACCESS_MODE_LEVELS[getAccessMode()] >= ACCESS_MODE_LEVELS[required];
}

在每个写操作工具的处理函数入口检查权限:

// kb_execute 中
const dmlType = classifyDML(params.sql);
const requiredMode: AccessMode = dmlType === "DELETE" ? "full" : "readwrite";
if (!hasAccess(requiredMode)) {
  return {
    isError: true,
    content: [{ type: "text", text: "Access denied..." }],
  };
}

// kb_execute_ddl 中
if (!hasAccess("admin")) {
  return {
    isError: true,
    content: [{ type: "text", text: "Access denied..." }],
  };
}

默认为 readonly,即使 MCP 客户端尝试调用写操作工具也会被拒绝。

6.2 二次确认机制

DML 和 DDL 工具内置两阶段确认,防止 LLM 误操作:

// 第一阶段:预览
if (!params.confirmed) {
  return {
    content: [
      {
        type: "text",
        text: `⚠️ Confirmation required for ${dmlType} operation.\n\nSQL:\n${sql}\n\nCall again with confirmed: true to execute.`,
      },
    ],
  };
}

// 第二阶段:confirmed: true 时真正执行
const result = await executeQuery(sql, params.params);

这个设计确保 LLM 在执行任何写操作前必须向用户展示预览并获得确认。

6.3 SQL 安全

  1. 只读检查isReadOnly() 通过关键字前缀判断 SQL 是否为只读
  2. 危险 DDL 检测isDangerousDDL() 检测 DROP/TRUNCATE/CASCADE
  3. 参数化查询:所有工具都支持 $1, $2, ... 参数化查询,防止 SQL 注入
  4. schema 自动限定:防止跨 schema 访问(详见 7.2 节)

七、辅助功能

7.1 工具发现端点(Tool Discovery)

除了 MCP 协议内置的 tools/list,本项目额外提供了 HTTP REST 端点,方便非 MCP 客户端浏览工具信息:

// 全局工具注册表
const TOOL_REGISTRY: Map<string, ToolMetadata> = new Map();

// 包装注册函数,同时写入注册表
function registerToolWithMetadata(server, name, config, handler) {
  TOOL_REGISTRY.set(name, {
    name,
    title,
    description,
    inputSchema,
    annotations,
  });
  server.registerTool(name, config, handler);
}

// REST 端点
app.get("/tools", (req, res) => {
  const tools = Array.from(TOOL_REGISTRY.values()).map((t) => ({
    name: t.name,
    title: t.title,
    description: t.description.split("\n")[0], // 仅返回第一行摘要
    annotations: t.annotations,
  }));
  res.json({ count: tools.length, tools });
});

app.get("/tools/:toolName", (req, res) => {
  const tool = TOOL_REGISTRY.get(toolName);
  // 使用 zod-to-json-schema 将 Zod schema 转为标准 JSON Schema
  res.json(toolMetadataToJson(tool));
});

这使得 HTTP 模式下可以通过 GET /tools 快速查看所有可用工具,GET /tools/kb_query 查看某个工具的完整 JSON Schema。

7.2 Schema 自动限定

自动为未限定的表名添加 schema 前缀,简化 LLM 的 SQL 生成:

// 输入: "SELECT * FROM users"   + schema="myapp"
// 输出: "SELECT * FROM myapp.users"

// 输入: "SELECT * FROM public.users"  (已有 schema 前缀)
// 输出: "SELECT * FROM public.users"  (不修改)

function qualifyTableNames(sql: string, schema: string): string {
  const keywords = ["FROM", "JOIN", "INTO", "UPDATE"];
  // 对每个关键字后的未限定表名添加 schema. 前缀
  // 使用 negative lookahead 避免重复限定
}

7.3 输出截断

防止大结果集导致 Token 爆炸:

const CHARACTER_LIMIT = 50000;

if (result.length > CHARACTER_LIMIT) {
  result =
    result.substring(0, CHARACTER_LIMIT) +
    "\n\n... [Output truncated. Use LIMIT or add WHERE conditions to reduce results.]";
}

7.4 自动文档生成

scripts/generate-docs.ts 通过正则解析 src/index.ts 中的工具注册代码,自动生成 TOOLS.md

// 正则匹配 registerToolWithMetadata 调用
const toolPattern = /registerToolWithMetadata\(server,\s*"([^"]+)",.../gi;

// 提取: name, title, description, annotations
// 按 Read-Only / Write 分类
// 生成 Markdown 文档

集成到构建流程:

{
  "scripts": {
    "build": "npm run docs:generate && tsc",
    "docs:generate": "npx tsx scripts/generate-docs.ts"
  }
}

每次构建时自动更新文档,确保文档与代码始终同步。


八、测试

调试 MCP Server

官方提供了一个 Inspector 调试工具,它是一款用于测试和调试 MCP 服务器的交互式开发者工具,详细介绍和使用方法如下:

https://modelcontextprotocol.io/docs/tools/inspector

启动调试参考命令:

sudo npx @modelcontextprotocol/inspector node dist/index.js

启动后会自动打开网页,配置上必要的参数即可连接测试

c6ecf59372bc7f4cc96ad212e296db03.png

8.1 测试策略

由于 src/index.ts 在 import 时就会启动 Server(连接数据库、绑定端口),直接对其进行单元测试是不可行的。因此采取的策略是:

  1. 提取纯函数src/utils.ts —— 不依赖任何副作用,可独立测试
  2. 对纯函数编写详尽的单元测试(49 个 test cases)
  3. 集成测试通过手动验证 + CI 构建验证

8.2 纯函数提取与单元测试

src/utils.ts 导出的 9 个函数 + 常量:

函数职责测试用例数
isReadOnly(sql)判断 SQL 是否只读10
classifyDML(sql)分类 DML 类型6
isDangerousDDL(sql)检测危险 DDL6
formatRows(rows)格式化结果为 ASCII 表5
handleDbError(error)提取 pg 错误详情4
qualifyTableNames(sql, schema)Schema 自动限定6
getAccessMode()获取当前权限级别5
hasAccess(required)权限检查4
getDefaultSchema()获取默认 schema3

测试使用 Vitest 框架:

import { describe, it, expect, afterEach } from "vitest";

describe("isReadOnly", () => {
  it("returns true for SELECT", () => {
    expect(isReadOnly("SELECT * FROM users")).toBe(true);
  });
  it("returns false for INSERT", () => {
    expect(isReadOnly("INSERT INTO users (name) VALUES ('a')")).toBe(false);
  });
  // ...
});

对于依赖环境变量的函数,使用 afterEach 恢复原始值:

describe("getAccessMode", () => {
  const originalEnv = process.env.ACCESS_MODE;
  afterEach(() => {
    if (originalEnv === undefined) delete process.env.ACCESS_MODE;
    else process.env.ACCESS_MODE = originalEnv;
  });
  // ...
});

九、CI/CD 与发布

9.1 CI 流水线

.github/workflows/ci.yml:每次 push/PR 到 main 时自动运行:

name: CI
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [20, 22, 24] # 多版本矩阵测试
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: npm
      - run: rm -f package-lock.json && npm install
      - run: npx tsc # 类型检查 + 编译
      - run: npm test # 单元测试

注意 rm -f package-lock.json && npm install 而非 npm ci:这是因为本项目没有固定 lock file(依赖含平台相关的 native 模块如 rollup,npm ci 在跨平台时可能失败)。

9.2 npm 发布流水线

.github/workflows/publish.yml:当推送 v* tag 时自动发布到 npm:

name: Publish to npm
on:
  push:
    tags: ["v*"]

jobs:
  publish:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write # OIDC Trusted Publishing 所需
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 24
          registry-url: https://registry.npmjs.org
          cache: npm
      - run: rm -f package-lock.json && npm install
      - run: npx tsc
      - run: npm test
      - run: npm publish # 使用 OIDC Trusted Publishing,无需 NPM_TOKEN

OIDC Trusted Publishing:使用 GitHub Actions 的 OIDC token 直接认证 npm,无需在仓库 Secrets 中存储 NPM_TOKEN。需要在 npm 网站上预先配置 Trusted Publisher。Node版本需要>=24

先去www.npmjs.com网站找到发布的包,打开settings,在Trusted Publisher设置中选择GitHub Actions,然后填写源码仓库的信息以及发布的CI文件名。其中id-token的配置以及Node的版本要求缺一不可。

image.png

流水线执行完成后即可查看包的信息

image.png

查看包信息(官方仓库有延时,可能搜不到),下面命令是不受延时限制

npm view kingbase-mcp-server

kingbase-mcp-server@1.0.1 | MIT | deps: 6 | versions: 2
MCP server for KingBase (PostgreSQL-compatible) database operations
https://github.com/NekoTarou/kingbase-mcp-server#readme

keywords: mcp, mcp-server, kingbase, database, postgresql, model-context-protocol, ai, llm

bin: kingbase-mcp-server

dist
.tarball: https://registry.npmjs.org/kingbase-mcp-server/-/kingbase-mcp-server-1.0.1.tgz
.shasum: dded23279686cfcca773cbbcd1dfca8e7c925a75
.integrity: sha512-9Wj02l2bdOeA70OBDRPbYtGupxcojMrFiosTiqU9w4JTmXoDXwft3sPh5WxKL4PkenVqQEUA8vcpKlsFqINbTA==
.unpackedSize: 122.0 kB

dependencies:
@modelcontextprotocol/sdk: ^1.6.1 dotenv: ^17.2.4                   express: ^5.2.1                   pg: ^8.13.1                       zod-to-json-schema: ^3.25.1       zod: ^3.23.8

maintainers:
- nekoutarou <nekoutarou@gmail.com>

dist-tags:
latest: 1.0.1

published an hour ago by GitHub Actions <npm-oidc-no-reply@github.com>

9.3 版本发布流程

# 1. 更新 package.json 中的 version
npm version patch   # 或 minor / major

# 2. 推送代码和 tag
git push && git push --tags

# 3. CI 自动:编译 → 测试 → 发布到 npm

发布后用户即可使用(https://www.npmjs.com/package/kingbase-mcp-server):

npx kingbase-mcp-server                  # 直接运行
npm install -g kingbase-mcp-server       # 全局安装

十、客户端配置

10.1 stdio 模式配置

适用于 Claude Desktop、Claude Code 等本地 MCP 客户端。

方式一:npx 运行(推荐,无需安装)

~/.claude.json 或项目根目录的 .mcp.json 中:

{
  "mcpServers": {
    "kingbase": {
      "command": "npx",
      "args": ["-y", "kingbase-mcp-server"],
      "env": {
        "DB_HOST": "localhost",
        "DB_PORT": "54321",
        "DB_USER": "system",
        "DB_PASSWORD": "your_password",
        "DB_NAME": "kingbase",
        "DB_SCHEMA": "public",
        "ACCESS_MODE": "readonly"
      }
    }
  }
}

opencode配置:

vim ~/.config/opencode/opencode.json
  "mcp": {
      "kingbase": {
              "type": "local",
              "command": ["npx","-y", "kingbase-mcp-server"],
              "environment": {
                    "DB_HOST": "localhost",
                    "DB_PORT": "54321",
                    "DB_USER": "system",
                    "DB_PASSWORD": "your_password",
                    "DB_NAME": "kingbase",
                    "DB_SCHEMA": "public",
                    "ACCESS_MODE": "readonly"
                    "TRANSPORT": "stdio"
                  }
       }
  }

方式二:本地编译运行

{
  "mcpServers": {
    "kingbase": {
      "command": "node",
      "args": ["/path/to/kingbase-mcp-server/dist/index.js"],
      "env": {
        "DB_HOST": "localhost",
        "DB_PASSWORD": "your_password"
      }
    }
  }
}

配置之后记得重启客户端,查看mcp列表是否连接

Claude查看效果如下:

image.png

opencode查看如下:

image.png

此时可以验证

image.png

10.2 HTTP 模式配置

服务端启动:

TRANSPORT=http MCP_PORT=3000 DB_HOST=192.168.1.100 DB_PASSWORD=xxx node dist/index.js

客户端配置(简洁,只需一个 URL):

{
  "mcpServers": {
    "kingbase": {
      "url": "http://your-server:3000/mcp"
    }
  }
}

验证连接:

# 健康检查
curl http://localhost:3000/health

# 查看工具列表
curl http://localhost:3000/tools

# 测试 MCP 初始化
curl -X POST http://localhost:3000/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}'

10.3 服务器部署(systemd)

创建环境变量文件 /etc/kingbase-mcp-server.env

TRANSPORT=http
MCP_PORT=3000
MCP_HOST=0.0.0.0
ACCESS_MODE=readonly
DB_HOST=192.168.1.100
DB_PORT=54321
DB_USER=system
DB_PASSWORD=your_password
DB_NAME=mydb
DB_SCHEMA=public

创建 systemd 服务 /etc/systemd/system/kingbase-mcp.service

[Unit]
Description=KingBase MCP Server
After=network.target

[Service]
Type=simple
EnvironmentFile=/etc/kingbase-mcp-server.env
WorkingDirectory=/opt/kingbase-mcp-server
ExecStart=/usr/bin/node dist/index.js
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable kingbase-mcp
sudo systemctl start kingbase-mcp
sudo journalctl -u kingbase-mcp -f    # 查看日志

十一、开发中的重点与踩坑总结

协议与 SDK 层面

  1. ESM 模块路径必须带 .js 后缀import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js" — 即使源码是 .ts,import 路径也要写 .js
  2. pg 的 ESM/CJS 互操作:必须 import pg from "pg"const { Pool } = pg,不能直接解构导入
  3. stdout 是 MCP 通道:stdio 模式下所有日志/诊断输出必须走 stderrconsole.error
  4. description 是工具的灵魂:LLM 完全依赖描述来理解工具,要写得清晰、完整、有示例
  5. Zod .strict() 很重要:防止 LLM 传入未定义的字段导致意外行为

架构层面

  1. 单文件 vs 多文件:小型 MCP Server 用单文件更易理解。仅在需要测试时提取纯函数
  2. 单实例 vs 多实例 HTTP:SDK 的 StreamableHTTPServerTransport 内部管理会话路由,开发者用单实例即可
  3. 工厂模式createServer() + registerTools() 分离让代码组织更清晰
  4. 连接池大小:MCP Server 通常不需要大连接池,max: 5 足够

安全层面

  1. 默认最小权限ACCESS_MODE 默认 readonly,需要写权限时显式配置
  2. 二次确认是刚需:LLM 可能"自作主张"执行 DELETE/DROP,确认机制是最后防线
  3. HTTP 无内置认证:生产环境必须加反向代理 + 认证

发布层面

  1. #!/usr/bin/env nodebin 入口文件必须有
  2. files 字段控制包内容:避免发布 src/tests/.env 等不需要的文件
  3. prepublishOnly:确保每次发布前自动构建
  4. OIDC Trusted Publishing:比 NPM_TOKEN 更安全,需 Node.js 24+ 支持
  5. CI 中 rm -f package-lock.json:解决跨平台 native 依赖不兼容的问题

开发体验

  1. npm run dev:使用 tsx watch 实现热重载开发
  2. .env 支持dotenv/config import 让本地开发不用每次在命令行传环境变量
  3. CLAUDE.md:为 AI 助手提供代码导航指南,加速协同开发
  4. 自动文档生成build 时自动更新 TOOLS.md,文档永远与代码同步

最后:MCP并不能完成所有事情,却是agent获取外部资源重要手段,在日常开发过程中搭配SKILLS才能更好发挥其作用。

References

OpenCode关于Mcp说明
Inspector 调试
Trusted publishing for npm packages
openid-connect
Connect Claude Code to tools via MCP