@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.0",
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
- // Create ephemeral session for stateless Cloudflare Workers
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: \`ephemeral-\${timestamp}-\${Math.random().toString(36).substring(2, 10)}\`,
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
- // Store in KV archive
1163
+ // Parallelize proof operations for better performance
1164
+ const proofOperations: Promise<void>[] = [];
1165
+
1166
+ // Add proof archive operation
473
1167
  if (this.proofArchive) {
474
- try {
475
- await this.proofArchive.store(proof, {
1168
+ proofOperations.push(
1169
+ this.proofArchive.store(proof, {
476
1170
  toolName: greetTool.name
477
- });
478
- console.log('[MCP-I] Proof stored in archive');
479
- } catch (archiveError) {
480
- console.error('[MCP-I] Archive error:', archiveError);
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
- // Submit to AgentShield with context
1179
+ // Add AgentShield submission operation
485
1180
  if (this.agentShieldConfig) {
486
- try {
487
- await this.submitProofToAgentShield(proof, session, greetTool.name, args, result);
488
- } catch (err: any) {
489
- console.error('[MCP-I] AgentShield failed:', err.message);
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
- app.use("/*", cors({
528
- origin: "*",
529
- allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
530
- allowHeaders: ["Content-Type", "Authorization", "mcp-session-id", "mcp-protocol-version"],
531
- exposeHeaders: ["mcp-session-id"],
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
- app.mount("/sse", ${pascalClassName}MCP.serveSSE("/sse").fetch, { replaceRequest: false });
667
- app.mount("/mcp", ${pascalClassName}MCP.serve("/mcp").fetch, { replaceRequest: false });
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
- ${apikey ? `AGENTSHIELD_API_KEY = "${apikey}" # Provided via --apikey flag` : '# AGENTSHIELD_API_KEY = "sk_your_api_key_here" # Get from https://kya.vouched.id/dashboard'}
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
- # Persistent Identity (generated by create-mcpi-app)
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
- MCP_IDENTITY_PRIVATE_KEY = "${identity.privateKey}"
790
- MCP_IDENTITY_PUBLIC_KEY = "${identity.publicKey}"
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.dim(` Keys stored in wrangler.toml [vars] section`));
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("āš ļø Security Note:"));
804
- console.log(chalk.dim(" For production, move secrets to wrangler using:"));
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
  }