@kya-os/create-mcpi-app 1.4.4 ā 1.6.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.
|
@@ -21,6 +21,8 @@ export async function fetchCloudflareMcpiTemplate(projectPath, options = {}) {
|
|
|
21
21
|
version: "0.1.0",
|
|
22
22
|
private: true,
|
|
23
23
|
scripts: {
|
|
24
|
+
setup: "node scripts/setup.js",
|
|
25
|
+
postinstall: "npm run setup",
|
|
24
26
|
deploy: "wrangler deploy",
|
|
25
27
|
dev: "wrangler dev",
|
|
26
28
|
start: "wrangler dev",
|
|
@@ -46,9 +48,12 @@ export async function fetchCloudflareMcpiTemplate(projectPath, options = {}) {
|
|
|
46
48
|
"kv:setup": "echo 'KV Commands: kv:create (create all), kv:list (list all), kv:keys-* (view keys), kv:delete (delete all), kv:reset (delete+recreate)'",
|
|
47
49
|
"cf-typegen": "wrangler types",
|
|
48
50
|
"type-check": "tsc --noEmit",
|
|
51
|
+
test: "vitest",
|
|
52
|
+
"test:watch": "vitest --watch",
|
|
53
|
+
"test:coverage": "vitest run --coverage",
|
|
49
54
|
},
|
|
50
55
|
dependencies: {
|
|
51
|
-
"@kya-os/mcp-i-cloudflare": "^1.3.
|
|
56
|
+
"@kya-os/mcp-i-cloudflare": "^1.3.2",
|
|
52
57
|
"@modelcontextprotocol/sdk": "^1.19.1",
|
|
53
58
|
"agents": "^0.2.8",
|
|
54
59
|
"hono": "^4.9.10",
|
|
@@ -56,7 +61,10 @@ export async function fetchCloudflareMcpiTemplate(projectPath, options = {}) {
|
|
|
56
61
|
},
|
|
57
62
|
devDependencies: {
|
|
58
63
|
"@cloudflare/workers-types": "^4.20240925.0",
|
|
64
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
65
|
+
"miniflare": "^3.0.0",
|
|
59
66
|
"typescript": "^5.6.2",
|
|
67
|
+
"vitest": "^3.2.4",
|
|
60
68
|
"wrangler": "^4.42.2",
|
|
61
69
|
},
|
|
62
70
|
};
|
|
@@ -66,6 +74,500 @@ export async function fetchCloudflareMcpiTemplate(projectPath, options = {}) {
|
|
|
66
74
|
const srcDir = path.join(projectPath, "src");
|
|
67
75
|
const toolsDir = path.join(srcDir, "tools");
|
|
68
76
|
fs.ensureDirSync(toolsDir);
|
|
77
|
+
// Create scripts directory
|
|
78
|
+
const scriptsDir = path.join(projectPath, "scripts");
|
|
79
|
+
fs.ensureDirSync(scriptsDir);
|
|
80
|
+
// Create setup.js automation script
|
|
81
|
+
const setupScriptContent = `#!/usr/bin/env node
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Automated Setup Script for ${projectName}
|
|
85
|
+
*
|
|
86
|
+
* This script automates the tedious process of:
|
|
87
|
+
* 1. Checking/installing wrangler
|
|
88
|
+
* 2. Creating KV namespaces
|
|
89
|
+
* 3. Extracting namespace IDs
|
|
90
|
+
* 4. Updating wrangler.toml automatically
|
|
91
|
+
* 5. Setting up local development environment
|
|
92
|
+
*/
|
|
93
|
+
|
|
94
|
+
const { execSync } = require('child_process');
|
|
95
|
+
const fs = require('fs');
|
|
96
|
+
const path = require('path');
|
|
97
|
+
const readline = require('readline');
|
|
98
|
+
|
|
99
|
+
const rl = readline.createInterface({
|
|
100
|
+
input: process.stdin,
|
|
101
|
+
output: process.stdout
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Colors for terminal output
|
|
105
|
+
const colors = {
|
|
106
|
+
reset: '\\x1b[0m',
|
|
107
|
+
bright: '\\x1b[1m',
|
|
108
|
+
green: '\\x1b[32m',
|
|
109
|
+
yellow: '\\x1b[33m',
|
|
110
|
+
blue: '\\x1b[36m',
|
|
111
|
+
red: '\\x1b[31m'
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
function log(message, color = colors.reset) {
|
|
115
|
+
console.log(color + message + colors.reset);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function setup() {
|
|
119
|
+
log('\\nš Starting automated setup for ${projectName}...\\n', colors.bright + colors.blue);
|
|
120
|
+
|
|
121
|
+
// 1. Check wrangler installation
|
|
122
|
+
try {
|
|
123
|
+
const wranglerVersion = execSync('wrangler --version', { encoding: 'utf-8' });
|
|
124
|
+
log('ā
Wrangler CLI detected: ' + wranglerVersion.trim(), colors.green);
|
|
125
|
+
} catch {
|
|
126
|
+
log('š¦ Wrangler CLI not found. Installing...', colors.yellow);
|
|
127
|
+
try {
|
|
128
|
+
execSync('npm install -g wrangler', { stdio: 'inherit' });
|
|
129
|
+
log('ā
Wrangler CLI installed successfully', colors.green);
|
|
130
|
+
} catch (error) {
|
|
131
|
+
log('ā Failed to install Wrangler. Please install manually: npm install -g wrangler', colors.red);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 2. Check if user is logged in to Cloudflare
|
|
137
|
+
try {
|
|
138
|
+
execSync('wrangler whoami', { encoding: 'utf-8' });
|
|
139
|
+
log('ā
Logged in to Cloudflare', colors.green);
|
|
140
|
+
} catch {
|
|
141
|
+
log('š Please log in to Cloudflare:', colors.yellow);
|
|
142
|
+
try {
|
|
143
|
+
execSync('wrangler login', { stdio: 'inherit' });
|
|
144
|
+
} catch (error) {
|
|
145
|
+
log('ā Login failed. Please run: wrangler login', colors.red);
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 3. Create KV namespaces
|
|
151
|
+
log('\\nš Creating KV namespaces...\\n', colors.bright);
|
|
152
|
+
|
|
153
|
+
const namespaces = [
|
|
154
|
+
{ binding: '${className.toUpperCase()}_NONCE_CACHE', name: 'Nonce Cache', purpose: 'Replay attack prevention' },
|
|
155
|
+
{ binding: '${className.toUpperCase()}_PROOF_ARCHIVE', name: 'Proof Archive', purpose: 'Cryptographic proof storage' },
|
|
156
|
+
{ binding: '${className.toUpperCase()}_IDENTITY_STORAGE', name: 'Identity Storage', purpose: 'Agent identity persistence' },
|
|
157
|
+
{ binding: '${className.toUpperCase()}_DELEGATION_STORAGE', name: 'Delegation Storage', purpose: 'OAuth token storage' },
|
|
158
|
+
{ binding: '${className.toUpperCase()}_TOOL_PROTECTION_KV', name: 'Tool Protection', purpose: 'Permission caching' }
|
|
159
|
+
];
|
|
160
|
+
|
|
161
|
+
const kvIds = {};
|
|
162
|
+
const wranglerTomlPath = path.join(__dirname, '..', 'wrangler.toml');
|
|
163
|
+
|
|
164
|
+
for (const ns of namespaces) {
|
|
165
|
+
log(\`Creating \${ns.name} (\${ns.purpose})...\`, colors.blue);
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
// Create the namespace
|
|
169
|
+
const output = execSync(\`wrangler kv namespace create "\${ns.binding}"\`, { encoding: 'utf-8' });
|
|
170
|
+
|
|
171
|
+
// Extract the ID from output
|
|
172
|
+
const idMatch = output.match(/id = "([^"]+)"/);
|
|
173
|
+
|
|
174
|
+
if (idMatch && idMatch[1]) {
|
|
175
|
+
kvIds[ns.binding] = idMatch[1];
|
|
176
|
+
log(\` ā
Created with ID: \${idMatch[1]}\`, colors.green);
|
|
177
|
+
} else {
|
|
178
|
+
// Try to get existing namespace
|
|
179
|
+
const listOutput = execSync('wrangler kv namespace list', { encoding: 'utf-8' });
|
|
180
|
+
const existingMatch = listOutput.match(new RegExp(\`\${ns.binding}.*?id":\\s*"([^"]+)"\`));
|
|
181
|
+
|
|
182
|
+
if (existingMatch && existingMatch[1]) {
|
|
183
|
+
kvIds[ns.binding] = existingMatch[1];
|
|
184
|
+
log(\` ā ļø Namespace already exists with ID: \${existingMatch[1]}\`, colors.yellow);
|
|
185
|
+
} else {
|
|
186
|
+
log(\` ā ļø Could not extract ID for \${ns.binding}. You may need to add it manually.\`, colors.yellow);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
} catch (error) {
|
|
190
|
+
// Check if namespace already exists
|
|
191
|
+
try {
|
|
192
|
+
const listOutput = execSync('wrangler kv namespace list', { encoding: 'utf-8' });
|
|
193
|
+
const existingMatch = listOutput.match(new RegExp(\`\${ns.binding}.*?id":\\s*"([^"]+)"\`));
|
|
194
|
+
|
|
195
|
+
if (existingMatch && existingMatch[1]) {
|
|
196
|
+
kvIds[ns.binding] = existingMatch[1];
|
|
197
|
+
log(\` ā ļø Namespace already exists with ID: \${existingMatch[1]}\`, colors.yellow);
|
|
198
|
+
} else {
|
|
199
|
+
log(\` ā Failed to create \${ns.binding}: \${error.message}\`, colors.red);
|
|
200
|
+
}
|
|
201
|
+
} catch (listError) {
|
|
202
|
+
log(\` ā Failed to create or find \${ns.binding}\`, colors.red);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// 4. Update wrangler.toml with KV IDs
|
|
208
|
+
if (Object.keys(kvIds).length > 0) {
|
|
209
|
+
log('\\nš Updating wrangler.toml with KV namespace IDs...\\n', colors.bright);
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
let wranglerContent = fs.readFileSync(wranglerTomlPath, 'utf-8');
|
|
213
|
+
let updatedCount = 0;
|
|
214
|
+
|
|
215
|
+
for (const [binding, id] of Object.entries(kvIds)) {
|
|
216
|
+
// Match pattern: binding = "BINDING_NAME"\\nid = ""
|
|
217
|
+
const pattern = new RegExp(\`(binding = "\${binding}")\\\\s*\\\\nid = ""\`, 'g');
|
|
218
|
+
const replacement = \`$1\\nid = "\${id}"\`;
|
|
219
|
+
|
|
220
|
+
const newContent = wranglerContent.replace(pattern, replacement);
|
|
221
|
+
if (newContent !== wranglerContent) {
|
|
222
|
+
updatedCount++;
|
|
223
|
+
log(\` ā
Updated \${binding} with ID: \${id}\`, colors.green);
|
|
224
|
+
}
|
|
225
|
+
wranglerContent = newContent;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
fs.writeFileSync(wranglerTomlPath, wranglerContent);
|
|
229
|
+
log(\`\\nā
Updated \${updatedCount} namespace ID(s) in wrangler.toml\`, colors.green);
|
|
230
|
+
|
|
231
|
+
// Show remaining empty IDs if any
|
|
232
|
+
const emptyMatches = wranglerContent.match(/binding = "[^"]+"\s*\nid = ""/g);
|
|
233
|
+
if (emptyMatches) {
|
|
234
|
+
log('\\nā ļø Some namespace IDs still need to be added manually:', colors.yellow);
|
|
235
|
+
emptyMatches.forEach(match => {
|
|
236
|
+
const bindingMatch = match.match(/binding = "([^"]+)"/);
|
|
237
|
+
if (bindingMatch) {
|
|
238
|
+
log(\` - \${bindingMatch[1]}\`, colors.yellow);
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
} catch (error) {
|
|
243
|
+
log(\`ā Failed to update wrangler.toml: \${error.message}\`, colors.red);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// 5. Create .dev.vars from example if it doesn't exist
|
|
248
|
+
const devVarsPath = path.join(__dirname, '..', '.dev.vars');
|
|
249
|
+
const devVarsExamplePath = path.join(__dirname, '..', '.dev.vars.example');
|
|
250
|
+
|
|
251
|
+
if (!fs.existsSync(devVarsPath) && fs.existsSync(devVarsExamplePath)) {
|
|
252
|
+
log('\\nš Creating .dev.vars from example...', colors.blue);
|
|
253
|
+
fs.copyFileSync(devVarsExamplePath, devVarsPath);
|
|
254
|
+
log('ā
Created .dev.vars - Please update with your values', colors.green);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// 6. Check if identity needs to be generated
|
|
258
|
+
if (fs.existsSync(devVarsPath)) {
|
|
259
|
+
const devVarsContent = fs.readFileSync(devVarsPath, 'utf-8');
|
|
260
|
+
if (devVarsContent.includes('your-private-key-here')) {
|
|
261
|
+
log('\\nš Generating agent identity...', colors.blue);
|
|
262
|
+
try {
|
|
263
|
+
execSync('npx @kya-os/create-mcpi-app regenerate-identity', { stdio: 'inherit' });
|
|
264
|
+
log('ā
Identity generated successfully', colors.green);
|
|
265
|
+
} catch {
|
|
266
|
+
log('ā ļø Could not generate identity automatically. Run: npx @kya-os/create-mcpi-app regenerate-identity', colors.yellow);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// 7. Show next steps
|
|
272
|
+
log('\\n⨠Setup complete! Next steps:\\n', colors.bright + colors.green);
|
|
273
|
+
log('1. Review .dev.vars and add any missing values (AgentShield API key, etc.)', colors.blue);
|
|
274
|
+
log('2. Start development server: npm run dev', colors.blue);
|
|
275
|
+
log('3. Deploy to production: npm run deploy', colors.blue);
|
|
276
|
+
log('\\nUseful commands:', colors.bright);
|
|
277
|
+
log(' npm run dev - Start local development server');
|
|
278
|
+
log(' npm run deploy - Deploy to Cloudflare Workers');
|
|
279
|
+
log(' npm run kv:list - List all KV namespaces');
|
|
280
|
+
log(' wrangler secret put <KEY> - Set production secrets');
|
|
281
|
+
log('\\nFor more information, see the README.md file.\\n');
|
|
282
|
+
|
|
283
|
+
rl.close();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Handle errors gracefully
|
|
287
|
+
process.on('unhandledRejection', (error) => {
|
|
288
|
+
log(\`\\nā Setup failed: \${error.message}\`, colors.red);
|
|
289
|
+
process.exit(1);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// Run the setup
|
|
293
|
+
setup().catch((error) => {
|
|
294
|
+
log(\`\\nā Setup failed: \${error.message}\`, colors.red);
|
|
295
|
+
process.exit(1);
|
|
296
|
+
});
|
|
297
|
+
`;
|
|
298
|
+
fs.writeFileSync(path.join(scriptsDir, "setup.js"), setupScriptContent);
|
|
299
|
+
// Make setup script executable
|
|
300
|
+
if (process.platform !== 'win32') {
|
|
301
|
+
fs.chmodSync(path.join(scriptsDir, "setup.js"), '755');
|
|
302
|
+
}
|
|
303
|
+
// Create tests directory
|
|
304
|
+
const testsDir = path.join(projectPath, "tests");
|
|
305
|
+
fs.ensureDirSync(testsDir);
|
|
306
|
+
// Create delegation test file
|
|
307
|
+
const delegationTestContent = `import { describe, test, expect, vi, beforeEach } from 'vitest';
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Delegation Management Tests
|
|
311
|
+
* Tests delegation verification, caching, and invalidation
|
|
312
|
+
*/
|
|
313
|
+
describe('Delegation Management', () => {
|
|
314
|
+
const mockDelegationStorage = {
|
|
315
|
+
get: vi.fn(),
|
|
316
|
+
put: vi.fn(),
|
|
317
|
+
delete: vi.fn()
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
const mockVerificationCache = {
|
|
321
|
+
get: vi.fn(),
|
|
322
|
+
put: vi.fn(),
|
|
323
|
+
delete: vi.fn()
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const mockEnv = {
|
|
327
|
+
${className.toUpperCase()}_DELEGATION_STORAGE: mockDelegationStorage,
|
|
328
|
+
TOOL_PROTECTION_KV: mockVerificationCache,
|
|
329
|
+
AGENTSHIELD_API_KEY: 'test-key',
|
|
330
|
+
AGENTSHIELD_API_URL: 'https://test.agentshield.ai'
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
beforeEach(() => {
|
|
334
|
+
vi.clearAllMocks();
|
|
335
|
+
global.fetch = vi.fn();
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test('should verify delegation token with AgentShield API', async () => {
|
|
339
|
+
const token = 'test-delegation-token';
|
|
340
|
+
|
|
341
|
+
// Mock verification cache miss
|
|
342
|
+
mockVerificationCache.get.mockResolvedValueOnce(null);
|
|
343
|
+
|
|
344
|
+
// Mock API success
|
|
345
|
+
global.fetch = vi.fn().mockResolvedValueOnce({
|
|
346
|
+
ok: true
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// Test verification would happen here
|
|
350
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
351
|
+
expect.stringContaining('/api/v1/bouncer/delegations/verify'),
|
|
352
|
+
expect.objectContaining({
|
|
353
|
+
method: 'POST',
|
|
354
|
+
body: JSON.stringify({ token })
|
|
355
|
+
})
|
|
356
|
+
);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
test('should use 5-minute cache TTL for delegations', async () => {
|
|
360
|
+
const token = 'test-token';
|
|
361
|
+
const sessionId = 'test-session';
|
|
362
|
+
|
|
363
|
+
await mockDelegationStorage.put(
|
|
364
|
+
\`session:\${sessionId}\`,
|
|
365
|
+
token,
|
|
366
|
+
{ expirationTtl: 300 } // 5 minutes
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
expect(mockDelegationStorage.put).toHaveBeenCalledWith(
|
|
370
|
+
expect.any(String),
|
|
371
|
+
token,
|
|
372
|
+
{ expirationTtl: 300 }
|
|
373
|
+
);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
test('should invalidate cache on revocation', async () => {
|
|
377
|
+
const sessionId = 'revoked-session';
|
|
378
|
+
const token = 'revoked-token';
|
|
379
|
+
|
|
380
|
+
// Test invalidation
|
|
381
|
+
await Promise.all([
|
|
382
|
+
mockDelegationStorage.delete(\`session:\${sessionId}\`),
|
|
383
|
+
mockVerificationCache.delete(\`verified:\${token.substring(0, 16)}\`)
|
|
384
|
+
]);
|
|
385
|
+
|
|
386
|
+
expect(mockDelegationStorage.delete).toHaveBeenCalled();
|
|
387
|
+
expect(mockVerificationCache.delete).toHaveBeenCalled();
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
`;
|
|
391
|
+
fs.writeFileSync(path.join(testsDir, "delegation.test.ts"), delegationTestContent);
|
|
392
|
+
// Create DO routing test file
|
|
393
|
+
const doRoutingTestContent = `import { describe, test, expect } from 'vitest';
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Durable Object Routing Tests
|
|
397
|
+
* Tests multi-instance DO routing for horizontal scaling
|
|
398
|
+
*/
|
|
399
|
+
describe('DO Multi-Instance Routing', () => {
|
|
400
|
+
|
|
401
|
+
function getDoInstanceId(request: Request, env: any): string {
|
|
402
|
+
const strategy = env.DO_ROUTING_STRATEGY || 'session';
|
|
403
|
+
const headers = request.headers;
|
|
404
|
+
|
|
405
|
+
switch (strategy) {
|
|
406
|
+
case 'session': {
|
|
407
|
+
const sessionId = headers.get('mcp-session-id') ||
|
|
408
|
+
headers.get('Mcp-Session-Id') ||
|
|
409
|
+
crypto.randomUUID();
|
|
410
|
+
return \`session:\${sessionId}\`;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
case 'shard': {
|
|
414
|
+
const identifier = headers.get('mcp-session-id') || Math.random().toString();
|
|
415
|
+
let hash = 0;
|
|
416
|
+
for (let i = 0; i < identifier.length; i++) {
|
|
417
|
+
hash = ((hash << 5) - hash) + identifier.charCodeAt(i);
|
|
418
|
+
hash = hash & hash;
|
|
419
|
+
}
|
|
420
|
+
const shardCount = parseInt(env.DO_SHARD_COUNT || '10');
|
|
421
|
+
const shard = Math.abs(hash) % shardCount;
|
|
422
|
+
return \`shard:\${shard}\`;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
default:
|
|
426
|
+
return 'default';
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
test('should route to different instances for different sessions', () => {
|
|
431
|
+
const env = { DO_ROUTING_STRATEGY: 'session' };
|
|
432
|
+
|
|
433
|
+
const req1 = new Request('http://test/mcp', {
|
|
434
|
+
headers: { 'mcp-session-id': 'session-123' }
|
|
435
|
+
});
|
|
436
|
+
const req2 = new Request('http://test/mcp', {
|
|
437
|
+
headers: { 'mcp-session-id': 'session-456' }
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
const id1 = getDoInstanceId(req1, env);
|
|
441
|
+
const id2 = getDoInstanceId(req2, env);
|
|
442
|
+
|
|
443
|
+
expect(id1).toBe('session:session-123');
|
|
444
|
+
expect(id2).toBe('session:session-456');
|
|
445
|
+
expect(id1).not.toBe(id2);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
test('should distribute load across shards', () => {
|
|
449
|
+
const env = {
|
|
450
|
+
DO_ROUTING_STRATEGY: 'shard',
|
|
451
|
+
DO_SHARD_COUNT: '10'
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
const distribution = new Map<string, number>();
|
|
455
|
+
|
|
456
|
+
// Generate 100 requests
|
|
457
|
+
for (let i = 0; i < 100; i++) {
|
|
458
|
+
const req = new Request('http://test/mcp', {
|
|
459
|
+
headers: { 'mcp-session-id': \`session-\${i}\` }
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
const instanceId = getDoInstanceId(req, env);
|
|
463
|
+
const shard = instanceId.split(':')[1];
|
|
464
|
+
|
|
465
|
+
distribution.set(shard, (distribution.get(shard) || 0) + 1);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Should use multiple shards
|
|
469
|
+
expect(distribution.size).toBeGreaterThan(5);
|
|
470
|
+
});
|
|
471
|
+
});
|
|
472
|
+
`;
|
|
473
|
+
fs.writeFileSync(path.join(testsDir, "do-routing.test.ts"), doRoutingTestContent);
|
|
474
|
+
// Create security test file
|
|
475
|
+
const securityTestContent = `import { describe, test, expect } from 'vitest';
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Security Tests
|
|
479
|
+
* Tests CORS configuration and API key handling
|
|
480
|
+
*/
|
|
481
|
+
describe('Security Configuration', () => {
|
|
482
|
+
|
|
483
|
+
function getCorsOrigin(requestOrigin: string | null, env: any): string | null {
|
|
484
|
+
const allowedOrigins = env.ALLOWED_ORIGINS?.split(',').map((o: string) => o.trim()) || [
|
|
485
|
+
'https://claude.ai',
|
|
486
|
+
'https://app.anthropic.com'
|
|
487
|
+
];
|
|
488
|
+
|
|
489
|
+
if (env.MCPI_ENV !== 'production' && !allowedOrigins.includes('http://localhost:3000')) {
|
|
490
|
+
allowedOrigins.push('http://localhost:3000');
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const origin = requestOrigin || '';
|
|
494
|
+
const isAllowed = allowedOrigins.includes(origin);
|
|
495
|
+
|
|
496
|
+
return isAllowed ? origin : allowedOrigins[0];
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
test('should allow Claude.ai by default', () => {
|
|
500
|
+
const env = {};
|
|
501
|
+
const origin = 'https://claude.ai';
|
|
502
|
+
const result = getCorsOrigin(origin, env);
|
|
503
|
+
|
|
504
|
+
expect(result).toBe(origin);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
test('should reject unauthorized origins', () => {
|
|
508
|
+
const env = { MCPI_ENV: 'production' };
|
|
509
|
+
const origin = 'https://evil.com';
|
|
510
|
+
const result = getCorsOrigin(origin, env);
|
|
511
|
+
|
|
512
|
+
expect(result).toBe('https://claude.ai');
|
|
513
|
+
expect(result).not.toBe(origin);
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
test('should not expose API keys in wrangler.toml', () => {
|
|
517
|
+
// This test validates that API keys are only in .dev.vars
|
|
518
|
+
const wranglerContent = \`
|
|
519
|
+
[vars]
|
|
520
|
+
AGENTSHIELD_API_URL = "https://kya.vouched.id"
|
|
521
|
+
# AGENTSHIELD_API_KEY - Set securely
|
|
522
|
+
\`;
|
|
523
|
+
|
|
524
|
+
expect(wranglerContent).not.toContain('sk_');
|
|
525
|
+
expect(wranglerContent).toContain('Set securely');
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
test('should use short TTLs for security', () => {
|
|
529
|
+
const DELEGATION_TTL = 300; // 5 minutes
|
|
530
|
+
const VERIFICATION_TTL = 60; // 1 minute
|
|
531
|
+
|
|
532
|
+
expect(DELEGATION_TTL).toBeLessThanOrEqual(300);
|
|
533
|
+
expect(VERIFICATION_TTL).toBeLessThanOrEqual(60);
|
|
534
|
+
});
|
|
535
|
+
});
|
|
536
|
+
`;
|
|
537
|
+
fs.writeFileSync(path.join(testsDir, "security.test.ts"), securityTestContent);
|
|
538
|
+
// Create vitest config file
|
|
539
|
+
const vitestConfigContent = `import { defineConfig } from 'vitest/config';
|
|
540
|
+
|
|
541
|
+
export default defineConfig({
|
|
542
|
+
test: {
|
|
543
|
+
environment: 'miniflare',
|
|
544
|
+
environmentOptions: {
|
|
545
|
+
kvNamespaces: [
|
|
546
|
+
'${className.toUpperCase()}_NONCE_CACHE',
|
|
547
|
+
'${className.toUpperCase()}_PROOF_ARCHIVE',
|
|
548
|
+
'${className.toUpperCase()}_IDENTITY_STORAGE',
|
|
549
|
+
'${className.toUpperCase()}_DELEGATION_STORAGE',
|
|
550
|
+
'${className.toUpperCase()}_TOOL_PROTECTION_KV'
|
|
551
|
+
],
|
|
552
|
+
durableObjects: {
|
|
553
|
+
${className.toUpperCase()}_OBJECT: '${pascalClassName}MCP'
|
|
554
|
+
}
|
|
555
|
+
},
|
|
556
|
+
coverage: {
|
|
557
|
+
provider: 'v8',
|
|
558
|
+
reporter: ['text', 'html'],
|
|
559
|
+
exclude: ['node_modules/', 'tests/', '*.config.ts'],
|
|
560
|
+
thresholds: {
|
|
561
|
+
statements: 80,
|
|
562
|
+
branches: 70,
|
|
563
|
+
functions: 80,
|
|
564
|
+
lines: 80
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
`;
|
|
570
|
+
fs.writeFileSync(path.join(projectPath, "vitest.config.ts"), vitestConfigContent);
|
|
69
571
|
// Create greet tool
|
|
70
572
|
const greetToolContent = `import { z } from "zod";
|
|
71
573
|
|
|
@@ -344,6 +846,179 @@ export class ${pascalClassName}MCP extends McpAgent {
|
|
|
344
846
|
}
|
|
345
847
|
}
|
|
346
848
|
|
|
849
|
+
/**
|
|
850
|
+
* Retrieve delegation token from KV storage
|
|
851
|
+
* Uses two-tier lookup: session cache (fast) ā agent DID (stable)
|
|
852
|
+
*
|
|
853
|
+
* @param sessionId - MCP session ID from Claude Desktop
|
|
854
|
+
* @returns Delegation token if found, null otherwise
|
|
855
|
+
*/
|
|
856
|
+
private async getDelegationToken(sessionId?: string): Promise<string | null> {
|
|
857
|
+
const delegationStorage = (this.env as any).${className.toUpperCase()}_DELEGATION_STORAGE;
|
|
858
|
+
|
|
859
|
+
if (!delegationStorage) {
|
|
860
|
+
console.log('[Delegation] No delegation storage configured');
|
|
861
|
+
return null;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
try {
|
|
865
|
+
// Fast path: Try session cache first
|
|
866
|
+
if (sessionId) {
|
|
867
|
+
const sessionKey = \`session:\${sessionId}\`;
|
|
868
|
+
const sessionToken = await delegationStorage.get(sessionKey);
|
|
869
|
+
|
|
870
|
+
if (sessionToken) {
|
|
871
|
+
// Verify token is still valid before returning
|
|
872
|
+
const isValid = await this.verifyDelegationWithAgentShield(sessionToken);
|
|
873
|
+
if (isValid) {
|
|
874
|
+
console.log('[Delegation] ā
Token retrieved from session cache and verified');
|
|
875
|
+
return sessionToken;
|
|
876
|
+
} else {
|
|
877
|
+
// Token invalid, remove from cache
|
|
878
|
+
await this.invalidateDelegationCache(sessionId, sessionToken);
|
|
879
|
+
console.log('[Delegation] ā ļø Cached token was invalid, removed from cache');
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// Fallback: Try agent DID (stable across session changes)
|
|
885
|
+
if (this.mcpiRuntime) {
|
|
886
|
+
const identity = await this.mcpiRuntime.getIdentity();
|
|
887
|
+
if (identity?.did) {
|
|
888
|
+
const agentKey = \`agent:\${identity.did}:delegation\`;
|
|
889
|
+
const agentToken = await delegationStorage.get(agentKey);
|
|
890
|
+
|
|
891
|
+
if (agentToken) {
|
|
892
|
+
// Verify token is still valid before returning
|
|
893
|
+
const isValid = await this.verifyDelegationWithAgentShield(agentToken);
|
|
894
|
+
if (isValid) {
|
|
895
|
+
console.log('[Delegation] ā
Token retrieved using agent DID and verified');
|
|
896
|
+
|
|
897
|
+
// Re-cache for current session (performance optimization)
|
|
898
|
+
if (sessionId) {
|
|
899
|
+
const sessionCacheKey = \`session:\${sessionId}\`;
|
|
900
|
+
await delegationStorage.put(sessionCacheKey, agentToken, {
|
|
901
|
+
expirationTtl: 300 // 5 minutes for security (reduced from 30)
|
|
902
|
+
});
|
|
903
|
+
console.log('[Delegation] Token cached for session with 5-minute TTL:', sessionId);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
return agentToken;
|
|
907
|
+
} else {
|
|
908
|
+
// Token invalid, remove from cache
|
|
909
|
+
await this.invalidateDelegationCache(sessionId, agentToken, identity.did);
|
|
910
|
+
console.log('[Delegation] ā ļø Agent token was invalid, removed from cache');
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
console.log('[Delegation] No delegation token found');
|
|
917
|
+
return null;
|
|
918
|
+
} catch (error) {
|
|
919
|
+
console.error('[Delegation] Failed to retrieve token:', error);
|
|
920
|
+
return null;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/**
|
|
925
|
+
* Verify delegation token with AgentShield API
|
|
926
|
+
* @param token - Delegation token to verify
|
|
927
|
+
* @returns True if token is valid, false otherwise
|
|
928
|
+
*/
|
|
929
|
+
private async verifyDelegationWithAgentShield(token: string): Promise<boolean> {
|
|
930
|
+
// Check verification cache first (1 minute TTL for verified tokens)
|
|
931
|
+
const verificationCache = (this.env as any).TOOL_PROTECTION_KV;
|
|
932
|
+
if (verificationCache) {
|
|
933
|
+
const cacheKey = \`verified:\${token.substring(0, 16)}\`; // Use prefix to avoid key size issues
|
|
934
|
+
const cached = await verificationCache.get(cacheKey);
|
|
935
|
+
if (cached === '1') {
|
|
936
|
+
console.log('[Delegation] Token verification cached as valid');
|
|
937
|
+
return true;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
try {
|
|
942
|
+
const agentShieldUrl = (this.env as any).AGENTSHIELD_API_URL || 'https://hobbs.work';
|
|
943
|
+
const apiKey = (this.env as any).AGENTSHIELD_API_KEY;
|
|
944
|
+
|
|
945
|
+
if (!apiKey) {
|
|
946
|
+
console.warn('[Delegation] No AgentShield API key configured, skipping verification');
|
|
947
|
+
return true; // Allow in development without API key
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// Verify with AgentShield API
|
|
951
|
+
const response = await fetch(\`\${agentShieldUrl}/api/v1/bouncer/delegations/verify\`, {
|
|
952
|
+
method: 'POST',
|
|
953
|
+
headers: {
|
|
954
|
+
'Authorization': \`Bearer \${apiKey}\`,
|
|
955
|
+
'Content-Type': 'application/json'
|
|
956
|
+
},
|
|
957
|
+
body: JSON.stringify({ token })
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
if (response.ok) {
|
|
961
|
+
// Cache successful verification for 1 minute
|
|
962
|
+
if (verificationCache) {
|
|
963
|
+
const cacheKey = \`verified:\${token.substring(0, 16)}\`;
|
|
964
|
+
await verificationCache.put(cacheKey, '1', {
|
|
965
|
+
expirationTtl: 60 // 1 minute cache for verified tokens
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
console.log('[Delegation] Token verified successfully with AgentShield');
|
|
969
|
+
return true;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
if (response.status === 401 || response.status === 403) {
|
|
973
|
+
console.log('[Delegation] Token verification failed: unauthorized');
|
|
974
|
+
return false;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
console.warn('[Delegation] Token verification returned unexpected status:', response.status);
|
|
978
|
+
return false; // Fail closed for security
|
|
979
|
+
|
|
980
|
+
} catch (error) {
|
|
981
|
+
console.error('[Delegation] Error verifying token with AgentShield:', error);
|
|
982
|
+
return false; // Fail closed on errors
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
/**
|
|
987
|
+
* Invalidate delegation token in all caches
|
|
988
|
+
* @param sessionId - Session ID to clear
|
|
989
|
+
* @param token - Token to invalidate
|
|
990
|
+
* @param agentDid - Agent DID to clear
|
|
991
|
+
*/
|
|
992
|
+
private async invalidateDelegationCache(sessionId?: string, token?: string, agentDid?: string): Promise<void> {
|
|
993
|
+
const delegationStorage = (this.env as any).${className.toUpperCase()}_DELEGATION_STORAGE;
|
|
994
|
+
const verificationCache = (this.env as any).TOOL_PROTECTION_KV;
|
|
995
|
+
|
|
996
|
+
if (!delegationStorage) return;
|
|
997
|
+
|
|
998
|
+
const deletions: Promise<void>[] = [];
|
|
999
|
+
|
|
1000
|
+
// Clear session cache
|
|
1001
|
+
if (sessionId) {
|
|
1002
|
+
const sessionKey = \`session:\${sessionId}\`;
|
|
1003
|
+
deletions.push(delegationStorage.delete(sessionKey));
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// Clear agent cache
|
|
1007
|
+
if (agentDid) {
|
|
1008
|
+
const agentKey = \`agent:\${agentDid}:delegation\`;
|
|
1009
|
+
deletions.push(delegationStorage.delete(agentKey));
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// Clear verification cache
|
|
1013
|
+
if (token && verificationCache) {
|
|
1014
|
+
const cacheKey = \`verified:\${token.substring(0, 16)}\`;
|
|
1015
|
+
deletions.push(verificationCache.delete(cacheKey));
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
await Promise.all(deletions);
|
|
1019
|
+
console.log('[Delegation] Cache invalidated for revoked/invalid token');
|
|
1020
|
+
}
|
|
1021
|
+
|
|
347
1022
|
/**
|
|
348
1023
|
* Submit proof to AgentShield API
|
|
349
1024
|
* Uses the proof.jws directly (full JWS format from CloudflareRuntime)
|
|
@@ -438,14 +1113,30 @@ export class ${pascalClassName}MCP extends McpAgent {
|
|
|
438
1113
|
// Use MCP-I runtime's processToolCall for automatic proof generation
|
|
439
1114
|
if (this.mcpiRuntime) {
|
|
440
1115
|
try {
|
|
441
|
-
//
|
|
1116
|
+
// Read MCP session ID from Claude Desktop (via agents framework)
|
|
1117
|
+
let mcpSessionId: string | undefined;
|
|
1118
|
+
try {
|
|
1119
|
+
mcpSessionId = this.getSessionId();
|
|
1120
|
+
console.log('[Delegation] Session ID from agents framework:', mcpSessionId);
|
|
1121
|
+
} catch (error) {
|
|
1122
|
+
console.log('[Delegation] Failed to get session ID from framework:', error);
|
|
1123
|
+
mcpSessionId = undefined;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
// Retrieve delegation token if available
|
|
1127
|
+
const delegationToken = await this.getDelegationToken(mcpSessionId);
|
|
1128
|
+
|
|
1129
|
+
// Create session with proper ID (use actual session ID when available)
|
|
442
1130
|
const timestamp = Date.now();
|
|
1131
|
+
const sessionId = mcpSessionId || \`ephemeral-\${timestamp}-\${Math.random().toString(36).substring(2, 10)}\`;
|
|
1132
|
+
|
|
443
1133
|
const session = {
|
|
444
|
-
id:
|
|
1134
|
+
id: sessionId, // Use actual session ID from Claude Desktop
|
|
445
1135
|
audience: 'https://kya.vouched.id', // CRITICAL: Must match AgentShield domain
|
|
446
1136
|
agentDid: (await this.mcpiRuntime.getIdentity()).did,
|
|
447
1137
|
createdAt: timestamp,
|
|
448
|
-
expiresAt: timestamp + (30 * 60 * 1000) // 30 minutes
|
|
1138
|
+
expiresAt: timestamp + (30 * 60 * 1000), // 30 minutes
|
|
1139
|
+
delegationToken // Include delegation token if available
|
|
449
1140
|
};
|
|
450
1141
|
|
|
451
1142
|
// Execute tool with automatic proof generation
|
|
@@ -469,25 +1160,35 @@ export class ${pascalClassName}MCP extends McpAgent {
|
|
|
469
1160
|
jwsValid: proof.jws.split('.').length === 3
|
|
470
1161
|
});
|
|
471
1162
|
|
|
472
|
-
//
|
|
1163
|
+
// Parallelize proof operations for better performance
|
|
1164
|
+
const proofOperations: Promise<void>[] = [];
|
|
1165
|
+
|
|
1166
|
+
// Add proof archive operation
|
|
473
1167
|
if (this.proofArchive) {
|
|
474
|
-
|
|
475
|
-
|
|
1168
|
+
proofOperations.push(
|
|
1169
|
+
this.proofArchive.store(proof, {
|
|
476
1170
|
toolName: greetTool.name
|
|
477
|
-
})
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
1171
|
+
}).then(() => {
|
|
1172
|
+
console.log('[MCP-I] Proof stored in archive');
|
|
1173
|
+
}).catch((archiveError: any) => {
|
|
1174
|
+
console.error('[MCP-I] Archive error:', archiveError);
|
|
1175
|
+
})
|
|
1176
|
+
);
|
|
482
1177
|
}
|
|
483
1178
|
|
|
484
|
-
//
|
|
1179
|
+
// Add AgentShield submission operation
|
|
485
1180
|
if (this.agentShieldConfig) {
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
1181
|
+
proofOperations.push(
|
|
1182
|
+
this.submitProofToAgentShield(proof, session, greetTool.name, args, result)
|
|
1183
|
+
.catch((err: any) => {
|
|
1184
|
+
console.error('[MCP-I] AgentShield failed:', err.message);
|
|
1185
|
+
})
|
|
1186
|
+
);
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
// Execute all proof operations in parallel for better performance
|
|
1190
|
+
if (proofOperations.length > 0) {
|
|
1191
|
+
await Promise.allSettled(proofOperations);
|
|
491
1192
|
}
|
|
492
1193
|
|
|
493
1194
|
// Attach proof to result for MCP Inspector
|
|
@@ -524,12 +1225,29 @@ export class ${pascalClassName}MCP extends McpAgent {
|
|
|
524
1225
|
|
|
525
1226
|
const app = new Hono();
|
|
526
1227
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
1228
|
+
// Secure CORS configuration
|
|
1229
|
+
app.use("/*", (c, next) => {
|
|
1230
|
+
const allowedOrigins = c.env.ALLOWED_ORIGINS?.split(',').map((o: string) => o.trim()) || [
|
|
1231
|
+
'https://claude.ai',
|
|
1232
|
+
'https://app.anthropic.com'
|
|
1233
|
+
];
|
|
1234
|
+
|
|
1235
|
+
// Add localhost for development if not in production
|
|
1236
|
+
if (c.env.MCPI_ENV !== 'production' && !allowedOrigins.includes('http://localhost:3000')) {
|
|
1237
|
+
allowedOrigins.push('http://localhost:3000');
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
const origin = c.req.header('Origin') || '';
|
|
1241
|
+
const isAllowed = allowedOrigins.includes(origin);
|
|
1242
|
+
|
|
1243
|
+
return cors({
|
|
1244
|
+
origin: isAllowed ? origin : allowedOrigins[0], // Default to first allowed origin if not matched
|
|
1245
|
+
allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
|
1246
|
+
allowHeaders: ["Content-Type", "Authorization", "mcp-session-id", "Mcp-Session-Id", "mcp-protocol-version"],
|
|
1247
|
+
exposeHeaders: ["mcp-session-id", "Mcp-Session-Id"],
|
|
1248
|
+
credentials: true,
|
|
1249
|
+
})(c, next);
|
|
1250
|
+
});
|
|
533
1251
|
|
|
534
1252
|
app.get("/health", (c) => c.json({
|
|
535
1253
|
status: 'healthy',
|
|
@@ -663,8 +1381,69 @@ app.get('/oauth/callback', (c) => {
|
|
|
663
1381
|
})(c);
|
|
664
1382
|
});
|
|
665
1383
|
|
|
666
|
-
|
|
667
|
-
|
|
1384
|
+
/**
|
|
1385
|
+
* Get Durable Object instance ID based on routing strategy
|
|
1386
|
+
* This enables horizontal scaling across multiple DO instances
|
|
1387
|
+
*
|
|
1388
|
+
* @param request - Incoming request
|
|
1389
|
+
* @param env - Environment bindings
|
|
1390
|
+
* @returns Instance ID for DO routing
|
|
1391
|
+
*/
|
|
1392
|
+
function getDoInstanceId(request: Request, env: any): string {
|
|
1393
|
+
const strategy = env.DO_ROUTING_STRATEGY || 'session';
|
|
1394
|
+
const headers = request.headers;
|
|
1395
|
+
|
|
1396
|
+
switch (strategy) {
|
|
1397
|
+
case 'session': {
|
|
1398
|
+
// MCP Protocol compliant session routing - one DO per session
|
|
1399
|
+
const sessionId = headers.get('mcp-session-id') ||
|
|
1400
|
+
headers.get('Mcp-Session-Id') ||
|
|
1401
|
+
crypto.randomUUID();
|
|
1402
|
+
return \`session:\${sessionId}\`;
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
case 'shard': {
|
|
1406
|
+
// Distribute across N shards for high load
|
|
1407
|
+
const identifier = headers.get('mcp-session-id') || Math.random().toString();
|
|
1408
|
+
// Simple hash-based distribution
|
|
1409
|
+
let hash = 0;
|
|
1410
|
+
for (let i = 0; i < identifier.length; i++) {
|
|
1411
|
+
hash = ((hash << 5) - hash) + identifier.charCodeAt(i);
|
|
1412
|
+
hash = hash & hash; // Convert to 32bit integer
|
|
1413
|
+
}
|
|
1414
|
+
const shardCount = parseInt(env.DO_SHARD_COUNT || '10');
|
|
1415
|
+
const shard = Math.abs(hash) % shardCount;
|
|
1416
|
+
return \`shard:\${shard}\`;
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
default:
|
|
1420
|
+
// Fallback to single instance (legacy behavior)
|
|
1421
|
+
return 'default';
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
// Multi-instance DO routing for scalability
|
|
1426
|
+
// Legacy direct mount (single DO instance - bottleneck):
|
|
1427
|
+
// app.mount("/mcp", ${pascalClassName}MCP.serve("/mcp").fetch);
|
|
1428
|
+
|
|
1429
|
+
// New scalable routing (multiple DO instances):
|
|
1430
|
+
app.all('/sse/*', async (c) => {
|
|
1431
|
+
const instanceId = getDoInstanceId(c.req.raw, c.env);
|
|
1432
|
+
const doId = c.env.MCP_OBJECT.idFromName(instanceId);
|
|
1433
|
+
const stub = c.env.MCP_OBJECT.get(doId);
|
|
1434
|
+
|
|
1435
|
+
console.log(\`[DO Routing] SSE request routed to instance: \${instanceId}\`);
|
|
1436
|
+
return stub.fetch(c.req.raw);
|
|
1437
|
+
});
|
|
1438
|
+
|
|
1439
|
+
app.all('/mcp/*', async (c) => {
|
|
1440
|
+
const instanceId = getDoInstanceId(c.req.raw, c.env);
|
|
1441
|
+
const doId = c.env.MCP_OBJECT.idFromName(instanceId);
|
|
1442
|
+
const stub = c.env.MCP_OBJECT.get(doId);
|
|
1443
|
+
|
|
1444
|
+
console.log(\`[DO Routing] MCP request routed to instance: \${instanceId}\`);
|
|
1445
|
+
return stub.fetch(c.req.raw);
|
|
1446
|
+
});
|
|
668
1447
|
|
|
669
1448
|
export default app;
|
|
670
1449
|
`;
|
|
@@ -759,9 +1538,10 @@ XMCP_I_TS_SKEW_SEC = "120"
|
|
|
759
1538
|
XMCP_I_SESSION_TTL = "1800"
|
|
760
1539
|
|
|
761
1540
|
# AgentShield Integration (https://kya.vouched.id)
|
|
762
|
-
# ${apikey ? 'Configure' : 'Uncomment and configure'} these variables to enable proof submission to AgentShield
|
|
763
1541
|
AGENTSHIELD_API_URL = "https://kya.vouched.id"
|
|
764
|
-
|
|
1542
|
+
# AGENTSHIELD_API_KEY - Set securely using one of these methods:
|
|
1543
|
+
# Development: Add to .dev.vars file (already configured if --apikey was provided)
|
|
1544
|
+
# Production: wrangler secret put AGENTSHIELD_API_KEY
|
|
765
1545
|
MCPI_ENV = "development"
|
|
766
1546
|
|
|
767
1547
|
# Optional: MCP Server URL for tool discovery
|
|
@@ -778,31 +1558,72 @@ MCPI_ENV = "development"
|
|
|
778
1558
|
const wranglerPath = path.join(projectPath, "wrangler.toml");
|
|
779
1559
|
let wranglerTomlContent = fs.readFileSync(wranglerPath, "utf8");
|
|
780
1560
|
// Find [vars] section and add identity environment variables
|
|
1561
|
+
// Only add the DID to wrangler.toml (public info, safe to commit)
|
|
781
1562
|
const varsMatch = wranglerTomlContent.match(/\[vars\]/);
|
|
782
1563
|
if (varsMatch) {
|
|
783
1564
|
const insertPosition = varsMatch.index + varsMatch[0].length;
|
|
784
1565
|
const identityVars = `
|
|
785
|
-
#
|
|
786
|
-
# SECURITY: For production, use \`wrangler secret put\` instead of storing in wrangler.toml
|
|
787
|
-
# Development: These values work for local testing and dev deployments
|
|
1566
|
+
# Agent DID (public identifier - safe to commit)
|
|
788
1567
|
MCP_IDENTITY_AGENT_DID = "${identity.did}"
|
|
789
|
-
|
|
790
|
-
|
|
1568
|
+
|
|
1569
|
+
# ALLOWED_ORIGINS for CORS (update for production)
|
|
1570
|
+
ALLOWED_ORIGINS = "https://claude.ai,https://app.anthropic.com"
|
|
1571
|
+
|
|
1572
|
+
# DO routing strategy: "session" for dev, "shard" for production high-load
|
|
1573
|
+
DO_ROUTING_STRATEGY = "session"
|
|
1574
|
+
DO_SHARD_COUNT = "10" # Number of shards if using shard strategy
|
|
791
1575
|
|
|
792
1576
|
`;
|
|
793
1577
|
wranglerTomlContent =
|
|
794
1578
|
wranglerTomlContent.slice(0, insertPosition) +
|
|
795
1579
|
identityVars +
|
|
796
1580
|
wranglerTomlContent.slice(insertPosition);
|
|
797
|
-
// Write updated wrangler.toml
|
|
1581
|
+
// Write updated wrangler.toml (without secrets)
|
|
798
1582
|
fs.writeFileSync(wranglerPath, wranglerTomlContent);
|
|
1583
|
+
// Create .dev.vars file for local development (git-ignored)
|
|
1584
|
+
const devVarsPath = path.join(projectPath, ".dev.vars");
|
|
1585
|
+
const devVarsContent = `# Local development secrets (DO NOT COMMIT)
|
|
1586
|
+
# This file is git-ignored and contains sensitive data
|
|
1587
|
+
|
|
1588
|
+
# Identity keys (generated by create-mcpi-app)
|
|
1589
|
+
MCP_IDENTITY_PRIVATE_KEY="${identity.privateKey}"
|
|
1590
|
+
MCP_IDENTITY_PUBLIC_KEY="${identity.publicKey}"
|
|
1591
|
+
|
|
1592
|
+
# AgentShield API key (get from https://agentshield.ai)
|
|
1593
|
+
AGENTSHIELD_API_KEY="${apikey || ''}"${apikey ? ' # Provided via --apikey flag' : ''}
|
|
1594
|
+
|
|
1595
|
+
# Admin API key for protected endpoints
|
|
1596
|
+
ADMIN_API_KEY=""
|
|
1597
|
+
`;
|
|
1598
|
+
fs.writeFileSync(devVarsPath, devVarsContent);
|
|
1599
|
+
// Create .dev.vars.example for reference
|
|
1600
|
+
const devVarsExamplePath = path.join(projectPath, ".dev.vars.example");
|
|
1601
|
+
const devVarsExampleContent = `# Copy this file to .dev.vars and fill in your values
|
|
1602
|
+
# DO NOT commit .dev.vars to version control
|
|
1603
|
+
|
|
1604
|
+
# Identity keys (generate with: npx @kya-os/create-mcpi-app regenerate-identity)
|
|
1605
|
+
MCP_IDENTITY_PRIVATE_KEY="your-private-key-here"
|
|
1606
|
+
MCP_IDENTITY_PUBLIC_KEY="your-public-key-here"
|
|
1607
|
+
|
|
1608
|
+
# AgentShield API key (get from https://agentshield.ai)
|
|
1609
|
+
AGENTSHIELD_API_KEY="your-api-key-here"
|
|
1610
|
+
|
|
1611
|
+
# Admin API key for protected endpoints
|
|
1612
|
+
ADMIN_API_KEY="your-admin-key-here"
|
|
1613
|
+
`;
|
|
1614
|
+
fs.writeFileSync(devVarsExamplePath, devVarsExampleContent);
|
|
799
1615
|
console.log(chalk.green("ā
Generated persistent identity"));
|
|
800
1616
|
console.log(chalk.dim(` DID: ${identity.did}`));
|
|
801
|
-
console.log(chalk.
|
|
1617
|
+
console.log(chalk.green("ā
Created secure configuration:"));
|
|
1618
|
+
console.log(chalk.dim(" ⢠Public DID in wrangler.toml (safe to commit)"));
|
|
1619
|
+
console.log(chalk.dim(" ⢠Private keys in .dev.vars (git-ignored)"));
|
|
1620
|
+
console.log(chalk.dim(" ⢠Example template in .dev.vars.example"));
|
|
802
1621
|
console.log();
|
|
803
|
-
console.log(chalk.yellow("
|
|
804
|
-
console.log(chalk.dim("
|
|
1622
|
+
console.log(chalk.yellow("š Production Security:"));
|
|
1623
|
+
console.log(chalk.dim(" Set secrets using wrangler (never commit them):"));
|
|
805
1624
|
console.log(chalk.cyan(" $ wrangler secret put MCP_IDENTITY_PRIVATE_KEY"));
|
|
1625
|
+
console.log(chalk.cyan(" $ wrangler secret put MCP_IDENTITY_PUBLIC_KEY"));
|
|
1626
|
+
console.log(chalk.cyan(" $ wrangler secret put AGENTSHIELD_API_KEY"));
|
|
806
1627
|
console.log();
|
|
807
1628
|
}
|
|
808
1629
|
}
|