@lightcone-ai/daemon 0.12.0 → 0.14.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.
- package/mcp-servers/mysql/core.js +270 -0
- package/mcp-servers/mysql/index.js +79 -151
- package/mcp-servers/mysql/manifest.json +8 -6
- package/package.json +1 -1
- package/src/chat-bridge.js +656 -1
- package/src/mcp-config.js +2 -1
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { createHash } from 'crypto';
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_LIMIT = 200;
|
|
4
|
+
export const MAX_LIMIT = 1000;
|
|
5
|
+
|
|
6
|
+
const READ_QUERY_PREFIX = /^(SELECT|SHOW|DESCRIBE|DESC|EXPLAIN|WITH)\b/i;
|
|
7
|
+
const REQUIRED_DB_KEYS = ['DB_HOST', 'DB_PORT', 'DB_USER', 'DB_PASSWORD', 'DB_NAME'];
|
|
8
|
+
|
|
9
|
+
function trimTrailingSemicolons(sql) {
|
|
10
|
+
return sql.replace(/;+\s*$/u, '');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function normalizeSql(rawSql) {
|
|
14
|
+
if (typeof rawSql !== 'string') return '';
|
|
15
|
+
const trimmed = rawSql.trim();
|
|
16
|
+
if (!trimmed) return '';
|
|
17
|
+
return trimTrailingSemicolons(trimmed);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function normalizeLimit(limit) {
|
|
21
|
+
const numeric = Number(limit);
|
|
22
|
+
if (!Number.isFinite(numeric) || numeric <= 0) return DEFAULT_LIMIT;
|
|
23
|
+
return Math.min(Math.trunc(numeric), MAX_LIMIT);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function isReadQuery(sql) {
|
|
27
|
+
return READ_QUERY_PREFIX.test(sql);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function hasLimitClause(sql) {
|
|
31
|
+
return /\bLIMIT\b/i.test(sql);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function prepareSql(sql, limit) {
|
|
35
|
+
const normalizedSql = normalizeSql(sql);
|
|
36
|
+
if (!normalizedSql) throw new Error('sql must be a non-empty string');
|
|
37
|
+
|
|
38
|
+
const effectiveLimit = normalizeLimit(limit);
|
|
39
|
+
if (!isReadQuery(normalizedSql) || hasLimitClause(normalizedSql)) {
|
|
40
|
+
return {
|
|
41
|
+
sql: normalizedSql,
|
|
42
|
+
limit: effectiveLimit,
|
|
43
|
+
limitParams: [],
|
|
44
|
+
limitInjected: false,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
sql: `${normalizedSql} LIMIT ?`,
|
|
50
|
+
limit: effectiveLimit,
|
|
51
|
+
limitParams: [effectiveLimit],
|
|
52
|
+
limitInjected: true,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function readRequiredKey(envVars, key) {
|
|
57
|
+
const raw = envVars?.[key];
|
|
58
|
+
if (raw == null || String(raw).trim() === '') return null;
|
|
59
|
+
return String(raw).trim();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function parseDbConfig(envVars) {
|
|
63
|
+
const missing = [];
|
|
64
|
+
const values = {};
|
|
65
|
+
for (const key of REQUIRED_DB_KEYS) {
|
|
66
|
+
const value = readRequiredKey(envVars, key);
|
|
67
|
+
if (value == null) {
|
|
68
|
+
missing.push(key);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
values[key] = value;
|
|
72
|
+
}
|
|
73
|
+
if (missing.length > 0) {
|
|
74
|
+
throw new Error(`credential payload missing required DB fields: ${missing.join(', ')}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const port = Number(values.DB_PORT);
|
|
78
|
+
if (!Number.isFinite(port) || port <= 0) {
|
|
79
|
+
throw new Error('credential payload has invalid DB_PORT');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
host: values.DB_HOST,
|
|
84
|
+
port,
|
|
85
|
+
user: values.DB_USER,
|
|
86
|
+
password: values.DB_PASSWORD,
|
|
87
|
+
database: values.DB_NAME,
|
|
88
|
+
waitForConnections: true,
|
|
89
|
+
connectionLimit: 3,
|
|
90
|
+
queueLimit: 0,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function fingerprintDbConfig(dbConfig) {
|
|
95
|
+
return createHash('sha256')
|
|
96
|
+
.update(JSON.stringify([
|
|
97
|
+
dbConfig.host,
|
|
98
|
+
dbConfig.port,
|
|
99
|
+
dbConfig.user,
|
|
100
|
+
dbConfig.password,
|
|
101
|
+
dbConfig.database,
|
|
102
|
+
]))
|
|
103
|
+
.digest('hex');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function createDataSourcePoolRegistry({ createPool, logger = () => {} } = {}) {
|
|
107
|
+
if (typeof createPool !== 'function') {
|
|
108
|
+
throw new Error('createPool function is required');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const pools = new Map();
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
getOrCreate(dataSourceId, dbConfig) {
|
|
115
|
+
const id = String(dataSourceId ?? '').trim();
|
|
116
|
+
if (!id) throw new Error('data_source_id must be a non-empty string');
|
|
117
|
+
const nextFingerprint = fingerprintDbConfig(dbConfig);
|
|
118
|
+
const existing = pools.get(id);
|
|
119
|
+
|
|
120
|
+
if (existing && existing.fingerprint === nextFingerprint) {
|
|
121
|
+
return existing.pool;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (existing) {
|
|
125
|
+
Promise.resolve(existing.pool.end())
|
|
126
|
+
.catch((error) => logger(`[mysql-mcp] failed to close previous pool for data_source_id=${id}: ${error.message}`));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const pool = createPool(dbConfig);
|
|
130
|
+
pools.set(id, { fingerprint: nextFingerprint, pool });
|
|
131
|
+
return pool;
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
async closeAll() {
|
|
135
|
+
const closing = [];
|
|
136
|
+
for (const { pool } of pools.values()) {
|
|
137
|
+
closing.push(
|
|
138
|
+
Promise.resolve(pool.end()).catch((error) => {
|
|
139
|
+
logger(`[mysql-mcp] failed to close pool: ${error.message}`);
|
|
140
|
+
})
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
pools.clear();
|
|
144
|
+
await Promise.all(closing);
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
size() {
|
|
148
|
+
return pools.size;
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function normalizeServerUrl(serverUrl) {
|
|
154
|
+
return String(serverUrl ?? '').trim().replace(/\/+$/u, '');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function createCredentialBrokerClient({
|
|
158
|
+
fetchFn = globalThis.fetch,
|
|
159
|
+
serverUrl = '',
|
|
160
|
+
machineApiKey = '',
|
|
161
|
+
agentId = '',
|
|
162
|
+
workspaceId = '',
|
|
163
|
+
bundleId = '',
|
|
164
|
+
} = {}) {
|
|
165
|
+
if (typeof fetchFn !== 'function') {
|
|
166
|
+
throw new Error('fetch function is required');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const normalizedServerUrl = normalizeServerUrl(serverUrl);
|
|
170
|
+
const normalizedMachineApiKey = String(machineApiKey ?? '').trim();
|
|
171
|
+
const normalizedAgentId = String(agentId ?? '').trim();
|
|
172
|
+
const normalizedWorkspaceId = String(workspaceId ?? '').trim();
|
|
173
|
+
const normalizedBundleId = String(bundleId ?? '').trim();
|
|
174
|
+
|
|
175
|
+
return async function fetchCredentialEnvVars(dataSourceId) {
|
|
176
|
+
const normalizedDataSourceId = String(dataSourceId ?? '').trim();
|
|
177
|
+
if (!normalizedDataSourceId) {
|
|
178
|
+
throw new Error('data_source_id is required');
|
|
179
|
+
}
|
|
180
|
+
if (!normalizedServerUrl || !normalizedMachineApiKey || !normalizedAgentId) {
|
|
181
|
+
throw new Error('mysql MCP missing SERVER_URL/MACHINE_API_KEY/AGENT_ID runtime context');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const response = await fetchFn(`${normalizedServerUrl}/governance/credential-broker`, {
|
|
185
|
+
method: 'POST',
|
|
186
|
+
headers: {
|
|
187
|
+
'Content-Type': 'application/json',
|
|
188
|
+
'Authorization': `Bearer ${normalizedMachineApiKey}`,
|
|
189
|
+
},
|
|
190
|
+
body: JSON.stringify({
|
|
191
|
+
agent_id: normalizedAgentId,
|
|
192
|
+
workspace_id: normalizedWorkspaceId || null,
|
|
193
|
+
required_credentials: [normalizedDataSourceId],
|
|
194
|
+
bundle_id: normalizedBundleId || null,
|
|
195
|
+
}),
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
let payload = null;
|
|
199
|
+
try {
|
|
200
|
+
payload = await response.json();
|
|
201
|
+
} catch {
|
|
202
|
+
payload = null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (!response.ok) {
|
|
206
|
+
const reason = typeof payload?.error === 'string'
|
|
207
|
+
? payload.error
|
|
208
|
+
: `status_${response.status}`;
|
|
209
|
+
throw new Error(`credential_broker_failed:${reason}`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const envVars = payload?.env_vars;
|
|
213
|
+
if (!envVars || typeof envVars !== 'object' || Array.isArray(envVars)) {
|
|
214
|
+
throw new Error('credential_broker_invalid_response');
|
|
215
|
+
}
|
|
216
|
+
return envVars;
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export async function executeSql({
|
|
221
|
+
dataSourceId,
|
|
222
|
+
sql,
|
|
223
|
+
params = [],
|
|
224
|
+
limit,
|
|
225
|
+
fetchCredentialEnvVars,
|
|
226
|
+
poolRegistry,
|
|
227
|
+
} = {}) {
|
|
228
|
+
const normalizedDataSourceId = String(dataSourceId ?? '').trim();
|
|
229
|
+
if (!normalizedDataSourceId) {
|
|
230
|
+
throw new Error('data_source_id is required');
|
|
231
|
+
}
|
|
232
|
+
if (!Array.isArray(params)) {
|
|
233
|
+
throw new Error('params must be an array');
|
|
234
|
+
}
|
|
235
|
+
if (typeof fetchCredentialEnvVars !== 'function') {
|
|
236
|
+
throw new Error('fetchCredentialEnvVars function is required');
|
|
237
|
+
}
|
|
238
|
+
if (!poolRegistry || typeof poolRegistry.getOrCreate !== 'function') {
|
|
239
|
+
throw new Error('poolRegistry.getOrCreate is required');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const prepared = prepareSql(sql, limit);
|
|
243
|
+
const credentialEnv = await fetchCredentialEnvVars(normalizedDataSourceId);
|
|
244
|
+
const dbConfig = parseDbConfig(credentialEnv);
|
|
245
|
+
const pool = poolRegistry.getOrCreate(normalizedDataSourceId, dbConfig);
|
|
246
|
+
|
|
247
|
+
const queryParams = [...params, ...prepared.limitParams];
|
|
248
|
+
const [result] = await pool.query(prepared.sql, queryParams);
|
|
249
|
+
|
|
250
|
+
if (Array.isArray(result)) {
|
|
251
|
+
const rows = result.length > prepared.limit
|
|
252
|
+
? result.slice(0, prepared.limit)
|
|
253
|
+
: result;
|
|
254
|
+
return {
|
|
255
|
+
data_source_id: normalizedDataSourceId,
|
|
256
|
+
row_count: rows.length,
|
|
257
|
+
limit: prepared.limit,
|
|
258
|
+
limit_applied: prepared.limitInjected || rows.length < result.length,
|
|
259
|
+
rows,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
data_source_id: normalizedDataSourceId,
|
|
265
|
+
affected_rows: Number(result?.affectedRows ?? 0),
|
|
266
|
+
changed_rows: Number(result?.changedRows ?? 0),
|
|
267
|
+
insert_id: result?.insertId ?? null,
|
|
268
|
+
warning_status: Number(result?.warningStatus ?? 0),
|
|
269
|
+
};
|
|
270
|
+
}
|
|
@@ -3,161 +3,89 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
3
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import mysql from 'mysql2/promise';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
import { pathToFileURL } from 'url';
|
|
7
|
+
import {
|
|
8
|
+
createCredentialBrokerClient,
|
|
9
|
+
createDataSourcePoolRegistry,
|
|
10
|
+
executeSql,
|
|
11
|
+
} from './core.js';
|
|
12
|
+
|
|
13
|
+
const TOOL_SCHEMA = {
|
|
14
|
+
data_source_id: z.string().describe('数据源 ID(当前实现映射为 credential_id)'),
|
|
15
|
+
sql: z.string().describe('要执行的 SQL。支持参数占位符 ?'),
|
|
16
|
+
params: z.array(z.any()).optional().describe('SQL 参数数组(按顺序对应 ? 占位符)'),
|
|
17
|
+
limit: z.number().int().positive().optional().describe('结果行数上限。默认 200,最大 1000'),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function isExecutedDirectly(metaUrl) {
|
|
21
|
+
const entry = process.argv[1];
|
|
22
|
+
if (!entry) return false;
|
|
23
|
+
try {
|
|
24
|
+
return pathToFileURL(entry).href === metaUrl;
|
|
25
|
+
} catch {
|
|
26
|
+
return false;
|
|
11
27
|
}
|
|
12
|
-
return value;
|
|
13
28
|
}
|
|
14
29
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
params.push(max);
|
|
60
|
-
const sql = `
|
|
61
|
-
SELECT job_id, job_title, company_name, salary, work_location, job_type, degree, tags, publish_date
|
|
62
|
-
FROM job_details
|
|
63
|
-
WHERE ${conditions.join(' AND ')}
|
|
64
|
-
ORDER BY created_at DESC
|
|
65
|
-
LIMIT ?
|
|
66
|
-
`;
|
|
67
|
-
|
|
68
|
-
console.error(`[mysql-mcp] search_jobs: ${sql.replace(/\s+/g, ' ').trim()} | params: ${JSON.stringify(params)}`);
|
|
69
|
-
const [rows] = await pool.query(sql, params);
|
|
70
|
-
if (rows.length === 0) {
|
|
71
|
-
return { content: [{ type: 'text', text: '未找到匹配的职位。' }] };
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const text = rows.map(r => [
|
|
75
|
-
`job_id: ${r.job_id}`,
|
|
76
|
-
`职位: ${r.job_title} @ ${r.company_name}`,
|
|
77
|
-
`类型: ${r.job_type ?? '-'} 地点: ${r.work_location ?? '-'} 学历: ${r.degree ?? '-'}`,
|
|
78
|
-
`薪资: ${r.salary || '未披露'}`,
|
|
79
|
-
`标签: ${(() => { try { return r.tags ? JSON.parse(r.tags).join(', ') : '-'; } catch { return r.tags ?? '-'; } })()}`,
|
|
80
|
-
`发布日期: ${r.publish_date ?? '-'}`,
|
|
81
|
-
].join('\n')).join('\n\n---\n\n');
|
|
82
|
-
|
|
83
|
-
return { content: [{ type: 'text', text: `共 ${rows.length} 条结果:\n\n${text}` }] };
|
|
84
|
-
}
|
|
85
|
-
);
|
|
86
|
-
|
|
87
|
-
// ── get_job_detail ────────────────────────────────────────────────────────────
|
|
88
|
-
server.tool('get_job_detail',
|
|
89
|
-
'根据 job_id 获取职位完整详情,包括职位描述全文。用于写手生成文案前获取完整信息。',
|
|
90
|
-
{
|
|
91
|
-
job_id: z.string().describe('职位 ID(从 search_jobs 结果中获取)'),
|
|
92
|
-
},
|
|
93
|
-
async ({ job_id }) => {
|
|
94
|
-
const detailSql = `SELECT jd.*, c.industry, c.scale, c.type as company_type, c.logo_url FROM job_details jd LEFT JOIN companies c ON jd.company_id = c.company_id WHERE jd.job_id = ?`;
|
|
95
|
-
console.error(`[mysql-mcp] get_job_detail: ${detailSql} | params: ${JSON.stringify([job_id])}`);
|
|
96
|
-
const [rows] = await pool.query(detailSql, [job_id]);
|
|
97
|
-
|
|
98
|
-
if (rows.length === 0) {
|
|
99
|
-
return { content: [{ type: 'text', text: `未找到 job_id=${job_id} 的职位。` }] };
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const r = rows[0];
|
|
103
|
-
const tags = (() => { try { return r.tags ? JSON.parse(r.tags) : []; } catch { return []; } })();
|
|
104
|
-
const text = [
|
|
105
|
-
`【职位详情】`,
|
|
106
|
-
`职位名称: ${r.job_title}`,
|
|
107
|
-
`公司: ${r.company_name}${r.industry ? `(${r.industry})` : ''}`,
|
|
108
|
-
`公司规模: ${r.scale ?? '-'} 公司类型: ${r.company_type ?? '-'}`,
|
|
109
|
-
`薪资: ${r.salary || '未披露'}`,
|
|
110
|
-
`工作地点: ${r.work_location ?? '-'}`,
|
|
111
|
-
`职位类型: ${r.job_type ?? '-'}`,
|
|
112
|
-
`学历要求: ${r.degree ?? '-'}`,
|
|
113
|
-
`技能标签: ${tags.join(', ') || '-'}`,
|
|
114
|
-
`发布日期: ${r.publish_date ?? '-'}`,
|
|
115
|
-
`截止日期: ${r.close_date ?? '-'}`,
|
|
116
|
-
`原始链接: ${r.job_detail_url}`,
|
|
117
|
-
``,
|
|
118
|
-
`【职位描述】`,
|
|
119
|
-
r.job_description ?? '(无描述)',
|
|
120
|
-
].join('\n');
|
|
121
|
-
|
|
122
|
-
return { content: [{ type: 'text', text }] };
|
|
123
|
-
}
|
|
124
|
-
);
|
|
125
|
-
|
|
126
|
-
// ── list_jobs ─────────────────────────────────────────────────────────────────
|
|
127
|
-
server.tool('list_jobs',
|
|
128
|
-
'列出最新职位,支持按类型、地点过滤。用于浏览近期职位。',
|
|
129
|
-
{
|
|
130
|
-
job_type: z.string().optional().describe('职位类型过滤,如 实习、校招、社招'),
|
|
131
|
-
location: z.string().optional().describe('工作地点过滤'),
|
|
132
|
-
limit: z.number().optional().describe('返回数量,默认 20,最大 50'),
|
|
133
|
-
offset: z.number().optional().describe('跳过前 N 条,用于分页'),
|
|
134
|
-
},
|
|
135
|
-
async ({ job_type, location, limit = 20, offset = 0 }) => {
|
|
136
|
-
const max = Math.min(limit, 50);
|
|
137
|
-
const conditions = ['is_valid != "false"'];
|
|
138
|
-
const params = [];
|
|
139
|
-
|
|
140
|
-
if (job_type) { conditions.push('job_type LIKE ?'); params.push(`%${job_type}%`); }
|
|
141
|
-
if (location) { conditions.push('work_location LIKE ?'); params.push(`%${location}%`); }
|
|
142
|
-
|
|
143
|
-
params.push(max, offset);
|
|
144
|
-
const listSql = `SELECT job_id, job_title, company_name, salary, work_location, job_type, degree, tags FROM job_details WHERE ${conditions.join(' AND ')} ORDER BY created_at DESC LIMIT ? OFFSET ?`;
|
|
145
|
-
console.error(`[mysql-mcp] list_jobs: ${listSql} | params: ${JSON.stringify(params)}`);
|
|
146
|
-
const [rows] = await pool.query(listSql, params);
|
|
147
|
-
|
|
148
|
-
if (rows.length === 0) {
|
|
149
|
-
return { content: [{ type: 'text', text: '暂无职位数据。' }] };
|
|
30
|
+
export function createMysqlMcpServer({
|
|
31
|
+
env = process.env,
|
|
32
|
+
fetchFn = globalThis.fetch,
|
|
33
|
+
mysqlModule = mysql,
|
|
34
|
+
logger = console.error,
|
|
35
|
+
} = {}) {
|
|
36
|
+
const brokerClient = createCredentialBrokerClient({
|
|
37
|
+
fetchFn,
|
|
38
|
+
serverUrl: env.SERVER_URL,
|
|
39
|
+
machineApiKey: env.MACHINE_API_KEY,
|
|
40
|
+
agentId: env.AGENT_ID,
|
|
41
|
+
workspaceId: env.WORKSPACE_ID,
|
|
42
|
+
bundleId: env.GOVERNANCE_SPAWN_BUNDLE_ID,
|
|
43
|
+
});
|
|
44
|
+
const poolRegistry = createDataSourcePoolRegistry({
|
|
45
|
+
createPool: mysqlModule.createPool.bind(mysqlModule),
|
|
46
|
+
logger,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const server = new McpServer({ name: 'mysql-universal-sql', version: '0.2.0' });
|
|
50
|
+
|
|
51
|
+
server.tool(
|
|
52
|
+
'query',
|
|
53
|
+
'执行通用 SQL。调用时通过 data_source_id 走 credential broker 动态获取 DB 凭证并路由连接。',
|
|
54
|
+
TOOL_SCHEMA,
|
|
55
|
+
async ({ data_source_id, sql, params = [], limit }) => {
|
|
56
|
+
try {
|
|
57
|
+
const payload = await executeSql({
|
|
58
|
+
dataSourceId: data_source_id,
|
|
59
|
+
sql,
|
|
60
|
+
params,
|
|
61
|
+
limit,
|
|
62
|
+
fetchCredentialEnvVars: brokerClient,
|
|
63
|
+
poolRegistry,
|
|
64
|
+
});
|
|
65
|
+
return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
|
|
66
|
+
} catch (error) {
|
|
67
|
+
logger(`[mysql-mcp] query failed for data_source_id=${data_source_id}: ${error.message}`);
|
|
68
|
+
return {
|
|
69
|
+
isError: true,
|
|
70
|
+
content: [{ type: 'text', text: `query failed: ${error.message}` }],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
150
73
|
}
|
|
74
|
+
);
|
|
151
75
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
).join('\n');
|
|
76
|
+
return { server, poolRegistry };
|
|
77
|
+
}
|
|
155
78
|
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
);
|
|
79
|
+
export async function startMysqlMcpServer(options = {}) {
|
|
80
|
+
const { server } = createMysqlMcpServer(options);
|
|
81
|
+
const transport = new StdioServerTransport();
|
|
82
|
+
await server.connect(transport);
|
|
83
|
+
console.error('[mysql-mcp] Server started (universal SQL mode)');
|
|
84
|
+
}
|
|
159
85
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
86
|
+
if (isExecutedDirectly(import.meta.url)) {
|
|
87
|
+
startMysqlMcpServer().catch((error) => {
|
|
88
|
+
console.error(`[mysql-mcp] failed to start: ${error.message}`);
|
|
89
|
+
process.exitCode = 1;
|
|
90
|
+
});
|
|
91
|
+
}
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "mysql",
|
|
3
3
|
"name": "MySQL MCP Server",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.2.0",
|
|
5
5
|
"runtime": "node",
|
|
6
6
|
"entrypoint": "index.js",
|
|
7
7
|
"tool_declarations": [
|
|
8
|
-
{ "name": "
|
|
9
|
-
{ "name": "get_job_detail", "classification": "cacheable" },
|
|
10
|
-
{ "name": "list_jobs", "classification": "cacheable" }
|
|
8
|
+
{ "name": "query", "classification": "mandatory" }
|
|
11
9
|
],
|
|
12
10
|
"smoke_test": {
|
|
13
|
-
"tool": "
|
|
14
|
-
"arguments": {
|
|
11
|
+
"tool": "query",
|
|
12
|
+
"arguments": {
|
|
13
|
+
"data_source_id": "smoke-test-data-source",
|
|
14
|
+
"sql": "SELECT 1",
|
|
15
|
+
"limit": 1
|
|
16
|
+
}
|
|
15
17
|
}
|
|
16
18
|
}
|
package/package.json
CHANGED
package/src/chat-bridge.js
CHANGED
|
@@ -14,9 +14,19 @@ function getArg(name) {
|
|
|
14
14
|
return idx !== -1 && cliArgs[idx + 1] ? cliArgs[idx + 1] : '';
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
function parseBooleanFlag(value, fallback = false) {
|
|
18
|
+
if (value == null) return fallback;
|
|
19
|
+
if (typeof value === 'boolean') return value;
|
|
20
|
+
const normalized = String(value).trim().toLowerCase();
|
|
21
|
+
if (['1', 'true', 'yes', 'y', 'on'].includes(normalized)) return true;
|
|
22
|
+
if (['0', 'false', 'no', 'n', 'off'].includes(normalized)) return false;
|
|
23
|
+
return fallback;
|
|
24
|
+
}
|
|
25
|
+
|
|
17
26
|
const SERVER_URL = process.env.SERVER_URL || getArg('--server-url') || 'http://localhost:9779';
|
|
18
27
|
const MACHINE_API_KEY = process.env.MACHINE_API_KEY || getArg('--auth-token') || '';
|
|
19
28
|
const AGENT_ID = process.env.AGENT_ID || getArg('--agent-id') || '';
|
|
29
|
+
const HOST_AGENT_HINT = process.env.IS_HOST_AGENT || getArg('--is-host-agent') || '';
|
|
20
30
|
const WORKSPACE_ID = process.env.WORKSPACE_ID || getArg('--workspace-id') || ''; // injected per-workspace at spawn time
|
|
21
31
|
const WORKSPACE_DIR = path.resolve(process.env.WORKSPACE_DIR || getArg('--workspace-dir') || process.cwd());
|
|
22
32
|
const WORKSPACE_ROOT_DIR = path.dirname(WORKSPACE_DIR);
|
|
@@ -102,6 +112,19 @@ const DEFAULT_TOOL_CLASSIFICATION = {
|
|
|
102
112
|
request_approval: 'mandatory',
|
|
103
113
|
execute_approved_action: 'mandatory',
|
|
104
114
|
promote_context: 'mandatory',
|
|
115
|
+
update_goal_field: 'mandatory',
|
|
116
|
+
supersede_goal_field: 'mandatory',
|
|
117
|
+
request_credential_auth: 'mandatory',
|
|
118
|
+
register_data_source: 'mandatory',
|
|
119
|
+
bind_workspace_scenario: 'mandatory',
|
|
120
|
+
create_workspace: 'mandatory',
|
|
121
|
+
rename_workspace: 'mandatory',
|
|
122
|
+
delete_workspace: 'mandatory',
|
|
123
|
+
revoke_credential: 'mandatory',
|
|
124
|
+
submit_feedback: 'mandatory',
|
|
125
|
+
escalate_to_human: 'mandatory',
|
|
126
|
+
navigate_to_workspace: 'mandatory',
|
|
127
|
+
navigate_to_settings: 'mandatory',
|
|
105
128
|
|
|
106
129
|
search_messages: 'cacheable',
|
|
107
130
|
list_server: 'cacheable',
|
|
@@ -111,6 +134,19 @@ const DEFAULT_TOOL_CLASSIFICATION = {
|
|
|
111
134
|
skill_list: 'cacheable',
|
|
112
135
|
skill_read: 'cacheable',
|
|
113
136
|
skill_search: 'cacheable',
|
|
137
|
+
search_scenarios: 'cacheable',
|
|
138
|
+
get_scenario: 'cacheable',
|
|
139
|
+
list_scenarios: 'cacheable',
|
|
140
|
+
recommend_scenario: 'cacheable',
|
|
141
|
+
list_workspaces: 'cacheable',
|
|
142
|
+
get_workspace_summary: 'cacheable',
|
|
143
|
+
query_user_history: 'cacheable',
|
|
144
|
+
list_accounts: 'cacheable',
|
|
145
|
+
list_credentials: 'cacheable',
|
|
146
|
+
get_data_locations: 'cacheable',
|
|
147
|
+
explain_concept: 'cacheable',
|
|
148
|
+
get_release_notes: 'cacheable',
|
|
149
|
+
get_known_issues: 'cacheable',
|
|
114
150
|
};
|
|
115
151
|
|
|
116
152
|
const TOOL_CLASSIFICATION = Object.freeze({
|
|
@@ -132,6 +168,19 @@ const CACHE_INVALIDATION_TOOLS = new Set([
|
|
|
132
168
|
'request_approval',
|
|
133
169
|
'execute_approved_action',
|
|
134
170
|
'promote_context',
|
|
171
|
+
'update_goal_field',
|
|
172
|
+
'supersede_goal_field',
|
|
173
|
+
'request_credential_auth',
|
|
174
|
+
'register_data_source',
|
|
175
|
+
'bind_workspace_scenario',
|
|
176
|
+
'create_workspace',
|
|
177
|
+
'rename_workspace',
|
|
178
|
+
'delete_workspace',
|
|
179
|
+
'revoke_credential',
|
|
180
|
+
'submit_feedback',
|
|
181
|
+
'escalate_to_human',
|
|
182
|
+
'navigate_to_workspace',
|
|
183
|
+
'navigate_to_settings',
|
|
135
184
|
]);
|
|
136
185
|
|
|
137
186
|
const governanceContext = {
|
|
@@ -196,11 +245,38 @@ function inferToolForApi(method, apiPath, body) {
|
|
|
196
245
|
if (method === 'PATCH' && cleanPath.startsWith('/skills/')) return 'skill_update';
|
|
197
246
|
if (method === 'POST' && cleanPath === '/actions/request') return 'request_approval';
|
|
198
247
|
if (method === 'POST' && /^\/actions\/[^/]+\/execute$/.test(cleanPath)) return 'execute_approved_action';
|
|
248
|
+
if (method === 'POST' && cleanPath === '/goal-fields/update') return 'update_goal_field';
|
|
249
|
+
if (method === 'POST' && cleanPath === '/goal-fields/supersede') return 'supersede_goal_field';
|
|
250
|
+
if (method === 'POST' && cleanPath === '/credential-auth/request') return 'request_credential_auth';
|
|
251
|
+
if (method === 'POST' && cleanPath === '/api/data-sources') return 'register_data_source';
|
|
199
252
|
if (method === 'POST' && cleanPath === '/orchestrate/decision') return 'write_governance_decision';
|
|
200
253
|
if (method === 'POST' && cleanPath === '/orchestrate/correction') return 'write_governance_correction';
|
|
201
254
|
if (method === 'GET' && cleanPath === '/orchestrate/context') return 'get_orchestrate_context';
|
|
202
255
|
if (method === 'POST' && cleanPath === '/orchestrate/complete') return 'complete_orchestrate_trigger';
|
|
203
256
|
if (method === 'POST' && cleanPath === '/context-proposals') return 'promote_context';
|
|
257
|
+
|
|
258
|
+
if (method === 'GET' && cleanPath === '/host/scenarios/search') return 'search_scenarios';
|
|
259
|
+
if (method === 'POST' && cleanPath === '/host/scenarios/recommend') return 'recommend_scenario';
|
|
260
|
+
if (method === 'GET' && /^\/host\/scenarios\/[^/]+$/.test(cleanPath)) return 'get_scenario';
|
|
261
|
+
if (method === 'GET' && cleanPath === '/host/scenarios') return 'list_scenarios';
|
|
262
|
+
if (method === 'POST' && cleanPath === '/host/workspaces/bind-scenario') return 'bind_workspace_scenario';
|
|
263
|
+
if (method === 'GET' && cleanPath === '/host/workspaces') return 'list_workspaces';
|
|
264
|
+
if (method === 'POST' && cleanPath === '/host/workspaces') return 'create_workspace';
|
|
265
|
+
if (method === 'PATCH' && /^\/host\/workspaces\/[^/]+$/.test(cleanPath)) return 'rename_workspace';
|
|
266
|
+
if (method === 'DELETE' && /^\/host\/workspaces\/[^/]+$/.test(cleanPath)) return 'delete_workspace';
|
|
267
|
+
if (method === 'GET' && /^\/host\/workspaces\/[^/]+\/summary$/.test(cleanPath)) return 'get_workspace_summary';
|
|
268
|
+
if (method === 'GET' && cleanPath === '/host/history') return 'query_user_history';
|
|
269
|
+
if (method === 'GET' && cleanPath === '/host/accounts') return 'list_accounts';
|
|
270
|
+
if (method === 'GET' && cleanPath === '/host/credentials') return 'list_credentials';
|
|
271
|
+
if (method === 'POST' && /^\/host\/credentials\/[^/]+\/revoke$/.test(cleanPath)) return 'revoke_credential';
|
|
272
|
+
if (method === 'GET' && cleanPath === '/host/data-locations') return 'get_data_locations';
|
|
273
|
+
if (method === 'GET' && cleanPath === '/host/docs/explain') return 'explain_concept';
|
|
274
|
+
if (method === 'GET' && cleanPath === '/host/docs/release-notes') return 'get_release_notes';
|
|
275
|
+
if (method === 'GET' && cleanPath === '/host/docs/known-issues') return 'get_known_issues';
|
|
276
|
+
if (method === 'POST' && cleanPath === '/host/feedback') return 'submit_feedback';
|
|
277
|
+
if (method === 'POST' && cleanPath === '/host/escalate') return 'escalate_to_human';
|
|
278
|
+
if (method === 'POST' && cleanPath === '/host/navigation/workspace') return 'navigate_to_workspace';
|
|
279
|
+
if (method === 'POST' && cleanPath === '/host/navigation/settings') return 'navigate_to_settings';
|
|
204
280
|
return null;
|
|
205
281
|
}
|
|
206
282
|
|
|
@@ -584,6 +660,29 @@ async function api(method, apiPath, body) {
|
|
|
584
660
|
}
|
|
585
661
|
}
|
|
586
662
|
|
|
663
|
+
async function resolveHostAgentFlag() {
|
|
664
|
+
const hinted = parseBooleanFlag(HOST_AGENT_HINT, false);
|
|
665
|
+
if (!AGENT_ID || !SERVER_URL || !MACHINE_API_KEY) return hinted;
|
|
666
|
+
|
|
667
|
+
try {
|
|
668
|
+
const res = await fetch(`${SERVER_URL}/internal/agent/${AGENT_ID}/profile`, {
|
|
669
|
+
method: 'GET',
|
|
670
|
+
headers: {
|
|
671
|
+
'Content-Type': 'application/json',
|
|
672
|
+
'Authorization': `Bearer ${MACHINE_API_KEY}`,
|
|
673
|
+
},
|
|
674
|
+
});
|
|
675
|
+
if (!res.ok) return hinted;
|
|
676
|
+
const data = await res.json();
|
|
677
|
+
if (typeof data?.isHostAgent === 'boolean') return data.isHostAgent;
|
|
678
|
+
return hinted;
|
|
679
|
+
} catch {
|
|
680
|
+
return hinted;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
const IS_HOST_AGENT = await resolveHostAgentFlag();
|
|
685
|
+
|
|
587
686
|
const server = new McpServer({ name: 'chat', version: '0.1.0' });
|
|
588
687
|
|
|
589
688
|
// ── check_messages ────────────────────────────────────────────────────────────
|
|
@@ -921,6 +1020,145 @@ server.tool('upload_image',
|
|
|
921
1020
|
}
|
|
922
1021
|
);
|
|
923
1022
|
|
|
1023
|
+
// ── update_goal_field ─────────────────────────────────────────────────────────
|
|
1024
|
+
server.tool('update_goal_field',
|
|
1025
|
+
'Incrementally update one goal field in workspace context. Use this during IM dialogue to capture user intent continuously, instead of waiting for a full goal form.',
|
|
1026
|
+
{
|
|
1027
|
+
field_path: z.string().describe('Dot path under goal context, e.g. "brand_positioning.voice" or "audience.primary".'),
|
|
1028
|
+
value: z.any().describe('Field value (string/number/object/array).'),
|
|
1029
|
+
confidence: z.number().min(0).max(1).optional().describe('Confidence in [0,1].'),
|
|
1030
|
+
status: z.enum(['candidate', 'user_confirmed']).optional().describe('candidate=tentative, user_confirmed=explicitly confirmed by user.'),
|
|
1031
|
+
source_turn_id: z.string().optional().describe('Optional conversation turn id for traceability.'),
|
|
1032
|
+
},
|
|
1033
|
+
async ({ field_path, value, confidence, status, source_turn_id }) => {
|
|
1034
|
+
if (!currentWorkspaceId) {
|
|
1035
|
+
return { isError: true, content: [{ type: 'text', text: 'No workspace context. Call check_messages first or specify workspace context in the current conversation.' }] };
|
|
1036
|
+
}
|
|
1037
|
+
const data = await api('POST', '/goal-fields/update', {
|
|
1038
|
+
workspace_id: currentWorkspaceId,
|
|
1039
|
+
field_path,
|
|
1040
|
+
value,
|
|
1041
|
+
confidence,
|
|
1042
|
+
status,
|
|
1043
|
+
source_turn_id,
|
|
1044
|
+
});
|
|
1045
|
+
return {
|
|
1046
|
+
content: [{
|
|
1047
|
+
type: 'text',
|
|
1048
|
+
text:
|
|
1049
|
+
`Goal field updated.\n` +
|
|
1050
|
+
`workspace=${data.workspaceId ?? currentWorkspaceId}\n` +
|
|
1051
|
+
`field_path=${data.fieldPath ?? field_path}\n` +
|
|
1052
|
+
`status=${data.status ?? status ?? 'candidate'}\n` +
|
|
1053
|
+
`context_item_id=${data.contextItemId ?? 'unknown'} version=${data.contextItemVersion ?? 'unknown'}`,
|
|
1054
|
+
}],
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
);
|
|
1058
|
+
|
|
1059
|
+
// ── supersede_goal_field ──────────────────────────────────────────────────────
|
|
1060
|
+
server.tool('supersede_goal_field',
|
|
1061
|
+
'Mark an existing goal field value as superseded when the user explicitly changes their mind.',
|
|
1062
|
+
{
|
|
1063
|
+
field_path: z.string().describe('Dot path to supersede, e.g. "audience.primary".'),
|
|
1064
|
+
reason: z.string().optional().describe('Why this field is superseded (optional).'),
|
|
1065
|
+
source_turn_id: z.string().optional().describe('Optional conversation turn id for traceability.'),
|
|
1066
|
+
},
|
|
1067
|
+
async ({ field_path, reason, source_turn_id }) => {
|
|
1068
|
+
if (!currentWorkspaceId) {
|
|
1069
|
+
return { isError: true, content: [{ type: 'text', text: 'No workspace context. Call check_messages first or specify workspace context in the current conversation.' }] };
|
|
1070
|
+
}
|
|
1071
|
+
const data = await api('POST', '/goal-fields/supersede', {
|
|
1072
|
+
workspace_id: currentWorkspaceId,
|
|
1073
|
+
field_path,
|
|
1074
|
+
reason,
|
|
1075
|
+
source_turn_id,
|
|
1076
|
+
});
|
|
1077
|
+
return {
|
|
1078
|
+
content: [{
|
|
1079
|
+
type: 'text',
|
|
1080
|
+
text:
|
|
1081
|
+
`Goal field superseded.\n` +
|
|
1082
|
+
`workspace=${data.workspaceId ?? currentWorkspaceId}\n` +
|
|
1083
|
+
`field_path=${data.fieldPath ?? field_path}\n` +
|
|
1084
|
+
`context_item_id=${data.contextItemId ?? 'unknown'} version=${data.contextItemVersion ?? 'unknown'}`,
|
|
1085
|
+
}],
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
);
|
|
1089
|
+
|
|
1090
|
+
// ── request_credential_auth ───────────────────────────────────────────────────
|
|
1091
|
+
server.tool('request_credential_auth',
|
|
1092
|
+
'Request just-in-time credential authorization for a platform. Returns a one-time OAuth URL that should be sent to the user as a clickable IM message.',
|
|
1093
|
+
{
|
|
1094
|
+
platform: z.string().describe('Target platform, e.g. "x".'),
|
|
1095
|
+
reason: z.string().describe('Human-readable reason why this credential is needed right now.'),
|
|
1096
|
+
source_turn_id: z.string().optional().describe('Optional conversation turn id for traceability.'),
|
|
1097
|
+
},
|
|
1098
|
+
async ({ platform, reason, source_turn_id }) => {
|
|
1099
|
+
if (!currentWorkspaceId) {
|
|
1100
|
+
return { isError: true, content: [{ type: 'text', text: 'No workspace context. Call check_messages first or specify workspace context in the current conversation.' }] };
|
|
1101
|
+
}
|
|
1102
|
+
const data = await api('POST', '/credential-auth/request', {
|
|
1103
|
+
workspace_id: currentWorkspaceId,
|
|
1104
|
+
platform,
|
|
1105
|
+
reason,
|
|
1106
|
+
source_turn_id,
|
|
1107
|
+
});
|
|
1108
|
+
return {
|
|
1109
|
+
content: [{
|
|
1110
|
+
type: 'text',
|
|
1111
|
+
text:
|
|
1112
|
+
`Credential auth requested.\n` +
|
|
1113
|
+
`platform=${data.platform ?? platform}\n` +
|
|
1114
|
+
`expires_at=${data.expiresAt ?? 'unknown'}\n` +
|
|
1115
|
+
`auth_url=${data.authUrl}\n` +
|
|
1116
|
+
`im_message_id=${data.messageId ?? 'unknown'}\n\n` +
|
|
1117
|
+
`A structured credential auth message has been posted to IM. Ask the user to click the authorization button there.`,
|
|
1118
|
+
}],
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
);
|
|
1122
|
+
|
|
1123
|
+
// ── register_data_source ───────────────────────────────────────────────────────
|
|
1124
|
+
server.tool('register_data_source',
|
|
1125
|
+
'Register a workspace data source without binding credential yet. Returns a one-time secure auth URL (10-minute expiry) that should be sent to the user via IM.',
|
|
1126
|
+
{
|
|
1127
|
+
workspace_id: z.string().optional().describe('Target workspace id. Defaults to current workspace context if omitted.'),
|
|
1128
|
+
display_name: z.string().describe('Human-readable data source name.'),
|
|
1129
|
+
source_type: z.string().describe('Data source type, e.g. "mysql", "postgresql", "api", "csv", "rss", "google_sheets".'),
|
|
1130
|
+
schema_hint: z.any().optional().describe('Optional schema hint JSON for downstream query planning.'),
|
|
1131
|
+
},
|
|
1132
|
+
async ({ workspace_id, display_name, source_type, schema_hint }) => {
|
|
1133
|
+
const targetWorkspaceId = (workspace_id ?? currentWorkspaceId ?? WORKSPACE_ID ?? '').trim();
|
|
1134
|
+
if (!targetWorkspaceId) {
|
|
1135
|
+
return { isError: true, content: [{ type: 'text', text: 'workspace_id is required (no current workspace context).' }] };
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
const body = {
|
|
1139
|
+
workspace_id: targetWorkspaceId,
|
|
1140
|
+
display_name,
|
|
1141
|
+
source_type,
|
|
1142
|
+
};
|
|
1143
|
+
if (schema_hint !== undefined) body.schema_hint = schema_hint;
|
|
1144
|
+
|
|
1145
|
+
const data = await api('POST', '/api/data-sources', body);
|
|
1146
|
+
return {
|
|
1147
|
+
content: [{
|
|
1148
|
+
type: 'text',
|
|
1149
|
+
text:
|
|
1150
|
+
`Data source registered.\n` +
|
|
1151
|
+
`workspace_id=${data.workspace_id ?? targetWorkspaceId}\n` +
|
|
1152
|
+
`data_source_id=${data.data_source_id}\n` +
|
|
1153
|
+
`source_type=${data.source_type ?? source_type}\n` +
|
|
1154
|
+
`expires_at=${data.expires_at ?? 'unknown'}\n` +
|
|
1155
|
+
`secure_auth_url=${data.secure_auth_url}\n\n` +
|
|
1156
|
+
`Send secure_auth_url to the user in IM so they can complete credential binding securely.`,
|
|
1157
|
+
}],
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
);
|
|
1161
|
+
|
|
924
1162
|
// ── request_approval ──────────────────────────────────────────────────────────
|
|
925
1163
|
server.tool('request_approval',
|
|
926
1164
|
'Request human approval before executing a sensitive platform action (posting, sending, publishing). Returns an action_id. After the human approves, call execute_approved_action with that ID.',
|
|
@@ -1122,7 +1360,424 @@ server.tool('complete_orchestrate_trigger',
|
|
|
1122
1360
|
}
|
|
1123
1361
|
);
|
|
1124
1362
|
|
|
1363
|
+
function hostJsonResult(data) {
|
|
1364
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
function hostErrorResult(err) {
|
|
1368
|
+
return { isError: true, content: [{ type: 'text', text: `Error: ${err.message}` }] };
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
function buildQuery(params = {}) {
|
|
1372
|
+
const query = new URLSearchParams();
|
|
1373
|
+
for (const [key, value] of Object.entries(params)) {
|
|
1374
|
+
if (value == null) continue;
|
|
1375
|
+
if (typeof value === 'string' && !value.trim()) continue;
|
|
1376
|
+
query.set(key, String(value));
|
|
1377
|
+
}
|
|
1378
|
+
const text = query.toString();
|
|
1379
|
+
return text ? `?${text}` : '';
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
if (IS_HOST_AGENT) {
|
|
1383
|
+
server.tool('search_scenarios',
|
|
1384
|
+
'Search scenario candidates by user intent for host routing decisions.',
|
|
1385
|
+
{
|
|
1386
|
+
intent_text: z.string().describe('User intent text to match against scenario catalog.'),
|
|
1387
|
+
category: z.string().optional().describe('Optional category filter.'),
|
|
1388
|
+
visibility: z.string().optional().describe('Optional visibility filter (public/system/user_private/org_shared).'),
|
|
1389
|
+
status: z.string().optional().describe('Optional status filter (draft/published/active/deprecated/archived).'),
|
|
1390
|
+
limit: z.number().int().min(1).max(20).optional().describe('Maximum number of returned scenarios.'),
|
|
1391
|
+
},
|
|
1392
|
+
async ({ intent_text, category, visibility, status, limit }) => {
|
|
1393
|
+
try {
|
|
1394
|
+
const query = buildQuery({ intent_text, category, visibility, status, limit });
|
|
1395
|
+
const data = await api('GET', `/host/scenarios/search${query}`);
|
|
1396
|
+
return hostJsonResult(data);
|
|
1397
|
+
} catch (err) {
|
|
1398
|
+
return hostErrorResult(err);
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
);
|
|
1402
|
+
|
|
1403
|
+
server.tool('get_scenario',
|
|
1404
|
+
'Get full scenario details (manifest + roles) for host explanation.',
|
|
1405
|
+
{
|
|
1406
|
+
scenario_id: z.string().describe('Scenario ID to inspect.'),
|
|
1407
|
+
},
|
|
1408
|
+
async ({ scenario_id }) => {
|
|
1409
|
+
try {
|
|
1410
|
+
const data = await api('GET', `/host/scenarios/${encodeURIComponent(scenario_id)}`);
|
|
1411
|
+
return hostJsonResult(data);
|
|
1412
|
+
} catch (err) {
|
|
1413
|
+
return hostErrorResult(err);
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
);
|
|
1417
|
+
|
|
1418
|
+
server.tool('list_scenarios',
|
|
1419
|
+
'List scenarios visible to host agent (public/system + owner-accessible).',
|
|
1420
|
+
{
|
|
1421
|
+
visibility: z.string().optional().describe('Optional visibility filter.'),
|
|
1422
|
+
category: z.string().optional().describe('Optional category filter.'),
|
|
1423
|
+
status: z.string().optional().describe('Optional status filter.'),
|
|
1424
|
+
include_archived: z.boolean().optional().describe('Include archived scenarios when true.'),
|
|
1425
|
+
limit: z.number().int().min(1).max(200).optional().describe('Maximum rows to return.'),
|
|
1426
|
+
},
|
|
1427
|
+
async ({ visibility, category, status, include_archived, limit }) => {
|
|
1428
|
+
try {
|
|
1429
|
+
const query = buildQuery({
|
|
1430
|
+
visibility,
|
|
1431
|
+
category,
|
|
1432
|
+
status,
|
|
1433
|
+
includeArchived: include_archived,
|
|
1434
|
+
limit,
|
|
1435
|
+
});
|
|
1436
|
+
const data = await api('GET', `/host/scenarios${query}`);
|
|
1437
|
+
return hostJsonResult(data);
|
|
1438
|
+
} catch (err) {
|
|
1439
|
+
return hostErrorResult(err);
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
);
|
|
1443
|
+
|
|
1444
|
+
server.tool('bind_workspace_scenario',
|
|
1445
|
+
'Bind a scenario to an existing workspace and ensure a primary agent.',
|
|
1446
|
+
{
|
|
1447
|
+
workspace_id: z.string().describe('Workspace ID to bind.'),
|
|
1448
|
+
scenario_id: z.string().describe('Scenario ID to bind.'),
|
|
1449
|
+
role_id: z.string().optional().describe('Optional preferred role id/name for primary agent binding.'),
|
|
1450
|
+
repair_role_binding: z.boolean().optional().describe('When true, ignore existing workspace role binding once and re-resolve primary agent for cleanup.'),
|
|
1451
|
+
},
|
|
1452
|
+
async ({ workspace_id, scenario_id, role_id, repair_role_binding }) => {
|
|
1453
|
+
try {
|
|
1454
|
+
const data = await api('POST', '/host/workspaces/bind-scenario', {
|
|
1455
|
+
workspace_id,
|
|
1456
|
+
scenario_id,
|
|
1457
|
+
role_id,
|
|
1458
|
+
repair_role_binding,
|
|
1459
|
+
});
|
|
1460
|
+
return hostJsonResult(data);
|
|
1461
|
+
} catch (err) {
|
|
1462
|
+
return hostErrorResult(err);
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
);
|
|
1466
|
+
|
|
1467
|
+
server.tool('recommend_scenario',
|
|
1468
|
+
'Recommend best-fit scenarios for a given user intent.',
|
|
1469
|
+
{
|
|
1470
|
+
user_intent: z.string().describe('Natural-language user intent.'),
|
|
1471
|
+
},
|
|
1472
|
+
async ({ user_intent }) => {
|
|
1473
|
+
try {
|
|
1474
|
+
const data = await api('POST', '/host/scenarios/recommend', { user_intent });
|
|
1475
|
+
return hostJsonResult(data);
|
|
1476
|
+
} catch (err) {
|
|
1477
|
+
return hostErrorResult(err);
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
);
|
|
1481
|
+
|
|
1482
|
+
server.tool('list_workspaces',
|
|
1483
|
+
'List all owner workspaces with scenario/mode/last activity for host overview.',
|
|
1484
|
+
{
|
|
1485
|
+
owner_id: z.string().optional().describe('Optional owner id (must match current host owner).'),
|
|
1486
|
+
limit: z.number().int().min(1).max(300).optional().describe('Maximum rows to return.'),
|
|
1487
|
+
},
|
|
1488
|
+
async ({ owner_id, limit }) => {
|
|
1489
|
+
try {
|
|
1490
|
+
const query = buildQuery({ owner_id, limit });
|
|
1491
|
+
const data = await api('GET', `/host/workspaces${query}`);
|
|
1492
|
+
return hostJsonResult(data);
|
|
1493
|
+
} catch (err) {
|
|
1494
|
+
return hostErrorResult(err);
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
);
|
|
1498
|
+
|
|
1499
|
+
server.tool('create_workspace',
|
|
1500
|
+
'Create a workspace and optionally bind scenario + primary role.',
|
|
1501
|
+
{
|
|
1502
|
+
name: z.string().optional().describe('Optional workspace name.'),
|
|
1503
|
+
description: z.string().optional().describe('Optional workspace description.'),
|
|
1504
|
+
scenario_id: z.string().optional().describe('Optional scenario id to bind immediately.'),
|
|
1505
|
+
role_id: z.string().optional().describe('Optional preferred role id/name when scenario is bound.'),
|
|
1506
|
+
},
|
|
1507
|
+
async ({ name, description, scenario_id, role_id }) => {
|
|
1508
|
+
try {
|
|
1509
|
+
const data = await api('POST', '/host/workspaces', {
|
|
1510
|
+
name,
|
|
1511
|
+
description,
|
|
1512
|
+
scenario_id,
|
|
1513
|
+
role_id,
|
|
1514
|
+
});
|
|
1515
|
+
return hostJsonResult(data);
|
|
1516
|
+
} catch (err) {
|
|
1517
|
+
return hostErrorResult(err);
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
);
|
|
1521
|
+
|
|
1522
|
+
server.tool('rename_workspace',
|
|
1523
|
+
'Rename an existing workspace.',
|
|
1524
|
+
{
|
|
1525
|
+
workspace_id: z.string().describe('Workspace ID to rename.'),
|
|
1526
|
+
new_name: z.string().describe('New workspace name.'),
|
|
1527
|
+
},
|
|
1528
|
+
async ({ workspace_id, new_name }) => {
|
|
1529
|
+
try {
|
|
1530
|
+
const data = await api('PATCH', `/host/workspaces/${encodeURIComponent(workspace_id)}`, {
|
|
1531
|
+
new_name,
|
|
1532
|
+
});
|
|
1533
|
+
return hostJsonResult(data);
|
|
1534
|
+
} catch (err) {
|
|
1535
|
+
return hostErrorResult(err);
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
);
|
|
1539
|
+
|
|
1540
|
+
server.tool('delete_workspace',
|
|
1541
|
+
'Delete (soft-delete) a workspace owned by the current user.',
|
|
1542
|
+
{
|
|
1543
|
+
workspace_id: z.string().describe('Workspace ID to delete.'),
|
|
1544
|
+
confirm: z.boolean().describe('Must be true to confirm deletion.'),
|
|
1545
|
+
},
|
|
1546
|
+
async ({ workspace_id, confirm }) => {
|
|
1547
|
+
try {
|
|
1548
|
+
const query = buildQuery({ confirm });
|
|
1549
|
+
const data = await api('DELETE', `/host/workspaces/${encodeURIComponent(workspace_id)}${query}`);
|
|
1550
|
+
return hostJsonResult(data);
|
|
1551
|
+
} catch (err) {
|
|
1552
|
+
return hostErrorResult(err);
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
);
|
|
1556
|
+
|
|
1557
|
+
server.tool('get_workspace_summary',
|
|
1558
|
+
'Get summary of workspace scenario, active agents, recent messages and publish stats.',
|
|
1559
|
+
{
|
|
1560
|
+
workspace_id: z.string().describe('Workspace ID to summarize.'),
|
|
1561
|
+
},
|
|
1562
|
+
async ({ workspace_id }) => {
|
|
1563
|
+
try {
|
|
1564
|
+
const data = await api('GET', `/host/workspaces/${encodeURIComponent(workspace_id)}/summary`);
|
|
1565
|
+
return hostJsonResult(data);
|
|
1566
|
+
} catch (err) {
|
|
1567
|
+
return hostErrorResult(err);
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
);
|
|
1571
|
+
|
|
1572
|
+
server.tool('query_user_history',
|
|
1573
|
+
'Search user history across owner workspaces for prior activities/messages.',
|
|
1574
|
+
{
|
|
1575
|
+
topic: z.string().describe('Topic keyword to search across workspaces.'),
|
|
1576
|
+
limit: z.number().int().min(1).max(50).optional().describe('Maximum number of results.'),
|
|
1577
|
+
},
|
|
1578
|
+
async ({ topic, limit }) => {
|
|
1579
|
+
try {
|
|
1580
|
+
const query = buildQuery({ topic, limit });
|
|
1581
|
+
const data = await api('GET', `/host/history${query}`);
|
|
1582
|
+
return hostJsonResult(data);
|
|
1583
|
+
} catch (err) {
|
|
1584
|
+
return hostErrorResult(err);
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
);
|
|
1588
|
+
|
|
1589
|
+
server.tool('list_accounts',
|
|
1590
|
+
'List platform accounts owned by the current user.',
|
|
1591
|
+
{
|
|
1592
|
+
owner_id: z.string().optional().describe('Optional owner id (must match current owner).'),
|
|
1593
|
+
workspace_id: z.string().optional().describe('Optional workspace filter; pass "null" for unbound accounts.'),
|
|
1594
|
+
platform: z.string().optional().describe('Optional platform filter.'),
|
|
1595
|
+
status: z.string().optional().describe('Optional status filter.'),
|
|
1596
|
+
include_archived: z.boolean().optional().describe('Include archived accounts.'),
|
|
1597
|
+
},
|
|
1598
|
+
async ({ owner_id, workspace_id, platform, status, include_archived }) => {
|
|
1599
|
+
try {
|
|
1600
|
+
const query = buildQuery({
|
|
1601
|
+
owner_id,
|
|
1602
|
+
workspace_id,
|
|
1603
|
+
platform,
|
|
1604
|
+
status,
|
|
1605
|
+
include_archived,
|
|
1606
|
+
});
|
|
1607
|
+
const data = await api('GET', `/host/accounts${query}`);
|
|
1608
|
+
return hostJsonResult(data);
|
|
1609
|
+
} catch (err) {
|
|
1610
|
+
return hostErrorResult(err);
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
);
|
|
1614
|
+
|
|
1615
|
+
server.tool('list_credentials',
|
|
1616
|
+
'List credentials as masked metadata (no plaintext).',
|
|
1617
|
+
{
|
|
1618
|
+
owner_id: z.string().optional().describe('Optional owner id (must match current owner).'),
|
|
1619
|
+
},
|
|
1620
|
+
async ({ owner_id }) => {
|
|
1621
|
+
try {
|
|
1622
|
+
const query = buildQuery({ owner_id });
|
|
1623
|
+
const data = await api('GET', `/host/credentials${query}`);
|
|
1624
|
+
return hostJsonResult(data);
|
|
1625
|
+
} catch (err) {
|
|
1626
|
+
return hostErrorResult(err);
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
);
|
|
1630
|
+
|
|
1631
|
+
server.tool('revoke_credential',
|
|
1632
|
+
'Revoke a credential with explicit confirmation.',
|
|
1633
|
+
{
|
|
1634
|
+
credential_id: z.string().describe('Credential ID to revoke.'),
|
|
1635
|
+
confirm: z.boolean().describe('Must be true to confirm revocation.'),
|
|
1636
|
+
},
|
|
1637
|
+
async ({ credential_id, confirm }) => {
|
|
1638
|
+
try {
|
|
1639
|
+
const data = await api('POST', `/host/credentials/${encodeURIComponent(credential_id)}/revoke`, {
|
|
1640
|
+
confirm,
|
|
1641
|
+
});
|
|
1642
|
+
return hostJsonResult(data);
|
|
1643
|
+
} catch (err) {
|
|
1644
|
+
return hostErrorResult(err);
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
);
|
|
1648
|
+
|
|
1649
|
+
server.tool('get_data_locations',
|
|
1650
|
+
'Describe where user/platform data is stored.',
|
|
1651
|
+
{},
|
|
1652
|
+
async () => {
|
|
1653
|
+
try {
|
|
1654
|
+
const data = await api('GET', '/host/data-locations');
|
|
1655
|
+
return hostJsonResult(data);
|
|
1656
|
+
} catch (err) {
|
|
1657
|
+
return hostErrorResult(err);
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
);
|
|
1661
|
+
|
|
1662
|
+
server.tool('explain_concept',
|
|
1663
|
+
'Explain a product concept from docs corpus.',
|
|
1664
|
+
{
|
|
1665
|
+
topic: z.string().describe('Concept/topic text to explain.'),
|
|
1666
|
+
},
|
|
1667
|
+
async ({ topic }) => {
|
|
1668
|
+
try {
|
|
1669
|
+
const query = buildQuery({ topic });
|
|
1670
|
+
const data = await api('GET', `/host/docs/explain${query}`);
|
|
1671
|
+
return hostJsonResult(data);
|
|
1672
|
+
} catch (err) {
|
|
1673
|
+
return hostErrorResult(err);
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
);
|
|
1677
|
+
|
|
1678
|
+
server.tool('get_release_notes',
|
|
1679
|
+
'Get release notes/events since a specified timestamp.',
|
|
1680
|
+
{
|
|
1681
|
+
since: z.string().optional().describe('Optional ISO timestamp (default: last 30 days).'),
|
|
1682
|
+
},
|
|
1683
|
+
async ({ since }) => {
|
|
1684
|
+
try {
|
|
1685
|
+
const query = buildQuery({ since });
|
|
1686
|
+
const data = await api('GET', `/host/docs/release-notes${query}`);
|
|
1687
|
+
return hostJsonResult(data);
|
|
1688
|
+
} catch (err) {
|
|
1689
|
+
return hostErrorResult(err);
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
);
|
|
1693
|
+
|
|
1694
|
+
server.tool('get_known_issues',
|
|
1695
|
+
'Get current known issues and limitations.',
|
|
1696
|
+
{},
|
|
1697
|
+
async () => {
|
|
1698
|
+
try {
|
|
1699
|
+
const data = await api('GET', '/host/docs/known-issues');
|
|
1700
|
+
return hostJsonResult(data);
|
|
1701
|
+
} catch (err) {
|
|
1702
|
+
return hostErrorResult(err);
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
);
|
|
1706
|
+
|
|
1707
|
+
server.tool('submit_feedback',
|
|
1708
|
+
'Submit user feedback into internal backlog workflow.',
|
|
1709
|
+
{
|
|
1710
|
+
category: z.enum(['bug', 'feature', 'ux', 'performance', 'security', 'other']).describe('Feedback category.'),
|
|
1711
|
+
content: z.string().describe('Feedback content.'),
|
|
1712
|
+
workspace_id: z.string().optional().describe('Optional workspace context for this feedback.'),
|
|
1713
|
+
},
|
|
1714
|
+
async ({ category, content, workspace_id }) => {
|
|
1715
|
+
try {
|
|
1716
|
+
const data = await api('POST', '/host/feedback', {
|
|
1717
|
+
category,
|
|
1718
|
+
content,
|
|
1719
|
+
workspace_id,
|
|
1720
|
+
});
|
|
1721
|
+
return hostJsonResult(data);
|
|
1722
|
+
} catch (err) {
|
|
1723
|
+
return hostErrorResult(err);
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
);
|
|
1727
|
+
|
|
1728
|
+
server.tool('escalate_to_human',
|
|
1729
|
+
'Escalate severe issue to human operators.',
|
|
1730
|
+
{
|
|
1731
|
+
issue: z.string().describe('Issue details to escalate.'),
|
|
1732
|
+
priority: z.enum(['low', 'medium', 'high', 'critical']).optional().describe('Escalation priority.'),
|
|
1733
|
+
workspace_id: z.string().optional().describe('Optional workspace context for escalation.'),
|
|
1734
|
+
},
|
|
1735
|
+
async ({ issue, priority, workspace_id }) => {
|
|
1736
|
+
try {
|
|
1737
|
+
const data = await api('POST', '/host/escalate', {
|
|
1738
|
+
issue,
|
|
1739
|
+
priority,
|
|
1740
|
+
workspace_id,
|
|
1741
|
+
});
|
|
1742
|
+
return hostJsonResult(data);
|
|
1743
|
+
} catch (err) {
|
|
1744
|
+
return hostErrorResult(err);
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
);
|
|
1748
|
+
|
|
1749
|
+
server.tool('navigate_to_workspace',
|
|
1750
|
+
'Emit navigation intent for UI to open a target workspace.',
|
|
1751
|
+
{
|
|
1752
|
+
workspace_id: z.string().describe('Target workspace ID.'),
|
|
1753
|
+
},
|
|
1754
|
+
async ({ workspace_id }) => {
|
|
1755
|
+
try {
|
|
1756
|
+
const data = await api('POST', '/host/navigation/workspace', { workspace_id });
|
|
1757
|
+
return hostJsonResult(data);
|
|
1758
|
+
} catch (err) {
|
|
1759
|
+
return hostErrorResult(err);
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
);
|
|
1763
|
+
|
|
1764
|
+
server.tool('navigate_to_settings',
|
|
1765
|
+
'Emit navigation intent for UI to open a settings section.',
|
|
1766
|
+
{
|
|
1767
|
+
section: z.string().describe('Settings section key, e.g. "credentials", "profile", "workspace".'),
|
|
1768
|
+
},
|
|
1769
|
+
async ({ section }) => {
|
|
1770
|
+
try {
|
|
1771
|
+
const data = await api('POST', '/host/navigation/settings', { section });
|
|
1772
|
+
return hostJsonResult(data);
|
|
1773
|
+
} catch (err) {
|
|
1774
|
+
return hostErrorResult(err);
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
);
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1125
1780
|
// ── start ─────────────────────────────────────────────────────────────────────
|
|
1126
1781
|
const transport = new StdioServerTransport();
|
|
1127
1782
|
await server.connect(transport);
|
|
1128
|
-
console.error(`[chat-bridge] MCP Server started (agentId=${AGENT_ID})`);
|
|
1783
|
+
console.error(`[chat-bridge] MCP Server started (agentId=${AGENT_ID}, host=${IS_HOST_AGENT ? 'true' : 'false'})`);
|
package/src/mcp-config.js
CHANGED
|
@@ -58,7 +58,8 @@ function baseEnvForServer(serverKey, { serverUrl, authToken, agentId, workspaceI
|
|
|
58
58
|
return { SERVER_URL: serverUrl, MACHINE_API_KEY: authToken, AGENT_ID: agentId };
|
|
59
59
|
}
|
|
60
60
|
if (
|
|
61
|
-
serverKey === '
|
|
61
|
+
serverKey === 'mysql'
|
|
62
|
+
|| serverKey === 'publisher'
|
|
62
63
|
|| serverKey === 'platform'
|
|
63
64
|
|| serverKey === 'research-fetch'
|
|
64
65
|
|| serverKey === 'market-data-query'
|