创建 Client-Tool-Execution MCP 服务器
这使您能够:
# 使用 Node(更好的兼容性)
npm i @mcpc-tech/cmcp
# 使用 Deno
deno add jsr:@mcpc/cmcp
这里是一个最小的工作示例:
服务器充当一个 代理和注册表——它没有预定义的工具,只是将执行路由到客户端:
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { createClientExecServer } from "@mcpc/cmcp";
// 服务器只是一个代理——没有工具,没有执行逻辑
const server = createClientExecServer(
new Server({ name: "dynamic-mcp-server", version: "1.0.0" }),
"dynamic-server",
);
// 服务器将所有工具调用路由到适当的客户端
// 所有执行都在客户端发生
客户端注册带有本地实现的工具,这些工具在客户端本地执行:
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
import { type ClientToolDefinition, createClientExecClient } from "@mcpc/cmcp";
const client = createClientExecClient(
new Client({ name: "browser-client", version: "1.0.0" }),
"browser-client-001",
);
// 定义带有本地实现的工具(在客户端执行)
const tools: ClientToolDefinition[] = [
{
name: "querySelector",
description: "使用 CSS 选择器查询 DOM 元素",
inputSchema: {
type: "object",
properties: {
selector: { type: "string", description: "要查询的 CSS 选择器" },
action: {
type: "string",
description: "要执行的操作",
enum: ["getText", "click", "getAttribute"],
},
attribute: { type: "string", description: "属性名称" },
},
required: ["selector", "action"],
},
// 🔥 实现在客户端运行——具有对 DOM、本地文件等的访问权限
implementation: async (args: Record<string, unknown>) => {
const { selector, action, attribute } = args;
const element = document.querySelector(selector as string);
if (!element) {
throw new Error(`未找到元素: ${selector}`);
}
switch (action) {
case "getText":
return element.textContent || "";
case "click":
element.click();
return `点击了元素: ${selector}`;
case "getAttribute":
return element.getAttribute(attribute as string);
default:
throw new Error(`未知操作: ${action}`);
}
},
},
];
// 注册工具(存储在本地直到连接)
client.registerTools(tools);
// 连接到服务器并注册工具
await client.connect(
new SSEClientTransport(new URL("http://localhost:9000/sse")),
);
console.log("客户端已连接并注册工具!");
// 客户端保持连接以处理工具执行请求
// 外部 MCP 客户端连接到服务器
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
const mcpClient = new Client({
name: "external-client",
version: "1.0.0",
});
await mcpClient.connect(
new SSEClientTransport(new URL("http://localhost:9000/sse")),
);
// 调用已连接客户端注册的工具
const result = await mcpClient.callTool({
name: "querySelector",
arguments: {
selector: "#my-button",
action: "click",
},
});
console.log(result); // "点击了元素: #my-button"
// ✨ 实际的 DOM 操作发生在客户端!
想要更多现成的客户端工具?在 AI 工具注册表中查找许多示例工具定义:https://ai-tools-registry.vercel.app/
传统 MCP:工具在服务器上执行
Client-Tool-Execution MCP:工具在客户端执行
client/register_tools 发送工具定义(仅模式)关键点:服务器从不执行工具——它只将调用路由到实际执行发生的客户端!
bindPuppet 将两个客户端传输连接起来,使得一个客户端可以使用另一个客户端的工具。
核心思想:绑定 Cursor 的传输到 Chrome 的传输 → Cursor 的请求转发到 Chrome。
import { bindPuppet, SSEServerTransport } from "@mcpc/cmcp";
// Chrome 的传输(连接到 Chrome 客户端,带有 DOM 工具)
const chromeTransport = new SSEServerTransport("/messages", "chrome");
// Cursor 的传输,绑定到 Chrome 的
const cursorTransport = new SSEServerTransport("/messages", "cursor");
const boundTransport = bindPuppet(
cursorTransport, // 主传输
chromeTransport, // 木偶——接收转发的调用
["tools/list", "tools/call"],
);
// 结果:当 Cursor 调用一个工具 → 转发到 Chrome → Chrome 执行
如何工作:
bindPuppet 指向 Chrome 的传输bindPuppet 转发到 Chrome → Chrome 执行实践中(使用 handleConnecting):
// Chrome: GET /sse?sessionId=chrome
// Cursor: GET /sse?sessionId=cursor&puppetId=chrome
// !!! 现在 Cursor 像木偶一样控制 Chrome
您可以委托的方法(来自 PUPPET_METHODS):
tools/list, tools/call - 工具操作resources/list, resources/read - 资源操作prompts/list - 提示操作