cloudflare-mcp-smart-proxy 1.1.1 → 1.3.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.
@@ -10,8 +10,9 @@ import { SymbolTools } from './tools/symbol-tools.js';
10
10
  import { Diagnostics } from './tools/diagnostics.js';
11
11
 
12
12
  export class LocalToolExecutor {
13
- constructor(workspaceRoot) {
13
+ constructor(workspaceRoot, connectorBridge = null) {
14
14
  this.workspaceRoot = workspaceRoot;
15
+ this.connectorBridge = connectorBridge;
15
16
  }
16
17
 
17
18
  /**
@@ -55,6 +56,26 @@ export class LocalToolExecutor {
55
56
 
56
57
  case 'get_diagnostics':
57
58
  return await Diagnostics.getDiagnostics(params, this.workspaceRoot);
59
+
60
+ case 'connector_bridge_status':
61
+ if (!this.connectorBridge) throw new Error('Connector bridge is not configured');
62
+ return this.connectorBridge.getBridgeStatus();
63
+
64
+ case 'connector_sync_profile':
65
+ if (!this.connectorBridge) throw new Error('Connector bridge is not configured');
66
+ return await this.connectorBridge.syncProfile();
67
+
68
+ case 'connector_report_status':
69
+ if (!this.connectorBridge) throw new Error('Connector bridge is not configured');
70
+ return await this.connectorBridge.reportStatus(params || {});
71
+
72
+ case 'connector_discover_project_probe':
73
+ if (!this.connectorBridge) throw new Error('Connector bridge is not configured');
74
+ return await this.connectorBridge.discoverProjectProbe();
75
+
76
+ case 'connector_report_project_probe':
77
+ if (!this.connectorBridge) throw new Error('Connector bridge is not configured');
78
+ return await this.connectorBridge.reportProjectProbe(params || {});
58
79
 
59
80
  default:
60
81
  throw new Error(`Unknown local tool: ${toolName}`);
@@ -314,8 +335,69 @@ export class LocalToolExecutor {
314
335
  },
315
336
  required: ['file_path']
316
337
  }
338
+ },
339
+ {
340
+ name: 'connector_bridge_status',
341
+ description: 'Get current connector bridge state, sync metadata, and workspace binding status',
342
+ inputSchema: {
343
+ type: 'object',
344
+ properties: {}
345
+ }
346
+ },
347
+ {
348
+ name: 'connector_sync_profile',
349
+ description: 'Pull the latest install plan and recent status reports for the configured client profile',
350
+ inputSchema: {
351
+ type: 'object',
352
+ properties: {}
353
+ }
354
+ },
355
+ {
356
+ name: 'connector_report_status',
357
+ description: 'Report local connector apply status back to CloudMCP',
358
+ inputSchema: {
359
+ type: 'object',
360
+ properties: {
361
+ status: {
362
+ type: 'string',
363
+ description: 'Overall status, for example succeeded, degraded, or failed'
364
+ },
365
+ summary: {
366
+ type: 'object',
367
+ description: 'High level summary of local apply results'
368
+ },
369
+ operationResults: {
370
+ type: 'array',
371
+ description: 'Per-operation execution results'
372
+ },
373
+ issues: {
374
+ type: 'array',
375
+ description: 'Reported install or runtime issues'
376
+ }
377
+ }
378
+ }
379
+ },
380
+ {
381
+ name: 'connector_discover_project_probe',
382
+ description: 'Discover the local workspace skeleton and build a project_probe candidate payload',
383
+ inputSchema: {
384
+ type: 'object',
385
+ properties: {}
386
+ }
387
+ },
388
+ {
389
+ name: 'connector_report_project_probe',
390
+ description: 'Report the local workspace project probe to CloudMCP and optionally auto-generate a draft context pack',
391
+ inputSchema: {
392
+ type: 'object',
393
+ properties: {
394
+ autoGenerateContextPack: {
395
+ type: 'boolean',
396
+ description: 'Whether CloudMCP should auto-generate a draft context pack from this probe'
397
+ }
398
+ }
399
+ }
317
400
  }
318
401
  ];
319
402
  }
320
403
  }
321
-
@@ -0,0 +1,137 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+
4
+ async function pathExists(target) {
5
+ try {
6
+ await fs.access(target);
7
+ return true;
8
+ } catch {
9
+ return false;
10
+ }
11
+ }
12
+
13
+ async function safeReadDir(target) {
14
+ try {
15
+ return await fs.readdir(target, { withFileTypes: true });
16
+ } catch {
17
+ return [];
18
+ }
19
+ }
20
+
21
+ function inferLanguages(fileNames = []) {
22
+ const set = new Set();
23
+ if (fileNames.some((name) => name.endsWith('.js') || name.endsWith('.mjs') || name.endsWith('.cjs'))) set.add('javascript');
24
+ if (fileNames.some((name) => name.endsWith('.ts') || name.endsWith('.tsx'))) set.add('typescript');
25
+ if (fileNames.some((name) => name.endsWith('.py'))) set.add('python');
26
+ if (fileNames.some((name) => name.endsWith('.go'))) set.add('go');
27
+ if (fileNames.some((name) => name.endsWith('.rs'))) set.add('rust');
28
+ if (fileNames.some((name) => name.endsWith('.java'))) set.add('java');
29
+ if (fileNames.some((name) => name.endsWith('.html'))) set.add('html');
30
+ if (fileNames.some((name) => name.endsWith('.css'))) set.add('css');
31
+ return Array.from(set);
32
+ }
33
+
34
+ function inferFrameworkHints(fileNames = [], dirs = []) {
35
+ const set = new Set();
36
+ if (fileNames.includes('wrangler.toml')) set.add('cloudflare_workers');
37
+ if (fileNames.includes('package.json')) set.add('node_workspace');
38
+ if (fileNames.includes('pnpm-workspace.yaml') || dirs.includes('packages') || dirs.includes('apps')) set.add('monorepo');
39
+ if (fileNames.includes('next.config.js') || fileNames.includes('next.config.mjs')) set.add('nextjs');
40
+ if (fileNames.includes('vite.config.js') || fileNames.includes('vite.config.ts')) set.add('vite');
41
+ if (fileNames.includes('go.mod')) set.add('go_module');
42
+ if (fileNames.includes('pyproject.toml')) set.add('python_project');
43
+ return Array.from(set);
44
+ }
45
+
46
+ function inferPackageManager(fileNames = []) {
47
+ if (fileNames.includes('pnpm-lock.yaml')) return 'pnpm';
48
+ if (fileNames.includes('yarn.lock')) return 'yarn';
49
+ if (fileNames.includes('package-lock.json')) return 'npm';
50
+ if (fileNames.includes('bun.lockb') || fileNames.includes('bun.lock')) return 'bun';
51
+ return '';
52
+ }
53
+
54
+ function buildWorkspaceLayout(dirs = []) {
55
+ const preferred = ['apps', 'packages', 'src', 'tests', 'docs', 'scripts'];
56
+ const found = preferred.filter((name) => dirs.includes(name));
57
+ return found.join(' + ');
58
+ }
59
+
60
+ export async function discoverProjectProbe({
61
+ workspaceRoot,
62
+ workspaceId,
63
+ clientProfileId = null,
64
+ connectorId = null,
65
+ connectorType = null,
66
+ generatedBy = 'connector_scan'
67
+ }) {
68
+ const repoRoot = path.resolve(workspaceRoot || process.cwd());
69
+ const repoName = path.basename(repoRoot);
70
+ const topLevel = await safeReadDir(repoRoot);
71
+ const dirNames = topLevel.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
72
+ const fileNames = topLevel.filter((entry) => entry.isFile()).map((entry) => entry.name);
73
+
74
+ const keyFiles = [
75
+ 'wrangler.toml',
76
+ 'package.json',
77
+ 'pnpm-workspace.yaml',
78
+ 'README.md',
79
+ 'src/worker.js',
80
+ 'src/admin-page.html',
81
+ 'docs/CLOUDMCP_CURRENT_DESIGN_FRAMEWORK_AND_DATAFLOW_2026-05-07.md'
82
+ ];
83
+ const docCandidates = [
84
+ 'README.md',
85
+ 'docs/CLOUDMCP_CURRENT_DESIGN_FRAMEWORK_AND_DATAFLOW_2026-05-07.md',
86
+ 'docs/CLOUDMCP_INTERNAL_CAPABILITY_BRIDGE_STANDARD_2026-05-06.md',
87
+ 'docs/CLOUDMCP_EXTERNAL_TOOL_SURFACE_STANDARD_2026-05-06.md'
88
+ ];
89
+
90
+ const resolvedKeyFiles = [];
91
+ for (const relativePath of keyFiles) {
92
+ if (await pathExists(path.join(repoRoot, relativePath))) {
93
+ resolvedKeyFiles.push(relativePath);
94
+ }
95
+ }
96
+
97
+ const resolvedDocCandidates = [];
98
+ for (const relativePath of docCandidates) {
99
+ if (await pathExists(path.join(repoRoot, relativePath))) {
100
+ resolvedDocCandidates.push(relativePath);
101
+ }
102
+ }
103
+
104
+ const languages = inferLanguages(fileNames.concat(resolvedKeyFiles));
105
+ const frameworkHints = inferFrameworkHints(fileNames, dirNames);
106
+ const packageManager = inferPackageManager(fileNames);
107
+ const workspaceLayout = buildWorkspaceLayout(dirNames);
108
+ const commandHints = [];
109
+ if (packageManager) {
110
+ commandHints.push(`${packageManager} test`);
111
+ commandHints.push(`${packageManager} run gen-admin`);
112
+ }
113
+
114
+ return {
115
+ id: `project_probe.${repoName}.${(workspaceId || 'default').replace(/[^a-zA-Z0-9_.-]/g, '_')}`,
116
+ clientProfileId,
117
+ connectorId,
118
+ connectorType,
119
+ projectId: repoName.toLowerCase(),
120
+ workspaceId: workspaceId || null,
121
+ repoName,
122
+ repoRoot,
123
+ gitRemote: null,
124
+ defaultBranch: 'main',
125
+ languages,
126
+ frameworkHints,
127
+ packageManager: packageManager || null,
128
+ workspaceLayout: workspaceLayout || null,
129
+ keyFiles: resolvedKeyFiles,
130
+ docCandidates: resolvedDocCandidates,
131
+ commandHints,
132
+ summary: `${repoName} 项目骨架自动探测结果`,
133
+ notes: '由本地 connector bridge 自动生成,后续应由 CloudMCP 生成候选上下文包并进入确认流程。',
134
+ generatedBy,
135
+ status: 'ready_for_context'
136
+ };
137
+ }
@@ -0,0 +1,315 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+
5
+ const DEFAULT_SERVER_NAME = 'cloudmcp';
6
+ const DEFAULT_PACKAGE_NAME = 'cloudflare-mcp-smart-proxy';
7
+ const DEFAULT_PACKAGE_BIN = 'cloudflare-mcp-proxy';
8
+ const MANAGED_BLOCK_PREFIX = '# BEGIN CLOUDMCP MANAGED MCP SERVER';
9
+ const MANAGED_BLOCK_SUFFIX = '# END CLOUDMCP MANAGED MCP SERVER';
10
+
11
+ function normalizeString(value, fallback = '') {
12
+ const normalized = typeof value === 'string' ? value.trim() : '';
13
+ return normalized || fallback;
14
+ }
15
+
16
+ function normalizeEnvValue(value) {
17
+ if (value == null) return '';
18
+ return String(value);
19
+ }
20
+
21
+ function sanitizeServerName(value) {
22
+ const candidate = normalizeString(value, DEFAULT_SERVER_NAME)
23
+ .replace(/[^a-zA-Z0-9_-]/g, '_');
24
+ return candidate || DEFAULT_SERVER_NAME;
25
+ }
26
+
27
+ function resolveHomeDir(homeDir = null) {
28
+ return homeDir || os.homedir();
29
+ }
30
+
31
+ function buildWorkspaceId(workspaceRoot) {
32
+ return `workspace.${path.basename(workspaceRoot || process.cwd()).replace(/[^a-zA-Z0-9_.-]/g, '_')}`;
33
+ }
34
+
35
+ function buildConnectorId(ecosystem, workspaceRoot) {
36
+ return `connector.${sanitizeServerName(ecosystem)}.${path.basename(workspaceRoot || process.cwd()).replace(/[^a-zA-Z0-9_.-]/g, '_')}`;
37
+ }
38
+
39
+ function ensureDir(targetPath) {
40
+ const dirPath = path.dirname(targetPath);
41
+ if (!fs.existsSync(dirPath)) {
42
+ fs.mkdirSync(dirPath, { recursive: true });
43
+ }
44
+ }
45
+
46
+ function readJson(filePath, fallback) {
47
+ try {
48
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
49
+ } catch {
50
+ return fallback;
51
+ }
52
+ }
53
+
54
+ function escapeTomlString(value) {
55
+ return String(value)
56
+ .replace(/\\/g, '\\\\')
57
+ .replace(/"/g, '\\"');
58
+ }
59
+
60
+ function formatTomlArray(values) {
61
+ return `[${values.map((value) => `"${escapeTomlString(value)}"`).join(', ')}]`;
62
+ }
63
+
64
+ function toManagedEnv({
65
+ cloudUrl,
66
+ cloudApiKey,
67
+ workspaceRoot,
68
+ clientProfileId,
69
+ connectorId,
70
+ connectorType,
71
+ workspaceId
72
+ }) {
73
+ const env = {
74
+ CLOUDFLARE_MCP_URL: normalizeEnvValue(cloudUrl),
75
+ CLOUDFLARE_MCP_API_KEY: normalizeEnvValue(cloudApiKey),
76
+ WORKSPACE_ROOT: normalizeEnvValue(workspaceRoot),
77
+ CLOUDMCP_CLIENT_PROFILE_ID: normalizeEnvValue(clientProfileId),
78
+ CLOUDMCP_CONNECTOR_ID: normalizeEnvValue(connectorId),
79
+ CLOUDMCP_CONNECTOR_TYPE: normalizeEnvValue(connectorType),
80
+ CLOUDMCP_WORKSPACE_ID: normalizeEnvValue(workspaceId)
81
+ };
82
+
83
+ return Object.fromEntries(
84
+ Object.entries(env).filter(([, value]) => value !== '')
85
+ );
86
+ }
87
+
88
+ export function resolveReferenceConnectorCommand({
89
+ packageRoot = null,
90
+ runtime = 'npm'
91
+ } = {}) {
92
+ if (runtime === 'npm') {
93
+ if (process.platform === 'win32') {
94
+ return {
95
+ command: 'cmd',
96
+ args: ['/c', 'npx', '-y', '-p', DEFAULT_PACKAGE_NAME, DEFAULT_PACKAGE_BIN]
97
+ };
98
+ }
99
+ return {
100
+ command: 'npx',
101
+ args: ['-y', '-p', DEFAULT_PACKAGE_NAME, DEFAULT_PACKAGE_BIN]
102
+ };
103
+ }
104
+
105
+ const resolvedPackageRoot = path.resolve(packageRoot || path.join(process.cwd(), 'local-proxy'));
106
+ return {
107
+ command: process.execPath,
108
+ args: [path.join(resolvedPackageRoot, 'index.js')]
109
+ };
110
+ }
111
+
112
+ export function buildReferenceConnectorServer({
113
+ ecosystem,
114
+ cloudUrl,
115
+ cloudApiKey,
116
+ workspaceRoot = process.cwd(),
117
+ clientProfileId,
118
+ connectorId = '',
119
+ connectorType = '',
120
+ workspaceId = '',
121
+ packageRoot = null,
122
+ runtime = 'npm'
123
+ }) {
124
+ const normalizedEcosystem = normalizeString(ecosystem).toLowerCase();
125
+ const resolvedWorkspaceRoot = path.resolve(workspaceRoot || process.cwd());
126
+ const resolvedConnectorType = normalizeString(connectorType, normalizedEcosystem || 'reference_connector');
127
+ const resolvedWorkspaceId = normalizeString(workspaceId, buildWorkspaceId(resolvedWorkspaceRoot));
128
+ const resolvedConnectorId = normalizeString(connectorId, buildConnectorId(normalizedEcosystem, resolvedWorkspaceRoot));
129
+ const commandDescriptor = resolveReferenceConnectorCommand({ packageRoot, runtime });
130
+
131
+ return {
132
+ type: 'stdio',
133
+ command: commandDescriptor.command,
134
+ args: commandDescriptor.args,
135
+ env: toManagedEnv({
136
+ cloudUrl,
137
+ cloudApiKey,
138
+ workspaceRoot: resolvedWorkspaceRoot,
139
+ clientProfileId,
140
+ connectorId: resolvedConnectorId,
141
+ connectorType: resolvedConnectorType,
142
+ workspaceId: resolvedWorkspaceId
143
+ }),
144
+ enabled: true,
145
+ connectorId: resolvedConnectorId,
146
+ connectorType: resolvedConnectorType,
147
+ workspaceId: resolvedWorkspaceId
148
+ };
149
+ }
150
+
151
+ export function getReferenceConnectorTarget({
152
+ ecosystem,
153
+ workspaceRoot = process.cwd(),
154
+ scope = 'user',
155
+ homeDir = null
156
+ }) {
157
+ const normalizedEcosystem = normalizeString(ecosystem).toLowerCase();
158
+ const normalizedScope = normalizeString(scope, 'user').toLowerCase();
159
+ const resolvedWorkspaceRoot = path.resolve(workspaceRoot || process.cwd());
160
+ const resolvedHomeDir = resolveHomeDir(homeDir);
161
+
162
+ if (normalizedEcosystem === 'codex') {
163
+ return {
164
+ ecosystem: normalizedEcosystem,
165
+ scope: 'user',
166
+ format: 'toml',
167
+ targetFile: path.join(resolvedHomeDir, '.codex', 'config.toml')
168
+ };
169
+ }
170
+
171
+ if (normalizedEcosystem === 'claude_code') {
172
+ if (normalizedScope === 'project') {
173
+ return {
174
+ ecosystem: normalizedEcosystem,
175
+ scope: 'project',
176
+ format: 'json',
177
+ targetFile: path.join(resolvedWorkspaceRoot, '.mcp.json')
178
+ };
179
+ }
180
+ return {
181
+ ecosystem: normalizedEcosystem,
182
+ scope: 'user',
183
+ format: 'json',
184
+ targetFile: path.join(resolvedHomeDir, '.claude.json')
185
+ };
186
+ }
187
+
188
+ throw new Error(`Unsupported reference connector ecosystem "${ecosystem}"`);
189
+ }
190
+
191
+ export function buildCodexManagedBlock(serverName, serverDefinition) {
192
+ const lines = [
193
+ `${MANAGED_BLOCK_PREFIX} ${serverName}`,
194
+ `[mcp_servers.${serverName}]`,
195
+ `command = "${escapeTomlString(serverDefinition.command)}"`,
196
+ `args = ${formatTomlArray(serverDefinition.args || [])}`,
197
+ `enabled = ${serverDefinition.enabled === false ? 'false' : 'true'}`
198
+ ];
199
+
200
+ const envEntries = Object.entries(serverDefinition.env || {});
201
+ if (envEntries.length > 0) {
202
+ lines.push('', `[mcp_servers.${serverName}.env]`);
203
+ for (const [key, value] of envEntries) {
204
+ lines.push(`${key} = "${escapeTomlString(value)}"`);
205
+ }
206
+ }
207
+
208
+ lines.push(MANAGED_BLOCK_SUFFIX);
209
+ return `${lines.join('\n')}\n`;
210
+ }
211
+
212
+ function replaceManagedBlock(existingContent, serverName, nextBlock) {
213
+ const startMarker = `${MANAGED_BLOCK_PREFIX} ${serverName}`;
214
+ const endMarker = MANAGED_BLOCK_SUFFIX;
215
+ const pattern = new RegExp(
216
+ `${startMarker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[\\s\\S]*?${endMarker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\n?`,
217
+ 'g'
218
+ );
219
+ const withoutPrevious = String(existingContent || '').replace(pattern, '').trimEnd();
220
+ if (!withoutPrevious) {
221
+ return nextBlock;
222
+ }
223
+ return `${withoutPrevious}\n\n${nextBlock}`;
224
+ }
225
+
226
+ export function mergeCodexConfig(existingContent, serverName, serverDefinition) {
227
+ return replaceManagedBlock(existingContent, serverName, buildCodexManagedBlock(serverName, serverDefinition));
228
+ }
229
+
230
+ export function mergeClaudeCodeConfig(existingConfig, serverName, serverDefinition) {
231
+ const nextConfig = existingConfig && typeof existingConfig === 'object' ? { ...existingConfig } : {};
232
+ nextConfig.mcpServers = nextConfig.mcpServers && typeof nextConfig.mcpServers === 'object'
233
+ ? { ...nextConfig.mcpServers }
234
+ : {};
235
+ nextConfig.mcpServers[serverName] = {
236
+ command: serverDefinition.command,
237
+ args: [...(serverDefinition.args || [])],
238
+ env: { ...(serverDefinition.env || {}) }
239
+ };
240
+ return nextConfig;
241
+ }
242
+
243
+ export function installReferenceConnector({
244
+ ecosystem,
245
+ cloudUrl,
246
+ cloudApiKey,
247
+ workspaceRoot = process.cwd(),
248
+ clientProfileId,
249
+ connectorId = '',
250
+ connectorType = '',
251
+ workspaceId = '',
252
+ packageRoot = null,
253
+ runtime = 'npm',
254
+ scope = 'user',
255
+ serverName = DEFAULT_SERVER_NAME,
256
+ homeDir = null,
257
+ dryRun = false
258
+ }) {
259
+ const normalizedServerName = sanitizeServerName(serverName);
260
+ const target = getReferenceConnectorTarget({
261
+ ecosystem,
262
+ workspaceRoot,
263
+ scope,
264
+ homeDir
265
+ });
266
+ const serverDefinition = buildReferenceConnectorServer({
267
+ ecosystem,
268
+ cloudUrl,
269
+ cloudApiKey,
270
+ workspaceRoot,
271
+ clientProfileId,
272
+ connectorId,
273
+ connectorType,
274
+ workspaceId,
275
+ packageRoot,
276
+ runtime
277
+ });
278
+
279
+ let renderedContent = '';
280
+ if (target.format === 'toml') {
281
+ const existingContent = fs.existsSync(target.targetFile)
282
+ ? fs.readFileSync(target.targetFile, 'utf8')
283
+ : '';
284
+ renderedContent = mergeCodexConfig(existingContent, normalizedServerName, serverDefinition);
285
+ } else {
286
+ const existingConfig = readJson(target.targetFile, {});
287
+ renderedContent = JSON.stringify(
288
+ mergeClaudeCodeConfig(existingConfig, normalizedServerName, serverDefinition),
289
+ null,
290
+ 2
291
+ );
292
+ }
293
+
294
+ if (!dryRun) {
295
+ ensureDir(target.targetFile);
296
+ fs.writeFileSync(target.targetFile, renderedContent, 'utf8');
297
+ }
298
+
299
+ return {
300
+ ecosystem: target.ecosystem,
301
+ scope: target.scope,
302
+ targetFile: target.targetFile,
303
+ serverName: normalizedServerName,
304
+ serverDefinition,
305
+ renderedContent,
306
+ written: !dryRun
307
+ };
308
+ }
309
+
310
+ export function printReferenceConnectorConfig(options) {
311
+ return installReferenceConnector({
312
+ ...options,
313
+ dryRun: true
314
+ });
315
+ }
package/src/router.js CHANGED
@@ -3,10 +3,12 @@
3
3
  */
4
4
 
5
5
  export class SmartRouter {
6
- constructor(cloudUrl, cloudApiKey, localTools) {
6
+ constructor(cloudUrl, cloudApiKey, localTools, workspaceRoot = null, deviceIdentity = null) {
7
7
  this.cloudUrl = cloudUrl.replace(/\/$/, ''); // 移除尾部斜杠
8
8
  this.cloudApiKey = cloudApiKey;
9
9
  this.localTools = localTools;
10
+ this.workspaceRoot = workspaceRoot || process.cwd();
11
+ this.deviceIdentity = deviceIdentity;
10
12
 
11
13
  // 工具路由规则
12
14
  this.routingRules = {
@@ -26,7 +28,12 @@ export class SmartRouter {
26
28
  'get_document_symbols',
27
29
  'replace_lines',
28
30
  'execute_command',
29
- 'execute_shell_command'
31
+ 'execute_shell_command',
32
+ 'connector_bridge_status',
33
+ 'connector_sync_profile',
34
+ 'connector_report_status',
35
+ 'connector_discover_project_probe',
36
+ 'connector_report_project_probe'
30
37
  ],
31
38
 
32
39
  // 云端工具(需要网络或云端资源)
@@ -100,26 +107,76 @@ export class SmartRouter {
100
107
  }
101
108
  }
102
109
 
110
+ /**
111
+ * 提取项目根路径(从工作区根路径)
112
+ * 返回项目根目录名,用于项目锚定
113
+ */
114
+ _extractProjectRootFromWorkspace() {
115
+ if (!this.workspaceRoot) {
116
+ return null;
117
+ }
118
+
119
+ // 规范化路径:统一使用正斜杠,移除尾部斜杠
120
+ let normalized = this.workspaceRoot.replace(/\\/g, '/').replace(/\/$/, '');
121
+
122
+ if (!normalized) {
123
+ return null;
124
+ }
125
+
126
+ // 提取最后一个目录名作为项目根路径
127
+ const parts = normalized.split('/').filter(p => p.length > 0);
128
+ return parts.length > 0 ? parts[parts.length - 1] : null;
129
+ }
130
+
103
131
  /**
104
132
  * 调用云端工具
105
133
  */
106
134
  async callCloudTool(toolName, params) {
135
+ // 对于 skill_* 工具,自动注入项目根路径信息
136
+ if (toolName.startsWith('skill_')) {
137
+ // 如果参数中没有 project_root 且没有 paths,自动添加 project_root
138
+ if (!params.project_root && (!params.paths || params.paths.length === 0)) {
139
+ const projectRoot = this._extractProjectRootFromWorkspace();
140
+ if (projectRoot) {
141
+ params.project_root = projectRoot;
142
+ }
143
+ }
144
+
145
+ // 如果参数中有 paths 但都是相对路径,尝试转换为相对于工作区根路径的路径
146
+ if (params.paths && Array.isArray(params.paths) && params.paths.length > 0) {
147
+ // 确保路径是相对于工作区根路径的
148
+ params.paths = params.paths.map(path => {
149
+ // 如果路径是绝对路径且在工作区根路径下,转换为相对路径
150
+ if (path.startsWith('/') && path.startsWith(this.workspaceRoot)) {
151
+ return path.replace(this.workspaceRoot + '/', '').replace(this.workspaceRoot, '');
152
+ }
153
+ // 如果路径已经是相对路径,保持不变
154
+ return path;
155
+ });
156
+ }
157
+ }
158
+
107
159
  try {
160
+ const body = JSON.stringify({
161
+ jsonrpc: '2.0',
162
+ id: Date.now(),
163
+ method: 'tools/call',
164
+ params: {
165
+ name: toolName,
166
+ arguments: params
167
+ }
168
+ });
169
+ const signedHeaders = this.deviceIdentity
170
+ ? await this.deviceIdentity.buildSignedHeaders({ method: 'POST', pathname: '/mcp', body })
171
+ : {};
108
172
  const response = await fetch(`${this.cloudUrl}/mcp`, {
109
173
  method: 'POST',
110
174
  headers: {
111
175
  'Authorization': `Bearer ${this.cloudApiKey}`,
112
- 'Content-Type': 'application/json'
176
+ 'Content-Type': 'application/json',
177
+ ...signedHeaders
113
178
  },
114
- body: JSON.stringify({
115
- jsonrpc: '2.0',
116
- id: Date.now(),
117
- method: 'tools/call',
118
- params: {
119
- name: toolName,
120
- arguments: params
121
- }
122
- })
179
+ body
123
180
  });
124
181
 
125
182
  if (!response.ok) {
@@ -217,18 +274,23 @@ export class SmartRouter {
217
274
  */
218
275
  async getCloudTools() {
219
276
  try {
277
+ const body = JSON.stringify({
278
+ jsonrpc: '2.0',
279
+ id: Date.now(),
280
+ method: 'tools/list',
281
+ params: {}
282
+ });
283
+ const signedHeaders = this.deviceIdentity
284
+ ? await this.deviceIdentity.buildSignedHeaders({ method: 'POST', pathname: '/mcp', body })
285
+ : {};
220
286
  const response = await fetch(`${this.cloudUrl}/mcp`, {
221
287
  method: 'POST',
222
288
  headers: {
223
289
  'Authorization': `Bearer ${this.cloudApiKey}`,
224
- 'Content-Type': 'application/json'
290
+ 'Content-Type': 'application/json',
291
+ ...signedHeaders
225
292
  },
226
- body: JSON.stringify({
227
- jsonrpc: '2.0',
228
- id: Date.now(),
229
- method: 'tools/list',
230
- params: {}
231
- })
293
+ body
232
294
  });
233
295
 
234
296
  if (!response.ok) {
@@ -248,4 +310,3 @@ export class SmartRouter {
248
310
  }
249
311
  }
250
312
  }
251
-