cloudflare-mcp-smart-proxy 1.4.1 → 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 +13 -15
- package/connector-cli.js +184 -8
- package/index.js +23 -6
- package/package.json +4 -2
- package/src/local-tools.js +2 -2
- package/src/reference-connectors.js +6 -0
- package/src/tools/command-executor.js +26 -12
- package/src/tools/file-operations.js +20 -6
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
|
-
|
|
44
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
--
|
|
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
|
-
- `
|
|
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
|
|
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>');
|
|
@@ -29,6 +38,8 @@ function printUsage() {
|
|
|
29
38
|
console.error(' --app-server-socket <path> Current runtime Codex app-server socket');
|
|
30
39
|
console.error(' --runtime-id <id> Current Codex runtime identity');
|
|
31
40
|
console.error(' --dry-run Show output without writing');
|
|
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');
|
|
32
43
|
console.error('');
|
|
33
44
|
console.error('The reload codex command uses the existing Codex config and requires no credentials.');
|
|
34
45
|
}
|
|
@@ -47,8 +58,8 @@ function parseArgs(argv) {
|
|
|
47
58
|
}
|
|
48
59
|
|
|
49
60
|
const key = token.slice(2);
|
|
50
|
-
if (key === 'dry-run') {
|
|
51
|
-
options
|
|
61
|
+
if (key === 'dry-run' || key === 'show-secrets') {
|
|
62
|
+
options[key === 'dry-run' ? 'dryRun' : key] = true;
|
|
52
63
|
continue;
|
|
53
64
|
}
|
|
54
65
|
|
|
@@ -95,7 +106,163 @@ function buildInstallOptions(options) {
|
|
|
95
106
|
};
|
|
96
107
|
}
|
|
97
108
|
|
|
98
|
-
function
|
|
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
|
+
|
|
265
|
+
function printInstallResult(result, { includeContent = false, includeSecrets = false } = {}) {
|
|
99
266
|
console.error(`ecosystem: ${result.ecosystem}`);
|
|
100
267
|
console.error(`scope: ${result.scope}`);
|
|
101
268
|
console.error(`target: ${result.targetFile}`);
|
|
@@ -111,7 +278,11 @@ function printInstallResult(result, { includeContent = false } = {}) {
|
|
|
111
278
|
}
|
|
112
279
|
}
|
|
113
280
|
if (includeContent) {
|
|
114
|
-
|
|
281
|
+
const apiKey = result.serverDefinition?.env?.CLOUDFLARE_MCP_API_KEY || '';
|
|
282
|
+
const renderedContent = includeSecrets
|
|
283
|
+
? result.renderedContent
|
|
284
|
+
: (apiKey ? result.renderedContent.split(apiKey).join('[REDACTED]') : result.renderedContent);
|
|
285
|
+
process.stdout.write(`${renderedContent}\n`);
|
|
115
286
|
}
|
|
116
287
|
}
|
|
117
288
|
|
|
@@ -133,7 +304,7 @@ async function main() {
|
|
|
133
304
|
try {
|
|
134
305
|
const options = parseArgs(process.argv.slice(2));
|
|
135
306
|
|
|
136
|
-
if (!['install', 'print', 'smoke', 'reload'].includes(options.command)) {
|
|
307
|
+
if (!['activate', 'install', 'print', 'smoke', 'reload'].includes(options.command)) {
|
|
137
308
|
printUsage();
|
|
138
309
|
process.exit(1);
|
|
139
310
|
}
|
|
@@ -142,6 +313,11 @@ async function main() {
|
|
|
142
313
|
process.exit(1);
|
|
143
314
|
}
|
|
144
315
|
|
|
316
|
+
if (options.command === 'activate') {
|
|
317
|
+
await runActivation(options);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
145
321
|
if (options.command === 'install') {
|
|
146
322
|
const result = installReferenceConnector(buildInstallOptions(options));
|
|
147
323
|
if (options.ecosystem === 'codex' && !options.dryRun) {
|
|
@@ -177,7 +353,7 @@ async function main() {
|
|
|
177
353
|
|
|
178
354
|
if (options.command === 'print') {
|
|
179
355
|
const result = printReferenceConnectorConfig(buildInstallOptions(options));
|
|
180
|
-
printInstallResult(result, { includeContent: true });
|
|
356
|
+
printInstallResult(result, { includeContent: true, includeSecrets: options['show-secrets'] === true });
|
|
181
357
|
return;
|
|
182
358
|
}
|
|
183
359
|
|
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
|
-
|
|
22
|
-
const
|
|
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.
|
|
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",
|
|
@@ -36,7 +36,9 @@
|
|
|
36
36
|
"@hono/node-server": "2.0.12",
|
|
37
37
|
"ajv": "8.20.0",
|
|
38
38
|
"body-parser": "2.3.0",
|
|
39
|
-
"fast-uri": "3.1.
|
|
39
|
+
"fast-uri": "3.1.5",
|
|
40
|
+
"hono": "4.12.34",
|
|
41
|
+
"ip-address": "10.4.0",
|
|
40
42
|
"path-to-regexp": "8.4.0",
|
|
41
43
|
"qs": "6.15.3"
|
|
42
44
|
}
|
package/src/local-tools.js
CHANGED
|
@@ -214,13 +214,13 @@ export class LocalToolExecutor {
|
|
|
214
214
|
},
|
|
215
215
|
{
|
|
216
216
|
name: 'execute_command',
|
|
217
|
-
description: 'Execute a shell
|
|
217
|
+
description: 'Execute a local process without shell interpretation',
|
|
218
218
|
inputSchema: {
|
|
219
219
|
type: 'object',
|
|
220
220
|
properties: {
|
|
221
221
|
command: {
|
|
222
222
|
type: 'string',
|
|
223
|
-
description: '
|
|
223
|
+
description: 'Executable and arguments to run'
|
|
224
224
|
},
|
|
225
225
|
cwd: {
|
|
226
226
|
type: 'string',
|
|
@@ -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,
|
|
@@ -4,6 +4,17 @@
|
|
|
4
4
|
|
|
5
5
|
import { spawn } from 'child_process';
|
|
6
6
|
import path from 'path';
|
|
7
|
+
import { FileOperations } from './file-operations.js';
|
|
8
|
+
|
|
9
|
+
function parseCommand(command) {
|
|
10
|
+
const tokens = [];
|
|
11
|
+
const pattern = /"((?:\\.|[^"\\])*)"|'([^']*)'|([^\s]+)/g;
|
|
12
|
+
let match;
|
|
13
|
+
while ((match = pattern.exec(command)) !== null) {
|
|
14
|
+
tokens.push((match[1] ?? match[2] ?? match[3]).replace(/\\([\\"])/g, '$1'));
|
|
15
|
+
}
|
|
16
|
+
return tokens;
|
|
17
|
+
}
|
|
7
18
|
|
|
8
19
|
export class CommandExecutor {
|
|
9
20
|
/**
|
|
@@ -16,17 +27,21 @@ export class CommandExecutor {
|
|
|
16
27
|
throw new Error('command parameter is required');
|
|
17
28
|
}
|
|
18
29
|
|
|
19
|
-
const workingDir = cwd ?
|
|
30
|
+
const workingDir = cwd ? FileOperations.validatePath(cwd, workspaceRoot) : FileOperations.validatePath('.', workspaceRoot);
|
|
20
31
|
|
|
21
32
|
return new Promise((resolve, reject) => {
|
|
22
33
|
// 解析命令和参数
|
|
23
|
-
const parts = command.trim()
|
|
34
|
+
const parts = parseCommand(command.trim());
|
|
24
35
|
const cmd = parts[0];
|
|
25
36
|
const args = parts.slice(1);
|
|
37
|
+
if (!cmd) {
|
|
38
|
+
reject(new Error('command parameter must include an executable'));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
26
41
|
|
|
27
42
|
// 安全限制:禁止某些危险命令
|
|
28
|
-
const dangerousCommands = ['rm', 'del', 'format', 'shutdown', 'reboot'];
|
|
29
|
-
if (dangerousCommands.
|
|
43
|
+
const dangerousCommands = new Set(['rm', 'del', 'format', 'shutdown', 'reboot']);
|
|
44
|
+
if (dangerousCommands.has(path.basename(cmd).toLowerCase())) {
|
|
30
45
|
reject(new Error(`Command "${cmd}" is not allowed for security reasons`));
|
|
31
46
|
return;
|
|
32
47
|
}
|
|
@@ -38,9 +53,13 @@ export class CommandExecutor {
|
|
|
38
53
|
// 执行命令
|
|
39
54
|
const proc = spawn(cmd, args, {
|
|
40
55
|
cwd: workingDir,
|
|
41
|
-
shell:
|
|
56
|
+
shell: false,
|
|
42
57
|
stdio: ['ignore', 'pipe', 'pipe']
|
|
43
58
|
});
|
|
59
|
+
const timeout = setTimeout(() => {
|
|
60
|
+
proc.kill();
|
|
61
|
+
reject(new Error('Command execution timeout (30s)'));
|
|
62
|
+
}, 30000);
|
|
44
63
|
|
|
45
64
|
// 收集输出
|
|
46
65
|
proc.stdout.on('data', (data) => {
|
|
@@ -53,6 +72,7 @@ export class CommandExecutor {
|
|
|
53
72
|
|
|
54
73
|
// 处理完成
|
|
55
74
|
proc.on('close', (code) => {
|
|
75
|
+
clearTimeout(timeout);
|
|
56
76
|
const duration = Date.now() - startTime;
|
|
57
77
|
|
|
58
78
|
resolve({
|
|
@@ -68,15 +88,9 @@ export class CommandExecutor {
|
|
|
68
88
|
|
|
69
89
|
// 处理错误
|
|
70
90
|
proc.on('error', (error) => {
|
|
91
|
+
clearTimeout(timeout);
|
|
71
92
|
reject(new Error(`Command execution failed: ${error.message}`));
|
|
72
93
|
});
|
|
73
|
-
|
|
74
|
-
// 超时保护(30秒)
|
|
75
|
-
setTimeout(() => {
|
|
76
|
-
proc.kill();
|
|
77
|
-
reject(new Error('Command execution timeout (30s)'));
|
|
78
|
-
}, 30000);
|
|
79
94
|
});
|
|
80
95
|
}
|
|
81
96
|
}
|
|
82
|
-
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import fs from 'fs/promises';
|
|
6
|
+
import fsSync from 'fs';
|
|
6
7
|
import path from 'path';
|
|
7
8
|
|
|
8
9
|
export class FileOperations {
|
|
@@ -16,14 +17,28 @@ export class FileOperations {
|
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
// 限制在工作目录内
|
|
19
|
-
const
|
|
20
|
-
const
|
|
20
|
+
const resolvedRoot = fsSync.realpathSync(path.resolve(workspaceRoot));
|
|
21
|
+
const resolvedPath = path.resolve(resolvedRoot, filePath);
|
|
22
|
+
const relativePath = path.relative(resolvedRoot, resolvedPath);
|
|
21
23
|
|
|
22
|
-
if (
|
|
24
|
+
if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
|
|
23
25
|
throw new Error('File path outside workspace root');
|
|
24
26
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
|
|
28
|
+
let existingAncestor = resolvedPath;
|
|
29
|
+
while (!fsSync.existsSync(existingAncestor)) {
|
|
30
|
+
const parent = path.dirname(existingAncestor);
|
|
31
|
+
if (parent === existingAncestor) break;
|
|
32
|
+
existingAncestor = parent;
|
|
33
|
+
}
|
|
34
|
+
const realAncestor = fsSync.realpathSync(existingAncestor);
|
|
35
|
+
const realTarget = path.resolve(realAncestor, path.relative(existingAncestor, resolvedPath));
|
|
36
|
+
const realRelativePath = path.relative(resolvedRoot, realTarget);
|
|
37
|
+
if (realRelativePath.startsWith('..') || path.isAbsolute(realRelativePath)) {
|
|
38
|
+
throw new Error('File path resolves outside workspace root');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return realTarget;
|
|
27
42
|
}
|
|
28
43
|
|
|
29
44
|
/**
|
|
@@ -208,4 +223,3 @@ export class FileOperations {
|
|
|
208
223
|
}
|
|
209
224
|
}
|
|
210
225
|
}
|
|
211
|
-
|