@kya-os/create-mcpi-app 1.7.17 → 1.7.20

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.
Files changed (98) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/.turbo/turbo-test$colon$coverage.log +315 -0
  3. package/.turbo/turbo-test.log +95 -0
  4. package/CHANGELOG.md +372 -0
  5. package/IMPLEMENTATION_SUMMARY.md +108 -0
  6. package/REMEDIATION_PLAN.md +99 -0
  7. package/coverage/base.css +224 -0
  8. package/coverage/block-navigation.js +87 -0
  9. package/coverage/clover.xml +252 -0
  10. package/coverage/config-builder.ts.html +580 -0
  11. package/coverage/coverage-final.json +7 -0
  12. package/coverage/favicon.png +0 -0
  13. package/coverage/fetch-cloudflare-mcpi-template.ts.html +7006 -0
  14. package/coverage/generate-config.ts.html +436 -0
  15. package/coverage/generate-identity.ts.html +574 -0
  16. package/coverage/index.html +191 -0
  17. package/coverage/install.ts.html +322 -0
  18. package/coverage/prettify.css +1 -0
  19. package/coverage/prettify.js +2 -0
  20. package/coverage/sort-arrow-sprite.png +0 -0
  21. package/coverage/sorter.js +210 -0
  22. package/coverage/validate-project-structure.ts.html +466 -0
  23. package/dist/.tsbuildinfo +1 -1
  24. package/dist/helpers/__tests__/config-builder.spec.d.ts +8 -0
  25. package/dist/helpers/__tests__/config-builder.spec.d.ts.map +1 -0
  26. package/dist/helpers/__tests__/config-builder.spec.js +182 -0
  27. package/dist/helpers/__tests__/config-builder.spec.js.map +1 -0
  28. package/dist/helpers/config-builder.d.ts +58 -0
  29. package/dist/helpers/config-builder.d.ts.map +1 -0
  30. package/dist/helpers/config-builder.js +102 -0
  31. package/dist/helpers/config-builder.js.map +1 -0
  32. package/dist/helpers/create.d.ts +1 -0
  33. package/dist/helpers/create.d.ts.map +1 -1
  34. package/dist/helpers/create.js +2 -1
  35. package/dist/helpers/create.js.map +1 -1
  36. package/dist/helpers/fetch-cloudflare-mcpi-template.d.ts +1 -0
  37. package/dist/helpers/fetch-cloudflare-mcpi-template.d.ts.map +1 -1
  38. package/dist/helpers/fetch-cloudflare-mcpi-template.js +209 -174
  39. package/dist/helpers/fetch-cloudflare-mcpi-template.js.map +1 -1
  40. package/dist/helpers/fetch-mcpi-template.d.ts.map +1 -1
  41. package/dist/helpers/fetch-mcpi-template.js +18 -3
  42. package/dist/helpers/fetch-mcpi-template.js.map +1 -1
  43. package/dist/helpers/generate-config.d.ts.map +1 -1
  44. package/dist/helpers/generate-config.js +27 -40
  45. package/dist/helpers/generate-config.js.map +1 -1
  46. package/dist/helpers/install.js +5 -0
  47. package/dist/helpers/install.js.map +1 -1
  48. package/dist/index.js +2 -0
  49. package/dist/index.js.map +1 -1
  50. package/package.json +18 -9
  51. package/scripts/prepare-pack.js +47 -0
  52. package/scripts/validate-no-workspace.js +79 -0
  53. package/src/__tests__/cloudflare-template.test.ts +488 -0
  54. package/src/__tests__/helpers/fetch-cloudflare-mcpi-template.test.ts +337 -0
  55. package/src/__tests__/helpers/generate-config.test.ts +312 -0
  56. package/src/__tests__/helpers/generate-identity.test.ts +271 -0
  57. package/src/__tests__/helpers/install.test.ts +362 -0
  58. package/src/__tests__/helpers/validate-project-structure.test.ts +467 -0
  59. package/src/__tests__.bak/regression.test.ts +434 -0
  60. package/src/effects/index.ts +80 -0
  61. package/src/helpers/__tests__/config-builder.spec.ts +231 -0
  62. package/src/helpers/apply-identity-preset.ts +209 -0
  63. package/src/helpers/config-builder.ts +165 -0
  64. package/src/helpers/copy-template.ts +11 -0
  65. package/src/helpers/create.ts +239 -0
  66. package/src/helpers/fetch-cloudflare-mcpi-template.ts +2311 -0
  67. package/src/helpers/fetch-cloudflare-template.ts +361 -0
  68. package/src/helpers/fetch-mcpi-template.ts +236 -0
  69. package/src/helpers/fetch-xmcp-template.ts +153 -0
  70. package/src/helpers/generate-config.ts +117 -0
  71. package/src/helpers/generate-identity.ts +163 -0
  72. package/src/helpers/identity-manager.ts +186 -0
  73. package/src/helpers/install.ts +79 -0
  74. package/src/helpers/rename.ts +17 -0
  75. package/src/helpers/validate-project-structure.ts +127 -0
  76. package/src/index.ts +480 -0
  77. package/src/utils/check-node.ts +17 -0
  78. package/src/utils/is-folder-empty.ts +60 -0
  79. package/src/utils/validate-project-name.ts +132 -0
  80. package/test-cloudflare/README.md +164 -0
  81. package/test-cloudflare/package.json +28 -0
  82. package/test-cloudflare/src/index.ts +340 -0
  83. package/test-cloudflare/src/tools/greet.ts +19 -0
  84. package/test-cloudflare/tests/cache-invalidation.test.ts +410 -0
  85. package/test-cloudflare/tests/cors-security.test.ts +349 -0
  86. package/test-cloudflare/tests/delegation.test.ts +335 -0
  87. package/test-cloudflare/tests/do-routing.test.ts +314 -0
  88. package/test-cloudflare/tests/integration.test.ts +205 -0
  89. package/test-cloudflare/tests/session-management.test.ts +359 -0
  90. package/test-cloudflare/tsconfig.json +22 -0
  91. package/test-cloudflare/vitest.config.ts +9 -0
  92. package/test-cloudflare/wrangler.toml +37 -0
  93. package/test-node/README.md +44 -0
  94. package/test-node/package.json +23 -0
  95. package/test-node/src/tools/greet.ts +25 -0
  96. package/test-node/xmcp.config.ts +20 -0
  97. package/tsconfig.json +26 -0
  98. package/vitest.config.ts +14 -0
@@ -0,0 +1,2311 @@
1
+ import fs from "fs-extra";
2
+ import path from "path";
3
+ import chalk from "chalk";
4
+ import { generateIdentity } from "./generate-identity.js";
5
+
6
+ interface CloudflareMcpiTemplateOptions {
7
+ packageManager?: string;
8
+ projectName?: string;
9
+ apikey?: string;
10
+ projectId?: string;
11
+ skipIdentity?: boolean;
12
+ }
13
+
14
+ /**
15
+ * Scaffold Cloudflare Worker MCP server
16
+ * Uses McpAgent from agents/mcp for MCP protocol support
17
+ */
18
+ export async function fetchCloudflareMcpiTemplate(
19
+ projectPath: string,
20
+ options: CloudflareMcpiTemplateOptions = {}
21
+ ): Promise<void> {
22
+ const {
23
+ packageManager = "npm",
24
+ projectName = path.basename(projectPath),
25
+ apikey,
26
+ projectId,
27
+ skipIdentity = false,
28
+ } = options;
29
+
30
+ // Sanitize project name for class names
31
+ const className = projectName
32
+ .replace(/[^a-zA-Z0-9]/g, "")
33
+ .replace(/^[0-9]/, "_$&");
34
+ const pascalClassName =
35
+ className.charAt(0).toUpperCase() + className.slice(1);
36
+
37
+ try {
38
+ console.log(chalk.blue("📦 Setting up Cloudflare Worker MCP server..."));
39
+
40
+ // Create package.json
41
+ const packageJson = {
42
+ name: projectName,
43
+ version: "0.1.0",
44
+ private: true,
45
+ scripts: {
46
+ setup: "node scripts/setup.js",
47
+ postinstall: "npm run setup",
48
+ deploy: "wrangler deploy",
49
+ dev: "wrangler dev",
50
+ start: "wrangler dev",
51
+ "kv:create":
52
+ "npm run kv:create-nonce && npm run kv:create-proof && npm run kv:create-identity && npm run kv:create-delegation && npm run kv:create-tool-protection",
53
+ "kv:create-nonce": `wrangler kv namespace create ${className.toUpperCase()}_NONCE_CACHE`,
54
+ "kv:create-proof": `wrangler kv namespace create ${className.toUpperCase()}_PROOF_ARCHIVE`,
55
+ "kv:create-identity": `wrangler kv namespace create ${className.toUpperCase()}_IDENTITY_STORAGE`,
56
+ "kv:create-delegation": `wrangler kv namespace create ${className.toUpperCase()}_DELEGATION_STORAGE`,
57
+ "kv:create-tool-protection": `wrangler kv namespace create ${className.toUpperCase()}_TOOL_PROTECTION_KV`,
58
+ "kv:list":
59
+ "wrangler kv namespace list | grep -E '(NONCE|PROOF|IDENTITY|DELEGATION|TOOL_PROTECTION|MCPI)' || wrangler kv namespace list",
60
+ "kv:keys-nonce": `wrangler kv key list --binding=${className.toUpperCase()}_NONCE_CACHE`,
61
+ "kv:keys-proof": `wrangler kv key list --binding=${className.toUpperCase()}_PROOF_ARCHIVE`,
62
+ "kv:keys-identity": `wrangler kv key list --binding=${className.toUpperCase()}_IDENTITY_STORAGE`,
63
+ "kv:keys-delegation": `wrangler kv key list --binding=${className.toUpperCase()}_DELEGATION_STORAGE`,
64
+ "kv:keys-tool-protection": `wrangler kv key list --binding=${className.toUpperCase()}_TOOL_PROTECTION_KV`,
65
+ "kv:delete-nonce": `wrangler kv namespace delete --binding=${className.toUpperCase()}_NONCE_CACHE`,
66
+ "kv:delete-proof": `wrangler kv namespace delete --binding=${className.toUpperCase()}_PROOF_ARCHIVE`,
67
+ "kv:delete-identity": `wrangler kv namespace delete --binding=${className.toUpperCase()}_IDENTITY_STORAGE`,
68
+ "kv:delete-delegation": `wrangler kv namespace delete --binding=${className.toUpperCase()}_DELEGATION_STORAGE`,
69
+ "kv:delete-tool-protection": `wrangler kv namespace delete --binding=${className.toUpperCase()}_TOOL_PROTECTION_KV`,
70
+ "kv:delete":
71
+ "npm run kv:delete-nonce && npm run kv:delete-proof && npm run kv:delete-identity && npm run kv:delete-delegation && npm run kv:delete-tool-protection",
72
+ "kv:reset": "npm run kv:delete && npm run kv:create",
73
+ "kv:setup":
74
+ "echo 'KV Commands: kv:create (create all), kv:list (list all), kv:keys-* (view keys), kv:delete (delete all), kv:reset (delete+recreate)'",
75
+ "cf-typegen": "wrangler types",
76
+ "type-check": "tsc --noEmit",
77
+ test: "vitest",
78
+ "test:watch": "vitest --watch",
79
+ "test:coverage": "vitest run --coverage",
80
+ },
81
+ dependencies: {
82
+ "@kya-os/mcp-i-cloudflare": "^1.3.2",
83
+ "@modelcontextprotocol/sdk": "^1.19.1",
84
+ agents: "^0.2.8",
85
+ hono: "^4.9.10",
86
+ zod: "^3.25.76",
87
+ },
88
+ devDependencies: {
89
+ "@cloudflare/workers-types": "^4.20240925.0",
90
+ "@vitest/coverage-v8": "^3.2.4",
91
+ miniflare: "^3.0.0",
92
+ typescript: "^5.6.2",
93
+ vitest: "^3.2.4",
94
+ wrangler: "^4.42.2",
95
+ },
96
+ };
97
+
98
+ fs.ensureDirSync(projectPath);
99
+ fs.writeJsonSync(path.join(projectPath, "package.json"), packageJson, {
100
+ spaces: 2,
101
+ });
102
+
103
+ // Create src directory and tools
104
+ const srcDir = path.join(projectPath, "src");
105
+ const toolsDir = path.join(srcDir, "tools");
106
+ fs.ensureDirSync(toolsDir);
107
+
108
+ // Create scripts directory
109
+ const scriptsDir = path.join(projectPath, "scripts");
110
+ fs.ensureDirSync(scriptsDir);
111
+
112
+ // Create setup.js automation script
113
+ const setupScriptContent = `#!/usr/bin/env node
114
+
115
+ /**
116
+ * Automated Setup Script for ${projectName}
117
+ *
118
+ * This script automates the tedious process of:
119
+ * 1. Checking/installing wrangler
120
+ * 2. Creating KV namespaces
121
+ * 3. Extracting namespace IDs
122
+ * 4. Updating wrangler.toml automatically
123
+ * 5. Setting up local development environment
124
+ */
125
+
126
+ const { execSync } = require('child_process');
127
+ const fs = require('fs');
128
+ const path = require('path');
129
+ const readline = require('readline');
130
+
131
+ const rl = readline.createInterface({
132
+ input: process.stdin,
133
+ output: process.stdout
134
+ });
135
+
136
+ // Colors for terminal output
137
+ const colors = {
138
+ reset: '\\x1b[0m',
139
+ bright: '\\x1b[1m',
140
+ green: '\\x1b[32m',
141
+ yellow: '\\x1b[33m',
142
+ blue: '\\x1b[36m',
143
+ red: '\\x1b[31m'
144
+ };
145
+
146
+ function log(message, color = colors.reset) {
147
+ console.log(color + message + colors.reset);
148
+ }
149
+
150
+ function skipToPostKVSetup() {
151
+ // Skip KV creation but continue with other setup steps
152
+ const devVarsPath = path.join(__dirname, '..', '.dev.vars');
153
+ const devVarsExamplePath = path.join(__dirname, '..', '.dev.vars.example');
154
+
155
+ // Create .dev.vars from example if it doesn't exist
156
+ if (!fs.existsSync(devVarsPath) && fs.existsSync(devVarsExamplePath)) {
157
+ log('\\n📋 Creating .dev.vars from example...', colors.blue);
158
+ fs.copyFileSync(devVarsExamplePath, devVarsPath);
159
+ log('✅ Created .dev.vars - Please update with your values', colors.green);
160
+ }
161
+
162
+ // Check if identity needs to be generated
163
+ if (fs.existsSync(devVarsPath)) {
164
+ const devVarsContent = fs.readFileSync(devVarsPath, 'utf-8');
165
+ if (devVarsContent.includes('your-private-key-here')) {
166
+ log('\\n🔑 Generating agent identity...', colors.blue);
167
+ try {
168
+ execSync('npx @kya-os/create-mcpi-app regenerate-identity', { stdio: 'inherit' });
169
+ log('✅ Identity generated successfully', colors.green);
170
+ } catch {
171
+ log('⚠️ Could not generate identity automatically. Run: npx @kya-os/create-mcpi-app regenerate-identity', colors.yellow);
172
+ }
173
+ }
174
+ }
175
+
176
+ // Show modified next steps
177
+ log('\\n✨ Setup partially complete! Next steps:\\n', colors.bright + colors.green);
178
+ log('1. Set your Cloudflare account ID (required for KV namespaces):', colors.blue);
179
+ log(' export CLOUDFLARE_ACCOUNT_ID=your-account-id', colors.reset);
180
+ log(' OR add to wrangler.toml: account_id = "your-account-id"\\n', colors.reset);
181
+ log('2. Create KV namespaces: npm run kv:create', colors.blue);
182
+ log('3. Review .dev.vars and add any missing values', colors.blue);
183
+ log('4. Start development server: npm run dev', colors.blue);
184
+ log('5. Deploy to production: npm run deploy', colors.blue);
185
+ log('\\nUseful commands:', colors.bright);
186
+ log(' npm run dev - Start local development server');
187
+ log(' npm run deploy - Deploy to Cloudflare Workers');
188
+ log(' npm run kv:list - List all KV namespaces');
189
+ log(' wrangler secret put <KEY> - Set production secrets');
190
+ log('\\nFor more information, see the README.md file.\\n');
191
+
192
+ rl.close();
193
+ }
194
+
195
+ async function setup() {
196
+ log('\\n🚀 Starting automated setup for ${projectName}...\\n', colors.bright + colors.blue);
197
+
198
+ // 1. Check wrangler installation
199
+ try {
200
+ const wranglerVersion = execSync('wrangler --version', { encoding: 'utf-8' });
201
+ log('✅ Wrangler CLI detected: ' + wranglerVersion.trim(), colors.green);
202
+ } catch {
203
+ log('📦 Wrangler CLI not found. Installing...', colors.yellow);
204
+ try {
205
+ execSync('npm install -g wrangler', { stdio: 'inherit' });
206
+ log('✅ Wrangler CLI installed successfully', colors.green);
207
+ } catch (error) {
208
+ log('❌ Failed to install Wrangler. Please install manually: npm install -g wrangler', colors.red);
209
+ process.exit(1);
210
+ }
211
+ }
212
+
213
+ // 2. Check if user is logged in to Cloudflare
214
+ try {
215
+ execSync('wrangler whoami', { encoding: 'utf-8' });
216
+ log('✅ Logged in to Cloudflare', colors.green);
217
+ } catch {
218
+ log('🔑 Please log in to Cloudflare:', colors.yellow);
219
+ try {
220
+ execSync('wrangler login', { stdio: 'inherit' });
221
+ } catch (error) {
222
+ log('❌ Login failed. Please run: wrangler login', colors.red);
223
+ process.exit(1);
224
+ }
225
+ }
226
+
227
+ // 2.5. Set up wrangler.toml path for later use
228
+ const wranglerTomlPath = path.join(__dirname, '..', 'wrangler.toml');
229
+ let wranglerContent = '';
230
+
231
+ try {
232
+ wranglerContent = fs.readFileSync(wranglerTomlPath, 'utf-8');
233
+ } catch (error) {
234
+ log('⚠️ Could not read wrangler.toml', colors.yellow);
235
+ }
236
+
237
+ // 3. Create KV namespaces
238
+ log('\\n📚 Creating KV namespaces...\\n', colors.bright);
239
+
240
+ const namespaces = [
241
+ { binding: '${className.toUpperCase()}_NONCE_CACHE', name: 'Nonce Cache', purpose: 'Replay attack prevention' },
242
+ { binding: '${className.toUpperCase()}_PROOF_ARCHIVE', name: 'Proof Archive', purpose: 'Cryptographic proof storage' },
243
+ { binding: '${className.toUpperCase()}_IDENTITY_STORAGE', name: 'Identity Storage', purpose: 'Agent identity persistence' },
244
+ { binding: '${className.toUpperCase()}_DELEGATION_STORAGE', name: 'Delegation Storage', purpose: 'OAuth token storage' },
245
+ { binding: '${className.toUpperCase()}_TOOL_PROTECTION_KV', name: 'Tool Protection', purpose: 'Permission caching' }
246
+ ];
247
+
248
+ const kvIds = {};
249
+ let multipleAccountsDetected = false;
250
+
251
+ for (const ns of namespaces) {
252
+ // If we already detected multiple accounts, skip remaining namespaces
253
+ if (multipleAccountsDetected) {
254
+ break;
255
+ }
256
+
257
+ log(\`Creating \${ns.name} (\${ns.purpose})...\`, colors.blue);
258
+
259
+ try {
260
+ // Create the namespace
261
+ const output = execSync(\`wrangler kv namespace create "\${ns.binding}"\`, { encoding: 'utf-8', stderr: 'pipe' });
262
+
263
+ // Extract the ID from output
264
+ const idMatch = output.match(/id = "([^"]+)"/);
265
+
266
+ if (idMatch && idMatch[1]) {
267
+ kvIds[ns.binding] = idMatch[1];
268
+ log(\` ✅ Created with ID: \${idMatch[1]}\`, colors.green);
269
+ } else {
270
+ // Try to get existing namespace
271
+ const listOutput = execSync('wrangler kv namespace list', { encoding: 'utf-8' });
272
+
273
+ try {
274
+ const namespaces = JSON.parse(listOutput);
275
+ const existingNamespace = namespaces.find(n => n.title === ns.binding);
276
+
277
+ if (existingNamespace && existingNamespace.id) {
278
+ kvIds[ns.binding] = existingNamespace.id;
279
+ log(\` ⚠️ Namespace already exists with ID: \${existingNamespace.id}\`, colors.yellow);
280
+ } else {
281
+ log(\` ⚠️ Could not extract ID for \${ns.binding}. You may need to add it manually.\`, colors.yellow);
282
+ }
283
+ } catch (parseError) {
284
+ // Fallback to regex if JSON parsing fails
285
+ const existingMatch = listOutput.match(new RegExp(\`"title":\\s*"\${ns.binding}"[^}]*"id":\\s*"([^"]+)"\`));
286
+
287
+ if (existingMatch && existingMatch[1]) {
288
+ kvIds[ns.binding] = existingMatch[1];
289
+ log(\` ⚠️ Namespace already exists with ID: \${existingMatch[1]}\`, colors.yellow);
290
+ } else {
291
+ log(\` ⚠️ Could not extract ID for \${ns.binding}. You may need to add it manually.\`, colors.yellow);
292
+ }
293
+ }
294
+ }
295
+ } catch (error) {
296
+ const errorMessage = error.message || error.toString();
297
+
298
+ // Check if this is a multiple accounts error
299
+ if (errorMessage.includes('More than one account') || errorMessage.includes('multiple accounts')) {
300
+ multipleAccountsDetected = true;
301
+ log('\\n⚠️ Multiple Cloudflare accounts detected!\\n', colors.yellow);
302
+ log('Wrangler cannot automatically select an account in non-interactive mode.\\n', colors.yellow);
303
+ log('To fix this, choose one of these options:\\n', colors.bright);
304
+ log('Option 1: Set environment variable (recommended):', colors.blue);
305
+ log(' export CLOUDFLARE_ACCOUNT_ID=your-account-id', colors.reset);
306
+ log(' npm run setup\\n', colors.reset);
307
+ log('Option 2: Add to wrangler.toml (permanent):', colors.blue);
308
+ log(' Edit wrangler.toml and add:', colors.reset);
309
+ log(' account_id = "your-account-id"\\n', colors.reset);
310
+ log('Find your account IDs in the error above or run:', colors.blue);
311
+ log(' wrangler whoami\\n', colors.reset);
312
+ log('⏭️ Skipping remaining KV namespace creation.', colors.yellow);
313
+ log('After setting account_id, run: npm run setup\\n', colors.yellow);
314
+ break;
315
+ }
316
+
317
+ // Check if namespace already exists
318
+ try {
319
+ const listOutput = execSync('wrangler kv namespace list', { encoding: 'utf-8' });
320
+
321
+ // Parse JSON output
322
+ try {
323
+ const namespaces = JSON.parse(listOutput);
324
+
325
+ // Look for namespace by title (which matches the binding name)
326
+ const existingNamespace = namespaces.find(n => n.title === ns.binding);
327
+
328
+ if (existingNamespace && existingNamespace.id) {
329
+ kvIds[ns.binding] = existingNamespace.id;
330
+ log(\` ⚠️ Found existing namespace with ID: \${existingNamespace.id}\`, colors.yellow);
331
+ } else {
332
+ log(\` ⚠️ Could not find existing namespace: \${ns.binding}\`, colors.yellow);
333
+ log(\` Error: \${errorMessage}\`, colors.yellow);
334
+ }
335
+ } catch (parseError) {
336
+ // If JSON parse fails, try regex as fallback
337
+ const existingMatch = listOutput.match(new RegExp(\`"title":\\s*"\${ns.binding}"[^}]*"id":\\s*"([^"]+)"\`));
338
+
339
+ if (existingMatch && existingMatch[1]) {
340
+ kvIds[ns.binding] = existingMatch[1];
341
+ log(\` ⚠️ Found existing namespace with ID: \${existingMatch[1]}\`, colors.yellow);
342
+ } else {
343
+ log(\` ❌ Failed to create \${ns.binding}: \${errorMessage}\`, colors.red);
344
+ }
345
+ }
346
+ } catch (listError) {
347
+ log(\` ❌ Failed to create or find \${ns.binding}\`, colors.red);
348
+ }
349
+ }
350
+ }
351
+
352
+ // If multiple accounts detected, skip to post-KV setup
353
+ if (multipleAccountsDetected) {
354
+ return skipToPostKVSetup();
355
+ }
356
+
357
+ // 4. Update wrangler.toml with KV IDs
358
+ if (Object.keys(kvIds).length > 0) {
359
+ log('\\n📝 Updating wrangler.toml with KV namespace IDs...\\n', colors.bright);
360
+
361
+ try {
362
+ wranglerContent = fs.readFileSync(wranglerTomlPath, 'utf-8');
363
+ let updatedCount = 0;
364
+
365
+ for (const [binding, id] of Object.entries(kvIds)) {
366
+ // Match pattern: binding = "BINDING_NAME"\\nid = "anything" (including placeholders)
367
+ const pattern = new RegExp(\`(binding = "\${binding}")\\\\s*\\\\nid = "[^"]*"\`, 'g');
368
+ const replacement = \`$1\\nid = "\${id}"\`;
369
+
370
+ const newContent = wranglerContent.replace(pattern, replacement);
371
+ if (newContent !== wranglerContent) {
372
+ updatedCount++;
373
+ log(\` ✅ Updated \${binding} with ID: \${id}\`, colors.green);
374
+ }
375
+ wranglerContent = newContent;
376
+ }
377
+
378
+ fs.writeFileSync(wranglerTomlPath, wranglerContent);
379
+ log(\`\\n✅ Updated \${updatedCount} namespace ID(s) in wrangler.toml\`, colors.green);
380
+
381
+ // Show remaining placeholder IDs if any
382
+ const placeholderMatches = wranglerContent.match(/binding = "[^"]+"\\s*\\nid = "your_[^"]+"/g);
383
+ if (placeholderMatches) {
384
+ log('\\n⚠️ Some namespace IDs still have placeholders:', colors.yellow);
385
+ placeholderMatches.forEach(match => {
386
+ const bindingMatch = match.match(/binding = "([^"]+)"/);
387
+ if (bindingMatch) {
388
+ log(\` - \${bindingMatch[1]}\`, colors.yellow);
389
+ }
390
+ });
391
+ }
392
+ } catch (error) {
393
+ log(\`❌ Failed to update wrangler.toml: \${error.message}\`, colors.red);
394
+ }
395
+ }
396
+
397
+ // 5. Create .dev.vars from example if it doesn't exist
398
+ const devVarsPath = path.join(__dirname, '..', '.dev.vars');
399
+ const devVarsExamplePath = path.join(__dirname, '..', '.dev.vars.example');
400
+
401
+ if (!fs.existsSync(devVarsPath) && fs.existsSync(devVarsExamplePath)) {
402
+ log('\\n📋 Creating .dev.vars from example...', colors.blue);
403
+ fs.copyFileSync(devVarsExamplePath, devVarsPath);
404
+ log('✅ Created .dev.vars - Please update with your values', colors.green);
405
+ }
406
+
407
+ // 6. Check if identity needs to be generated
408
+ if (fs.existsSync(devVarsPath)) {
409
+ const devVarsContent = fs.readFileSync(devVarsPath, 'utf-8');
410
+ if (devVarsContent.includes('your-private-key-here')) {
411
+ log('\\n🔑 Generating agent identity...', colors.blue);
412
+ try {
413
+ execSync('npx @kya-os/create-mcpi-app regenerate-identity', { stdio: 'inherit' });
414
+ log('✅ Identity generated successfully', colors.green);
415
+ } catch {
416
+ log('⚠️ Could not generate identity automatically. Run: npx @kya-os/create-mcpi-app regenerate-identity', colors.yellow);
417
+ }
418
+ }
419
+ }
420
+
421
+ // 7. Show next steps
422
+ log('\\n✨ Setup complete! Next steps:\\n', colors.bright + colors.green);
423
+ log('1. Review .dev.vars and add any missing values (AgentShield API key, etc.)', colors.blue);
424
+ log('2. Start development server: npm run dev', colors.blue);
425
+ log('3. Deploy to production: npm run deploy', colors.blue);
426
+ log('\\nUseful commands:', colors.bright);
427
+ log(' npm run dev - Start local development server');
428
+ log(' npm run deploy - Deploy to Cloudflare Workers');
429
+ log(' npm run kv:list - List all KV namespaces');
430
+ log(' wrangler secret put <KEY> - Set production secrets');
431
+ log('\\nFor more information, see the README.md file.\\n');
432
+
433
+ rl.close();
434
+ }
435
+
436
+ // Handle errors gracefully
437
+ process.on('unhandledRejection', (error) => {
438
+ log(\`\\n❌ Setup failed: \${error.message}\`, colors.red);
439
+ process.exit(1);
440
+ });
441
+
442
+ // Run the setup
443
+ setup().catch((error) => {
444
+ log(\`\\n❌ Setup failed: \${error.message}\`, colors.red);
445
+ process.exit(1);
446
+ });
447
+ `;
448
+ fs.writeFileSync(path.join(scriptsDir, "setup.js"), setupScriptContent);
449
+
450
+ // Make setup script executable
451
+ if (process.platform !== "win32") {
452
+ fs.chmodSync(path.join(scriptsDir, "setup.js"), "755");
453
+ }
454
+
455
+ // Create tests directory
456
+ const testsDir = path.join(projectPath, "tests");
457
+ fs.ensureDirSync(testsDir);
458
+
459
+ // Create delegation test file
460
+ const delegationTestContent = `import { describe, test, expect, vi, beforeEach } from 'vitest';
461
+
462
+ /**
463
+ * Delegation Management Tests
464
+ * Tests delegation verification, caching, and invalidation
465
+ */
466
+ describe('Delegation Management', () => {
467
+ const mockDelegationStorage = {
468
+ get: vi.fn(),
469
+ put: vi.fn(),
470
+ delete: vi.fn()
471
+ };
472
+
473
+ const mockVerificationCache = {
474
+ get: vi.fn(),
475
+ put: vi.fn(),
476
+ delete: vi.fn()
477
+ };
478
+
479
+ const mockEnv = {
480
+ ${className.toUpperCase()}_DELEGATION_STORAGE: mockDelegationStorage,
481
+ TOOL_PROTECTION_KV: mockVerificationCache,
482
+ AGENTSHIELD_API_KEY: 'test-key',
483
+ AGENTSHIELD_API_URL: 'https://test.agentshield.ai'
484
+ };
485
+
486
+ beforeEach(() => {
487
+ vi.clearAllMocks();
488
+ global.fetch = vi.fn();
489
+ });
490
+
491
+ test('should verify delegation token with AgentShield API', async () => {
492
+ const token = 'test-delegation-token';
493
+
494
+ // Mock verification cache miss
495
+ mockVerificationCache.get.mockResolvedValueOnce(null);
496
+
497
+ // Mock API success
498
+ global.fetch = vi.fn().mockResolvedValueOnce({
499
+ ok: true
500
+ });
501
+
502
+ // Test verification would happen here
503
+ expect(global.fetch).toHaveBeenCalledWith(
504
+ expect.stringContaining('/api/v1/bouncer/delegations/verify'),
505
+ expect.objectContaining({
506
+ method: 'POST',
507
+ body: JSON.stringify({ token })
508
+ })
509
+ );
510
+ });
511
+
512
+ test('should use 5-minute cache TTL for delegations', async () => {
513
+ const token = 'test-token';
514
+ const sessionId = 'test-session';
515
+
516
+ await mockDelegationStorage.put(
517
+ \`session:\${sessionId}\`,
518
+ token,
519
+ { expirationTtl: 300 } // 5 minutes
520
+ );
521
+
522
+ expect(mockDelegationStorage.put).toHaveBeenCalledWith(
523
+ expect.any(String),
524
+ token,
525
+ { expirationTtl: 300 }
526
+ );
527
+ });
528
+
529
+ test('should invalidate cache on revocation', async () => {
530
+ const sessionId = 'revoked-session';
531
+ const token = 'revoked-token';
532
+
533
+ // Test invalidation
534
+ await Promise.all([
535
+ mockDelegationStorage.delete(\`session:\${sessionId}\`),
536
+ mockVerificationCache.delete(\`verified:\${token.substring(0, 16)}\`)
537
+ ]);
538
+
539
+ expect(mockDelegationStorage.delete).toHaveBeenCalled();
540
+ expect(mockVerificationCache.delete).toHaveBeenCalled();
541
+ });
542
+ });
543
+ `;
544
+ fs.writeFileSync(
545
+ path.join(testsDir, "delegation.test.ts"),
546
+ delegationTestContent
547
+ );
548
+
549
+ // Create DO routing test file
550
+ const doRoutingTestContent = `import { describe, test, expect } from 'vitest';
551
+
552
+ /**
553
+ * Durable Object Routing Tests
554
+ * Tests multi-instance DO routing for horizontal scaling
555
+ */
556
+ describe('DO Multi-Instance Routing', () => {
557
+
558
+ function getDoInstanceId(request: Request, env: { DO_ROUTING_STRATEGY?: string; DO_SHARD_COUNT?: string }): string {
559
+ const strategy = env.DO_ROUTING_STRATEGY || 'session';
560
+ const headers = request.headers;
561
+
562
+ switch (strategy) {
563
+ case 'session': {
564
+ const sessionId = headers.get('mcp-session-id') ||
565
+ headers.get('Mcp-Session-Id') ||
566
+ crypto.randomUUID();
567
+ return \`session:\${sessionId}\`;
568
+ }
569
+
570
+ case 'shard': {
571
+ const identifier = headers.get('mcp-session-id') || Math.random().toString();
572
+ let hash = 0;
573
+ for (let i = 0; i < identifier.length; i++) {
574
+ hash = ((hash << 5) - hash) + identifier.charCodeAt(i);
575
+ hash = hash & hash;
576
+ }
577
+ const shardCount = parseInt(env.DO_SHARD_COUNT || '10');
578
+ // Validate shard count - must be a valid positive number
579
+ const validShardCount = (!isNaN(shardCount) && shardCount > 0) ? shardCount : 10;
580
+ const shard = Math.abs(hash) % validShardCount;
581
+ return \`shard:\${shard}\`;
582
+ }
583
+
584
+ default:
585
+ return 'default';
586
+ }
587
+ }
588
+
589
+ test('should route to different instances for different sessions', () => {
590
+ const env = { DO_ROUTING_STRATEGY: 'session' };
591
+
592
+ const req1 = new Request('http://test/mcp', {
593
+ headers: { 'mcp-session-id': 'session-123' }
594
+ });
595
+ const req2 = new Request('http://test/mcp', {
596
+ headers: { 'mcp-session-id': 'session-456' }
597
+ });
598
+
599
+ const id1 = getDoInstanceId(req1, env);
600
+ const id2 = getDoInstanceId(req2, env);
601
+
602
+ expect(id1).toBe('session:session-123');
603
+ expect(id2).toBe('session:session-456');
604
+ expect(id1).not.toBe(id2);
605
+ });
606
+
607
+ test('should distribute load across shards', () => {
608
+ const env = {
609
+ DO_ROUTING_STRATEGY: 'shard',
610
+ DO_SHARD_COUNT: '10'
611
+ };
612
+
613
+ const distribution = new Map<string, number>();
614
+
615
+ // Generate 100 requests
616
+ for (let i = 0; i < 100; i++) {
617
+ const req = new Request('http://test/mcp', {
618
+ headers: { 'mcp-session-id': \`session-\${i}\` }
619
+ });
620
+
621
+ const instanceId = getDoInstanceId(req, env);
622
+ const shard = instanceId.split(':')[1];
623
+
624
+ distribution.set(shard, (distribution.get(shard) || 0) + 1);
625
+ }
626
+
627
+ // Should use multiple shards
628
+ expect(distribution.size).toBeGreaterThan(5);
629
+ });
630
+ });
631
+ `;
632
+ fs.writeFileSync(
633
+ path.join(testsDir, "do-routing.test.ts"),
634
+ doRoutingTestContent
635
+ );
636
+
637
+ // Create security test file
638
+ const securityTestContent = `import { describe, test, expect } from 'vitest';
639
+
640
+ /**
641
+ * Security Tests
642
+ * Tests CORS configuration and API key handling
643
+ */
644
+ describe('Security Configuration', () => {
645
+
646
+ function getCorsOrigin(requestOrigin: string | null, env: { ALLOWED_ORIGINS?: string; MCPI_ENV?: string }): string | null {
647
+ const allowedOrigins = env.ALLOWED_ORIGINS?.split(',').map((o: string) => o.trim()) || [
648
+ 'https://claude.ai',
649
+ 'https://app.anthropic.com'
650
+ ];
651
+
652
+ if (env.MCPI_ENV !== 'production' && !allowedOrigins.includes('http://localhost:3000')) {
653
+ allowedOrigins.push('http://localhost:3000');
654
+ }
655
+
656
+ const origin = requestOrigin || '';
657
+ const isAllowed = allowedOrigins.includes(origin);
658
+
659
+ return isAllowed ? origin : allowedOrigins[0];
660
+ }
661
+
662
+ test('should allow Claude.ai by default', () => {
663
+ const env = {};
664
+ const origin = 'https://claude.ai';
665
+ const result = getCorsOrigin(origin, env);
666
+
667
+ expect(result).toBe(origin);
668
+ });
669
+
670
+ test('should reject unauthorized origins', () => {
671
+ const env = { MCPI_ENV: 'production' };
672
+ const origin = 'https://evil.com';
673
+ const result = getCorsOrigin(origin, env);
674
+
675
+ expect(result).toBe('https://claude.ai');
676
+ expect(result).not.toBe(origin);
677
+ });
678
+
679
+ test('should not expose API keys in wrangler.toml', () => {
680
+ // This test validates that API keys are only in .dev.vars
681
+ const wranglerContent = \`
682
+ [vars]
683
+ AGENTSHIELD_API_URL = "https://kya.vouched.id"
684
+ # AGENTSHIELD_API_KEY - Set securely
685
+ \`;
686
+
687
+ expect(wranglerContent).not.toContain('sk_');
688
+ expect(wranglerContent).toContain('Set securely');
689
+ });
690
+
691
+ test('should use short TTLs for security', () => {
692
+ const DELEGATION_TTL = 300; // 5 minutes
693
+ const VERIFICATION_TTL = 60; // 1 minute
694
+
695
+ expect(DELEGATION_TTL).toBeLessThanOrEqual(300);
696
+ expect(VERIFICATION_TTL).toBeLessThanOrEqual(60);
697
+ });
698
+ });
699
+ `;
700
+ fs.writeFileSync(
701
+ path.join(testsDir, "security.test.ts"),
702
+ securityTestContent
703
+ );
704
+
705
+ // Create vitest config file
706
+ const vitestConfigContent = `import { defineConfig } from 'vitest/config';
707
+
708
+ export default defineConfig({
709
+ test: {
710
+ environment: 'miniflare',
711
+ environmentOptions: {
712
+ kvNamespaces: [
713
+ '${className.toUpperCase()}_NONCE_CACHE',
714
+ '${className.toUpperCase()}_PROOF_ARCHIVE',
715
+ '${className.toUpperCase()}_IDENTITY_STORAGE',
716
+ '${className.toUpperCase()}_DELEGATION_STORAGE',
717
+ '${className.toUpperCase()}_TOOL_PROTECTION_KV'
718
+ ],
719
+ durableObjects: {
720
+ ${className.toUpperCase()}_OBJECT: '${pascalClassName}MCP'
721
+ }
722
+ },
723
+ coverage: {
724
+ provider: 'v8',
725
+ reporter: ['text', 'html'],
726
+ exclude: ['node_modules/', 'tests/', '*.config.ts'],
727
+ thresholds: {
728
+ statements: 80,
729
+ branches: 70,
730
+ functions: 80,
731
+ lines: 80
732
+ }
733
+ }
734
+ }
735
+ });
736
+ `;
737
+ fs.writeFileSync(
738
+ path.join(projectPath, "vitest.config.ts"),
739
+ vitestConfigContent
740
+ );
741
+
742
+ // Create greet tool
743
+ const greetToolContent = `import { z } from "zod";
744
+
745
+ /**
746
+ * Greet Tool - Example MCP tool with AgentShield integration
747
+ *
748
+ * This tool demonstrates proper scopeId configuration for tool auto-discovery.
749
+ *
750
+ * Configure the corresponding scope in mcpi-runtime-config.ts:
751
+ * \`\`\`typescript
752
+ * toolProtections: {
753
+ * greet: {
754
+ * requiresDelegation: false,
755
+ * requiredScopes: ["greet:execute"], // ← This becomes the scopeId in proofs
756
+ * }
757
+ * }
758
+ * \`\`\`
759
+ *
760
+ * The scopeId format is "toolName:action":
761
+ * - Tool name: "greet" (extracted before the ":")
762
+ * - Action: "execute" (extracted after the ":")
763
+ * - Risk level: Auto-determined from action keyword (execute = high)
764
+ *
765
+ * Other scopeId examples:
766
+ * - "files:read" → Medium risk
767
+ * - "files:write" → High risk
768
+ * - "database:delete" → Critical risk
769
+ */
770
+ export const greetTool = {
771
+ name: "greet",
772
+ description: "Greet a user by name",
773
+ inputSchema: z.object({
774
+ name: z.string().describe("The name of the user to greet")
775
+ }),
776
+ handler: async ({ name }: { name: string }) => {
777
+ return {
778
+ content: [
779
+ {
780
+ type: "text" as const,
781
+ text: \`Hello, \${name}! Welcome to your Cloudflare MCP server.\`
782
+ }
783
+ ]
784
+ };
785
+ }
786
+ };
787
+ `;
788
+ fs.writeFileSync(path.join(toolsDir, "greet.ts"), greetToolContent);
789
+
790
+ // Create mcpi-runtime-config.ts for AgentShield integration
791
+ const runtimeConfigContent = `import type { MCPIConfig } from '@kya-os/contracts/config';
792
+ import type { CloudflareRuntimeConfig } from '@kya-os/mcp-i-cloudflare/config';
793
+ import type { CloudflareEnv } from '@kya-os/mcp-i-cloudflare';
794
+ import { buildBaseConfig } from '@kya-os/create-mcpi-app/config-builder';
795
+ // Always import CloudflareRuntime for tool protection service creation
796
+ import { CloudflareRuntime } from "@kya-os/mcp-i-cloudflare";
797
+
798
+ /**
799
+ * Runtime configuration for MCP-I server
800
+ *
801
+ * This file configures runtime features like proof submission to AgentShield,
802
+ * delegation verification, and audit logging.
803
+ *
804
+ * Environment variables are automatically injected from wrangler.toml (Cloudflare)
805
+ * or .env (Node.js). Configure them there:
806
+ * - AGENTSHIELD_API_URL: AgentShield API base URL
807
+ * - AGENTSHIELD_API_KEY: Your AgentShield API key
808
+ * - MCPI_ENV: "development" or "production"
809
+ *
810
+ * Note: The service fetches tool protection config by agent DID automatically.
811
+ * No project ID configuration needed!
812
+ *
813
+ * This config uses the unified MCPIConfig architecture, ensuring the same
814
+ * base configuration shape across all platforms (Cloudflare, Node.js, Vercel).
815
+ */
816
+ export function getRuntimeConfig(env: CloudflareEnv): CloudflareRuntimeConfig {
817
+ // Build base config that works across all platforms
818
+ const baseConfig = buildBaseConfig(env);
819
+
820
+ // Extend with Cloudflare-specific properties
821
+ return {
822
+ ...baseConfig,
823
+ // Identity configuration
824
+ identity: {
825
+ ...baseConfig.identity,
826
+ enabled: true,
827
+ environment: (env.ENVIRONMENT || env.MCPI_ENV || 'development') as 'development' | 'production'
828
+ },
829
+ // Proofing configuration
830
+ proofing: {
831
+ enabled: true,
832
+ batchQueue: {
833
+ destinations: [
834
+ {
835
+ type: "agentshield" as const,
836
+ apiUrl: env.AGENTSHIELD_API_URL || 'https://kya.vouched.id',
837
+ apiKey: env.AGENTSHIELD_API_KEY || ''
838
+ }
839
+ ],
840
+ maxBatchSize: 10,
841
+ flushIntervalMs: 5000,
842
+ maxRetries: 3,
843
+ debug: (env.ENVIRONMENT || env.MCPI_ENV || 'development') === 'development'
844
+ }
845
+ },
846
+ // Delegation configuration
847
+ delegation: {
848
+ ...baseConfig.delegation
849
+ },
850
+ // Audit configuration
851
+ audit: {
852
+ ...baseConfig.audit
853
+ },
854
+ // Cloudflare Workers-specific configuration
855
+ workers: {
856
+ cpuMs: 50,
857
+ memoryMb: 128
858
+ },
859
+ // KV namespace bindings
860
+ kv: env.TOOL_PROTECTION_KV ? [{
861
+ name: 'TOOL_PROTECTION_KV',
862
+ purpose: 'cache' as const
863
+ }] : [],
864
+ // Environment variable bindings
865
+ vars: {
866
+ ENVIRONMENT: env.ENVIRONMENT || env.MCPI_ENV || 'development',
867
+ AGENTSHIELD_API_KEY: env.AGENTSHIELD_API_KEY
868
+ },
869
+ // Tool protection service (Cloudflare-specific)
870
+ toolProtection: env.TOOL_PROTECTION_KV && env.AGENTSHIELD_API_KEY ? {
871
+ source: 'agentshield' as const,
872
+ agentShield: {
873
+ apiUrl: env.AGENTSHIELD_API_URL || 'https://kya.vouched.id',
874
+ apiKey: env.AGENTSHIELD_API_KEY,
875
+ projectId: env.AGENTSHIELD_PROJECT_ID,
876
+ cacheTtl: 300000 // 5 minutes
877
+ },
878
+ fallback: {
879
+ greet: {
880
+ requiresDelegation: false,
881
+ requiredScopes: ['greet:execute']
882
+ }
883
+ }
884
+ } : undefined
885
+ } as CloudflareRuntimeConfig;
886
+ }
887
+ `;
888
+ fs.writeFileSync(
889
+ path.join(srcDir, "mcpi-runtime-config.ts"),
890
+ runtimeConfigContent
891
+ );
892
+
893
+ // Create main index.ts using McpAgent with MCP-I runtime
894
+ const indexContent = `import { McpAgent } from "agents/mcp";
895
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
896
+ import { createCloudflareRuntime, type CloudflareEnv, KVProofArchive, type DetachedProof, createOAuthCallbackHandler, CloudflareRuntime } from "@kya-os/mcp-i-cloudflare";
897
+ import { DelegationRequiredError } from "@kya-os/mcp-i-core";
898
+ import type { ToolProtectionService } from "@kya-os/mcp-i-core";
899
+ import { Hono } from "hono";
900
+ import { cors } from "hono/cors";
901
+ import { greetTool } from "./tools/greet";
902
+ import { getRuntimeConfig } from "./mcpi-runtime-config";
903
+ import type { CloudflareRuntimeConfig } from "@kya-os/mcp-i-cloudflare/config";
904
+
905
+ /**
906
+ * Extended CloudflareEnv with prefixed KV bindings for multi-agent deployments
907
+ * This allows multiple agents to share the same Cloudflare account without conflicts
908
+ */
909
+ interface PrefixedCloudflareEnv extends CloudflareEnv {
910
+ // Prefixed KV bindings (e.g., MYAGENT_NONCE_CACHE, MYAGENT_PROOF_ARCHIVE)
911
+ [key: string]: KVNamespace | string | DurableObjectState | undefined;
912
+ // Optional routing configuration
913
+ DO_ROUTING_STRATEGY?: string;
914
+ DO_SHARD_COUNT?: string;
915
+ // Prefixed KV namespaces (dynamically accessed)
916
+ ${className.toUpperCase()}_NONCE_CACHE?: KVNamespace;
917
+ ${className.toUpperCase()}_PROOF_ARCHIVE?: KVNamespace;
918
+ ${className.toUpperCase()}_IDENTITY_STORAGE?: KVNamespace;
919
+ ${className.toUpperCase()}_DELEGATION_STORAGE?: KVNamespace;
920
+ ${className.toUpperCase()}_TOOL_PROTECTION_KV?: KVNamespace;
921
+ }
922
+
923
+ export class ${pascalClassName}MCP extends McpAgent {
924
+ server = new McpServer({
925
+ name: "${projectName}",
926
+ version: "1.0.0"
927
+ });
928
+
929
+ private mcpiRuntime?: ReturnType<typeof createCloudflareRuntime>;
930
+ private proofArchive?: KVProofArchive;
931
+ private agentShieldConfig?: { apiUrl: string; apiKey: string };
932
+ private env: PrefixedCloudflareEnv;
933
+ private mcpServerUrl?: string;
934
+
935
+ constructor(state: DurableObjectState, env: PrefixedCloudflareEnv) {
936
+ super(state, env);
937
+ this.env = env;
938
+
939
+ // Create CloudflareEnv adapter to map prefixed KV bindings to expected names
940
+ // This allows multiple agents to be deployed without KV namespace conflicts
941
+ const mappedEnv: CloudflareEnv = {
942
+ // Map prefixed bindings to standard names expected by createCloudflareRuntime
943
+ NONCE_CACHE: env.${className.toUpperCase()}_NONCE_CACHE,
944
+ PROOF_ARCHIVE: env.${className.toUpperCase()}_PROOF_ARCHIVE,
945
+ IDENTITY_STORAGE: env.${className.toUpperCase()}_IDENTITY_STORAGE,
946
+ DELEGATION_STORAGE: env.${className.toUpperCase()}_DELEGATION_STORAGE,
947
+ TOOL_PROTECTION_KV: env.${className.toUpperCase()}_TOOL_PROTECTION_KV,
948
+ // Pass through environment variables unchanged
949
+ MCP_IDENTITY_PRIVATE_KEY: env.MCP_IDENTITY_PRIVATE_KEY,
950
+ MCP_IDENTITY_PUBLIC_KEY: env.MCP_IDENTITY_PUBLIC_KEY,
951
+ MCP_IDENTITY_AGENT_DID: env.MCP_IDENTITY_AGENT_DID,
952
+ // Pass through other env vars for runtime config
953
+ AGENTSHIELD_API_URL: env.AGENTSHIELD_API_URL,
954
+ AGENTSHIELD_API_KEY: env.AGENTSHIELD_API_KEY,
955
+ AGENTSHIELD_PROJECT_ID: env.AGENTSHIELD_PROJECT_ID,
956
+ MCPI_ENV: env.MCPI_ENV,
957
+ MCP_SERVER_URL: env.MCP_SERVER_URL,
958
+ // Pass Durable Object state for identity persistence
959
+ // NOTE: Without this, identity will be ephemeral (new DID every call)!
960
+ // Note: createCloudflareRuntime will automatically use KVIdentityProvider if IDENTITY_STORAGE KV is available
961
+ _durableObjectState: state,
962
+ };
963
+
964
+ // Store MCP server URL for proof submission context
965
+ this.mcpServerUrl = env.MCP_SERVER_URL;
966
+ if (this.mcpServerUrl) {
967
+ console.log('[MCP-I] MCP Server URL configured:', this.mcpServerUrl);
968
+ } else {
969
+ console.log('[MCP-I] Warning: MCP_SERVER_URL not configured');
970
+ }
971
+
972
+ // Load runtime configuration for AgentShield integration
973
+ // Pass mappedEnv so it can access KV bindings with standard names
974
+ const runtimeConfig = getRuntimeConfig(mappedEnv);
975
+
976
+ // ✅ Create tool protection service helper function
977
+ // Always import CloudflareRuntime but conditionally instantiate
978
+ function createToolProtectionService(env: CloudflareEnv, runtimeConfig: CloudflareRuntimeConfig): ToolProtectionService | undefined {
979
+ if (!runtimeConfig.toolProtection) {
980
+ return undefined;
981
+ }
982
+
983
+ if (!env.TOOL_PROTECTION_KV || !env.AGENTSHIELD_API_KEY) {
984
+ console.log('[MCP-I] Tool protection disabled - configure TOOL_PROTECTION_KV and AGENTSHIELD_API_KEY to enable');
985
+ return undefined;
986
+ }
987
+
988
+ return CloudflareRuntime.createToolProtectionService(
989
+ env.TOOL_PROTECTION_KV,
990
+ {
991
+ apiUrl: runtimeConfig.toolProtection.agentShield?.apiUrl || env.AGENTSHIELD_API_URL || 'https://kya.vouched.id',
992
+ apiKey: env.AGENTSHIELD_API_KEY,
993
+ projectId: runtimeConfig.toolProtection.agentShield?.projectId || env.AGENTSHIELD_PROJECT_ID,
994
+ cacheTtl: runtimeConfig.toolProtection.agentShield?.cacheTtl || 300000,
995
+ debug: runtimeConfig.environment === 'development',
996
+ fallbackConfig: runtimeConfig.toolProtection.fallback
997
+ }
998
+ );
999
+ }
1000
+
1001
+ // Create tool protection service if configured
1002
+ // Note: createCloudflareRuntime will automatically use KVIdentityProvider if IDENTITY_STORAGE KV is available
1003
+ const toolProtectionService = createToolProtectionService(mappedEnv, runtimeConfig);
1004
+
1005
+ // Initialize MCP-I runtime for cryptographic proofs and identity
1006
+ this.mcpiRuntime = createCloudflareRuntime({
1007
+ env: mappedEnv,
1008
+ environment: runtimeConfig.environment,
1009
+ audit: {
1010
+ enabled: runtimeConfig.audit?.enabled ?? true,
1011
+ logFunction: runtimeConfig.audit?.logFunction || ((record) => console.log('[MCP-I Audit]', record))
1012
+ },
1013
+ toolProtectionService
1014
+ });
1015
+
1016
+ // Initialize proof archive if PROOF_ARCHIVE KV is available
1017
+ if (mappedEnv.PROOF_ARCHIVE) {
1018
+ this.proofArchive = new KVProofArchive(mappedEnv.PROOF_ARCHIVE);
1019
+ console.log('[MCP-I] Proof archive enabled');
1020
+ }
1021
+
1022
+ // Load AgentShield config for proof submission
1023
+ if (runtimeConfig.proofing?.enabled && runtimeConfig.proofing.batchQueue) {
1024
+ const agentShieldDest = runtimeConfig.proofing.batchQueue.destinations?.find(
1025
+ (dest) => dest.type === "agentshield" && dest.apiKey
1026
+ );
1027
+ if (agentShieldDest) {
1028
+ this.agentShieldConfig = {
1029
+ apiUrl: agentShieldDest.apiUrl,
1030
+ apiKey: agentShieldDest.apiKey!
1031
+ };
1032
+ console.log('[MCP-I] AgentShield enabled:', agentShieldDest.apiUrl);
1033
+ }
1034
+ }
1035
+ }
1036
+
1037
+ /**
1038
+ * Override getInstanceId() to enable multi-instance Durable Object routing
1039
+ *
1040
+ * This method is called internally by McpAgent to determine which DO instance
1041
+ * should handle the request. By overriding it, we can implement custom routing
1042
+ * strategies (session-based, shard-based, etc.) while maintaining full
1043
+ * McpAgent compatibility and preserving PartyServer routing context.
1044
+ *
1045
+ * @returns Instance ID used by McpAgent for DO routing
1046
+ */
1047
+ getInstanceId(): string {
1048
+ try {
1049
+ // Get session ID from McpAgent's built-in extraction
1050
+ const sessionId = this.getSessionId();
1051
+
1052
+ // Get routing strategy from environment (default: session)
1053
+ const strategy = this.env.DO_ROUTING_STRATEGY || 'session';
1054
+
1055
+ if (strategy === 'session') {
1056
+ // One DO instance per MCP session (recommended for most use cases)
1057
+ // Sessions are isolated, ensuring data consistency per client
1058
+ return \`session:\${sessionId}\`;
1059
+ } else if (strategy === 'shard') {
1060
+ // Hash-based sharding across N DO instances (for high load)
1061
+ // Distributes load evenly while maintaining session affinity
1062
+ const shardCount = parseInt(this.env.DO_SHARD_COUNT || '10');
1063
+ // Validate shard count - must be a valid positive number
1064
+ const validShardCount = (!isNaN(shardCount) && shardCount > 0) ? shardCount : 10;
1065
+
1066
+ // Simple hash function for session ID
1067
+ let hash = 0;
1068
+ for (let i = 0; i < sessionId.length; i++) {
1069
+ hash = ((hash << 5) - hash) + sessionId.charCodeAt(i);
1070
+ hash = hash & hash; // Convert to 32bit integer
1071
+ }
1072
+
1073
+ const shard = Math.abs(hash) % validShardCount;
1074
+ return \`shard:\${shard}\`;
1075
+ }
1076
+
1077
+ // Fallback to single instance (legacy behavior)
1078
+ return 'default';
1079
+ } catch (error) {
1080
+ // If session extraction fails, fall back to default instance
1081
+ console.error('[DO Routing] Failed to extract session ID:', error);
1082
+ return 'default';
1083
+ }
1084
+ }
1085
+
1086
+ /**
1087
+ * Retrieve delegation token from KV storage
1088
+ * Uses two-tier lookup: session cache (fast) → agent DID (stable)
1089
+ *
1090
+ * @param sessionId - MCP session ID from Claude Desktop
1091
+ * @returns Delegation token if found, null otherwise
1092
+ */
1093
+ private async getDelegationToken(sessionId?: string): Promise<string | null> {
1094
+ const delegationStorage = this.env.${className.toUpperCase()}_DELEGATION_STORAGE;
1095
+
1096
+ if (!delegationStorage) {
1097
+ console.log('[Delegation] No delegation storage configured');
1098
+ return null;
1099
+ }
1100
+
1101
+ try {
1102
+ // Fast path: Try session cache first
1103
+ if (sessionId) {
1104
+ const sessionKey = \`session:\${sessionId}\`;
1105
+ const sessionToken = await delegationStorage.get(sessionKey);
1106
+
1107
+ if (sessionToken) {
1108
+ // Verify token is still valid before returning
1109
+ const isValid = await this.verifyDelegationWithAgentShield(sessionToken);
1110
+ if (isValid) {
1111
+ console.log('[Delegation] ✅ Token retrieved from session cache and verified');
1112
+ return sessionToken;
1113
+ } else {
1114
+ // Token invalid, remove from cache
1115
+ await this.invalidateDelegationCache(sessionId, sessionToken);
1116
+ console.log('[Delegation] ⚠️ Cached token was invalid, removed from cache');
1117
+ }
1118
+ }
1119
+ }
1120
+
1121
+ // Fallback: Try agent DID (stable across session changes)
1122
+ if (this.mcpiRuntime) {
1123
+ const identity = await this.mcpiRuntime.getIdentity();
1124
+ if (identity?.did) {
1125
+ const agentKey = \`agent:\${identity.did}:delegation\`;
1126
+ const agentToken = await delegationStorage.get(agentKey);
1127
+
1128
+ if (agentToken) {
1129
+ // Verify token is still valid before returning
1130
+ const isValid = await this.verifyDelegationWithAgentShield(agentToken);
1131
+ if (isValid) {
1132
+ console.log('[Delegation] ✅ Token retrieved using agent DID and verified');
1133
+
1134
+ // Re-cache for current session (performance optimization)
1135
+ if (sessionId) {
1136
+ const sessionCacheKey = \`session:\${sessionId}\`;
1137
+ await delegationStorage.put(sessionCacheKey, agentToken, {
1138
+ expirationTtl: 300 // 5 minutes for security (reduced from 30)
1139
+ });
1140
+ console.log('[Delegation] Token cached for session with 5-minute TTL:', sessionId);
1141
+ }
1142
+
1143
+ return agentToken;
1144
+ } else {
1145
+ // Token invalid, remove from cache
1146
+ await this.invalidateDelegationCache(sessionId, agentToken, identity.did);
1147
+ console.log('[Delegation] ⚠️ Agent token was invalid, removed from cache');
1148
+ }
1149
+ }
1150
+ }
1151
+ }
1152
+
1153
+ console.log('[Delegation] No delegation token found');
1154
+ return null;
1155
+ } catch (error) {
1156
+ console.error('[Delegation] Failed to retrieve token:', error);
1157
+ return null;
1158
+ }
1159
+ }
1160
+
1161
+ /**
1162
+ * Verify delegation token with AgentShield API
1163
+ * @param token - Delegation token to verify
1164
+ * @returns True if token is valid, false otherwise
1165
+ */
1166
+ private async verifyDelegationWithAgentShield(token: string): Promise<boolean> {
1167
+ // Check verification cache first (1 minute TTL for verified tokens)
1168
+ const verificationCache = this.env.TOOL_PROTECTION_KV;
1169
+ if (verificationCache) {
1170
+ const cacheKey = \`verified:\${token.substring(0, 16)}\`; // Use prefix to avoid key size issues
1171
+ const cached = await verificationCache.get(cacheKey);
1172
+ if (cached === '1') {
1173
+ console.log('[Delegation] Token verification cached as valid');
1174
+ return true;
1175
+ }
1176
+ }
1177
+
1178
+ try {
1179
+ const agentShieldUrl = this.env.AGENTSHIELD_API_URL || 'https://kya.vouched.id';
1180
+ const apiKey = this.env.AGENTSHIELD_API_KEY;
1181
+
1182
+ if (!apiKey) {
1183
+ console.warn('[Delegation] No AgentShield API key configured, skipping verification');
1184
+ return true; // Allow in development without API key
1185
+ }
1186
+
1187
+ // Verify with AgentShield API
1188
+ const response = await fetch(\`\${agentShieldUrl}/api/v1/bouncer/delegations/verify\`, {
1189
+ method: 'POST',
1190
+ headers: {
1191
+ 'Authorization': \`Bearer \${apiKey}\`,
1192
+ 'Content-Type': 'application/json'
1193
+ },
1194
+ body: JSON.stringify({ token })
1195
+ });
1196
+
1197
+ if (response.ok) {
1198
+ // Cache successful verification for 1 minute
1199
+ if (verificationCache) {
1200
+ const cacheKey = \`verified:\${token.substring(0, 16)}\`;
1201
+ await verificationCache.put(cacheKey, '1', {
1202
+ expirationTtl: 60 // 1 minute cache for verified tokens
1203
+ });
1204
+ }
1205
+ console.log('[Delegation] Token verified successfully with AgentShield');
1206
+ return true;
1207
+ }
1208
+
1209
+ if (response.status === 401 || response.status === 403) {
1210
+ console.log('[Delegation] Token verification failed: unauthorized');
1211
+ return false;
1212
+ }
1213
+
1214
+ console.warn('[Delegation] Token verification returned unexpected status:', response.status);
1215
+ return false; // Fail closed for security
1216
+
1217
+ } catch (error) {
1218
+ console.error('[Delegation] Error verifying token with AgentShield:', error);
1219
+ return false; // Fail closed on errors
1220
+ }
1221
+ }
1222
+
1223
+ /**
1224
+ * Invalidate delegation token in all caches
1225
+ * @param sessionId - Session ID to clear
1226
+ * @param token - Token to invalidate
1227
+ * @param agentDid - Agent DID to clear
1228
+ */
1229
+ private async invalidateDelegationCache(sessionId?: string, token?: string, agentDid?: string): Promise<void> {
1230
+ const delegationStorage = this.env.${className.toUpperCase()}_DELEGATION_STORAGE;
1231
+ const verificationCache = this.env.TOOL_PROTECTION_KV;
1232
+
1233
+ if (!delegationStorage) return;
1234
+
1235
+ const deletions: Promise<void>[] = [];
1236
+
1237
+ // Clear session cache
1238
+ if (sessionId) {
1239
+ const sessionKey = \`session:\${sessionId}\`;
1240
+ deletions.push(delegationStorage.delete(sessionKey));
1241
+ }
1242
+
1243
+ // Clear agent cache
1244
+ if (agentDid) {
1245
+ const agentKey = \`agent:\${agentDid}:delegation\`;
1246
+ deletions.push(delegationStorage.delete(agentKey));
1247
+ }
1248
+
1249
+ // Clear verification cache
1250
+ if (token && verificationCache) {
1251
+ const cacheKey = \`verified:\${token.substring(0, 16)}\`;
1252
+ deletions.push(verificationCache.delete(cacheKey));
1253
+ }
1254
+
1255
+ await Promise.all(deletions);
1256
+ console.log('[Delegation] Cache invalidated for revoked/invalid token');
1257
+ }
1258
+
1259
+ /**
1260
+ * Submit proof to AgentShield API
1261
+ * Uses the proof.jws directly (full JWS format from CloudflareRuntime)
1262
+ *
1263
+ * Also submits optional context for AgentShield dashboard integration.
1264
+ * Context provides plaintext tool/args data while proof provides cryptographic verification.
1265
+ */
1266
+ private async submitProofToAgentShield(
1267
+ proof: DetachedProof,
1268
+ session: { id: string },
1269
+ toolName: string,
1270
+ args: Record<string, unknown>,
1271
+ result: unknown
1272
+ ): Promise<void> {
1273
+ if (!this.agentShieldConfig || !proof.jws || !proof.meta) return;
1274
+
1275
+ const { apiUrl, apiKey } = this.agentShieldConfig;
1276
+
1277
+ // Get tool call context from runtime (if available)
1278
+ const toolCallContext = this.mcpiRuntime?.getLastToolCallContext();
1279
+
1280
+ // Proof already has correct format from CloudflareRuntime
1281
+ // Adding optional context for AgentShield dashboard (Option A architecture)
1282
+ const requestBody = {
1283
+ session_id: session.id,
1284
+ delegation_id: null,
1285
+ proofs: [{
1286
+ jws: proof.jws, // Already in full JWS format
1287
+ meta: proof.meta // Already has all required fields
1288
+ }],
1289
+ // ✅ NEW: Optional context for dashboard integration
1290
+ context: {
1291
+ toolCalls: toolCallContext ? [toolCallContext] : [{
1292
+ // Fallback if context not available from runtime
1293
+ tool: toolName,
1294
+ args: args,
1295
+ result: result,
1296
+ scopeId: proof.meta.scopeId || \`\${toolName}:execute\`
1297
+ }],
1298
+ // ✅ NEW: MCP server URL for tool discovery (optional, only needed once)
1299
+ mcpServerUrl: this.mcpServerUrl
1300
+ }
1301
+ };
1302
+
1303
+ console.log('[AgentShield] Submitting proof with context:', {
1304
+ did: proof.meta.did,
1305
+ sessionId: proof.meta.sessionId,
1306
+ jwsFormat: proof.jws.split('.').length === 3 ? 'valid (3 parts)' : 'invalid',
1307
+ contextTool: requestBody.context.toolCalls[0]?.tool,
1308
+ contextScopeId: requestBody.context.toolCalls[0]?.scopeId,
1309
+ mcpServerUrl: requestBody.context.mcpServerUrl || 'not-set'
1310
+ });
1311
+
1312
+ const response = await fetch(\`\${apiUrl}/api/v1/bouncer/proofs\`, {
1313
+ method: 'POST',
1314
+ headers: {
1315
+ 'Content-Type': 'application/json',
1316
+ 'Authorization': \`Bearer \${apiKey}\`
1317
+ },
1318
+ body: JSON.stringify(requestBody)
1319
+ });
1320
+
1321
+ if (!response.ok) {
1322
+ const errorText = await response.text();
1323
+ console.error('[AgentShield] Submission failed:', response.status, errorText);
1324
+ throw new Error(\`AgentShield error: \${response.status}\`);
1325
+ }
1326
+
1327
+ const responseData = await response.json() as { success?: boolean; received?: number; processed?: number; accepted?: number; rejected?: number; errors?: Array<{ proofId: string; error: string }> };
1328
+ console.log('[AgentShield] Response:', responseData);
1329
+
1330
+ if (responseData.accepted) {
1331
+ console.log('[AgentShield] ✅ Proofs accepted:', responseData.accepted);
1332
+ }
1333
+ if (responseData.rejected) {
1334
+ console.log('[AgentShield] ❌ Proofs rejected:', responseData.rejected);
1335
+ }
1336
+ }
1337
+
1338
+ async init() {
1339
+ // Initialize MCP-I runtime (generates/loads identity, sets up nonce cache)
1340
+ await this.mcpiRuntime?.initialize();
1341
+
1342
+ const identity = await this.mcpiRuntime?.getIdentity();
1343
+ console.log('[MCP-I] Initialized with DID:', identity?.did);
1344
+
1345
+ this.server.tool(
1346
+ greetTool.name,
1347
+ greetTool.description,
1348
+ greetTool.inputSchema.shape,
1349
+ async (args: { name: string }) => {
1350
+ // Use MCP-I runtime's processToolCall for automatic proof generation
1351
+ if (this.mcpiRuntime) {
1352
+ try {
1353
+ // Read MCP session ID from Claude Desktop (via agents framework)
1354
+ let mcpSessionId: string | undefined;
1355
+ try {
1356
+ mcpSessionId = this.getSessionId();
1357
+ console.log('[Delegation] Session ID from agents framework:', mcpSessionId);
1358
+ } catch (error) {
1359
+ console.log('[Delegation] Failed to get session ID from framework:', error);
1360
+ mcpSessionId = undefined;
1361
+ }
1362
+
1363
+ // Retrieve delegation token if available
1364
+ const delegationToken = await this.getDelegationToken(mcpSessionId);
1365
+
1366
+ // Create session with proper ID (use actual session ID when available)
1367
+ const timestamp = Date.now();
1368
+ const sessionId = mcpSessionId || \`ephemeral-\${timestamp}-\${Math.random().toString(36).substring(2, 10)}\`;
1369
+
1370
+ const session = {
1371
+ id: sessionId, // Use actual session ID from Claude Desktop
1372
+ audience: 'https://kya.vouched.id', // CRITICAL: Must match AgentShield domain
1373
+ agentDid: (await this.mcpiRuntime.getIdentity()).did,
1374
+ createdAt: timestamp,
1375
+ expiresAt: timestamp + (30 * 60 * 1000), // 30 minutes
1376
+ delegationToken // Include delegation token if available
1377
+ };
1378
+
1379
+ // Execute tool with automatic proof generation
1380
+ const result = await this.mcpiRuntime.processToolCall(
1381
+ greetTool.name,
1382
+ args,
1383
+ greetTool.handler,
1384
+ session
1385
+ );
1386
+
1387
+ // Get proof in DetachedProof format
1388
+ const proof = this.mcpiRuntime.getLastProof() as DetachedProof;
1389
+
1390
+ if (proof && proof.jws && proof.meta) {
1391
+ // Log proof details (using DetachedProof format)
1392
+ console.log('[MCP-I Proof]', {
1393
+ tool: greetTool.name,
1394
+ did: proof.meta.did,
1395
+ timestamp: proof.meta.ts,
1396
+ jws: proof.jws.substring(0, 50) + '...',
1397
+ jwsValid: proof.jws.split('.').length === 3
1398
+ });
1399
+
1400
+ // Parallelize proof operations for better performance
1401
+ const proofOperations: Promise<void>[] = [];
1402
+
1403
+ // Add proof archive operation
1404
+ if (this.proofArchive) {
1405
+ proofOperations.push(
1406
+ this.proofArchive.store(proof, {
1407
+ toolName: greetTool.name
1408
+ }).then(() => {
1409
+ console.log('[MCP-I] Proof stored in archive');
1410
+ }).catch((archiveError: unknown) => {
1411
+ console.error('[MCP-I] Archive error:', archiveError instanceof Error ? archiveError.message : String(archiveError));
1412
+ })
1413
+ );
1414
+ }
1415
+
1416
+ // Add AgentShield submission operation
1417
+ if (this.agentShieldConfig) {
1418
+ proofOperations.push(
1419
+ this.submitProofToAgentShield(proof, session, greetTool.name, args, result)
1420
+ .catch((err: unknown) => {
1421
+ console.error('[MCP-I] AgentShield failed:', err instanceof Error ? err.message : String(err));
1422
+ })
1423
+ );
1424
+ }
1425
+
1426
+ // Execute all proof operations in parallel for better performance
1427
+ if (proofOperations.length > 0) {
1428
+ await Promise.allSettled(proofOperations);
1429
+ }
1430
+
1431
+ // Attach proof to result for MCP Inspector
1432
+ if (result && typeof result === 'object' && result !== null) {
1433
+ (result as Record<string, unknown>)._meta = {
1434
+ proof: {
1435
+ jws: proof.jws,
1436
+ did: proof.meta.did,
1437
+ kid: proof.meta.kid,
1438
+ timestamp: proof.meta.ts,
1439
+ nonce: proof.meta.nonce,
1440
+ sessionId: proof.meta.sessionId,
1441
+ requestHash: proof.meta.requestHash,
1442
+ responseHash: proof.meta.responseHash
1443
+ }
1444
+ };
1445
+ }
1446
+ }
1447
+
1448
+ return result;
1449
+ } catch (error: unknown) {
1450
+ // If this is a DelegationRequiredError, re-throw it so the MCP framework can handle it properly
1451
+ // The agents/mcp framework will format it as a proper error response to Claude Desktop
1452
+ if (error instanceof DelegationRequiredError) {
1453
+ console.warn('[MCP-I] Delegation required, propagating error:', {
1454
+ tool: error.toolName,
1455
+ requiredScopes: error.requiredScopes,
1456
+ consentUrl: error.consentUrl
1457
+ });
1458
+ throw error;
1459
+ }
1460
+ // Check for DelegationRequiredError by name (for cases where error is not instanceof)
1461
+ if (error && typeof error === 'object' && 'name' in error && error.name === 'DelegationRequiredError') {
1462
+ const delegationError = error as DelegationRequiredError;
1463
+ console.warn('[MCP-I] Delegation required, propagating error:', {
1464
+ tool: delegationError.toolName,
1465
+ requiredScopes: delegationError.requiredScopes,
1466
+ consentUrl: delegationError.consentUrl
1467
+ });
1468
+ throw error;
1469
+ }
1470
+
1471
+ // For other errors, log and fallback to direct execution
1472
+ console.error('[MCP-I] Failed to process tool with runtime:', error);
1473
+ // Fallback to direct execution
1474
+ return await greetTool.handler(args);
1475
+ }
1476
+ }
1477
+
1478
+ // Fallback if runtime not available
1479
+ return await greetTool.handler(args);
1480
+ }
1481
+ );
1482
+ }
1483
+ }
1484
+
1485
+ const app = new Hono();
1486
+
1487
+ // Secure CORS configuration
1488
+ app.use("/*", (c, next) => {
1489
+ const allowedOrigins = c.env.ALLOWED_ORIGINS?.split(',').map((o: string) => o.trim()) || [
1490
+ 'https://claude.ai',
1491
+ 'https://app.anthropic.com'
1492
+ ];
1493
+
1494
+ // Add localhost for development if not in production
1495
+ if (c.env.MCPI_ENV !== 'production' && !allowedOrigins.includes('http://localhost:3000')) {
1496
+ allowedOrigins.push('http://localhost:3000');
1497
+ }
1498
+
1499
+ const origin = c.req.header('Origin') || '';
1500
+ const isAllowed = allowedOrigins.includes(origin);
1501
+
1502
+ return cors({
1503
+ origin: isAllowed ? origin : allowedOrigins[0], // Default to first allowed origin if not matched
1504
+ allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
1505
+ allowHeaders: ["Content-Type", "Authorization", "mcp-session-id", "Mcp-Session-Id", "mcp-protocol-version"],
1506
+ exposeHeaders: ["mcp-session-id", "Mcp-Session-Id"],
1507
+ credentials: true,
1508
+ })(c, next);
1509
+ });
1510
+
1511
+ app.get("/health", (c) => c.json({
1512
+ status: 'healthy',
1513
+ timestamp: new Date().toISOString(),
1514
+ transport: { sse: '/sse', streamableHttp: '/mcp' }
1515
+ }));
1516
+
1517
+ /**
1518
+ * Admin endpoint to clear tool protection cache
1519
+ *
1520
+ * This allows AgentShield dashboard to invalidate cached tool protection
1521
+ * config immediately after changing delegation requirements.
1522
+ *
1523
+ * The API key is validated by making a test call to AgentShield API.
1524
+ *
1525
+ * Usage:
1526
+ * POST /admin/clear-cache
1527
+ * Headers: Authorization: Bearer <AGENTSHIELD_ADMIN_API_KEY>
1528
+ * Body: { "agent_did": "did:key:z6Mk..." }
1529
+ *
1530
+ * Response:
1531
+ * { "success": true, "message": "Cache cleared", "agent_did": "..." }
1532
+ */
1533
+ app.post("/admin/clear-cache", async (c) => {
1534
+ const env = c.env as PrefixedCloudflareEnv;
1535
+
1536
+ // Parse request body first to get agent_did
1537
+ const body = await c.req.json().catch(() => ({}));
1538
+ const agentDid = body.agent_did;
1539
+
1540
+ if (!agentDid || typeof agentDid !== 'string') {
1541
+ return c.json({
1542
+ success: false,
1543
+ error: "Bad Request - agent_did required in body"
1544
+ }, 400);
1545
+ }
1546
+
1547
+ // Extract API key from Authorization header
1548
+ const authHeader = c.req.header("Authorization");
1549
+ if (!authHeader || !authHeader.startsWith("Bearer ")) {
1550
+ return c.json({
1551
+ success: false,
1552
+ error: "Unauthorized - Missing or invalid Authorization header"
1553
+ }, 401);
1554
+ }
1555
+
1556
+ const apiKey = authHeader.slice(7); // Remove "Bearer " prefix
1557
+
1558
+ // Validate API key by making a test call to AgentShield
1559
+ // Use the bouncer config endpoint as the validation mechanism
1560
+ const agentShieldUrl = env.AGENTSHIELD_API_URL || "https://kya.vouched.id";
1561
+ const validationUrl = \`\${agentShieldUrl}/api/v1/bouncer/config?agent_did=\${encodeURIComponent(agentDid)}\`;
1562
+
1563
+ try {
1564
+ const validationResponse = await fetch(validationUrl, {
1565
+ method: 'GET',
1566
+ headers: {
1567
+ 'Content-Type': 'application/json',
1568
+ 'Authorization': \`Bearer \${apiKey}\`
1569
+ }
1570
+ });
1571
+
1572
+ if (!validationResponse.ok) {
1573
+ console.warn('[Admin] API key validation failed:', validationResponse.status);
1574
+ return c.json({
1575
+ success: false,
1576
+ error: "Unauthorized - Invalid API key"
1577
+ }, 401);
1578
+ }
1579
+
1580
+ // API key is valid, proceed to clear cache
1581
+ console.log('[Admin] API key validated successfully');
1582
+ } catch (error) {
1583
+ console.error('[Admin] API key validation error:', error);
1584
+ return c.json({
1585
+ success: false,
1586
+ error: "Failed to validate API key with AgentShield"
1587
+ }, 500);
1588
+ }
1589
+
1590
+ // Clear cache from KV
1591
+ // Cache key format: KVToolProtectionCache uses 'tool-protection:' prefix + agentDid
1592
+ // Since we're accessing KV directly (not through cache service), we need the full key
1593
+ const cacheKey = \`tool-protection:\${agentDid}\`;
1594
+ const kvNamespace = env.${className.toUpperCase()}_TOOL_PROTECTION_KV;
1595
+
1596
+ if (!kvNamespace) {
1597
+ return c.json({
1598
+ success: false,
1599
+ error: "Tool protection KV namespace not configured"
1600
+ }, 500);
1601
+ }
1602
+
1603
+ try {
1604
+ // Log before and after for debugging
1605
+ const before = await kvNamespace.get(cacheKey);
1606
+ await kvNamespace.delete(cacheKey);
1607
+ const after = await kvNamespace.get(cacheKey);
1608
+
1609
+ console.log('[Admin] Cache clear operation', {
1610
+ agentDid: agentDid.slice(0, 20) + '...',
1611
+ cacheKey,
1612
+ hadValue: !!before,
1613
+ cleared: !after,
1614
+ });
1615
+
1616
+ return c.json({
1617
+ success: true,
1618
+ message: "Cache cleared successfully. Next tool call will fetch fresh config from AgentShield.",
1619
+ agent_did: agentDid,
1620
+ cache_key: cacheKey,
1621
+ had_value: !!before,
1622
+ });
1623
+ } catch (error) {
1624
+ console.error('[Admin] Failed to clear cache:', error);
1625
+ return c.json({
1626
+ success: false,
1627
+ error: "Internal error clearing cache",
1628
+ details: error instanceof Error ? error.message : String(error)
1629
+ }, 500);
1630
+ }
1631
+ });
1632
+
1633
+ /**
1634
+ * OAuth Authorization Code Flow callback handler
1635
+ *
1636
+ * Handles the redirect from AgentShield after user approves delegation.
1637
+ * Exchanges authorization code for delegation token and stores in KV.
1638
+ *
1639
+ * This endpoint is called by AgentShield after user approves delegation:
1640
+ * 1. Receives authorization code and state from query params
1641
+ * 2. Exchanges code for delegation token with AgentShield API
1642
+ * 3. Stores token in KV with session ID as key
1643
+ * 4. Returns success page to user
1644
+ */
1645
+ app.get('/oauth/callback', (c) => {
1646
+ const env = c.env as PrefixedCloudflareEnv;
1647
+ return createOAuthCallbackHandler({
1648
+ agentShieldApiUrl: env.AGENTSHIELD_API_URL || 'https://kya.vouched.id',
1649
+ delegationStorage: env.${className.toUpperCase()}_DELEGATION_STORAGE,
1650
+ autoClose: true,
1651
+ autoCloseDelay: 5000
1652
+ })(c);
1653
+ });
1654
+
1655
+ // Multi-instance DO routing using McpAgent's getInstanceId() override
1656
+ // The ${pascalClassName}MCP class overrides getInstanceId() to enable session-based
1657
+ // or shard-based routing while preserving PartyServer compatibility.
1658
+ //
1659
+ // Routing strategies (configured via DO_ROUTING_STRATEGY env var):
1660
+ // - 'session' (default): One DO per MCP session - ensures data isolation
1661
+ // - 'shard': Hash-based distribution across N shards - for high load
1662
+ //
1663
+ // McpAgent automatically routes to the correct DO instance using the ID
1664
+ // returned by getInstanceId(), maintaining full routing context.
1665
+ app.mount("/sse", ${pascalClassName}MCP.serveSSE("/sse").fetch, { replaceRequest: false });
1666
+ app.mount("/mcp", ${pascalClassName}MCP.serve("/mcp").fetch, { replaceRequest: false });
1667
+
1668
+ export default app;
1669
+ `;
1670
+ fs.writeFileSync(path.join(srcDir, "index.ts"), indexContent);
1671
+
1672
+ // Create wrangler.toml with optional API key
1673
+ const wranglerContent = `#:schema node_modules/wrangler/config-schema.json
1674
+ name = "${projectName}"
1675
+ main = "src/index.ts"
1676
+ compatibility_date = "2025-06-18"
1677
+ compatibility_flags = ["nodejs_compat"]
1678
+
1679
+ [[durable_objects.bindings]]
1680
+ name = "MCP_OBJECT"
1681
+ class_name = "${pascalClassName}MCP"
1682
+
1683
+ [[migrations]]
1684
+ tag = "v1"
1685
+ new_sqlite_classes = ["${pascalClassName}MCP"]
1686
+
1687
+ # KV Namespace for nonce cache (REQUIRED for replay attack prevention)
1688
+ #
1689
+ # RECOMMENDED: Share a single NONCE_CACHE namespace across all MCP-I workers
1690
+ # This namespace is automatically created by the setup script (npm run setup)
1691
+ # If you need to recreate it: npm run kv:create-nonce
1692
+ [[kv_namespaces]]
1693
+ binding = "${className.toUpperCase()}_NONCE_CACHE"
1694
+ id = "your_nonce_kv_namespace_id" # Auto-filled by setup script
1695
+
1696
+ # KV Namespace for proof archive (RECOMMENDED for auditability)
1697
+ #
1698
+ # Stores detached cryptographic proofs for all tool calls
1699
+ # Enables proof querying, session tracking, and audit trails
1700
+ # This namespace is automatically created by the setup script (npm run setup)
1701
+ # If you need to recreate it: npm run kv:create-proof
1702
+ #
1703
+ # Note: Comment out if you don't need proof archiving
1704
+ [[kv_namespaces]]
1705
+ binding = "${className.toUpperCase()}_PROOF_ARCHIVE"
1706
+ id = "your_proof_kv_namespace_id" # Auto-filled by setup script
1707
+
1708
+ # KV Namespace for identity storage (RECOMMENDED for persistent agent identity)
1709
+ #
1710
+ # Stores the agent's cryptographic identity (DID, keys) in KV
1711
+ # Ensures consistent identity across Worker restarts and deployments
1712
+ # This namespace is automatically created by the setup script (npm run setup)
1713
+ # If you need to recreate it: npm run kv:create-identity
1714
+ [[kv_namespaces]]
1715
+ binding = "${className.toUpperCase()}_IDENTITY_STORAGE"
1716
+ id = "your_identity_kv_namespace_id" # Auto-filled by setup script
1717
+
1718
+ # KV Namespace for delegation storage (REQUIRED for OAuth/delegation flows)
1719
+ #
1720
+ # Stores active delegations from users to agents
1721
+ # Enables OAuth consent flows and scope-based authorization
1722
+ # This namespace is automatically created by the setup script (npm run setup)
1723
+ # If you need to recreate it: npm run kv:create-delegation
1724
+ [[kv_namespaces]]
1725
+ binding = "${className.toUpperCase()}_DELEGATION_STORAGE"
1726
+ id = "your_delegation_kv_namespace_id" # Auto-filled by setup script
1727
+
1728
+ # KV Namespace for tool protection config (${apikey ? "ENABLED" : "OPTIONAL"} for dashboard-controlled delegation)
1729
+ #
1730
+ # 🆕 Enables dynamic tool protection configuration from AgentShield dashboard
1731
+ # Caches which tools require user delegation based on dashboard toggle switches
1732
+ #
1733
+ # Benefits:
1734
+ # - Control tool permissions from AgentShield dashboard without code changes
1735
+ # - Update delegation requirements in real-time (5-minute cache)
1736
+ # - No redeployments needed to change tool permissions
1737
+ #
1738
+ # Setup:
1739
+ # This namespace is automatically created by the setup script (npm run setup)
1740
+ # If you need to recreate it: npm run kv:create-tool-protection
1741
+ # After deployment, toggle delegation requirements in AgentShield dashboard
1742
+ #
1743
+ # Note: This namespace is REQUIRED when using AgentShield API key (--apikey)
1744
+ # It will be automatically created by the setup script (npm run setup)
1745
+ [[kv_namespaces]]
1746
+ binding = "${className.toUpperCase()}_TOOL_PROTECTION_KV"
1747
+ id = "your_tool_protection_kv_id" # Auto-filled by setup script
1748
+
1749
+ [vars]
1750
+ XMCP_I_TS_SKEW_SEC = "120"
1751
+ XMCP_I_SESSION_TTL = "1800"
1752
+
1753
+ # AgentShield Integration (https://kya.vouched.id)
1754
+ AGENTSHIELD_API_URL = "https://kya.vouched.id"
1755
+ # AGENTSHIELD_API_KEY - MUST be declared here for .dev.vars to work
1756
+ # Development: Add to .dev.vars file (already configured if --apikey was provided)
1757
+ # Production: wrangler secret put AGENTSHIELD_API_KEY
1758
+ AGENTSHIELD_API_KEY = ""
1759
+ # AGENTSHIELD_PROJECT_ID - Your project ID from AgentShield dashboard (e.g., "batman-txh0ae")
1760
+ # Required for project-scoped tool protection configuration (recommended)
1761
+ # Find it in your dashboard URL: https://kya.vouched.id/dashboard/projects/{PROJECT_ID}
1762
+ # Or in your project settings
1763
+ # Development: Add to .dev.vars file${projectId ? ` (already configured: ${projectId})` : ""}
1764
+ # Production: wrangler secret put AGENTSHIELD_PROJECT_ID
1765
+ AGENTSHIELD_PROJECT_ID = "${projectId || ""}"
1766
+ MCPI_ENV = "development"
1767
+
1768
+ # Optional: MCP Server URL for tool discovery
1769
+ # Uncomment to explicitly set your MCP server URL (auto-detected if not set)
1770
+ # MCP_SERVER_URL = "https://your-worker.workers.dev/mcp"
1771
+ `;
1772
+ fs.writeFileSync(path.join(projectPath, "wrangler.toml"), wranglerContent);
1773
+
1774
+ // Generate persistent identity for Cloudflare Worker
1775
+ if (!skipIdentity) {
1776
+ console.log(chalk.cyan("🔐 Generating persistent identity..."));
1777
+
1778
+ try {
1779
+ const identity = await generateIdentity();
1780
+
1781
+ // Read existing wrangler.toml
1782
+ const wranglerPath = path.join(projectPath, "wrangler.toml");
1783
+ let wranglerTomlContent = fs.readFileSync(wranglerPath, "utf8");
1784
+
1785
+ // Find [vars] section and add identity environment variables
1786
+ // Add ALL identity variables (empty values will be overridden by .dev.vars)
1787
+ const varsMatch = wranglerTomlContent.match(/\[vars\]/);
1788
+ if (varsMatch) {
1789
+ const insertPosition = varsMatch.index! + varsMatch[0].length;
1790
+ const identityVars = `
1791
+ # Agent DID (public identifier - safe to commit)
1792
+ MCP_IDENTITY_AGENT_DID = "${identity.did}"
1793
+
1794
+ # Identity keys - MUST be declared here for .dev.vars to work!
1795
+ # Development: Values in .dev.vars will override these empty strings
1796
+ # Production: Use wrangler secret put to set these
1797
+ MCP_IDENTITY_PRIVATE_KEY = ""
1798
+ MCP_IDENTITY_PUBLIC_KEY = ""
1799
+
1800
+ # ALLOWED_ORIGINS for CORS (update for production)
1801
+ ALLOWED_ORIGINS = "https://claude.ai,https://app.anthropic.com"
1802
+
1803
+ # DO routing strategy: "session" for dev, "shard" for production high-load
1804
+ DO_ROUTING_STRATEGY = "session"
1805
+ DO_SHARD_COUNT = "10" # Number of shards if using shard strategy
1806
+
1807
+ `;
1808
+
1809
+ // Check if API key variables already exist in wrangler.toml
1810
+ // (They may have been added by the initial template)
1811
+ const hasAgentshieldApiKey = /AGENTSHIELD_API_KEY\s*=/.test(
1812
+ wranglerTomlContent
1813
+ );
1814
+ const hasAgentshieldProjectId = /AGENTSHIELD_PROJECT_ID\s*=/.test(
1815
+ wranglerTomlContent
1816
+ );
1817
+ const hasAdminApiKey = /ADMIN_API_KEY\s*=/.test(wranglerTomlContent);
1818
+
1819
+ // Build API key declarations only for variables that don't already exist
1820
+ // Cloudflare Workers REQUIRE variables to be declared in [vars] for .dev.vars to work
1821
+ const apiKeyVarsParts: string[] = [];
1822
+
1823
+ if (
1824
+ !hasAgentshieldApiKey ||
1825
+ !hasAgentshieldProjectId ||
1826
+ !hasAdminApiKey
1827
+ ) {
1828
+ apiKeyVarsParts.push(
1829
+ `# API keys - MUST be declared here for .dev.vars to work!`
1830
+ );
1831
+ apiKeyVarsParts.push(
1832
+ `# Development: Values in .dev.vars will override these empty strings`
1833
+ );
1834
+ apiKeyVarsParts.push(
1835
+ `# Production: Use wrangler secret put to set these`
1836
+ );
1837
+ }
1838
+
1839
+ if (!hasAgentshieldApiKey) {
1840
+ apiKeyVarsParts.push(`AGENTSHIELD_API_KEY = ""`);
1841
+ }
1842
+ if (!hasAdminApiKey) {
1843
+ apiKeyVarsParts.push(`ADMIN_API_KEY = ""`);
1844
+ }
1845
+ if (!hasAgentshieldProjectId) {
1846
+ apiKeyVarsParts.push(`AGENTSHIELD_PROJECT_ID = ""`);
1847
+ }
1848
+
1849
+ const apiKeyVars =
1850
+ apiKeyVarsParts.length > 0
1851
+ ? `\n${apiKeyVarsParts.join("\n")}\n`
1852
+ : "";
1853
+
1854
+ wranglerTomlContent =
1855
+ wranglerTomlContent.slice(0, insertPosition) +
1856
+ identityVars +
1857
+ apiKeyVars +
1858
+ wranglerTomlContent.slice(insertPosition);
1859
+
1860
+ // Write updated wrangler.toml (without secrets)
1861
+ fs.writeFileSync(wranglerPath, wranglerTomlContent);
1862
+
1863
+ // Create .dev.vars file for local development (git-ignored)
1864
+ const devVarsPath = path.join(projectPath, ".dev.vars");
1865
+ const devVarsContent = `# Local development secrets (DO NOT COMMIT)
1866
+ # This file is git-ignored and contains sensitive data
1867
+ #
1868
+ # HOW IT WORKS:
1869
+ # 1. Variables are declared in wrangler.toml [vars] as empty strings
1870
+ # 2. This file (.dev.vars) overrides them for local development
1871
+ # 3. Production uses: wrangler secret put VARIABLE_NAME
1872
+
1873
+ # Identity keys (generated by create-mcpi-app)
1874
+ MCP_IDENTITY_PRIVATE_KEY="${identity.privateKey}"
1875
+ MCP_IDENTITY_PUBLIC_KEY="${identity.publicKey}"
1876
+
1877
+ # AgentShield API key (get from https://kya.vouched.id/dashboard)
1878
+ AGENTSHIELD_API_KEY="${apikey || ""}"${apikey ? " # Provided via --apikey flag" : ""}
1879
+
1880
+ # AgentShield Project ID (from dashboard URL: /dashboard/projects/{PROJECT_ID})
1881
+ AGENTSHIELD_PROJECT_ID="${projectId || ""}"${projectId ? " # Provided via --project flag" : ""}
1882
+
1883
+ # Admin API key for protected endpoints
1884
+ ADMIN_API_KEY=""
1885
+ `;
1886
+ fs.writeFileSync(devVarsPath, devVarsContent);
1887
+
1888
+ // Create .dev.vars.example for reference
1889
+ const devVarsExamplePath = path.join(
1890
+ projectPath,
1891
+ ".dev.vars.example"
1892
+ );
1893
+ const devVarsExampleContent = `# Copy this file to .dev.vars and fill in your values
1894
+ # DO NOT commit .dev.vars to version control
1895
+
1896
+ # Identity keys (generate with: npx @kya-os/create-mcpi-app regenerate-identity)
1897
+ MCP_IDENTITY_PRIVATE_KEY="your-private-key-here"
1898
+ MCP_IDENTITY_PUBLIC_KEY="your-public-key-here"
1899
+
1900
+ # AgentShield API key (get from https://agentshield.ai)
1901
+ AGENTSHIELD_API_KEY="your-api-key-here"
1902
+
1903
+ # Admin API key for protected endpoints
1904
+ ADMIN_API_KEY="your-admin-key-here"
1905
+ `;
1906
+ fs.writeFileSync(devVarsExamplePath, devVarsExampleContent);
1907
+
1908
+ console.log(chalk.green("✅ Generated persistent identity"));
1909
+ console.log(chalk.dim(` DID: ${identity.did}`));
1910
+ console.log(chalk.green("✅ Created secure configuration:"));
1911
+ console.log(
1912
+ chalk.dim(" • Public DID in wrangler.toml (safe to commit)")
1913
+ );
1914
+ console.log(
1915
+ chalk.dim(" • Private keys in .dev.vars (git-ignored)")
1916
+ );
1917
+ console.log(chalk.dim(" • Example template in .dev.vars.example"));
1918
+ console.log();
1919
+ console.log(chalk.yellow("🔒 Production Security:"));
1920
+ console.log(
1921
+ chalk.dim(" Set secrets using wrangler (never commit them):")
1922
+ );
1923
+ console.log(
1924
+ chalk.cyan(" $ wrangler secret put MCP_IDENTITY_PRIVATE_KEY")
1925
+ );
1926
+ console.log(
1927
+ chalk.cyan(" $ wrangler secret put MCP_IDENTITY_PUBLIC_KEY")
1928
+ );
1929
+ console.log(
1930
+ chalk.cyan(" $ wrangler secret put AGENTSHIELD_API_KEY")
1931
+ );
1932
+ console.log();
1933
+ }
1934
+ } catch (error: any) {
1935
+ console.log(
1936
+ chalk.yellow("⚠️ Failed to generate identity:"),
1937
+ error.message
1938
+ );
1939
+ console.log(chalk.dim(" You can generate one later with:"));
1940
+ console.log(
1941
+ chalk.cyan(" $ npx @kya-os/create-mcpi-app regenerate-identity")
1942
+ );
1943
+ console.log();
1944
+ }
1945
+ }
1946
+
1947
+ // Create tsconfig.json
1948
+ const tsconfigContent = {
1949
+ compilerOptions: {
1950
+ target: "ES2022",
1951
+ module: "ES2022",
1952
+ lib: ["ES2022"],
1953
+ types: ["@cloudflare/workers-types"],
1954
+ moduleResolution: "bundler",
1955
+ resolveJsonModule: true,
1956
+ allowSyntheticDefaultImports: true,
1957
+ esModuleInterop: true,
1958
+ strict: true,
1959
+ skipLibCheck: true,
1960
+ forceConsistentCasingInFileNames: true,
1961
+ },
1962
+ include: ["src/**/*"],
1963
+ };
1964
+ fs.writeJsonSync(path.join(projectPath, "tsconfig.json"), tsconfigContent, {
1965
+ spaces: 2,
1966
+ });
1967
+
1968
+ // Create .gitignore
1969
+ const gitignoreContent = `node_modules/
1970
+ dist/
1971
+ .wrangler/
1972
+ .dev.vars
1973
+ .env
1974
+ .env.local
1975
+ *.log
1976
+ `;
1977
+ fs.writeFileSync(path.join(projectPath, ".gitignore"), gitignoreContent);
1978
+
1979
+ // Create README.md
1980
+ const readmeContent = `# ${projectName}
1981
+
1982
+ MCP server running on Cloudflare Workers with MCP-I identity features, cryptographic proofs, and full SSE/HTTP streaming support.
1983
+
1984
+ ## Features
1985
+
1986
+ - ✅ **MCP Protocol Support**: SSE and HTTP streaming transports
1987
+ - ✅ **Cryptographic Identity**: DID-based agent identity with Ed25519 signatures
1988
+ - ✅ **Proof Generation**: Every tool call generates a cryptographic proof
1989
+ - ✅ **Audit Logging**: Track all operations with proof IDs and signatures
1990
+ - ✅ **Nonce Protection**: Replay attack prevention via KV-backed nonce cache
1991
+ - ✅ **Proof Archiving**: Optional KV storage for proof history
1992
+
1993
+ ## Quick Start
1994
+
1995
+ ### 1. Install Dependencies
1996
+
1997
+ \`\`\`bash
1998
+ ${packageManager} install
1999
+ \`\`\`
2000
+
2001
+ ### 2. Create KV Namespaces
2002
+
2003
+ #### Create All KV Namespaces (Recommended)
2004
+
2005
+ \`\`\`bash
2006
+ ${packageManager === "npm" ? "npm run" : packageManager} kv:create
2007
+ \`\`\`
2008
+
2009
+ This creates all 5 KV namespaces at once:
2010
+ - \`NONCE_CACHE\` - Replay attack prevention (Required)
2011
+ - \`PROOF_ARCHIVE\` - Cryptographic proof storage (Recommended)
2012
+ - \`IDENTITY_STORAGE\` - Agent identity persistence (Recommended)
2013
+ - \`DELEGATION_STORAGE\` - OAuth delegation storage (Required for delegation)
2014
+ - \`TOOL_PROTECTION_KV\` - Dashboard-controlled permissions (Optional)
2015
+
2016
+ Copy the namespace IDs from the output and update each one in \`wrangler.toml\`:
2017
+
2018
+ \`\`\`toml
2019
+ [[kv_namespaces]]
2020
+ binding = "NONCE_CACHE"
2021
+ id = "your_nonce_kv_id_here" # ← Update this
2022
+
2023
+ [[kv_namespaces]]
2024
+ binding = "PROOF_ARCHIVE"
2025
+ id = "your_proof_kv_id_here" # ← Update this
2026
+
2027
+ [[kv_namespaces]]
2028
+ binding = "IDENTITY_STORAGE"
2029
+ id = "your_identity_kv_id_here" # ← Update this
2030
+
2031
+ [[kv_namespaces]]
2032
+ binding = "DELEGATION_STORAGE"
2033
+ id = "your_delegation_kv_id_here" # ← Update this
2034
+
2035
+ [[kv_namespaces]]
2036
+ binding = "TOOL_PROTECTION_KV"
2037
+ id = "your_tool_protection_kv_id_here" # ← Update this
2038
+ \`\`\`
2039
+
2040
+ **Note:** You can also create namespaces individually:
2041
+ - \`${packageManager === "npm" ? "npm run" : packageManager} kv:create-nonce\` - Create nonce cache only
2042
+ - \`${packageManager === "npm" ? "npm run" : packageManager} kv:create-proof\` - Create proof archive only
2043
+ - \`${packageManager === "npm" ? "npm run" : packageManager} kv:create-identity\` - Create identity storage only
2044
+ - \`${packageManager === "npm" ? "npm run" : packageManager} kv:create-delegation\` - Create delegation storage only
2045
+ - \`${packageManager === "npm" ? "npm run" : packageManager} kv:create-tool-protection\` - Create tool protection cache only
2046
+
2047
+ ### 3. Test Locally
2048
+
2049
+ \`\`\`bash
2050
+ ${packageManager === "npm" ? "npm run" : packageManager} dev
2051
+ \`\`\`
2052
+
2053
+ ### 4. Deploy
2054
+
2055
+ \`\`\`bash
2056
+ ${packageManager === "npm" ? "npm run" : packageManager} deploy
2057
+ \`\`\`
2058
+
2059
+ ## Connect with Claude Desktop
2060
+
2061
+ Add to \`~/Library/Application Support/Claude/claude_desktop_config.json\`:
2062
+
2063
+ \`\`\`json
2064
+ {
2065
+ "mcpServers": {
2066
+ "${projectName}": {
2067
+ "command": "npx",
2068
+ "args": ["mcp-remote", "https://your-worker.workers.dev/sse"]
2069
+ }
2070
+ }
2071
+ }
2072
+ \`\`\`
2073
+
2074
+ **Note:** Use the \`/sse\` endpoint for Claude Desktop compatibility.
2075
+
2076
+ Restart Claude Desktop and test: "Use the greet tool to say hello to Alice"
2077
+
2078
+ ## Adding Tools
2079
+
2080
+ Create tools in \`src/tools/\`:
2081
+
2082
+ \`\`\`typescript
2083
+ import { z } from "zod";
2084
+
2085
+ export const myTool = {
2086
+ name: "my_tool",
2087
+ description: "Tool description",
2088
+ inputSchema: z.object({
2089
+ input: z.string().describe("Input parameter")
2090
+ }),
2091
+ handler: async ({ input }: { input: string }) => ({
2092
+ content: [{ type: "text" as const, text: \`Result: \${input}\` }]
2093
+ })
2094
+ };
2095
+ \`\`\`
2096
+
2097
+ Register in \`src/index.ts\` init method:
2098
+
2099
+ \`\`\`typescript
2100
+ this.server.tool(
2101
+ myTool.name,
2102
+ myTool.description,
2103
+ myTool.inputSchema.shape,
2104
+ myTool.handler
2105
+ );
2106
+ \`\`\`
2107
+
2108
+ ## Endpoints
2109
+
2110
+ - \`/health\` - Health check
2111
+ - \`/sse\` - SSE transport for MCP
2112
+ - \`/mcp\` - Streamable HTTP transport for MCP
2113
+ - \`/oauth/callback\` - OAuth callback for delegation flows
2114
+ - \`/admin/clear-cache\` - Clear tool protection cache (requires API key)
2115
+
2116
+ ## Viewing Cryptographic Proofs
2117
+
2118
+ Every tool call generates a cryptographic proof that's logged to the console:
2119
+
2120
+ \`\`\`bash
2121
+ ${packageManager === "npm" ? "npm run" : packageManager} dev
2122
+ \`\`\`
2123
+
2124
+ When you call a tool, you'll see logs like:
2125
+
2126
+ \`\`\`
2127
+ [MCP-I] Initialized with DID: did:web:localhost:agents:key-abc123
2128
+ [MCP-I Proof] {
2129
+ tool: 'greet',
2130
+ did: 'did:web:localhost:agents:key-abc123',
2131
+ proofId: 'proof_1234567890_abcd',
2132
+ signature: 'mNYP8x2k9FqV3...'
2133
+ }
2134
+ \`\`\`
2135
+
2136
+ ### Proof Archives (Optional)
2137
+
2138
+ If you configured the \`PROOF_ARCHIVE\` KV namespace, proofs are also stored for querying:
2139
+
2140
+ \`\`\`bash
2141
+ # List all proofs
2142
+ wrangler kv:key list --namespace-id=your_proof_kv_id
2143
+
2144
+ # View a specific proof
2145
+ wrangler kv:key get "proof_1234567890_abcd" --namespace-id=your_proof_kv_id
2146
+ \`\`\`
2147
+
2148
+ ## Identity Management
2149
+
2150
+ Your agent's cryptographic identity is stored in Durable Objects state. To view your agent's DID:
2151
+
2152
+ 1. Check the logs during \`init()\` - it prints the DID
2153
+ 2. Or query the runtime: \`await mcpiRuntime.getIdentity()\`
2154
+
2155
+ The identity includes:
2156
+ - \`did\`: Decentralized identifier (e.g., \`did:web:your-worker.workers.dev:agents:key-xyz\`)
2157
+ - \`publicKey\`: Ed25519 public key for signature verification
2158
+ - \`privateKey\`: Ed25519 private key (secured in Durable Object state)
2159
+
2160
+ ## AgentShield Integration
2161
+
2162
+ This project is configured to send cryptographic proofs to AgentShield for audit trails and compliance monitoring.
2163
+
2164
+ ### Setup
2165
+
2166
+ 1. **Get your AgentShield API key**:
2167
+ - Sign up at https://kya.vouched.id
2168
+ - Create a project
2169
+ - Copy your API key from the dashboard
2170
+
2171
+ 2. **Update \`wrangler.toml\`**:
2172
+ \`\`\`toml
2173
+ [vars]
2174
+ AGENTSHIELD_API_URL = "https://kya.vouched.id"
2175
+ AGENTSHIELD_API_KEY = "sk_your_actual_key_here" # ← Replace this
2176
+ MCPI_ENV = "development"
2177
+ \`\`\`
2178
+
2179
+ 3. **Test proof submission**:
2180
+ \`\`\`bash
2181
+ ${packageManager === "npm" ? "npm run" : packageManager} dev
2182
+ \`\`\`
2183
+
2184
+ Call a tool and check the logs:
2185
+ \`\`\`
2186
+ [AgentShield] Submitting proof: { did: 'did:web:...', sessionId: '...', jwsFormat: 'valid (3 parts)' }
2187
+ [AgentShield] ✅ Proofs accepted: 1
2188
+ \`\`\`
2189
+
2190
+ 4. **View proofs in dashboard**:
2191
+ - Go to https://kya.vouched.id/dashboard
2192
+ - Select your project
2193
+ - Click "Interactions" tab
2194
+ - See your proofs in real-time
2195
+
2196
+ ### Configuration
2197
+
2198
+ The AgentShield integration is configured in \`src/mcpi-runtime-config.ts\`. You can customize:
2199
+ - Proof batch size (\`maxBatchSize\`)
2200
+ - Flush interval (\`flushIntervalMs\`)
2201
+ - Retry policy (\`maxRetries\`)
2202
+ - Tool protection rules (\`toolProtections\`)
2203
+
2204
+ ### Dashboard-Controlled Tool Protection (Advanced)
2205
+
2206
+ 🆕 **NEW**: Control which tools require user delegation directly from the AgentShield dashboard - no code changes needed!
2207
+
2208
+ Instead of hardcoding \`requiresDelegation\` in your config, enable dynamic tool protection:
2209
+
2210
+ 1. **Create Tool Protection KV namespace**:
2211
+ \`\`\`bash
2212
+ ${packageManager === "npm" ? "npm run" : packageManager} kv:create-tool-protection
2213
+ \`\`\`
2214
+
2215
+ 2. **Uncomment TOOL_PROTECTION_KV in \`wrangler.toml\`**:
2216
+ \`\`\`toml
2217
+ [[kv_namespaces]]
2218
+ binding = "TOOL_PROTECTION_KV"
2219
+ id = "your_tool_protection_kv_id" # ← Add the ID from step 1
2220
+ \`\`\`
2221
+
2222
+ 3. **Enable Tool Protection Service in \`src/mcpi-runtime-config.ts\`**:
2223
+ - Uncomment the import: \`import { CloudflareRuntime } from "@kya-os/mcp-i-cloudflare";\`
2224
+ - Uncomment the \`toolProtectionService\` configuration block
2225
+
2226
+ 4. **Deploy and test**:
2227
+ \`\`\`bash
2228
+ ${packageManager === "npm" ? "npm run" : packageManager} deploy
2229
+ \`\`\`
2230
+
2231
+ 5. **Control delegation from dashboard**:
2232
+ - Go to https://kya.vouched.id/dashboard
2233
+ - Select your project → "Tools" tab
2234
+ - Toggle "Require Delegation" for any tool
2235
+ - Changes apply in real-time (5-minute cache)
2236
+
2237
+ **Benefits:**
2238
+ - Update tool permissions without redeploying
2239
+ - Test delegation flows instantly
2240
+ - Different requirements per environment (dev vs prod)
2241
+ - Automatic tool discovery from proof submissions
2242
+
2243
+ **Note:** The first time a tool is called, it auto-discovers in the dashboard. The \`requiresDelegation\` toggle will appear after the first proof is submitted.
2244
+
2245
+ ### Disable AgentShield (Optional)
2246
+
2247
+ If you don't want to use AgentShield, edit \`src/mcpi-runtime-config.ts\`:
2248
+
2249
+ \`\`\`typescript
2250
+ proofing: {
2251
+ enabled: false, // Disable proof submission
2252
+ // ...
2253
+ }
2254
+ \`\`\`
2255
+
2256
+ Or simply don't configure the \`AGENTSHIELD_API_KEY\` environment variable.
2257
+
2258
+ ## References
2259
+
2260
+ - [Cloudflare Agents MCP](https://developers.cloudflare.com/agents/model-context-protocol/)
2261
+ - [MCP Specification](https://spec.modelcontextprotocol.io/)
2262
+ - [MCP-I Documentation](https://github.com/kya-os/xmcp-i)
2263
+ `;
2264
+ fs.writeFileSync(path.join(projectPath, "README.md"), readmeContent);
2265
+
2266
+ console.log(chalk.green("✅ Cloudflare Worker MCP server created"));
2267
+ console.log();
2268
+
2269
+ if (apikey) {
2270
+ console.log(
2271
+ chalk.green("🔑 AgentShield API key configured in wrangler.toml")
2272
+ );
2273
+ console.log(
2274
+ chalk.dim(" Your API key has been added to the [vars] section")
2275
+ );
2276
+ console.log(chalk.dim(" Tool protection enforcement is ready to use!"));
2277
+ console.log();
2278
+ } else {
2279
+ console.log(chalk.yellow("⚠️ No AgentShield API key provided"));
2280
+ console.log(
2281
+ chalk.dim(
2282
+ " Add your API key to wrangler.toml [vars] section before deployment"
2283
+ )
2284
+ );
2285
+ console.log(
2286
+ chalk.dim(" Get your key at: https://kya.vouched.id/dashboard")
2287
+ );
2288
+ console.log();
2289
+ }
2290
+
2291
+ console.log(chalk.bold("📦 All KV Namespaces Configured"));
2292
+ console.log(chalk.dim(" - NONCE_CACHE: Replay attack prevention"));
2293
+ console.log(chalk.dim(" - PROOF_ARCHIVE: Cryptographic proof storage"));
2294
+ console.log(chalk.dim(" - IDENTITY_STORAGE: Agent identity persistence"));
2295
+ console.log(chalk.dim(" - DELEGATION_STORAGE: OAuth delegation storage"));
2296
+ console.log(
2297
+ chalk.dim(" - TOOL_PROTECTION_KV: Dashboard-controlled permissions")
2298
+ );
2299
+ console.log();
2300
+ console.log(
2301
+ chalk.cyan(" Run 'npm run kv:create' to create all namespaces")
2302
+ );
2303
+ console.log();
2304
+ } catch (error) {
2305
+ console.error(
2306
+ chalk.red("Failed to set up Cloudflare Worker MCP server:"),
2307
+ error
2308
+ );
2309
+ throw error;
2310
+ }
2311
+ }