@kya-os/create-mcpi-app 1.7.38-canary.2 → 1.7.39-canary.0

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