cloudflare-mcp-smart-proxy 1.5.9 → 1.5.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudflare-mcp-smart-proxy",
3
- "version": "1.5.9",
3
+ "version": "1.5.10",
4
4
  "description": "Smart proxy for Cloudflare MCP - routes tools to cloud or local execution",
5
5
  "repository": {
6
6
  "type": "git",
@@ -40,7 +40,12 @@ function normalizeStringArray(value, limit = 32) {
40
40
  }
41
41
 
42
42
  export function assertNoSensitiveMemory(input = {}) {
43
- const text = [input.title, input.content, ...(input.evidenceRefs || input.evidence_refs || [])]
43
+ const text = [
44
+ input.title,
45
+ input.content,
46
+ ...(input.tags || []),
47
+ ...(input.evidenceRefs || input.evidence_refs || [])
48
+ ]
44
49
  .filter(Boolean).join('\n');
45
50
  if (SENSITIVE_MEMORY_PATTERNS.some((pattern) => pattern.test(text))) {
46
51
  throw new Error('Memory content appears to contain a secret or approval credential');
@@ -44,19 +44,40 @@ function ensureDir(targetPath) {
44
44
  }
45
45
  }
46
46
 
47
- function resolveAtomicWriteTarget(filePath) {
47
+ function isPathWithin(rootPath, candidatePath) {
48
+ const relative = path.relative(rootPath, candidatePath);
49
+ return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));
50
+ }
51
+
52
+ function resolveAtomicWriteTarget(filePath, { allowedRoot = null, followFinalSymlink = true } = {}) {
53
+ const resolvedFilePath = path.resolve(filePath);
54
+ const resolvedAllowedRoot = allowedRoot ? fs.realpathSync(path.resolve(allowedRoot)) : null;
55
+ if (resolvedAllowedRoot) {
56
+ const physicalParent = fs.realpathSync(path.dirname(resolvedFilePath));
57
+ if (!isPathWithin(resolvedAllowedRoot, physicalParent)) {
58
+ throw new Error(`Connector configuration target escapes the selected workspace: ${filePath}`);
59
+ }
60
+ }
61
+
48
62
  try {
49
- if (fs.lstatSync(filePath).isSymbolicLink()) {
50
- return fs.realpathSync(filePath);
63
+ if (fs.lstatSync(resolvedFilePath).isSymbolicLink()) {
64
+ if (!followFinalSymlink) {
65
+ throw new Error(`Connector configuration target must not be a symbolic link: ${filePath}`);
66
+ }
67
+ const physicalTarget = fs.realpathSync(resolvedFilePath);
68
+ if (resolvedAllowedRoot && !isPathWithin(resolvedAllowedRoot, physicalTarget)) {
69
+ throw new Error(`Connector configuration target escapes the selected workspace: ${filePath}`);
70
+ }
71
+ return physicalTarget;
51
72
  }
52
73
  } catch (error) {
53
74
  if (error?.code !== 'ENOENT') throw error;
54
75
  }
55
- return filePath;
76
+ return resolvedFilePath;
56
77
  }
57
78
 
58
- export function writeFileAtomically(filePath, content) {
59
- const writeTarget = resolveAtomicWriteTarget(filePath);
79
+ export function writeFileAtomically(filePath, content, options = {}) {
80
+ const writeTarget = resolveAtomicWriteTarget(filePath, options);
60
81
  ensureDir(writeTarget);
61
82
  const temporaryPath = `${writeTarget}.${process.pid}.${Date.now()}.tmp`;
62
83
  try {
@@ -65,6 +86,7 @@ export function writeFileAtomically(filePath, content) {
65
86
  flag: 'wx',
66
87
  mode: 0o600
67
88
  });
89
+ resolveAtomicWriteTarget(filePath, options);
68
90
  fs.renameSync(temporaryPath, writeTarget);
69
91
  } catch (error) {
70
92
  if (fs.existsSync(temporaryPath)) fs.unlinkSync(temporaryPath);
@@ -367,6 +389,13 @@ export function installReferenceConnector({
367
389
  runtime
368
390
  });
369
391
 
392
+ const writeOptions = target.scope === 'project'
393
+ ? { allowedRoot: workspaceRoot, followFinalSymlink: false }
394
+ : {};
395
+ if (target.scope === 'project') {
396
+ resolveAtomicWriteTarget(target.targetFile, writeOptions);
397
+ }
398
+
370
399
  let renderedContent = '';
371
400
  if (target.format === 'toml') {
372
401
  const existingContent = fs.existsSync(target.targetFile)
@@ -383,7 +412,7 @@ export function installReferenceConnector({
383
412
  }
384
413
 
385
414
  if (!dryRun) {
386
- writeFileAtomically(target.targetFile, renderedContent);
415
+ writeFileAtomically(target.targetFile, renderedContent, writeOptions);
387
416
  }
388
417
 
389
418
  return {
package/src/router.js CHANGED
@@ -45,7 +45,6 @@ export class SmartRouter {
45
45
  this.workspaceRoot = workspaceRoot || process.cwd();
46
46
  this.deviceIdentity = deviceIdentity;
47
47
  this.getScopeHeaders = typeof getScopeHeaders === 'function' ? getScopeHeaders : () => ({});
48
-
49
48
  // 工具路由规则
50
49
  this.routingRules = {
51
50
  // 本地工具(需要文件系统访问)
@@ -136,30 +135,22 @@ export class SmartRouter {
136
135
  return 'cloud';
137
136
  }
138
137
 
139
- getWorkspaceWarnings() {
140
- const resolution = this.localTools?.connectorBridge?.state?.workspaceResolution;
141
- if (resolution?.workspaceId) return [];
142
- return Array.isArray(resolution?.warnings) ? resolution.warnings : [];
143
- }
144
-
145
138
  /**
146
139
  * 执行工具调用
147
140
  */
148
141
  async executeTool(toolName, params) {
149
142
  const route = this.routeTool(toolName);
150
- const workspaceWarnings = this.getWorkspaceWarnings();
151
- if (route === 'cloud' && workspaceWarnings.length) {
152
- return {
153
- objectType: 'connector_workspace_warning',
154
- executed: false,
155
- warnings: workspaceWarnings
156
- };
157
- }
158
-
143
+
159
144
  if (route === 'local') {
160
145
  return await this.localTools.execute(toolName, params);
161
146
  } else {
162
- return await this.callCloudTool(toolName, params);
147
+ const recoveryParams = toolName === 'request_project_workspace_registration'
148
+ ? {
149
+ ...params,
150
+ projectIdentity: this.localTools?.connectorBridge?.state?.projectIdentity
151
+ }
152
+ : params;
153
+ return await this.callCloudTool(toolName, recoveryParams);
163
154
  }
164
155
  }
165
156
 
@@ -187,7 +178,7 @@ export class SmartRouter {
187
178
  /**
188
179
  * 调用云端工具
189
180
  */
190
- async callCloudTool(toolName, params) {
181
+ async callCloudTool(toolName, params, { workspaceId = null } = {}) {
191
182
  // 对于 skill_* 工具,自动注入项目根路径信息
192
183
  if (toolName.startsWith('skill_')) {
193
184
  // 如果参数中没有 project_root 且没有 paths,自动添加 project_root
@@ -231,6 +222,7 @@ export class SmartRouter {
231
222
  'Authorization': `Bearer ${this.cloudApiKey}`,
232
223
  'Content-Type': 'application/json',
233
224
  ...this.getScopeHeaders(),
225
+ ...(workspaceId ? { 'X-CloudMCP-Workspace-ID': workspaceId } : {}),
234
226
  ...signedHeaders
235
227
  },
236
228
  body
@@ -302,14 +294,6 @@ export class SmartRouter {
302
294
  * 获取所有工具列表(合并云端和本地)
303
295
  */
304
296
  async getAllTools() {
305
- const workspaceWarnings = this.getWorkspaceWarnings();
306
- if (workspaceWarnings.length) {
307
- const localTools = await this.localTools.listTools();
308
- const recoveryTools = new Set(['connector_bridge_status', 'connector_discover_project_probe']);
309
- return localTools
310
- .filter((tool) => recoveryTools.has(tool.name))
311
- .map((tool) => ({ ...tool, source: 'local' }));
312
- }
313
297
  const [cloudTools, localTools] = await Promise.all([
314
298
  this.getCloudTools(),
315
299
  this.localTools.listTools()
@@ -334,7 +318,7 @@ export class SmartRouter {
334
318
  /**
335
319
  * 获取云端工具列表
336
320
  */
337
- async getCloudTools() {
321
+ async getCloudTools({ workspaceId = null } = {}) {
338
322
  const body = JSON.stringify({
339
323
  jsonrpc: '2.0',
340
324
  id: Date.now(),
@@ -350,6 +334,7 @@ export class SmartRouter {
350
334
  'Authorization': `Bearer ${this.cloudApiKey}`,
351
335
  'Content-Type': 'application/json',
352
336
  ...this.getScopeHeaders(),
337
+ ...(workspaceId ? { 'X-CloudMCP-Workspace-ID': workspaceId } : {}),
353
338
  ...signedHeaders
354
339
  },
355
340
  body