cloudflare-mcp-smart-proxy 1.5.6 → 1.5.8

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/connector-cli.js CHANGED
@@ -9,6 +9,7 @@ import { reloadCodexMcpServer } from './src/codex-app-server.js';
9
9
  import { installReferenceConnector, printReferenceConnectorConfig } from './src/reference-connectors.js';
10
10
  import { DeviceIdentity } from './src/device-identity.js';
11
11
  import { writeFileAtomically } from './src/reference-connectors.js';
12
+ import { discoverProjectIdentity } from './src/project-identity.js';
12
13
 
13
14
  const __filename = fileURLToPath(import.meta.url);
14
15
  const __dirname = path.dirname(__filename);
@@ -202,6 +203,7 @@ async function runActivation(options) {
202
203
  };
203
204
 
204
205
  const createApprovalRequest = async ({ replacesRequestId = '' } = {}) => {
206
+ const projectIdentity = await discoverProjectIdentity(paths.workspaceRoot);
205
207
  const requested = await activationFetch(`${paths.cloudUrl}/connectors/activation-requests`, {
206
208
  method: 'POST',
207
209
  headers: { 'Content-Type': 'application/json' },
@@ -211,6 +213,7 @@ async function runActivation(options) {
211
213
  publicKeyJwk: device.publicKeyJwk,
212
214
  deviceLabel: options['device-label'] || os.hostname(),
213
215
  workspaceLabel: path.basename(paths.workspaceRoot),
216
+ projectIdentity,
214
217
  connectorId: options['connector-id'] || defaultConnectorId(options.ecosystem, paths.workspaceRoot)
215
218
  })
216
219
  });
package/index.js CHANGED
@@ -38,11 +38,14 @@ function readManagedCredential() {
38
38
  const MANAGED_CREDENTIAL = readManagedCredential();
39
39
  const CLOUD_URL = MANAGED_CREDENTIAL?.cloudUrl || process.env.CLOUDFLARE_MCP_URL || process.env.MCP_URL;
40
40
  const CLOUD_API_KEY = MANAGED_CREDENTIAL?.apiKey || process.env.CLOUDFLARE_MCP_API_KEY || process.env.MCP_API_KEY;
41
- const WORKSPACE_ROOT = process.env.WORKSPACE_ROOT || process.cwd();
41
+ const FIXED_PROJECT_CONTEXT = process.env.CLOUDMCP_FIXED_PROJECT_CONTEXT === 'true';
42
+ const WORKSPACE_ROOT = FIXED_PROJECT_CONTEXT ? (process.env.WORKSPACE_ROOT || process.cwd()) : process.cwd();
42
43
  const CLIENT_PROFILE_ID = MANAGED_CREDENTIAL?.clientProfileId || process.env.CLOUDMCP_CLIENT_PROFILE_ID || process.env.CLIENT_PROFILE_ID || '';
43
44
  const CONNECTOR_ID = MANAGED_CREDENTIAL?.connectorId || process.env.CLOUDMCP_CONNECTOR_ID || process.env.CONNECTOR_ID || '';
44
45
  const CONNECTOR_TYPE = MANAGED_CREDENTIAL?.connectorType || process.env.CLOUDMCP_CONNECTOR_TYPE || process.env.CONNECTOR_TYPE || 'smart_proxy';
45
- const WORKSPACE_ID = MANAGED_CREDENTIAL?.workspaceId || process.env.CLOUDMCP_WORKSPACE_ID || process.env.WORKSPACE_ID || '';
46
+ const WORKSPACE_ID = FIXED_PROJECT_CONTEXT
47
+ ? (process.env.CLOUDMCP_WORKSPACE_ID || process.env.WORKSPACE_ID || '')
48
+ : '';
46
49
  const DEVICE_IDENTITY_PATH = process.env.CLOUDMCP_DEVICE_IDENTITY_PATH || '';
47
50
  const AUTO_SYNC_PROFILE = (process.env.CLOUDMCP_AUTO_SYNC_PROFILE || 'true') !== 'false';
48
51
  const AUTO_APPLY_BRAIN = (process.env.CLOUDMCP_AUTO_APPLY_BRAIN || 'true') !== 'false';
@@ -77,7 +80,14 @@ const memoryCoordinator = new LocalMemoryCoordinator({
77
80
  connectorId: CONNECTOR_ID
78
81
  });
79
82
  const localTools = new LocalToolExecutor(WORKSPACE_ROOT, connectorBridge, memoryCoordinator);
80
- const router = new SmartRouter(CLOUD_URL, CLOUD_API_KEY, localTools, WORKSPACE_ROOT, connectorBridge.deviceIdentity);
83
+ const router = new SmartRouter(
84
+ CLOUD_URL,
85
+ CLOUD_API_KEY,
86
+ localTools,
87
+ WORKSPACE_ROOT,
88
+ connectorBridge.deviceIdentity,
89
+ () => connectorBridge.getScopeHeaders()
90
+ );
81
91
  localTools.setCloudToolCaller((tool, params) => router.callCloudTool(tool, params));
82
92
 
83
93
  // 创建 MCP 服务器
@@ -163,7 +173,8 @@ server.setRequestHandler(ListPromptsRequestSchema, async () => {
163
173
  method: 'POST',
164
174
  headers: {
165
175
  'Authorization': `Bearer ${CLOUD_API_KEY}`,
166
- 'Content-Type': 'application/json'
176
+ 'Content-Type': 'application/json',
177
+ ...connectorBridge.getScopeHeaders()
167
178
  },
168
179
  body: JSON.stringify({
169
180
  jsonrpc: '2.0',
@@ -199,7 +210,8 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
199
210
  method: 'POST',
200
211
  headers: {
201
212
  'Authorization': `Bearer ${CLOUD_API_KEY}`,
202
- 'Content-Type': 'application/json'
213
+ 'Content-Type': 'application/json',
214
+ ...connectorBridge.getScopeHeaders()
203
215
  },
204
216
  body: JSON.stringify({
205
217
  jsonrpc: '2.0',
@@ -240,18 +252,22 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
240
252
  // 启动服务器
241
253
  async function main() {
242
254
  try {
243
- const transport = new StdioServerTransport();
244
- await server.connect(transport);
245
- console.error('Cloudflare MCP Smart Proxy started');
246
- console.error(`Workspace root: ${WORKSPACE_ROOT}`);
247
- console.error(`Cloud URL: ${CLOUD_URL}`);
255
+ let bridgeStatus = null;
248
256
  if (connectorBridge.isConfigured()) {
249
- const bridgeStatus = await connectorBridge.initialize({
257
+ bridgeStatus = await connectorBridge.initialize({
250
258
  autoApplyBrain: AUTO_APPLY_BRAIN,
251
259
  autoSyncProfile: AUTO_SYNC_PROFILE,
252
260
  autoReportProjectProbe: AUTO_REPORT_PROJECT_PROBE,
253
261
  autoGenerateContextPack: AUTO_GENERATE_CONTEXT_PACK
254
262
  });
263
+ memoryCoordinator.workspaceId = connectorBridge.workspaceId;
264
+ }
265
+ const transport = new StdioServerTransport();
266
+ await server.connect(transport);
267
+ console.error('Cloudflare MCP Smart Proxy started');
268
+ console.error(`Workspace root: ${WORKSPACE_ROOT}`);
269
+ console.error(`Cloud URL: ${CLOUD_URL}`);
270
+ if (bridgeStatus) {
255
271
  console.error(`Connector bridge profile: ${bridgeStatus.clientProfileId}`);
256
272
  console.error(`Connector bridge workspace: ${bridgeStatus.workspaceId}`);
257
273
  const applyResult = bridgeStatus.state?.lastBrainApplyResult;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudflare-mcp-smart-proxy",
3
- "version": "1.5.6",
3
+ "version": "1.5.8",
4
4
  "description": "Smart proxy for Cloudflare MCP - routes tools to cloud or local execution",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,10 +1,11 @@
1
1
  const CONNECTOR_CONTRACT_VERSION = 'a2.v1';
2
2
 
3
3
  export class CloudClient {
4
- constructor({ cloudUrl, cloudApiKey, deviceIdentity = null }) {
4
+ constructor({ cloudUrl, cloudApiKey, deviceIdentity = null, getScopeHeaders = null }) {
5
5
  this.cloudUrl = String(cloudUrl || '').replace(/\/$/, '');
6
6
  this.cloudApiKey = cloudApiKey || '';
7
7
  this.deviceIdentity = deviceIdentity;
8
+ this.getScopeHeaders = typeof getScopeHeaders === 'function' ? getScopeHeaders : () => ({});
8
9
  if (!this.cloudUrl) {
9
10
  throw new Error('cloudUrl is required');
10
11
  }
@@ -42,6 +43,7 @@ export class CloudClient {
42
43
  'Authorization': `Bearer ${this.cloudApiKey}`,
43
44
  'Content-Type': 'application/json',
44
45
  'X-CloudMCP-Connector-Contract-Version': CONNECTOR_CONTRACT_VERSION,
46
+ ...this.getScopeHeaders(),
45
47
  ...signedHeaders,
46
48
  ...(idempotencyKey ? { 'X-Idempotency-Key': idempotencyKey } : {})
47
49
  },
@@ -73,6 +75,13 @@ export class CloudClient {
73
75
  });
74
76
  }
75
77
 
78
+ async resolveWorkspace({ projectIdentity }) {
79
+ return this.request('/connectors/workspace-resolution', {
80
+ method: 'POST',
81
+ body: { projectIdentity }
82
+ });
83
+ }
84
+
76
85
  async listConnectorStatusReports({ clientProfileId, workspaceId, connectorId, limit = 5 }) {
77
86
  return this.request('/connectors/status-reports', {
78
87
  query: { clientProfileId, workspaceId, connectorId, limit }
@@ -1,9 +1,9 @@
1
1
  import os from 'os';
2
- import path from 'path';
3
2
  import { CloudClient } from './cloud-client.js';
4
3
  import { DeviceIdentity } from './device-identity.js';
5
4
  import { discoverProjectProbe } from './project-probe-discovery.js';
6
5
  import { detectIde, applyBrainSnapshot } from './ide-configurator.js';
6
+ import { discoverProjectIdentity } from './project-identity.js';
7
7
 
8
8
  function normalizeString(value, fallback = '') {
9
9
  const normalized = typeof value === 'string' ? value.trim() : '';
@@ -14,10 +14,6 @@ function defaultConnectorId() {
14
14
  return `connector.${os.hostname().replace(/[^a-zA-Z0-9_.-]/g, '_')}.local`;
15
15
  }
16
16
 
17
- function defaultWorkspaceId(workspaceRoot) {
18
- return `workspace.${path.basename(workspaceRoot || process.cwd()).replace(/[^a-zA-Z0-9_.-]/g, '_')}`;
19
- }
20
-
21
17
  function buildIdempotencyKey(...parts) {
22
18
  return parts
23
19
  .map((entry) => normalizeString(entry))
@@ -42,12 +38,13 @@ export class ConnectorBridge {
42
38
  this.clientProfileId = normalizeString(clientProfileId);
43
39
  this.connectorId = normalizeString(connectorId, defaultConnectorId());
44
40
  this.connectorType = normalizeString(connectorType, 'smart_proxy');
45
- this.workspaceId = normalizeString(workspaceId, defaultWorkspaceId(this.workspaceRoot));
41
+ this.workspaceId = normalizeString(workspaceId);
46
42
  this.deviceIdentity = deviceIdentity || new DeviceIdentity({ identityPath: deviceIdentityPath });
47
43
  this.cloudClient = cloudClient || new CloudClient({
48
44
  cloudUrl,
49
45
  cloudApiKey,
50
- deviceIdentity: this.deviceIdentity
46
+ deviceIdentity: this.deviceIdentity,
47
+ getScopeHeaders: () => this.getScopeHeaders()
51
48
  });
52
49
  this.state = {
53
50
  initializedAt: Date.now(),
@@ -66,6 +63,25 @@ export class ConnectorBridge {
66
63
  return Boolean(this.clientProfileId);
67
64
  }
68
65
 
66
+ getScopeHeaders() {
67
+ return {
68
+ ...(this.workspaceId ? { 'X-CloudMCP-Workspace-ID': this.workspaceId } : {}),
69
+ ...(this.clientProfileId ? { 'X-CloudMCP-Client-Profile-ID': this.clientProfileId } : {}),
70
+ ...(this.connectorId ? { 'X-CloudMCP-Connector-ID': this.connectorId } : {})
71
+ };
72
+ }
73
+
74
+ async resolveCurrentWorkspace() {
75
+ const projectIdentity = await discoverProjectIdentity(this.workspaceRoot);
76
+ const response = await this.cloudClient.resolveWorkspace({ projectIdentity });
77
+ const resolution = response?.workspaceResolution;
78
+ if (!resolution) throw new Error('CloudMCP returned no workspace resolution result');
79
+ this.workspaceId = normalizeString(resolution.workspaceId);
80
+ this.state.projectIdentity = projectIdentity;
81
+ this.state.workspaceResolution = resolution;
82
+ return resolution;
83
+ }
84
+
69
85
  getBridgeStatus() {
70
86
  return {
71
87
  objectType: 'connector_bridge_status',
@@ -89,6 +105,8 @@ export class ConnectorBridge {
89
105
  return this.getBridgeStatus();
90
106
  }
91
107
 
108
+ const workspaceResolution = await this.resolveCurrentWorkspace();
109
+ if (!workspaceResolution.workspaceId) return this.getBridgeStatus();
92
110
  try {
93
111
  if (typeof this.cloudClient?.registerDevice === 'function') {
94
112
  await this.ensureDeviceRegistration();
@@ -0,0 +1,75 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+
4
+ function normalizeString(value, fallback = '') {
5
+ const normalized = typeof value === 'string' ? value.trim() : '';
6
+ return normalized || fallback;
7
+ }
8
+
9
+ function normalizeAlias(value) {
10
+ return normalizeString(value)
11
+ .toLowerCase()
12
+ .replace(/\.git$/i, '')
13
+ .replace(/\/+$/, '');
14
+ }
15
+
16
+ function normalizeGitRemote(value) {
17
+ const remote = normalizeString(value);
18
+ if (!remote) return '';
19
+ const scpMatch = remote.match(/^(?:[^@]+@)?([^:]+):(.+)$/);
20
+ if (scpMatch && !remote.includes('://')) {
21
+ return normalizeAlias(`${scpMatch[1]}/${scpMatch[2]}`);
22
+ }
23
+ try {
24
+ const parsed = new URL(remote);
25
+ return normalizeAlias(`${parsed.hostname}${parsed.pathname}`);
26
+ } catch {
27
+ return normalizeAlias(remote);
28
+ }
29
+ }
30
+
31
+ async function readText(filePath) {
32
+ try {
33
+ return await fs.readFile(filePath, 'utf8');
34
+ } catch {
35
+ return '';
36
+ }
37
+ }
38
+
39
+ async function readPackageName(repoRoot) {
40
+ try {
41
+ const payload = JSON.parse(await fs.readFile(path.join(repoRoot, 'package.json'), 'utf8'));
42
+ return normalizeAlias(payload?.name);
43
+ } catch {
44
+ return '';
45
+ }
46
+ }
47
+
48
+ async function readGitRemote(repoRoot) {
49
+ const gitConfig = await readText(path.join(repoRoot, '.git', 'config'));
50
+ const originSection = gitConfig.match(/\[remote\s+"origin"\]([\s\S]*?)(?=\n\[|$)/i)?.[1] || '';
51
+ const originUrl = originSection.match(/^\s*url\s*=\s*(.+)$/mi)?.[1] || '';
52
+ return normalizeGitRemote(originUrl);
53
+ }
54
+
55
+ export async function discoverProjectIdentity(workspaceRoot = process.cwd()) {
56
+ const repoRoot = path.resolve(workspaceRoot || process.cwd());
57
+ const repoName = path.basename(repoRoot);
58
+ const [gitRemote, packageName] = await Promise.all([
59
+ readGitRemote(repoRoot),
60
+ readPackageName(repoRoot)
61
+ ]);
62
+ const aliases = Array.from(new Set([
63
+ gitRemote ? `git:${gitRemote}` : '',
64
+ packageName ? `package:${packageName}` : '',
65
+ `repo:${normalizeAlias(repoName)}`
66
+ ].filter(Boolean)));
67
+
68
+ return {
69
+ canonicalId: aliases[0],
70
+ aliases,
71
+ repoName,
72
+ gitRemote: gitRemote || null,
73
+ packageName: packageName || null
74
+ };
75
+ }
@@ -100,7 +100,8 @@ function toManagedEnv({
100
100
  connectorType,
101
101
  workspaceId,
102
102
  deviceIdentityPath,
103
- codexConfigPath
103
+ codexConfigPath,
104
+ fixedProjectContext = false
104
105
  }) {
105
106
  const env = {
106
107
  CLOUDFLARE_MCP_URL: normalizeEnvValue(cloudUrl),
@@ -112,7 +113,8 @@ function toManagedEnv({
112
113
  CLOUDMCP_CONNECTOR_TYPE: normalizeEnvValue(connectorType),
113
114
  CLOUDMCP_WORKSPACE_ID: normalizeEnvValue(workspaceId),
114
115
  CLOUDMCP_DEVICE_IDENTITY_PATH: normalizeEnvValue(deviceIdentityPath),
115
- CODEX_SHARED_CONFIG_PATH: normalizeEnvValue(codexConfigPath)
116
+ CODEX_SHARED_CONFIG_PATH: normalizeEnvValue(codexConfigPath),
117
+ CLOUDMCP_FIXED_PROJECT_CONTEXT: fixedProjectContext ? 'true' : ''
116
118
  };
117
119
 
118
120
  return Object.fromEntries(
@@ -156,6 +158,7 @@ export function buildReferenceConnectorServer({
156
158
  workspaceId = '',
157
159
  deviceIdentityPath = '',
158
160
  codexConfigPath = '',
161
+ fixedProjectContext = true,
159
162
  packageRoot = null,
160
163
  runtime = 'npm'
161
164
  }) {
@@ -174,13 +177,14 @@ export function buildReferenceConnectorServer({
174
177
  cloudUrl,
175
178
  cloudApiKey,
176
179
  credentialPath,
177
- workspaceRoot: resolvedWorkspaceRoot,
180
+ workspaceRoot: fixedProjectContext ? resolvedWorkspaceRoot : '',
178
181
  clientProfileId,
179
182
  connectorId: resolvedConnectorId,
180
183
  connectorType: resolvedConnectorType,
181
- workspaceId: resolvedWorkspaceId,
184
+ workspaceId: fixedProjectContext ? resolvedWorkspaceId : '',
182
185
  deviceIdentityPath,
183
- codexConfigPath: normalizedEcosystem === 'codex' ? codexConfigPath : ''
186
+ codexConfigPath: normalizedEcosystem === 'codex' ? codexConfigPath : '',
187
+ fixedProjectContext
184
188
  }),
185
189
  enabled: true,
186
190
  startupTimeoutSec: normalizedEcosystem === 'codex' ? DEFAULT_CODEX_STARTUP_TIMEOUT_SEC : null,
@@ -358,6 +362,7 @@ export function installReferenceConnector({
358
362
  workspaceId,
359
363
  deviceIdentityPath,
360
364
  codexConfigPath: target.ecosystem === 'codex' ? target.targetFile : '',
365
+ fixedProjectContext: target.scope === 'project',
361
366
  packageRoot,
362
367
  runtime
363
368
  });
package/src/router.js CHANGED
@@ -38,12 +38,13 @@ export function sanitizeProxyError(error, params = {}) {
38
38
  }
39
39
 
40
40
  export class SmartRouter {
41
- constructor(cloudUrl, cloudApiKey, localTools, workspaceRoot = null, deviceIdentity = null) {
41
+ constructor(cloudUrl, cloudApiKey, localTools, workspaceRoot = null, deviceIdentity = null, getScopeHeaders = null) {
42
42
  this.cloudUrl = cloudUrl.replace(/\/$/, ''); // 移除尾部斜杠
43
43
  this.cloudApiKey = cloudApiKey;
44
44
  this.localTools = localTools;
45
45
  this.workspaceRoot = workspaceRoot || process.cwd();
46
46
  this.deviceIdentity = deviceIdentity;
47
+ this.getScopeHeaders = typeof getScopeHeaders === 'function' ? getScopeHeaders : () => ({});
47
48
 
48
49
  // 工具路由规则
49
50
  this.routingRules = {
@@ -135,11 +136,25 @@ export class SmartRouter {
135
136
  return 'cloud';
136
137
  }
137
138
 
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
+
138
145
  /**
139
146
  * 执行工具调用
140
147
  */
141
148
  async executeTool(toolName, params) {
142
149
  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
+ }
143
158
 
144
159
  if (route === 'local') {
145
160
  return await this.localTools.execute(toolName, params);
@@ -215,6 +230,7 @@ export class SmartRouter {
215
230
  headers: {
216
231
  'Authorization': `Bearer ${this.cloudApiKey}`,
217
232
  'Content-Type': 'application/json',
233
+ ...this.getScopeHeaders(),
218
234
  ...signedHeaders
219
235
  },
220
236
  body
@@ -286,6 +302,14 @@ export class SmartRouter {
286
302
  * 获取所有工具列表(合并云端和本地)
287
303
  */
288
304
  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
+ }
289
313
  const [cloudTools, localTools] = await Promise.all([
290
314
  this.getCloudTools(),
291
315
  this.localTools.listTools()
@@ -325,6 +349,7 @@ export class SmartRouter {
325
349
  headers: {
326
350
  'Authorization': `Bearer ${this.cloudApiKey}`,
327
351
  'Content-Type': 'application/json',
352
+ ...this.getScopeHeaders(),
328
353
  ...signedHeaders
329
354
  },
330
355
  body