@lightcone-ai/daemon 0.13.0 → 0.14.1
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/mcp-servers/official/page-understanding/index.js +93 -0
- package/mcp-servers/official/page-understanding/manifest.json +20 -0
- package/mcp-servers/official/video-narration-planner/core.js +1436 -0
- package/mcp-servers/official/video-narration-planner/index.js +98 -0
- package/mcp-servers/official/video-narration-planner/manifest.json +30 -0
- package/mcp-servers/sophon-data/index.js +449 -0
- package/mcp-servers/sophon-data/manifest.json +19 -0
- package/package.json +1 -1
- package/src/_vendor/video/composer/index.js +377 -0
- package/src/agent-manager.js +10 -1
- package/src/chat-bridge.js +440 -15
- package/src/drivers/claude.js +10 -3
- package/src/mcp-config.js +3 -1
- package/src/workspace-file-upload.js +71 -0
|
@@ -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
|
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
analyzePage,
|
|
8
|
+
analyzePageFromHtmlFixture,
|
|
9
|
+
validatePageUnderstanding,
|
|
10
|
+
} from '../../../../src/video/understanding/index.js';
|
|
11
|
+
|
|
12
|
+
function toText(payload) {
|
|
13
|
+
return {
|
|
14
|
+
content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function toError(message) {
|
|
19
|
+
return {
|
|
20
|
+
isError: true,
|
|
21
|
+
content: [{ type: 'text', text: `Error: ${message}` }],
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const AnalyzePageOptionsSchema = z.object({
|
|
26
|
+
settleMs: z.number().int().min(500).max(30000).optional(),
|
|
27
|
+
timeoutMs: z.number().int().min(5000).max(240000).optional(),
|
|
28
|
+
viewportWidth: z.number().int().min(360).max(2160).optional(),
|
|
29
|
+
viewportHeight: z.number().int().min(480).max(3840).optional(),
|
|
30
|
+
minTextBins: z.number().int().min(3).max(40).optional(),
|
|
31
|
+
minBinChars: z.number().int().min(12).max(200).optional(),
|
|
32
|
+
allowVisionFallback: z.boolean().optional(),
|
|
33
|
+
useLlm: z.boolean().optional(),
|
|
34
|
+
fixture_mode: z.boolean().optional(),
|
|
35
|
+
}).passthrough();
|
|
36
|
+
|
|
37
|
+
const server = new McpServer({
|
|
38
|
+
name: 'official-page-understanding',
|
|
39
|
+
version: '0.1.0',
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
server.tool(
|
|
43
|
+
'analyze_page',
|
|
44
|
+
'Analyze webpage structure for short-video narration planning. Returns page_understanding schema.',
|
|
45
|
+
{
|
|
46
|
+
url: z.string().url().describe('Target page URL.'),
|
|
47
|
+
persona: z.string().optional().describe('Audience persona, e.g. "校招求职学生".'),
|
|
48
|
+
options: AnalyzePageOptionsSchema.optional(),
|
|
49
|
+
},
|
|
50
|
+
async ({ url, persona = '', options = {} }) => {
|
|
51
|
+
try {
|
|
52
|
+
let payload;
|
|
53
|
+
if (options?.fixture_mode) {
|
|
54
|
+
payload = await analyzePageFromHtmlFixture({
|
|
55
|
+
url,
|
|
56
|
+
hostname: new URL(url).hostname,
|
|
57
|
+
persona,
|
|
58
|
+
bins: [
|
|
59
|
+
{ y_center: 380, text: '这是一个用于 smoke test 的结构化 bin 内容。' },
|
|
60
|
+
{ y_center: 980, text: '该页面包含若干重点段落,用于验证 schema 输出。' },
|
|
61
|
+
],
|
|
62
|
+
hotspots: [
|
|
63
|
+
{ y: 420, reason: '标题区域', weight: 9, type: 'h1' },
|
|
64
|
+
{ y: 1100, reason: '正文重点', weight: 8, type: 'h2' },
|
|
65
|
+
],
|
|
66
|
+
meta: {
|
|
67
|
+
title: 'Fixture Page',
|
|
68
|
+
og_title: 'Fixture Page',
|
|
69
|
+
og_description: 'Fixture mode output for analyze_page smoke test.',
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
} else {
|
|
73
|
+
payload = await analyzePage({
|
|
74
|
+
url,
|
|
75
|
+
persona,
|
|
76
|
+
options,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const validation = validatePageUnderstanding(payload);
|
|
81
|
+
if (!validation.ok) {
|
|
82
|
+
return toError(`page_understanding_validation_failed: ${validation.errors.join('; ')}`);
|
|
83
|
+
}
|
|
84
|
+
return toText(payload);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
return toError(error?.message ?? 'analyze_page_failed');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const transport = new StdioServerTransport();
|
|
92
|
+
await server.connect(transport);
|
|
93
|
+
console.error('[page-understanding] MCP Server started');
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "page-understanding",
|
|
3
|
+
"name": "Official Page Understanding MCP",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"runtime": "node",
|
|
6
|
+
"entrypoint": "index.js",
|
|
7
|
+
"tool_declarations": [
|
|
8
|
+
{ "name": "analyze_page", "classification": "cacheable" }
|
|
9
|
+
],
|
|
10
|
+
"smoke_test": {
|
|
11
|
+
"tool": "analyze_page",
|
|
12
|
+
"arguments": {
|
|
13
|
+
"url": "https://example.com",
|
|
14
|
+
"persona": "通用用户",
|
|
15
|
+
"options": {
|
|
16
|
+
"fixture_mode": true
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|