feat(wecom): 企微 API 插件增加 MCP Streamable HTTP 端点

与 REST/OpenAPI 并存,按实例权限自动 tools/list,免手填工具。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-08 16:57:42 +08:00
parent cae426fbc2
commit 976bde37cc
23 changed files with 1212 additions and 63 deletions
@@ -0,0 +1,71 @@
import { Injectable, Logger } from '@nestjs/common';
import { allowedWecomPluginMcpTools } from '@dukang/shared-types';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import type { IncomingMessage, ServerResponse } from 'node:http';
import { WECOM_PLUGIN_MCP_TOOL_DEFS } from './wecom-plugin-mcp.tools';
import type { WecomPluginQueryService } from './wecom-plugin-query.service';
import type { WecomPluginRuntime } from './wecom-plugin.types';
import { wecomPluginCaller } from './wecom-plugin.util';
export function createWecomPluginMcpServer(
query: WecomPluginQueryService,
plugin: WecomPluginRuntime,
wecomUserId: string,
): McpServer {
const server = new McpServer({
name: plugin.name || 'dukang-wecom-plugin',
version: '1.0.0',
});
const allowed = new Set(allowedWecomPluginMcpTools(plugin.permissions));
const ctx = { query, plugin, wecomUserId };
for (const def of WECOM_PLUGIN_MCP_TOOL_DEFS) {
if (!allowed.has(def.name)) continue;
server.registerTool(
def.name,
{ title: def.name, description: def.description, inputSchema: def.inputSchema },
async (args) => def.invoke(ctx, (args ?? {}) as Record<string, unknown>),
);
}
return server;
}
@Injectable()
export class WecomPluginMcpFactory {
private readonly logger = new Logger(WecomPluginMcpFactory.name);
constructor(private readonly query: WecomPluginQueryService) {}
async handle(
req: IncomingMessage & { body?: unknown; headers: IncomingMessage['headers'] },
res: ServerResponse,
plugin: WecomPluginRuntime,
): Promise<void> {
const server = createWecomPluginMcpServer(this.query, plugin, wecomPluginCaller(req));
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
res.on('close', () => {
void transport.close();
void server.close();
});
try {
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
this.logger.error(`wecom plugin MCP failed: ${message}`);
if (!res.headersSent) {
res.statusCode = 500;
res.setHeader('Content-Type', 'application/json');
res.end(
JSON.stringify({
jsonrpc: '2.0',
error: { code: -32603, message },
id: null,
}),
);
}
}
}
}