mcp_llm-debate 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * LLM 客户端 - 封装 OpenAI 兼容 API 调用
5
+ * 支持通过环境变量配置 API 密钥、基础 URL 和模型
6
+ */
7
+
8
+ export class LLMClient {
9
+ constructor(options = {}) {
10
+ this.apiKey = options.apiKey || process.env.LLM_API_KEY || process.env.OPENAI_API_KEY || '';
11
+ this.baseUrl = options.baseUrl || process.env.LLM_BASE_URL || 'https://api.openai.com/v1';
12
+ this.model = options.model || process.env.LLM_MODEL || 'gpt-4o';
13
+ }
14
+
15
+ /**
16
+ * 发送聊天请求
17
+ * @param {Array} messages - 消息数组 [{role, content}]
18
+ * @param {Object} options - 额外选项
19
+ * @returns {Promise<string>} LLM 响应内容
20
+ */
21
+ async chat(messages, options = {}) {
22
+ if (!this.apiKey) {
23
+ throw new Error('未配置 LLM API 密钥。请设置 LLM_API_KEY 或 OPENAI_API_KEY 环境变量。');
24
+ }
25
+
26
+ const response = await fetch(`${this.baseUrl}/chat/completions`, {
27
+ method: 'POST',
28
+ headers: {
29
+ 'Content-Type': 'application/json',
30
+ 'Authorization': `Bearer ${this.apiKey}`
31
+ },
32
+ body: JSON.stringify({
33
+ model: options.model || this.model,
34
+ messages: messages,
35
+ temperature: options.temperature ?? 0.7,
36
+ max_tokens: options.max_tokens || 2048,
37
+ top_p: options.top_p ?? 0.95
38
+ })
39
+ });
40
+
41
+ if (!response.ok) {
42
+ const errorText = await response.text();
43
+ throw new Error(`LLM API 错误 (${response.status}): ${errorText}`);
44
+ }
45
+
46
+ const data = await response.json();
47
+ return data.choices[0].message.content;
48
+ }
49
+
50
+ /**
51
+ * 并行为多个角色生成响应
52
+ * @param {Array} roleMessages - 角色消息数组 [{role, messages, options}]
53
+ * @returns {Promise<Array<string>>} 响应数组
54
+ */
55
+ async chatBatch(roleMessages) {
56
+ return Promise.all(
57
+ roleMessages.map(({ messages, options = {} }) =>
58
+ this.chat(messages, options).catch(err => `[生成错误] ${err.message}`)
59
+ )
60
+ );
61
+ }
62
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Session 管理器 - 负责辩论 session 的创建、查询和持久化
3
+ * 使用 JSON 文件存储到 data/ 目录
4
+ */
5
+
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ import crypto from 'crypto';
9
+ import { fileURLToPath } from 'url';
10
+
11
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
+ const DATA_DIR = path.join(__dirname, '..', 'data');
13
+
14
+ export class SessionManager {
15
+ constructor(dataDir = DATA_DIR) {
16
+ this.dataDir = dataDir;
17
+ this._ensureDataDir();
18
+ }
19
+
20
+ _ensureDataDir() {
21
+ if (!fs.existsSync(this.dataDir)) {
22
+ fs.mkdirSync(this.dataDir, { recursive: true });
23
+ }
24
+ }
25
+
26
+ /**
27
+ * 创建新的辩论 session
28
+ */
29
+ createSession(graph, subject, context = {}) {
30
+ const session = {
31
+ session_id: crypto.randomUUID(),
32
+ graph_name: typeof graph === 'string' ? graph : graph.name,
33
+ subject: subject,
34
+ context: context,
35
+ created_at: new Date().toISOString(),
36
+ updated_at: new Date().toISOString(),
37
+ status: 'active',
38
+ rounds: [],
39
+ result: null
40
+ };
41
+ this._save(session);
42
+ return session;
43
+ }
44
+
45
+ /**
46
+ * 获取 session 详情
47
+ */
48
+ getSession(sessionId) {
49
+ const filePath = path.join(this.dataDir, `${sessionId}.json`);
50
+ if (!fs.existsSync(filePath)) {
51
+ return null;
52
+ }
53
+ try {
54
+ return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
55
+ } catch {
56
+ return null;
57
+ }
58
+ }
59
+
60
+ /**
61
+ * 向 session 追加一轮发言
62
+ */
63
+ appendRound(sessionId, roundData) {
64
+ const session = this.getSession(sessionId);
65
+ if (!session) {
66
+ throw new Error(`Session 未找到: ${sessionId}`);
67
+ }
68
+
69
+ const roundEntry = {
70
+ ...roundData,
71
+ round_number: session.rounds.length + 1,
72
+ timestamp: new Date().toISOString()
73
+ };
74
+
75
+ session.rounds.push(roundEntry);
76
+ session.updated_at = new Date().toISOString();
77
+ this._save(session);
78
+ return session;
79
+ }
80
+
81
+ /**
82
+ * 完成辩论并记录结果
83
+ */
84
+ finalizeSession(sessionId, result) {
85
+ const session = this.getSession(sessionId);
86
+ if (!session) {
87
+ throw new Error(`Session 未找到: ${sessionId}`);
88
+ }
89
+
90
+ session.status = 'completed';
91
+ session.result = result;
92
+ session.updated_at = new Date().toISOString();
93
+ this._save(session);
94
+ return session;
95
+ }
96
+
97
+ _save(session) {
98
+ const filePath = path.join(this.dataDir, `${session.session_id}.json`);
99
+ fs.writeFileSync(filePath, JSON.stringify(session, null, 2), 'utf-8');
100
+ }
101
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * MCP Streamable HTTP 传输层
3
+ *
4
+ * 使用 MCP SDK 内置的 StreamableHTTPServerTransport,
5
+ * 将辩论服务暴露为 HTTP 端点,供远程 MCP Agent 调用。
6
+ *
7
+ * 客户端连接地址:POST/GET http://<host>:<port>/mcp
8
+ */
9
+
10
+ import http from 'http';
11
+ import crypto from 'crypto';
12
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
13
+
14
+ /**
15
+ * 启动 MCP HTTP 服务器
16
+ * @param {Object} server - MCP Server 实例
17
+ * @param {Object} config - { port, host, authToken }
18
+ * @returns {Promise<http.Server>}
19
+ */
20
+ export async function startHTTPServer(server, config = {}) {
21
+ const port = config.port || 3000;
22
+ const host = config.host || '127.0.0.1';
23
+ const authToken = config.authToken || '';
24
+
25
+ if (authToken) {
26
+ console.error(`[MCP HTTP] 认证已启用 (Bearer Token)`);
27
+ }
28
+
29
+ // 创建 Streamable HTTP 传输层(有状态模式)
30
+ const transport = new StreamableHTTPServerTransport({
31
+ sessionIdGenerator: () => crypto.randomUUID(),
32
+ });
33
+
34
+ // 将 MCP Server 连接到传输层
35
+ await server.connect(transport);
36
+
37
+ /**
38
+ * 验证请求的 Bearer Token
39
+ */
40
+ function checkAuth(req) {
41
+ if (!authToken) return true;
42
+ const authHeader = req.headers['authorization'] || '';
43
+ const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : '';
44
+ return token === authToken;
45
+ }
46
+
47
+ // 创建 HTTP 服务器,路由 /mcp 到 MCP 传输层
48
+ const httpServer = http.createServer(async (req, res) => {
49
+ // CORS 头
50
+ res.setHeader('Access-Control-Allow-Origin', '*');
51
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
52
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Mcp-Session-Id, Authorization');
53
+
54
+ if (req.method === 'OPTIONS') {
55
+ res.writeHead(204);
56
+ res.end();
57
+ return;
58
+ }
59
+
60
+ // MCP 端点
61
+ if (req.url === '/mcp' || req.url?.startsWith('/mcp')) {
62
+ // 认证检查
63
+ if (!checkAuth(req)) {
64
+ res.writeHead(401, { 'Content-Type': 'application/json' });
65
+ res.end(JSON.stringify({ error: '未授权:请提供有效的 Bearer Token' }));
66
+ return;
67
+ }
68
+ try {
69
+ await transport.handleRequest(req, res);
70
+ } catch (err) {
71
+ console.error('MCP 请求处理错误:', err.message);
72
+ if (!res.headersSent) {
73
+ res.writeHead(500, { 'Content-Type': 'application/json' });
74
+ res.end(JSON.stringify({ error: 'Internal server error' }));
75
+ }
76
+ }
77
+ return;
78
+ }
79
+
80
+ // 健康检查
81
+ if (req.url === '/health') {
82
+ res.writeHead(200, { 'Content-Type': 'application/json' });
83
+ res.end(JSON.stringify({ status: 'ok', transport: 'mcp-http' }));
84
+ return;
85
+ }
86
+
87
+ // 404
88
+ res.writeHead(404, { 'Content-Type': 'application/json' });
89
+ res.end(JSON.stringify({ error: 'Not found' }));
90
+ });
91
+
92
+ return new Promise((resolve, reject) => {
93
+ httpServer.on('error', reject);
94
+ httpServer.listen(port, host, () => {
95
+ console.error(`MCP HTTP 服务器已启动: http://${host}:${port}/mcp`);
96
+ resolve(httpServer);
97
+ });
98
+ });
99
+ }
@@ -0,0 +1,167 @@
1
+ /**
2
+ * REST API 传输层
3
+ *
4
+ * 基于 Node.js 内置 http 模块,将 5 个辩论工具映射为 REST 端点,
5
+ * 供不支持 MCP 协议的 Agent 直接通过 HTTP 调用。
6
+ *
7
+ * 端点映射:
8
+ * POST /api/run_debate → 执行辩论
9
+ * POST /api/continue_debate → 续辩
10
+ * POST /api/get_debate_graph → 获取辩论图
11
+ * POST /api/get_debate_result → 获取辩论结果
12
+ * POST /api/list_debate_graphs→ 列出辩论图
13
+ * GET /api/health → 健康检查
14
+ */
15
+
16
+ import http from 'http';
17
+
18
+ /**
19
+ * 解析 HTTP 请求体为 JSON
20
+ */
21
+ function parseBody(req) {
22
+ return new Promise((resolve, reject) => {
23
+ if (req.method === 'GET') {
24
+ resolve({});
25
+ return;
26
+ }
27
+ let body = '';
28
+ req.on('data', chunk => { body += chunk; });
29
+ req.on('end', () => {
30
+ try {
31
+ resolve(body ? JSON.parse(body) : {});
32
+ } catch {
33
+ reject(new Error('请求体必须是有效的 JSON'));
34
+ }
35
+ });
36
+ req.on('error', reject);
37
+ });
38
+ }
39
+
40
+ /**
41
+ * 发送 JSON 响应
42
+ */
43
+ function sendJSON(res, statusCode, data) {
44
+ res.writeHead(statusCode, {
45
+ 'Content-Type': 'application/json',
46
+ 'Access-Control-Allow-Origin': '*',
47
+ 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
48
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization',
49
+ });
50
+ res.end(JSON.stringify(data));
51
+ }
52
+
53
+ /**
54
+ * 验证 Bearer Token
55
+ * @param {Object} req - HTTP 请求
56
+ * @param {string} authToken - 配置的认证令牌(空字符串表示不强制认证)
57
+ * @returns {boolean} 是否通过认证
58
+ */
59
+ function authenticate(req, authToken) {
60
+ // 未配置 token 时跳过认证
61
+ if (!authToken) return true;
62
+
63
+ const authHeader = req.headers['authorization'] || '';
64
+ const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : '';
65
+ return token === authToken;
66
+ }
67
+
68
+ /**
69
+ * 工具名到参数的映射
70
+ */
71
+ const TOOL_PARAM_MAP = {
72
+ run_debate: { required: ['graph', 'input'], optional: ['options'] },
73
+ continue_debate: { required: ['session_id', 'graph'], optional: ['subject', 'context'] },
74
+ get_debate_graph: { required: ['name'], optional: [] },
75
+ get_debate_result:{ required: ['session_id'], optional: [] },
76
+ list_debate_graphs:{ required: [], optional: [] },
77
+ };
78
+
79
+ /**
80
+ * 启动 REST API 服务器
81
+ * @param {Function} executeTool - 工具执行函数 async (toolName, args) => result
82
+ * @param {Object} config - { port, host }
83
+ * @returns {Promise<http.Server>}
84
+ */
85
+ export async function startRESTServer(executeTool, config = {}) {
86
+ const port = config.port || 3000;
87
+ const host = config.host || '127.0.0.1';
88
+ const authToken = config.authToken || '';
89
+
90
+ if (authToken) {
91
+ console.error(`[REST] 认证已启用 (Bearer Token)`);
92
+ }
93
+
94
+ const server = http.createServer(async (req, res) => {
95
+ // CORS 预检
96
+ if (req.method === 'OPTIONS') {
97
+ res.writeHead(204, {
98
+ 'Access-Control-Allow-Origin': '*',
99
+ 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
100
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization',
101
+ });
102
+ res.end();
103
+ return;
104
+ }
105
+
106
+ // 路由匹配
107
+ const url = req.url.split('?')[0]; // 去掉 query string
108
+
109
+ // 健康检查(不需要认证)
110
+ if (req.method === 'GET' && url === '/api/health') {
111
+ sendJSON(res, 200, { status: 'ok', transport: 'rest' });
112
+ return;
113
+ }
114
+
115
+ // API 路由需要认证
116
+ if (url.startsWith('/api/')) {
117
+ // 认证检查
118
+ if (!authenticate(req, authToken)) {
119
+ sendJSON(res, 401, { error: '未授权:请提供有效的 Bearer Token' });
120
+ return;
121
+ }
122
+ const toolName = url.replace('/api/', '');
123
+
124
+ if (!TOOL_PARAM_MAP[toolName]) {
125
+ sendJSON(res, 404, { error: `未知工具: "${toolName}"` });
126
+ return;
127
+ }
128
+
129
+ try {
130
+ const body = await parseBody(req);
131
+ const { required, optional } = TOOL_PARAM_MAP[toolName];
132
+
133
+ // 构建参数:可选字段从 body 中取
134
+ const args = { ...body };
135
+
136
+ // 验证必填字段
137
+ for (const field of required) {
138
+ if (args[field] === undefined || args[field] === null) {
139
+ sendJSON(res, 400, { error: `缺少必填参数: "${field}"` });
140
+ return;
141
+ }
142
+ }
143
+
144
+ // 执行工具
145
+ console.error(`[REST] 调用工具: ${toolName}`);
146
+ const result = await executeTool(toolName, args);
147
+ sendJSON(res, 200, result);
148
+ } catch (err) {
149
+ console.error(`[REST] 工具执行错误 (${toolName}):`, err.message);
150
+ sendJSON(res, 500, { error: err.message });
151
+ }
152
+ return;
153
+ }
154
+
155
+ // 404
156
+ sendJSON(res, 404, { error: 'Not found' });
157
+ });
158
+
159
+ return new Promise((resolve, reject) => {
160
+ server.on('error', reject);
161
+ server.listen(port, host, () => {
162
+ console.error(`REST API 服务器已启动: http://${host}:${port}`);
163
+ console.error(` 端点: /api/run_debate, /api/continue_debate, /api/get_debate_graph, /api/get_debate_result, /api/list_debate_graphs`);
164
+ resolve(server);
165
+ });
166
+ });
167
+ }