拓冰建站拓冰建站
首页 / 资讯中心 / 正文

用MCP Server打造AI原生CRM:从零实现智能销售数据访问

做销售管理和业务增长的同学应该都有类似感受CRM 系统里录了成千上万条客户数据可每到复盘销售预测、梳理 pipeline 的时候还是要让人从系统里导出表格再粘贴到 Excel 或各种模型里做汇总。数据明明都在却离“直接使用”总是差一步。等 AI 真正参与收入流程时这个问题会被继续放大AI 没有稳定、标准的访问方式去读取 CRM 里的客户、商机和金额数据自然也就无法帮你跟进、分析、预测。最近开源社区里讨论度比较高的Salestrics项目给出了一个值得关注的思路把MCP server和CRM放在同一个开源项目里专门服务AI-native revenue teams。这篇文章不会只停留在项目介绍层面我会带着你把一个简化版 Salestrics 完整实现出来用 TypeScript 实现 MCP server用 SQLite 模拟 CRM 数据层最终让 AI 客户端能够直接查询客户、创建商机、更新销售阶段。无论你是在企业里做 AI Agent 落地还是想研究 MCP 协议本身这个项目都值得完整走一遍。1. 背景传统 CRM 与 AI-Native 收入团队1.1 传统 CRM 的困境CRMCustomer Relationship Management客户关系管理系统本质上是一个记录客户全生命周期数据的系统从线索进入、客户分配、商机推进到合同签订、客户成功所有环节本应沉淀在同一个平台中。但传统 CRM 更像一个“数据仓库”而不是“工作流引擎”。它的问题通常出现在几个环节数据录入依赖人工销售不愿意花时间维护。数据分析需要特定技能普通业务人员很难直接访问 SQL 层。系统之间相互孤立CRM 里的数据无法被其他工具自动调用。AI 能力即便接入了 CRM也只是内置几个固定按钮无法按需查询和编排。换句话说传统 CRM 更多是“人找数据”。AI-Native 团队想要实现的恰恰相反是“数据找人”准确说是“数据被 AI Agent 主动调用”。1.2 AI-Native Revenue Team 是什么AI-Native revenue team可以理解成销售、客户成功、增长团队把 AI Agent 当作日常协作伙伴而不仅仅是后台报表工具。一个典型的场景是销售每天早晨向 AI 助手下指令“帮我看看所有超过 7 天没有跟进的商机并按金额从高到低列出来。”AI 助手需要实时查询 CRM 中的商机列表、最近跟进时间、客户所属行业、负责人等信息。销售确认后AI 助手还需要自动创建一条跟进任务或者给客户负责人写一封个性化的跟进邮件。这个流程看起来很简单但背后的技术挑战不小AI 模型本身并不了解你的 CRM 表结构也不知道如何把自然语言转换成 CRM 查询。过去要实现这种能力需要开发团队为每一个场景写死 API 调用成本和维护压力都很大。MCP 协议的出现改变了这种局面。1.3 MCP 是什么MCP 全称 Model Context Protocol模型上下文协议。它定义了一套统一标准让 AI 应用客户端能够通过标准方式访问外部数据源和工具。你可以把 MCP 理解为 AI 世界的“USB-C 接口”。在 USB-C 普及之前不同设备有不同充电线接口互相不兼容。MCP 做的事情就是统一 AI 应用与外部系统之间的通信协议让一个 MCP Server 可以用同样的方式被 Claude Desktop、Cursor、自研 Agent 等客户端连接。一个 MCP Server 通常暴露三类能力Tools工具允许 AI 调用的函数例如查询客户列表、创建商机。Resources资源只读数据例如一个固定的报表模板或文档。Prompts提示词预定义的提示模板帮助 AI 完成特定任务。Salestrics 这类项目的核心思路就是让 CRM 成为 MCP Server 的一个数据源把客户查询、商机创建、销售阶段更新等操作封装成标准 Tools。AI 客户端只要支持 MCP就能直接获得 CRM 能力。2. 集成原理与架构设计2.1 为什么用 MCP 连接 CRM 而不是写普通 API有同学可能会问我直接写一个 REST API 给 AI 调用不行吗为什么一定要用 MCP可以但代价不同。REST API 只能解决“接口存在”的问题解决不了“AI 如何知道接口存在”以及“AI 如何决定何时调用哪个接口”的问题。MCP 的核心价值有两个能力发现AI 客户端连接 MCP Server 后可以自动获取工具列表和参数 schema不需要人为编写固定调用链。标准化集成无论底层是腾讯会议、GitHub、CRM 还是数据库接入方式统一。对 AI Agent 开发者来说学习成本大幅降低。所以一个 AI-Native 收入团队如果要构建销售 Copilot使用 MCP 集成 CRM 是当前路径最短、社区生态也最活跃的方案。2.2 整体架构分层按 Salestrics 的架构思路我们可以把系统拆成三层┌──────────────────────────────────────┐ │ AI Agent / MCP Client │ │ Claude Desktop / Cursor / 自研 Agent │ └──────────────────┬───────────────────┘ │ MCP 协议stdio/SSE/HTTP ┌──────────────────▼───────────────────┐ │ MCP Server 层 │ │ 工具注册 / 参数校验 / 鉴权 / 结果格式化 │ └──────────────────┬───────────────────┘ │ SQL / REST / GraphQL ┌──────────────────▼───────────────────┐ │ CRM 数据层 │ │ 自建数据库 / Twenty CRM / 悟空 CRM │ └──────────────────────────────────────┘这种分层带来的好处是上层 Agent 不关心 CRM 数据底层在哪里。MCP Server 是唯一需要掌握业务逻辑的服务。数据层可以替换先用自建数据库做 POC再切换到已有 CRM 系统。2.3 MCP Server 与 CRM 合体 or 拆开Salestrics 选择把 MCP Server 和 CRM 放在同一个项目里。这种“合体”模式优势很明显数据结构天然贴合 MCP 工具定义不需要额外做字段翻译。本地部署时数据完全可控无需把客户数据传输给第三方 CRM。可以让 AI 收入团队开箱即用不必先解决“已有 CRM 系统版本陈旧”的问题。但也有团队已经有 Twenty CRM、悟空 CRM 等开源系统。这种情况下更合理的方式是让 MCP Server 作为独立服务存在通过 GraphQL 或 REST API 连接已有 CRM。两种模式没有绝对优劣取决于你的现状。本文后面的实战会以“自建 SQLite 数据层”为例这也是最容易迁移到真实 CRM 的方式。3. 环境准备与开源 CRM 选型3.1 运行环境说明开始写代码之前先确认本机环境。如果你只有浏览器环境也可以把数据库切换为在线 SQLite 服务但为了教程通用性我假设你使用本地环境。下面是我这次示例使用的环境你可以按自己本机情况微调操作系统macOS / Linux / Windows 均可Node.js18 或更高版本包管理器npm 或 pnpm数据库SQLite 文件数据库使用 better-sqlite3 驱动语言TypeScript 5MCP SDKmodelcontextprotocol/sdk版本不需要完全一致MCP SDK 仍在快速发展中API 可能会有小幅调整。实际安装时以你安装到的版本为准思路是通用的。3.2 开源 CRM 选型对比如果你不想从零建设数据层可以直接选择成熟开源 CRM 进行二次开发。下表对几个常见方案做了对比方案技术栈特点适合场景Twenty CRMTypeScript / React / GraphQL现代感强数据结构清晰API 适合二次开发愿意投入前端资源的新团队悟空 CRMPHP国内开源 CRM功能完整部署资料多需要快速私有化部署的传统团队自建 SQLite / PostgreSQL任意灵活无历史包袱适合做 MCP 原型POC 验证、学习协议、内部工具Salestrics 这类项目选择“合体”模式其实和自建数据层思路很接近先用一套轻量数据结构跑通 MCP后续再平滑升级到企业级 CRM。3.3 本文示例定位考虑到大部分开发者本地实践方便本文用 SQLite 文件模拟 CRM 数据层再通过真实 MCP SDK 暴露工具接口。数据访问层会单独封装你可以把crmService.ts中的实现替换成 Twenty CRM 的 GraphQL 调用或者悟空 CRM 的 REST API上层 MCP 工具无需大改。4. 初始化项目4.1 创建项目目录打开终端执行下面的命令mkdir crm-mcp-server cd crm-mcp-server npm init -y4.2 安装依赖安装 MCP SDK 和数据库相关依赖npm install modelcontextprotocol/sdk better-sqlite3 zod dotenv npm install -D typescript tsx types/better-sqlite3 types/node说明一下各个包的用途modelcontextprotocol/sdk提供 MCP Server / Client 的类型和通信能力。better-sqlite3同步 API 的 SQLite 驱动适合本地原型不用处理异步回调。zod用于定义 MCP 工具输入参数的 schema并做运行时校验。dotenv读取.env配置文件中的环境变量。tsx开发模式下直接运行 TypeScript 文件避免每次手动编译。4.3 配置 TypeScript修改tsconfig.json这是一个能正常编译 NodeNext 模块的配置{ compilerOptions: { target: ES2022, module: NodeNext, moduleResolution: NodeNext, outDir: dist, rootDir: src, strict: true, esModuleInterop: true, skipLibCheck: true, declaration: false, sourceMap: true }, include: [src] }为了避免模块解析问题推荐在package.json中显式设置type: module{ name: crm-mcp-server, version: 0.1.0, type: module, scripts: { build: tsc, start: node dist/index.js, dev: tsx src/index.ts, test: tsx scripts/testClient.ts } }4.4 目录结构项目创建完成后我们约定如下目录结构crm-mcp-server/ ├── package.json ├── tsconfig.json ├── schema.sql ├── .env.example ├── src/ │ ├── index.ts # MCP Server 入口注册工具 │ ├── db.ts # 数据库连接与初始化 │ └── crmService.ts # CRM 业务逻辑 └── scripts/ └── testClient.ts # MCP Client 测试脚本5. 实现数据层5.1 设计 CRM 表结构对于 AI-Native 收入团队最常查询的数据通常围绕“客户”和“商机”展开。我们设计两张表accounts客户账户表。opportunities商机表记录每个客户的销售机会和阶段。先创建schema.sql文件CREATE TABLE IF NOT EXISTS accounts ( id TEXT PRIMARY KEY, name TEXT NOT NULL, owner_email TEXT NOT NULL, industry TEXT, arr INTEGER DEFAULT 0, created_at TEXT DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS opportunities ( id TEXT PRIMARY KEY, account_id TEXT NOT NULL REFERENCES accounts(id), title TEXT NOT NULL, stage TEXT NOT NULL DEFAULT prospecting, amount INTEGER DEFAULT 0, owner_email TEXT NOT NULL, updated_at TEXT DEFAULT CURRENT_TIMESTAMP );字段设计上有几个细节需要解释金额字段arr和amount使用 INTEGER单位按“分”存储避免浮点精度问题。stage字段使用字符串而不是多个布尔字段是因为销售阶段本身是一个可枚举状态字符串更便于扩展和展示。owner_email放在两张表中而不是只通过account_id关联核心原因是销售数据访问隔离AI Agent 查询时通常需要按负责人过滤冗余字段能显著降低查询复杂度。5.2 初始化数据库连接接着创建src/db.ts负责初始化数据库并自动建表// 文件路径src/db.ts import Database from better-sqlite3; import fs from node:fs; import path from node:path; import { fileURLToPath } from node:url; const __dirname path.dirname(fileURLToPath(import.meta.url)); const defaultDbPath path.resolve(__dirname, ../crm.db); export type CRMDB Database.Database; export function createDb(dbPath: string defaultDbPath): CRMDB { const db new Database(dbPath); db.pragma(journal_mode WAL); // 读取 schema.sql 并执行 const schemaPath path.resolve(__dirname, ../schema.sql); const schema fs.readFileSync(schemaPath, utf-8); db.exec(schema); return db; }这段代码有两个关键点db.pragma(journal_mode WAL)开启 SQLite 的 WAL 模式。WAL 模式可以显著减少 MCP Server 多线程访问时的写入锁冲突是本地开发最推荐的配置。每次启动都执行一次CREATE TABLE IF NOT EXISTS建表语句保证表结构存在同时不会覆盖已有数据。5.3 业务服务层创建src/crmService.ts把数据操作封装为纯函数。这样的好处是MCP 工具层只关心参数和返回值底层更换数据库时不会影响 MCP 接口定义。// 文件路径src/crmService.ts import { CRMDB } from ./db; export interface Account { id: string; name: string; ownerEmail: string; industry: string; arr: number; } export interface Opportunity { id: string; accountId: string; title: string; stage: string; amount: number; ownerEmail: string; updatedAt: string; } export function listAccounts(db: CRMDB, ownerEmail?: string): Account[] { if (ownerEmail) { return db .prepare(SELECT id, name, owner_email, industry, arr FROM accounts WHERE owner_email ?) .all(ownerEmail) as Account[]; } return db .prepare(SELECT id, name, owner_email, industry, arr FROM accounts) .all() as Account[]; } export function createOpportunity( db: CRMDB, input: { accountId: string; title: string; amount: number; ownerEmail: string; } ): Opportunity { const id opp_${Date.now()}; db.prepare( INSERT INTO opportunities (id, account_id, title, stage, amount, owner_email) VALUES (?, ?, ?, prospecting, ?, ?) ).run(id, input.accountId, input.title, input.amount, input.ownerEmail); return { id, accountId: input.accountId, title: input.title, stage: prospecting, amount: input.amount, ownerEmail: input.ownerEmail, updatedAt: new Date().toISOString(), }; } export function updateOpportunityStage( db: CRMDB, opportunityId: string, stage: string ): Opportunity | null { const result db .prepare(UPDATE opportunities SET stage ?, updated_at CURRENT_TIMESTAMP WHERE id ?) .run(stage, opportunityId); if (result.changes 0) { return null; } const row db .prepare( SELECT id, account_id, title, stage, amount, owner_email, updated_at FROM opportunities WHERE id ? ) .get(opportunityId) as any; return { id: row.id, accountId: row.account_id, title: row.title, stage: row.stage, amount: row.amount, ownerEmail: row.owner_email, updatedAt: row.updated_at, }; } export function getPipelineOverview(db: CRMDB, ownerEmail?: string) { const params ownerEmail ? [ownerEmail] : []; const where ownerEmail ? WHERE owner_email ? : ; const rows db .prepare( SELECT stage, COUNT(*) as count, SUM(amount) as total_amount FROM opportunities ${where} GROUP BY stage ) .all(...params) as { stage: string; count: number; total_amount: number }[]; return rows; }这段代码里我没有复用owner_email字段名和 TypeScript 风格完全一致而是通过 SQL 别名和返回值解析做了兼容。实际项目里建议直接把数据库字段命名规范对齐减少转换代码。6. 实现 MCP 工具接口6.1 创建 MCP Server 入口现在进入核心部分创建src/index.ts初始化 MCP Server并把上述业务函数注册为 MCP Tools。// 文件路径src/index.ts import { McpServer } from modelcontextprotocol/sdk/server/mcp.js; import { StdioServerTransport } from modelcontextprotocol/sdk/server/stdio.js; import { z } from zod; import { createDb } from ./db; import { listAccounts, createOpportunity, updateOpportunityStage, getPipelineOverview, } from ./crmService; const db createDb(); const server new McpServer({ name: salestrics-local-crm, version: 0.1.0, });这里说明一下StdioServerTransport。MCP SDK 支持很多传输方式stdio 是最常用于本地开发的方式客户端启动一个子进程通过标准输入输出和 MCP Server 通信。对于我们这种本地 CRM 场景stdio 简单稳定不需要额外开端口。6.2 注册 get_accounts 工具第一个工具是查询客户列表支持按负责人邮箱过滤server.registerTool( get_accounts, { description: 获取 CRM 客户账户列表可按销售负责人邮箱过滤。用于查看客户信息、客户所属行业、年度经常性收入等。, inputSchema: { ownerEmail: z.string().email().optional().describe(销售负责人邮箱), }, }, async ({ ownerEmail }) { const accounts listAccounts(db, ownerEmail); return { content: [ { type: text as const, text: JSON.stringify(accounts, null, 2), }, ], }; } );这段代码有几个值得注意的地方inputSchema使用 zod 描述参数结构MCP SDK 会自动生成 JSON Schema 下发给客户端。AI 模型会通过工具描述决定何时调用该工具所以描述要写得像“给 AI 的说明书”而不是给人的注释。工具返回值必须是content数组type: text是最常见的文本返回方式。6.3 注册 create_opportunity 工具第二个工具是创建商机这是一个写操作。在示例中我们直接允许执行但在生产环境中写操作权限需要慎重控制。server.registerTool( create_opportunity, { description: 为指定客户创建新的销售商机。需要提供客户账户 ID、商机标题、金额和负责人邮箱。, inputSchema: { accountId: z.string().describe(客户账户 ID), title: z.string().describe(商机标题), amount: z.number().int().nonnegative().describe(商机金额单位分), ownerEmail: z.string().email().describe(销售负责人邮箱), }, }, async ({ accountId, title, amount, ownerEmail }) { const opportunity createOpportunity(db, { accountId, title, amount, ownerEmail, }); return { content: [ { type: text as const, text: JSON.stringify(opportunity, null, 2), }, ], }; } );注意金额单位。我在 schema 里说明单位分这是为了避免 AI 模型把“元”和“分”混淆。工程上建议所有金额字段都统一成最小货币单位并在工具描述中显式说明否则模型很容易把用户口头说的“万元”直接当成数值。6.4 注册 update_deal_stage 工具第三个工具是更新商机阶段。销售流程中这个操作频率很高但也最容易出现误操作所以适当加一些约束const DEAL_STAGES [ prospecting, qualification, proposal, negotiation, closed_won, closed_lost, ] as const; server.registerTool( update_deal_stage, { description: 更新指定商机的销售阶段stage。可选值prospecting、qualification、proposal、negotiation、closed_won、closed_lost。, inputSchema: { opportunityId: z.string().describe(商机 ID), stage: z.enum(DEAL_STAGES).describe(新的销售阶段), }, }, async ({ opportunityId, stage }) { const updated updateOpportunityStage(db, opportunityId, stage); if (!updated) { return { content: [ { type: text as const, text: JSON.stringify({ error: 商机不存在或更新失败 }, null, 2), }, ], }; } return { content: [ { type: text as const, text: JSON.stringify(updated, null, 2), }, ], }; } );使用z.enum约束阶段值比让 AI 自由输入字符串更安全。模型在调用工具前会先读到可选值大多数情况下会自动选出合法参数。6.5 注册 get_pipeline_overview 工具最后一个工具是销售 pipeline 汇总。这个工具适合每天例会时让 AI 快速汇报server.registerTool( get_pipeline_overview, { description: 获取销售管道总览按商机阶段分组统计商机数量和金额总和。可按负责人邮箱过滤。, inputSchema: { ownerEmail: z.string().email().optional().describe(销售负责人邮箱), }, }, async ({ ownerEmail }) { const overview getPipelineOverview(db, ownerEmail); return { content: [ { type: text as const, text: JSON.stringify(overview, null, 2), }, ], }; } );6.6 启动 MCP Server工具全部注册完成后最后连接传输层并启动const transport new StdioServerTransport(); await server.connect(transport); console.error(CRM MCP Server running on stdio);这里刻意用console.error输出日志而不是console.log。原因很关键stdio 模式下MCP Server 的标准输出被协议占用不能用console.log打印业务日志否则会污染通信内容。只能用标准错误输出打日志。7. 用 MCP Client 验证工具7.1 编写测试客户端运行 MCP Server 后建议先不要直接接入大型客户端而是写一个轻量测试脚本验证工具是否可用。创建scripts/testClient.ts// 文件路径scripts/testClient.ts import { Client } from modelcontextprotocol/sdk/client/index.js; import { StdioClientTransport } from modelcontextprotocol/sdk/client/stdio.js; const transport new StdioClientTransport({ command: node, args: [dist/index.js], }); const client new Client({ name: crm-test-client, version: 0.1.0, }); await client.connect(transport); // 1. 查看工具列表 const toolsResult await client.listTools(); console.log(工具列表, toolsResult.tools.map((tool) tool.name)); // 2. 调用 get_accounts const accountsResult await client.callTool({ name: get_accounts, arguments: {}, }); console.log(客户列表, accountsResult.content[0].text); // 3. 调用 get_pipeline_overview const pipelineResult await client.callTool({ name: get_pipeline_overview, arguments: {}, }); console.log(管道总览, pipelineResult.content[0].text); await client.close();7.2 运行与预期输出先用npm run build编译 TypeScript再运行测试脚本npm run build npm test预期输出大致如下由于没有种子数据可能需要你根据实际情况调整工具列表 [ get_accounts, create_opportunity, update_deal_stage, get_pipeline_overview ] 客户列表 [] 管道总览 []如果你希望测试时有数据可看可以在createDb之后手动插入几条演示数据或提供一个 seed 脚本。7.3 配置到 Claude Desktop测试通过后可以把 MCP Server 配置到 Claude Desktop 中。找到 Claude Desktop 的配置文件加入mcpServers字段{ mcpServers: { crm: { command: node, args: [/绝对路径/crm-mcp-server/dist/index.js] } } }重启 Claude Desktop然后在对话中尝试输入请帮我查看当前所有的客户账户并按年收入从高到低展示。Claude 会识别到get_accounts工具并主动调用查询。7.4 配置到 CursorCursor 同样支持 MCP 配置。打开 Cursor 设置中的 MCP 面板添加 server类型: stdio 命令: node 参数: /绝对路径/crm-mcp-server/dist/index.js配置完成后AI 对话中可以直接引用 MCP 工具的数据。比如帮我创建一条商机客户是刚才查到的 acc_001金额 30000 分。8. 常见问题与排查MCP Server CRM 在集成过程中问题往往出现在工具注册、数据访问和客户端配置三个层面。下面整理一份高频问题排查表问题现象常见原因解决思路客户端连接后看不到工具列表MCP Server 启动失败或工具注册时报错先用npm run dev直接运行看报错信息工具调用返回空数据数据库没有种子数据或 WHERE 条件过严先直接 SQL 查询验证数据是否存在SQLITE_BUSY 锁错误多个进程并发写同一个 SQLite 文件开启 WAL 模式或迁移 PostgreSQL模型传参不符合 schema工具描述不够清晰模型猜错字段含义在描述中显式说明字段单位和枚举值数据泄露成员看到他人客户ownerEmail 过滤没有作为硬约束服务层强制校验而不是靠模型自觉标准输出被日志污染使用了console.log打日志换成console.error输出日志下面展开几个典型问题的排查过程。8.1 MCP Server 启动失败最常见的原因是 SDK 版本与代码语法不一致。MCP SDK 更新速度快registerTool的参数和返回值类型在版本之间有过调整。遇到这种情况先执行npx tsx src/index.ts观察启动日志。如果代码本身有语法错误会直接输出到终端。另外注意包版本锁定。建议在package.json中锁住大版本不要直接使用latest避免协作成员安装到不一致版本。8.2 AI 调用工具返回“参数缺少”这通常不是代码 bug而是 schema 描述不清晰。例如create_opportunity里的amount字段如果描述写得不清楚模型可能把“3万元”直接转成30000但我们的单位是“分”正确值应该是3000000。解决办法字段使用语义化命名。在describe中写明单位。如果需要可以在 MCP Server 内部做一次单位校验超出合理范围时提示模型重新传参。8.3 数据权限问题MCP 工具一旦暴露给所有能连接当前客户端的模型调用内部就要有完整的权限边界。get_accounts里的ownerEmail如果只是可选项那么模型可以用“不传 ownerEmail”的方式查全量客户。生产环境建议服务层强制ownerEmail通过鉴权上下文获取而不是由模型参数自由传入。至少也要在 MCP Server 内部区分“当前登录用户”和“被查询用户”防止越权。9. 生产环境最佳实践从本地原型演进到生产环境有六个方面需要特别关注。9.1 权限最小化MCP 工具不是越全越好。默认只暴露“查询”类工具写操作经过审批流。你可以把工具分为只读和可写两组通过客户端权限配置控制谁能调用create_opportunity、update_deal_stage这类操作。9.2 Secret 管理示例中数据库是本地文件没有涉及数据库密码但一旦接入真实 CRM如 Twenty CRM 的 GraphQL APIAPI Key、访问令牌都会出现。任何时候都不要把这些信息硬编码在代码中统一放到环境变量或密钥管理服务中。CRM_API_URLhttps://your-crm.example.com/graphql CRM_API_TOKENyour-token-here9.3 数据同步策略如果 MCP Server 直接连接已有 CRM要设计好数据同步频率。实时同步压力大离线同步数据滞后。比较稳妥的方案是查询类工具读取 MCP Server 本地缓存。写操作实时写入上游 CRM并同步更新缓存。缓存设置 5 到 10 分钟过期敏感数据缩短过期时间。9.4 日志与可观测性每个 MCP 工具调用都应该有 trace 上下文。建议在返回结果中带上request_id输出类似[2025-06-01T10:00:00Z] request_idreq_123 toolget_accounts params{ownerEmail:aliceexample.com} duration12ms日志中不要记录客户姓名、邮箱、金额等敏感字段。如果调试确实需要对邮箱做部分脱敏。9.5 测试策略MCP Server 的逻辑并不复杂但边界条件很多。推荐三层测试单元测试针对crmService各函数。集成测试启动真实 MCP Server用 Client 调用工具。场景测试用 AI 客户端的自然语言提示词跑一轮端到端流程。在 CI 中至少跑单元测试和集成测试场景测试可以带 tag 手动触发。9.6 从本地到云端本地使用 stdio 传输很方便但部署到服务器后AI Agent 可能无法直接启动子进程。这时候需要把 MCP Server 暴露为 HTTP/SSE 方式SDK 中有StreamableHTTPServerTransport可以使用。切换后注意三点增加鉴权中间件MCP 不是自带认证机制。对敏感写操作增加审批确认步骤。配置反向代理时保留 SSE 长连接参数避免连接被提前断开。10. 总结与学习建议如果你正在搭建 AI 销售助手核心不是把 CRM 所有接口都暴露给模型而是先定义清楚销售 Agent 在什么场景下需要什么数据再把这些能力收敛成少量稳定的 MCP 工具。Salestrics 这类开源项目提供了一个很好的起点通过 MCP 把客户查询、商机创建、销售阶段更新这些高频动作标准化让 AI 真正走进收入团队的工作流。动手建议按这个顺序推进先把本文示例跑通观察 MCP 工具发现和参数校验机制再接入 Twenty CRM 或你们已有的 CRM 数据最后根据业务需求定制新的 MCP 工具比如收入预测、商机评分、客户流失预警。等到 Agent 能在每天例会前自动汇总销售 pipeline 并给出跟进建议时你就已经完成了一个 AI-Native Revenue Team 的基础建设。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门