cloudflare-mcp-smart-proxy 1.4.2 → 1.5.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.
package/README.md CHANGED
@@ -37,25 +37,23 @@ node index.js
37
37
 
38
38
  当前已发布包名与可执行名不同,因此正式入口需要显式指定包与命令。仓库内入口只保留给本地开发调试,通过 `--runtime local` 显式选择。
39
39
 
40
- ### 2. 作为参考连接器安装器运行
40
+ ### 2. 经过审批后自动安装(正式主线)
41
41
 
42
42
  ```bash
43
- cd /home/coder/project/CLOUDMCP/local-proxy
44
- node connector-cli.js install codex \
45
- --cloud-url https://your-cloudmcp.example.com \
46
- --api-key your_api_key \
47
- --client-profile-id client_profile.codex.default
43
+ npx -y -p cloudflare-mcp-smart-proxy cloudmcp-connector activate codex \
44
+ --cloud-url https://your-cloudmcp.example.com
48
45
  ```
49
46
 
47
+ 第一次运行会生成设备密钥并发起接入申请,不会取得任何租户配置或凭证。管理员在 CloudMCP 后台的“生态连接中心”选择精确生态与工作区并批准后,将一次性审批密令回传给智能体。智能体随后运行:
48
+
50
49
  ```bash
51
- cd /home/coder/project/CLOUDMCP/local-proxy
52
- node connector-cli.js install claude_code \
53
- --scope project \
54
- --cloud-url https://your-cloudmcp.example.com \
55
- --api-key your_api_key \
56
- --client-profile-id client_profile.claude_code.default
50
+ CLOUDMCP_APPROVAL_CODE='<一次性审批密令>' \
51
+ npx -y -p cloudflare-mcp-smart-proxy cloudmcp-connector activate codex \
52
+ --cloud-url https://your-cloudmcp.example.com
57
53
  ```
58
54
 
55
+ 审批密令 10 分钟内有效、只能成功领取一次,并且只能由原申请设备签名领取。连接凭证写入权限为 `0600` 的独立文件,IDE 配置只保存该文件路径。CloudMCP 的机器可发现入口为 `/.well-known/cloudmcp`。
56
+
59
57
  ## 目标配置文件
60
58
 
61
59
  ### Codex
@@ -74,8 +72,7 @@ node connector-cli.js install claude_code \
74
72
 
75
73
  ## 默认注入环境变量
76
74
 
77
- - `CLOUDFLARE_MCP_URL`
78
- - `CLOUDFLARE_MCP_API_KEY`
75
+ - `CLOUDMCP_CREDENTIAL_PATH`
79
76
  - `WORKSPACE_ROOT`
80
77
  - `CLOUDMCP_CLIENT_PROFILE_ID`
81
78
  - `CLOUDMCP_CONNECTOR_ID`
@@ -83,7 +80,7 @@ node connector-cli.js install claude_code \
83
80
  - `CLOUDMCP_WORKSPACE_ID`
84
81
  - `CLOUDMCP_DEVICE_IDENTITY_PATH`
85
82
 
86
- 这些字段会让连接器启动后直接进入:
83
+ 凭证文件保存服务 URL、设备绑定 API Key 和批准后的精确范围;这些字段会让连接器启动后直接进入:
87
84
 
88
85
  - `PUT /connectors/workspace-binding`
89
86
  - `GET /connectors/install-plan`
@@ -131,5 +128,6 @@ node --test /home/coder/project/CLOUDMCP/tests/reference-connector-a3.test.js \
131
128
 
132
129
  ## 备注
133
130
 
131
+ - `install --api-key` 只保留给已经绑定设备的旧连接器迁移;未绑定 API Key 无法注册新设备。
134
132
  - 旧的 Cursor / Claude Desktop 入口仍保留兼容,但它们不再是 A3 主线。
135
133
  - A3 没有把 `systemPrompt` 直接写进项目根级 `AGENTS.md` 或 `CLAUDE.md`,以避免覆盖用户已有 agent 规范文件。
package/connector-cli.js CHANGED
@@ -1,17 +1,26 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import fs from 'fs';
4
+ import os from 'os';
3
5
  import path from 'path';
6
+ import { createHash } from 'crypto';
4
7
  import { fileURLToPath } from 'url';
5
8
  import { reloadCodexMcpServer } from './src/codex-app-server.js';
6
9
  import { installReferenceConnector, printReferenceConnectorConfig } from './src/reference-connectors.js';
10
+ import { DeviceIdentity } from './src/device-identity.js';
11
+ import { writeFileAtomically } from './src/reference-connectors.js';
7
12
 
8
13
  const __filename = fileURLToPath(import.meta.url);
9
14
  const __dirname = path.dirname(__filename);
10
15
 
11
16
  function printUsage() {
12
- console.error('Usage: cloudmcp-connector <install|print|smoke|reload> <codex|claude_code> [options]');
17
+ console.error('Usage: cloudmcp-connector <activate|install|print|smoke|reload> <codex|claude_code> [options]');
13
18
  console.error('');
14
- console.error('Required options for install, print, and smoke:');
19
+ console.error('Required option for activate:');
20
+ console.error(' --cloud-url <url>');
21
+ console.error(' First run creates an approval request; rerun with --approval-code after admin approval.');
22
+ console.error('');
23
+ console.error('Required options for legacy install, print, and smoke:');
15
24
  console.error(' --cloud-url <url>');
16
25
  console.error(' --api-key <key>');
17
26
  console.error(' --client-profile-id <id>');
@@ -30,6 +39,7 @@ function printUsage() {
30
39
  console.error(' --runtime-id <id> Current Codex runtime identity');
31
40
  console.error(' --dry-run Show output without writing');
32
41
  console.error(' --show-secrets Explicitly include credentials in print output');
42
+ console.error(' --approval-code <code> One-time code shown by the CloudMCP administrator');
33
43
  console.error('');
34
44
  console.error('The reload codex command uses the existing Codex config and requires no credentials.');
35
45
  }
@@ -96,6 +106,162 @@ function buildInstallOptions(options) {
96
106
  };
97
107
  }
98
108
 
109
+ function activationPaths(options) {
110
+ const homeDir = os.homedir();
111
+ const workspaceRoot = path.resolve(options['workspace-root'] || process.cwd());
112
+ const cloudUrl = resolveRequiredOption(options, 'cloud-url', ['CLOUDFLARE_MCP_URL', 'MCP_URL']).replace(/\/$/, '');
113
+ const fingerprint = createHash('sha256')
114
+ .update(`${cloudUrl}\n${options.ecosystem}\n${workspaceRoot}`)
115
+ .digest('hex')
116
+ .slice(0, 20);
117
+ const deviceIdentityPath = options['codex-device-identity-path']
118
+ || (options.ecosystem === 'codex'
119
+ ? path.join(homeDir, '.codex', 'cloudmcp', 'device-identity.json')
120
+ : path.join(homeDir, '.cloudmcp', 'device-identity.json'));
121
+ return {
122
+ cloudUrl,
123
+ workspaceRoot,
124
+ deviceIdentityPath,
125
+ pendingPath: path.join(homeDir, '.cloudmcp', 'activation-requests', `${fingerprint}.json`),
126
+ credentialPath: path.join(homeDir, '.cloudmcp', 'credentials', `${fingerprint}.json`)
127
+ };
128
+ }
129
+
130
+ function defaultConnectorId(ecosystem, workspaceRoot) {
131
+ const workspaceName = path.basename(workspaceRoot).replace(/[^a-zA-Z0-9_.-]/g, '_');
132
+ return `connector.${ecosystem}.${workspaceName}`;
133
+ }
134
+
135
+ async function activationFetch(url, options = {}) {
136
+ const response = await fetch(url, options);
137
+ const data = await response.json().catch(() => ({}));
138
+ if (!response.ok) throw new Error(data.error || `${response.status} ${response.statusText}`);
139
+ return data;
140
+ }
141
+
142
+ async function runActivation(options) {
143
+ const paths = activationPaths(options);
144
+ const identity = new DeviceIdentity({ identityPath: paths.deviceIdentityPath });
145
+ const device = await identity.loadOrCreate();
146
+ const approvalCode = options['approval-code'] || process.env.CLOUDMCP_APPROVAL_CODE || '';
147
+ let pending = null;
148
+ try {
149
+ pending = JSON.parse(fs.readFileSync(paths.pendingPath, 'utf8'));
150
+ } catch (error) {
151
+ if (error?.code !== 'ENOENT') throw error;
152
+ }
153
+
154
+ const installClaimedConnection = async (connection) => {
155
+ const result = installReferenceConnector({
156
+ ecosystem: options.ecosystem,
157
+ cloudUrl: connection.cloudUrl,
158
+ cloudApiKey: '',
159
+ credentialPath: paths.credentialPath,
160
+ clientProfileId: connection.clientProfileId,
161
+ workspaceId: connection.workspaceId,
162
+ connectorId: connection.connectorId,
163
+ connectorType: connection.connectorType,
164
+ workspaceRoot: paths.workspaceRoot,
165
+ scope: options.scope || 'user',
166
+ serverName: options['server-name'] || 'cloudmcp',
167
+ runtime: options.runtime || 'npm',
168
+ codexConfigPath: options['codex-config-path'] || process.env.CODEX_SHARED_CONFIG_PATH || '',
169
+ codexDeviceIdentityPath: paths.deviceIdentityPath,
170
+ packageRoot: __dirname,
171
+ dryRun: options.dryRun === true
172
+ });
173
+ if (options.ecosystem === 'codex' && !options.dryRun) {
174
+ result.activation = await reloadCodexMcpServer({
175
+ serverName: result.serverName,
176
+ socketPath: options['app-server-socket'] || process.env.CODEX_APP_SERVER_SOCKET || '',
177
+ runtimeId: options['runtime-id'] || process.env.CODEX_RUNTIME_ID || ''
178
+ });
179
+ }
180
+ if (!options.dryRun) fs.chmodSync(paths.credentialPath, 0o600);
181
+ if (!options.dryRun && pending) {
182
+ writeFileAtomically(paths.pendingPath, `${JSON.stringify({
183
+ requestId: pending.requestId,
184
+ cloudUrl: pending.cloudUrl,
185
+ ecosystem: pending.ecosystem,
186
+ workspaceRoot: pending.workspaceRoot,
187
+ status: 'claimed',
188
+ claimedAt: Date.now()
189
+ }, null, 2)}\n`);
190
+ }
191
+ printInstallResult(result);
192
+ console.error(`credential: ${paths.credentialPath}`);
193
+ };
194
+
195
+ if (fs.existsSync(paths.credentialPath)) {
196
+ const connection = JSON.parse(fs.readFileSync(paths.credentialPath, 'utf8'));
197
+ await installClaimedConnection(connection);
198
+ return;
199
+ }
200
+
201
+ if (!pending) {
202
+ if (approvalCode) throw new Error('No local activation request matches this CloudMCP URL and workspace');
203
+ const requested = await activationFetch(`${paths.cloudUrl}/connectors/activation-requests`, {
204
+ method: 'POST',
205
+ headers: { 'Content-Type': 'application/json' },
206
+ body: JSON.stringify({
207
+ ecosystem: options.ecosystem,
208
+ deviceId: device.deviceId,
209
+ publicKeyJwk: device.publicKeyJwk,
210
+ deviceLabel: options['device-label'] || os.hostname(),
211
+ workspaceLabel: path.basename(paths.workspaceRoot),
212
+ connectorId: options['connector-id'] || defaultConnectorId(options.ecosystem, paths.workspaceRoot)
213
+ })
214
+ });
215
+ pending = {
216
+ requestId: requested.activationRequest.id,
217
+ requestToken: requested.requestToken,
218
+ cloudUrl: paths.cloudUrl,
219
+ ecosystem: options.ecosystem,
220
+ workspaceRoot: paths.workspaceRoot,
221
+ createdAt: Date.now()
222
+ };
223
+ writeFileAtomically(paths.pendingPath, `${JSON.stringify(pending, null, 2)}\n`);
224
+ console.error(`approval request: ${pending.requestId}`);
225
+ console.error('status: pending administrator approval');
226
+ console.error('After approval, rerun this command with --approval-code <code>.');
227
+ return;
228
+ }
229
+
230
+ if (!approvalCode) {
231
+ const status = await activationFetch(
232
+ `${paths.cloudUrl}/connectors/activation-requests/${encodeURIComponent(pending.requestId)}`,
233
+ { headers: { Authorization: `Activation ${pending.requestToken}` } }
234
+ );
235
+ console.error(`approval request: ${pending.requestId}`);
236
+ console.error(`status: ${status.activationRequest.status}`);
237
+ console.error('When approved, rerun with --approval-code <code>.');
238
+ return;
239
+ }
240
+
241
+ const claimUrl = new URL(`${paths.cloudUrl}/connectors/activation-requests/${encodeURIComponent(pending.requestId)}/claim`);
242
+ const claimBody = JSON.stringify({ approvalCode });
243
+ const signedHeaders = await identity.buildSignedHeaders({
244
+ method: 'POST',
245
+ pathname: claimUrl.pathname,
246
+ search: claimUrl.search,
247
+ body: claimBody
248
+ });
249
+ const claimed = await activationFetch(
250
+ claimUrl.toString(),
251
+ {
252
+ method: 'POST',
253
+ headers: {
254
+ Authorization: `Activation ${pending.requestToken}`,
255
+ 'Content-Type': 'application/json',
256
+ ...signedHeaders
257
+ },
258
+ body: claimBody
259
+ }
260
+ );
261
+ writeFileAtomically(paths.credentialPath, `${JSON.stringify(claimed.connection, null, 2)}\n`);
262
+ await installClaimedConnection(claimed.connection);
263
+ }
264
+
99
265
  function printInstallResult(result, { includeContent = false, includeSecrets = false } = {}) {
100
266
  console.error(`ecosystem: ${result.ecosystem}`);
101
267
  console.error(`scope: ${result.scope}`);
@@ -138,7 +304,7 @@ async function main() {
138
304
  try {
139
305
  const options = parseArgs(process.argv.slice(2));
140
306
 
141
- if (!['install', 'print', 'smoke', 'reload'].includes(options.command)) {
307
+ if (!['activate', 'install', 'print', 'smoke', 'reload'].includes(options.command)) {
142
308
  printUsage();
143
309
  process.exit(1);
144
310
  }
@@ -147,6 +313,11 @@ async function main() {
147
313
  process.exit(1);
148
314
  }
149
315
 
316
+ if (options.command === 'activate') {
317
+ await runActivation(options);
318
+ return;
319
+ }
320
+
150
321
  if (options.command === 'install') {
151
322
  const result = installReferenceConnector(buildInstallOptions(options));
152
323
  if (options.ecosystem === 'codex' && !options.dryRun) {
package/index.js CHANGED
@@ -16,15 +16,32 @@ import {
16
16
  import { sanitizeProxyError, SmartRouter } from './src/router.js';
17
17
  import { LocalToolExecutor } from './src/local-tools.js';
18
18
  import { ConnectorBridge } from './src/connector-bridge.js';
19
+ import fs from 'fs';
19
20
 
20
21
  // 从环境变量读取配置
21
- const CLOUD_URL = process.env.CLOUDFLARE_MCP_URL || process.env.MCP_URL;
22
- const CLOUD_API_KEY = process.env.CLOUDFLARE_MCP_API_KEY || process.env.MCP_API_KEY;
22
+ function readManagedCredential() {
23
+ const credentialPath = process.env.CLOUDMCP_CREDENTIAL_PATH || '';
24
+ if (!credentialPath) return null;
25
+ try {
26
+ const stat = fs.statSync(credentialPath);
27
+ if (process.platform !== 'win32' && (stat.mode & 0o077) !== 0) {
28
+ throw new Error('credential file permissions must be 0600');
29
+ }
30
+ return JSON.parse(fs.readFileSync(credentialPath, 'utf8'));
31
+ } catch (error) {
32
+ console.error(`Error: unable to read CloudMCP credential file: ${error.message}`);
33
+ process.exit(1);
34
+ }
35
+ }
36
+
37
+ const MANAGED_CREDENTIAL = readManagedCredential();
38
+ const CLOUD_URL = MANAGED_CREDENTIAL?.cloudUrl || process.env.CLOUDFLARE_MCP_URL || process.env.MCP_URL;
39
+ const CLOUD_API_KEY = MANAGED_CREDENTIAL?.apiKey || process.env.CLOUDFLARE_MCP_API_KEY || process.env.MCP_API_KEY;
23
40
  const WORKSPACE_ROOT = process.env.WORKSPACE_ROOT || process.cwd();
24
- const CLIENT_PROFILE_ID = process.env.CLOUDMCP_CLIENT_PROFILE_ID || process.env.CLIENT_PROFILE_ID || '';
25
- const CONNECTOR_ID = process.env.CLOUDMCP_CONNECTOR_ID || process.env.CONNECTOR_ID || '';
26
- const CONNECTOR_TYPE = process.env.CLOUDMCP_CONNECTOR_TYPE || process.env.CONNECTOR_TYPE || 'smart_proxy';
27
- const WORKSPACE_ID = process.env.CLOUDMCP_WORKSPACE_ID || process.env.WORKSPACE_ID || '';
41
+ const CLIENT_PROFILE_ID = MANAGED_CREDENTIAL?.clientProfileId || process.env.CLOUDMCP_CLIENT_PROFILE_ID || process.env.CLIENT_PROFILE_ID || '';
42
+ const CONNECTOR_ID = MANAGED_CREDENTIAL?.connectorId || process.env.CLOUDMCP_CONNECTOR_ID || process.env.CONNECTOR_ID || '';
43
+ const CONNECTOR_TYPE = MANAGED_CREDENTIAL?.connectorType || process.env.CLOUDMCP_CONNECTOR_TYPE || process.env.CONNECTOR_TYPE || 'smart_proxy';
44
+ const WORKSPACE_ID = MANAGED_CREDENTIAL?.workspaceId || process.env.CLOUDMCP_WORKSPACE_ID || process.env.WORKSPACE_ID || '';
28
45
  const DEVICE_IDENTITY_PATH = process.env.CLOUDMCP_DEVICE_IDENTITY_PATH || '';
29
46
  const AUTO_SYNC_PROFILE = (process.env.CLOUDMCP_AUTO_SYNC_PROFILE || 'true') !== 'false';
30
47
  const AUTO_APPLY_BRAIN = (process.env.CLOUDMCP_AUTO_APPLY_BRAIN || 'true') !== 'false';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudflare-mcp-smart-proxy",
3
- "version": "1.4.2",
3
+ "version": "1.5.0",
4
4
  "description": "Smart proxy for Cloudflare MCP - routes tools to cloud or local execution",
5
5
  "repository": {
6
6
  "type": "git",
@@ -93,6 +93,7 @@ function formatTomlArray(values) {
93
93
  function toManagedEnv({
94
94
  cloudUrl,
95
95
  cloudApiKey,
96
+ credentialPath,
96
97
  workspaceRoot,
97
98
  clientProfileId,
98
99
  connectorId,
@@ -104,6 +105,7 @@ function toManagedEnv({
104
105
  const env = {
105
106
  CLOUDFLARE_MCP_URL: normalizeEnvValue(cloudUrl),
106
107
  CLOUDFLARE_MCP_API_KEY: normalizeEnvValue(cloudApiKey),
108
+ CLOUDMCP_CREDENTIAL_PATH: normalizeEnvValue(credentialPath),
107
109
  WORKSPACE_ROOT: normalizeEnvValue(workspaceRoot),
108
110
  CLOUDMCP_CLIENT_PROFILE_ID: normalizeEnvValue(clientProfileId),
109
111
  CLOUDMCP_CONNECTOR_ID: normalizeEnvValue(connectorId),
@@ -146,6 +148,7 @@ export function buildReferenceConnectorServer({
146
148
  ecosystem,
147
149
  cloudUrl,
148
150
  cloudApiKey,
151
+ credentialPath = '',
149
152
  workspaceRoot = process.cwd(),
150
153
  clientProfileId,
151
154
  connectorId = '',
@@ -170,6 +173,7 @@ export function buildReferenceConnectorServer({
170
173
  env: toManagedEnv({
171
174
  cloudUrl,
172
175
  cloudApiKey,
176
+ credentialPath,
173
177
  workspaceRoot: resolvedWorkspaceRoot,
174
178
  clientProfileId,
175
179
  connectorId: resolvedConnectorId,
@@ -292,6 +296,7 @@ export function installReferenceConnector({
292
296
  ecosystem,
293
297
  cloudUrl,
294
298
  cloudApiKey,
299
+ credentialPath = '',
295
300
  workspaceRoot = process.cwd(),
296
301
  clientProfileId,
297
302
  connectorId = '',
@@ -345,6 +350,7 @@ export function installReferenceConnector({
345
350
  ecosystem,
346
351
  cloudUrl,
347
352
  cloudApiKey,
353
+ credentialPath,
348
354
  workspaceRoot,
349
355
  clientProfileId,
350
356
  connectorId,