@lightcone-ai/daemon 0.15.56 → 0.15.58
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.
|
@@ -4,6 +4,7 @@ export const DEFAULT_LIMIT = 200;
|
|
|
4
4
|
export const MAX_LIMIT = 1000;
|
|
5
5
|
|
|
6
6
|
const READ_QUERY_PREFIX = /^(SELECT|SHOW|DESCRIBE|DESC|EXPLAIN|WITH)\b/i;
|
|
7
|
+
const AUTO_LIMIT_PREFIX = /^(SELECT|WITH)\b/i;
|
|
7
8
|
const REQUIRED_DB_KEYS = ['DB_HOST', 'DB_PORT', 'DB_USER', 'DB_PASSWORD', 'DB_NAME'];
|
|
8
9
|
|
|
9
10
|
function trimTrailingSemicolons(sql) {
|
|
@@ -27,6 +28,10 @@ export function isReadQuery(sql) {
|
|
|
27
28
|
return READ_QUERY_PREFIX.test(sql);
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
export function supportsAutoLimit(sql) {
|
|
32
|
+
return AUTO_LIMIT_PREFIX.test(sql);
|
|
33
|
+
}
|
|
34
|
+
|
|
30
35
|
export function hasLimitClause(sql) {
|
|
31
36
|
return /\bLIMIT\b/i.test(sql);
|
|
32
37
|
}
|
|
@@ -36,7 +41,7 @@ export function prepareSql(sql, limit) {
|
|
|
36
41
|
if (!normalizedSql) throw new Error('sql must be a non-empty string');
|
|
37
42
|
|
|
38
43
|
const effectiveLimit = normalizeLimit(limit);
|
|
39
|
-
if (!
|
|
44
|
+
if (!supportsAutoLimit(normalizedSql) || hasLimitClause(normalizedSql)) {
|
|
40
45
|
return {
|
|
41
46
|
sql: normalizedSql,
|
|
42
47
|
limit: effectiveLimit,
|
|
@@ -111,9 +116,9 @@ export function createDataSourcePoolRegistry({ createPool, logger = () => {} } =
|
|
|
111
116
|
const pools = new Map();
|
|
112
117
|
|
|
113
118
|
return {
|
|
114
|
-
getOrCreate(
|
|
115
|
-
const id = String(
|
|
116
|
-
if (!id) throw new Error('
|
|
119
|
+
getOrCreate(dataSource, dbConfig) {
|
|
120
|
+
const id = String(dataSource ?? '').trim();
|
|
121
|
+
if (!id) throw new Error('data_source must be a non-empty string');
|
|
117
122
|
const nextFingerprint = fingerprintDbConfig(dbConfig);
|
|
118
123
|
const existing = pools.get(id);
|
|
119
124
|
|
|
@@ -123,7 +128,7 @@ export function createDataSourcePoolRegistry({ createPool, logger = () => {} } =
|
|
|
123
128
|
|
|
124
129
|
if (existing) {
|
|
125
130
|
Promise.resolve(existing.pool.end())
|
|
126
|
-
.catch((error) => logger(`[mysql-mcp] failed to close previous pool for
|
|
131
|
+
.catch((error) => logger(`[mysql-mcp] failed to close previous pool for data_source=${id}: ${error.message}`));
|
|
127
132
|
}
|
|
128
133
|
|
|
129
134
|
const pool = createPool(dbConfig);
|
|
@@ -172,10 +177,10 @@ export function createCredentialBrokerClient({
|
|
|
172
177
|
const normalizedWorkspaceId = String(workspaceId ?? '').trim();
|
|
173
178
|
const normalizedBundleId = String(bundleId ?? '').trim();
|
|
174
179
|
|
|
175
|
-
return async function fetchCredentialEnvVars(
|
|
176
|
-
const
|
|
177
|
-
if (!
|
|
178
|
-
throw new Error('
|
|
180
|
+
return async function fetchCredentialEnvVars(dataSource) {
|
|
181
|
+
const normalizedDataSource = String(dataSource ?? '').trim();
|
|
182
|
+
if (!normalizedDataSource) {
|
|
183
|
+
throw new Error('data_source is required');
|
|
179
184
|
}
|
|
180
185
|
if (!normalizedServerUrl || !normalizedMachineApiKey || !normalizedAgentId) {
|
|
181
186
|
throw new Error('mysql MCP missing SERVER_URL/MACHINE_API_KEY/AGENT_ID runtime context');
|
|
@@ -190,7 +195,7 @@ export function createCredentialBrokerClient({
|
|
|
190
195
|
body: JSON.stringify({
|
|
191
196
|
agent_id: normalizedAgentId,
|
|
192
197
|
workspace_id: normalizedWorkspaceId || null,
|
|
193
|
-
required_credentials: [
|
|
198
|
+
required_credentials: [normalizedDataSource],
|
|
194
199
|
bundle_id: normalizedBundleId || null,
|
|
195
200
|
}),
|
|
196
201
|
});
|
|
@@ -218,16 +223,16 @@ export function createCredentialBrokerClient({
|
|
|
218
223
|
}
|
|
219
224
|
|
|
220
225
|
export async function executeSql({
|
|
221
|
-
|
|
226
|
+
dataSource,
|
|
222
227
|
sql,
|
|
223
228
|
params = [],
|
|
224
229
|
limit,
|
|
225
230
|
fetchCredentialEnvVars,
|
|
226
231
|
poolRegistry,
|
|
227
232
|
} = {}) {
|
|
228
|
-
const
|
|
229
|
-
if (!
|
|
230
|
-
throw new Error('
|
|
233
|
+
const normalizedDataSource = String(dataSource ?? '').trim();
|
|
234
|
+
if (!normalizedDataSource) {
|
|
235
|
+
throw new Error('data_source is required');
|
|
231
236
|
}
|
|
232
237
|
if (!Array.isArray(params)) {
|
|
233
238
|
throw new Error('params must be an array');
|
|
@@ -240,9 +245,9 @@ export async function executeSql({
|
|
|
240
245
|
}
|
|
241
246
|
|
|
242
247
|
const prepared = prepareSql(sql, limit);
|
|
243
|
-
const credentialEnv = await fetchCredentialEnvVars(
|
|
248
|
+
const credentialEnv = await fetchCredentialEnvVars(normalizedDataSource);
|
|
244
249
|
const dbConfig = parseDbConfig(credentialEnv);
|
|
245
|
-
const pool = poolRegistry.getOrCreate(
|
|
250
|
+
const pool = poolRegistry.getOrCreate(normalizedDataSource, dbConfig);
|
|
246
251
|
|
|
247
252
|
const queryParams = [...params, ...prepared.limitParams];
|
|
248
253
|
const [result] = await pool.query(prepared.sql, queryParams);
|
|
@@ -252,7 +257,7 @@ export async function executeSql({
|
|
|
252
257
|
? result.slice(0, prepared.limit)
|
|
253
258
|
: result;
|
|
254
259
|
return {
|
|
255
|
-
|
|
260
|
+
data_source: normalizedDataSource,
|
|
256
261
|
row_count: rows.length,
|
|
257
262
|
limit: prepared.limit,
|
|
258
263
|
limit_applied: prepared.limitInjected || rows.length < result.length,
|
|
@@ -261,7 +266,7 @@ export async function executeSql({
|
|
|
261
266
|
}
|
|
262
267
|
|
|
263
268
|
return {
|
|
264
|
-
|
|
269
|
+
data_source: normalizedDataSource,
|
|
265
270
|
affected_rows: Number(result?.affectedRows ?? 0),
|
|
266
271
|
changed_rows: Number(result?.changedRows ?? 0),
|
|
267
272
|
insert_id: result?.insertId ?? null,
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
} from './core.js';
|
|
12
12
|
|
|
13
13
|
const TOOL_SCHEMA = {
|
|
14
|
-
|
|
14
|
+
data_source: z.string().describe('工作区数据源名称,例如 sophon_cub。不要填写数据库密码或内部 ID。'),
|
|
15
15
|
sql: z.string().describe('要执行的 SQL。支持参数占位符 ?'),
|
|
16
16
|
params: z.array(z.any()).optional().describe('SQL 参数数组(按顺序对应 ? 占位符)'),
|
|
17
17
|
limit: z.number().int().positive().optional().describe('结果行数上限。默认 200,最大 1000'),
|
|
@@ -50,12 +50,12 @@ export function createMysqlMcpServer({
|
|
|
50
50
|
|
|
51
51
|
server.tool(
|
|
52
52
|
'query',
|
|
53
|
-
'执行通用 SQL
|
|
53
|
+
'执行通用 SQL。调用时通过数据源名称走 credential broker 动态获取 DB 凭证并路由连接。',
|
|
54
54
|
TOOL_SCHEMA,
|
|
55
|
-
async ({
|
|
55
|
+
async ({ data_source, sql, params = [], limit }) => {
|
|
56
56
|
try {
|
|
57
57
|
const payload = await executeSql({
|
|
58
|
-
|
|
58
|
+
dataSource: data_source,
|
|
59
59
|
sql,
|
|
60
60
|
params,
|
|
61
61
|
limit,
|
|
@@ -64,7 +64,7 @@ export function createMysqlMcpServer({
|
|
|
64
64
|
});
|
|
65
65
|
return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
|
|
66
66
|
} catch (error) {
|
|
67
|
-
logger(`[mysql-mcp] query failed for
|
|
67
|
+
logger(`[mysql-mcp] query failed for data_source=${data_source}: ${error.message}`);
|
|
68
68
|
return {
|
|
69
69
|
isError: true,
|
|
70
70
|
content: [{ type: 'text', text: `query failed: ${error.message}` }],
|
package/package.json
CHANGED
package/src/chat-bridge.js
CHANGED
|
@@ -188,6 +188,7 @@ const DEFAULT_TOOL_CLASSIFICATION = {
|
|
|
188
188
|
query_user_history: 'cacheable',
|
|
189
189
|
list_accounts: 'cacheable',
|
|
190
190
|
list_credentials: 'cacheable',
|
|
191
|
+
list_data_sources: 'cacheable',
|
|
191
192
|
get_data_locations: 'cacheable',
|
|
192
193
|
explain_concept: 'cacheable',
|
|
193
194
|
get_release_notes: 'cacheable',
|
|
@@ -296,6 +297,7 @@ function inferToolForApi(method, apiPath, body) {
|
|
|
296
297
|
if (method === 'POST' && cleanPath === '/credential-auth/request') return 'request_credential_auth';
|
|
297
298
|
if (method === 'POST' && cleanPath === '/content-library/submit') return 'submit_to_library';
|
|
298
299
|
if (method === 'POST' && cleanPath === '/api/data-sources') return 'register_data_source';
|
|
300
|
+
if (method === 'GET' && cleanPath === '/api/data-sources') return 'list_data_sources';
|
|
299
301
|
if (method === 'POST' && cleanPath === '/orchestrate/decision') return 'write_governance_decision';
|
|
300
302
|
if (method === 'POST' && cleanPath === '/orchestrate/correction') return 'write_governance_correction';
|
|
301
303
|
if (method === 'GET' && cleanPath === '/orchestrate/context') return 'get_orchestrate_context';
|
|
@@ -933,6 +935,29 @@ server.tool('list_server', 'List workspaces, agents, and humans on the server',
|
|
|
933
935
|
return { content: [{ type: 'text', text: `Workspaces:\n${workspaces}\n\nAgents:\n${agents}\n\nHumans:\n${humans}` }] };
|
|
934
936
|
});
|
|
935
937
|
|
|
938
|
+
// ── list_data_sources ─────────────────────────────────────────────────────────
|
|
939
|
+
server.tool('list_data_sources', 'List named data sources available in the current workspace. Returns names and status only; no internal IDs or secrets.', {
|
|
940
|
+
workspace_id: z.string().optional().describe('Target workspace id. Defaults to current workspace context if omitted.'),
|
|
941
|
+
}, async ({ workspace_id }) => {
|
|
942
|
+
const targetWorkspaceId = (workspace_id ?? currentWorkspaceId ?? WORKSPACE_ID ?? '').trim();
|
|
943
|
+
if (!targetWorkspaceId) {
|
|
944
|
+
return { isError: true, content: [{ type: 'text', text: 'workspace_id is required (no current workspace context).' }] };
|
|
945
|
+
}
|
|
946
|
+
const params = new URLSearchParams({ workspace_id: targetWorkspaceId });
|
|
947
|
+
const data = await api('GET', `/api/data-sources?${params}`);
|
|
948
|
+
const sources = Array.isArray(data.data_sources) ? data.data_sources : [];
|
|
949
|
+
if (sources.length === 0) {
|
|
950
|
+
return { content: [{ type: 'text', text: 'No data sources are configured for this workspace.' }] };
|
|
951
|
+
}
|
|
952
|
+
const text = sources.map((source) => {
|
|
953
|
+
const name = String(source.name ?? '未命名数据源').trim();
|
|
954
|
+
const type = String(source.source_type ?? 'unknown').trim();
|
|
955
|
+
const status = String(source.credential_status ?? 'unknown').trim();
|
|
956
|
+
return `- ${name} (${type}) - connection ${status}`;
|
|
957
|
+
}).join('\n');
|
|
958
|
+
return { content: [{ type: 'text', text }] };
|
|
959
|
+
});
|
|
960
|
+
|
|
936
961
|
// ── list_tasks ────────────────────────────────────────────────────────────────
|
|
937
962
|
server.tool('list_tasks', 'List tasks in a workspace', {
|
|
938
963
|
workspace: z.string().describe('Target: #workspace-name'),
|
|
@@ -1457,7 +1482,7 @@ server.tool('submit_to_library',
|
|
|
1457
1482
|
|
|
1458
1483
|
// ── register_data_source ───────────────────────────────────────────────────────
|
|
1459
1484
|
server.tool('register_data_source',
|
|
1460
|
-
'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.',
|
|
1485
|
+
'Register a named 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.',
|
|
1461
1486
|
{
|
|
1462
1487
|
workspace_id: z.string().optional().describe('Target workspace id. Defaults to current workspace context if omitted.'),
|
|
1463
1488
|
display_name: z.string().describe('Human-readable data source name.'),
|
|
@@ -1484,7 +1509,7 @@ server.tool('register_data_source',
|
|
|
1484
1509
|
text:
|
|
1485
1510
|
`Data source registered.\n` +
|
|
1486
1511
|
`workspace_id=${data.workspace_id ?? targetWorkspaceId}\n` +
|
|
1487
|
-
`
|
|
1512
|
+
`display_name=${display_name}\n` +
|
|
1488
1513
|
`source_type=${data.source_type ?? source_type}\n` +
|
|
1489
1514
|
`expires_at=${data.expires_at ?? 'unknown'}\n` +
|
|
1490
1515
|
`secure_auth_url=${data.secure_auth_url}\n\n` +
|