@kya-os/create-mcpi-app 1.7.39-canary.1 → 1.7.39-canary.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -76,22 +76,20 @@ npx create-mcpi-app my-quick-agent --yes
76
76
  ```
77
77
  my-agent/
78
78
  ├── src/
79
- │ ├── server.ts # Main agent server with identity
80
- └── tools/
81
- ├── index.ts # Tool exports
82
- │ └── hello.ts # Example tool with identity integration
83
- ├── identity-info.ts # Display agent identity information
84
- ├── sign-message.ts # Sign messages with agent's key
85
- └── verify-signature.ts # Verify ownership and signatures
86
- ├── package.json # Dependencies including xmcp-i
87
- ├── tsconfig.json # TypeScript configuration
88
- ├── .env.example # Environment variables for identity
89
- ├── .gitignore # Includes identity security rules
90
- ├── .eslintrc.json # Code quality configuration
91
- └── README.md # Agent-specific documentation
92
- ```
93
-
94
- **Security Note**: Identity management is now handled by the `mcpi` CLI. Development identities are stored in `.mcpi/identity.json` (gitignored) and production identities use environment variables.
79
+ │ ├── index.ts # Main agent server (~40 lines - super clean!)
80
+ ├── mcpi-runtime-config.ts # Runtime configuration (~50 lines)
81
+ └── tools/ # Your business logic only
82
+ │ └── greet.ts # Example tool
83
+ ├── wrangler.toml # Cloudflare Workers config
84
+ ├── package.json # Dependencies
85
+ └── README.md # Quick start guide
86
+ ```
87
+
88
+ **Key Features:**
89
+ - **Next.js-Style Architecture**: All framework complexity hidden in `@kya-os/mcp-i-cloudflare`
90
+ - **Consent Pages**: Server-hosted consent flow with XSS prevention
91
+ - **Auto-Detection**: Server URL automatically detected from requests
92
+ - **Clean Code**: Only your business logic visible - no implementation details
95
93
 
96
94
  ## Platform-Specific Features
97
95
 
@@ -102,7 +100,48 @@ my-agent/
102
100
  - **Durable Objects**: Built-in state management for MCP sessions
103
101
  - **Zero Cold Starts**: Instant response times with Durable Objects
104
102
  - **KV Storage**: Efficient nonce caching for security
105
- - **WebSockets**: Real-time bidirectional communication support
103
+ - **Consent Pages**: Server-hosted consent flow with auto-detection
104
+ - **XSS Prevention**: Secure consent page rendering with CSP headers
105
+ - **Proof Batching**: Automatic proof submission with cron-based flushing
106
+
107
+ #### Proof Batching & Cron Jobs
108
+
109
+ MCP-I automatically batches proofs for efficient submission to AgentShield. Proofs are:
110
+
111
+ 1. **Batched within requests**: When multiple tools are called, proofs are collected and submitted together
112
+ 2. **Flushed via cron**: A cron job flushes pending proofs every minute (configurable)
113
+
114
+ **Cron Configuration:**
115
+
116
+ The scaffolder automatically adds a cron trigger to `wrangler.toml`:
117
+
118
+ ```toml
119
+ [[triggers.crons]]
120
+ cron = "* * * * *" # Every minute (default)
121
+ ```
122
+
123
+ To adjust the flush frequency, edit `wrangler.toml`:
124
+
125
+ ```toml
126
+ [[triggers.crons]]
127
+ cron = "*/5 * * * *" # Every 5 minutes (recommended for production)
128
+ ```
129
+
130
+ **Verifying Proof Submission:**
131
+
132
+ 1. Check Cloudflare Worker logs for `[ProofService]` messages:
133
+ - `[ProofService] Initialized:` - Service started successfully
134
+ - `[ProofService] Enqueuing proof` - Proof added to batch queue
135
+ - `[ProofService] ✅ Proofs accepted` - Proofs submitted successfully
136
+
137
+ 2. View proofs in AgentShield dashboard:
138
+ - Navigate to `/proofs` page for your project
139
+ - Proofs appear within 5 minutes of tool execution
140
+
141
+ 3. Verify tool discovery:
142
+ - Navigate to `/tools` page
143
+ - Tools are automatically discovered from proof submissions
144
+ - Ensure `MCP_SERVER_URL` is set correctly (without `/mcp` suffix)
106
145
 
107
146
  ### Vercel - Edge Identity
108
147
 
@@ -9,7 +9,8 @@
9
9
  * NOTE: buildBaseConfig has been moved to @kya-os/contracts/config for shared use.
10
10
  * This module re-exports it for backward compatibility and adds remote fetching support.
11
11
  */
12
- export { buildBaseConfig } from '@kya-os/contracts/config';
12
+ import { buildBaseConfig } from '@kya-os/contracts/config';
13
+ export { buildBaseConfig };
13
14
  import type { MCPIConfig } from '@kya-os/contracts/config';
14
15
  import { type RemoteConfigCache } from '../utils/fetch-remote-config.js';
15
16
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"config-builder.d.ts","sourceRoot":"","sources":["../../src/helpers/config-builder.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAG3D,OAAO,KAAK,EACV,UAAU,EAQX,MAAM,0BAA0B,CAAC;AAElC,OAAO,EAGL,KAAK,iBAAiB,EACvB,MAAM,iCAAiC,CAAC;AAEzC;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC;;;OAGG;IACH,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAEzB;;;;OAIG;IACH,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;IAEzE;;;OAGG;IACH,KAAK,CAAC,EAAE,iBAAiB,CAAC;IAE1B;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;GASG;AACH,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,UAAU,CAAC,CAwBrB"}
1
+ {"version":3,"file":"config-builder.d.ts","sourceRoot":"","sources":["../../src/helpers/config-builder.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAG3D,OAAO,EAAE,eAAe,EAAE,CAAC;AAE3B,OAAO,KAAK,EACV,UAAU,EAQX,MAAM,0BAA0B,CAAC;AAElC,OAAO,EAGL,KAAK,iBAAiB,EACvB,MAAM,iCAAiC,CAAC;AAEzC;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC;;;OAGG;IACH,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAEzB;;;;OAIG;IACH,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;IAEzE;;;OAGG;IACH,KAAK,CAAC,EAAE,iBAAiB,CAAC;IAE1B;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;GASG;AACH,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,UAAU,CAAC,CAwBrB"}
@@ -9,9 +9,10 @@
9
9
  * NOTE: buildBaseConfig has been moved to @kya-os/contracts/config for shared use.
10
10
  * This module re-exports it for backward compatibility and adds remote fetching support.
11
11
  */
12
- // Re-export buildBaseConfig from contracts (single source of truth)
13
- export { buildBaseConfig } from '@kya-os/contracts/config';
12
+ // Import buildBaseConfig from contracts for local use and re-export
14
13
  import { buildBaseConfig } from '@kya-os/contracts/config';
14
+ // Re-export buildBaseConfig for backward compatibility
15
+ export { buildBaseConfig };
15
16
  import { fetchRemoteConfig } from '../utils/fetch-remote-config.js';
16
17
  /**
17
18
  * Build config with remote fetching support
@@ -1 +1 @@
1
- {"version":3,"file":"config-builder.js","sourceRoot":"","sources":["../../src/helpers/config-builder.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,oEAAoE;AACpE,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAC3D,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAa3D,OAAO,EACL,iBAAiB,EAGlB,MAAM,iCAAiC,CAAC;AAgCzC;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CACzC,OAA6B;IAE7B,MAAM,EAAE,GAAG,EAAE,aAAa,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,OAAO,CAAC;IAExD,sEAAsE;IACtE,IAAI,GAAG,CAAC,mBAAmB,IAAI,aAAa,EAAE,CAAC;QAC7C,MAAM,MAAM,GAAG,MAAM,iBAAiB,CACpC;YACE,MAAM,EAAE,GAAG,CAAC,mBAAmB,IAAI,wBAAwB;YAC3D,MAAM,EAAE,GAAG,CAAC,mBAAmB;YAC/B,SAAS,EAAE,GAAG,CAAC,sBAAsB;YACrC,QAAQ;YACR,QAAQ,EAAE,MAAM,EAAE,YAAY;YAC9B,aAAa;SACd,EACD,KAAK,CACN,CAAC;QAEF,IAAI,MAAM,EAAE,CAAC;YACX,OAAO,MAAM,CAAC;QAChB,CAAC;IACH,CAAC;IAED,4EAA4E;IAC5E,OAAO,eAAe,CAAC,GAAG,CAAC,CAAC;AAC9B,CAAC"}
1
+ {"version":3,"file":"config-builder.js","sourceRoot":"","sources":["../../src/helpers/config-builder.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,oEAAoE;AACpE,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAE3D,uDAAuD;AACvD,OAAO,EAAE,eAAe,EAAE,CAAC;AAa3B,OAAO,EACL,iBAAiB,EAGlB,MAAM,iCAAiC,CAAC;AAgCzC;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CACzC,OAA6B;IAE7B,MAAM,EAAE,GAAG,EAAE,aAAa,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,OAAO,CAAC;IAExD,sEAAsE;IACtE,IAAI,GAAG,CAAC,mBAAmB,IAAI,aAAa,EAAE,CAAC;QAC7C,MAAM,MAAM,GAAG,MAAM,iBAAiB,CACpC;YACE,MAAM,EAAE,GAAG,CAAC,mBAAmB,IAAI,wBAAwB;YAC3D,MAAM,EAAE,GAAG,CAAC,mBAAmB;YAC/B,SAAS,EAAE,GAAG,CAAC,sBAAsB;YACrC,QAAQ;YACR,QAAQ,EAAE,MAAM,EAAE,YAAY;YAC9B,aAAa;SACd,EACD,KAAK,CACN,CAAC;QAEF,IAAI,MAAM,EAAE,CAAC;YACX,OAAO,MAAM,CAAC;QAChB,CAAC;IACH,CAAC;IAED,4EAA4E;IAC5E,OAAO,eAAe,CAAC,GAAG,CAAC,CAAC;AAC9B,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"fetch-cloudflare-mcpi-template.d.ts","sourceRoot":"","sources":["../../src/helpers/fetch-cloudflare-mcpi-template.ts"],"names":[],"mappings":"AAKA,UAAU,6BAA6B;IACrC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED;;;GAGG;AACH,wBAAsB,2BAA2B,CAC/C,WAAW,EAAE,MAAM,EACnB,OAAO,GAAE,6BAAkC,GAC1C,OAAO,CAAC,IAAI,CAAC,CA09Cf"}
1
+ {"version":3,"file":"fetch-cloudflare-mcpi-template.d.ts","sourceRoot":"","sources":["../../src/helpers/fetch-cloudflare-mcpi-template.ts"],"names":[],"mappings":"AAKA,UAAU,6BAA6B;IACrC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED;;;GAGG;AACH,wBAAsB,2BAA2B,CAC/C,WAAW,EAAE,MAAM,EACnB,OAAO,GAAE,6BAAkC,GAC1C,OAAO,CAAC,IAAI,CAAC,CAgyCf"}
@@ -9,10 +9,24 @@ import { generateIdentity } from "./generate-identity.js";
9
9
  export async function fetchCloudflareMcpiTemplate(projectPath, options = {}) {
10
10
  const { packageManager = "npm", projectName = path.basename(projectPath), apikey, projectId, skipIdentity = false, } = options;
11
11
  // Sanitize project name for class names
12
- const className = projectName
12
+ let className = projectName
13
13
  .replace(/[^a-zA-Z0-9]/g, "")
14
14
  .replace(/^[0-9]/, "_$&");
15
+ // Fallback to prevent empty class names
16
+ if (!className || className.length === 0) {
17
+ className = "Project";
18
+ }
15
19
  const pascalClassName = className.charAt(0).toUpperCase() + className.slice(1);
20
+ // Sanitize project name for wrangler.toml (alphanumeric, lowercase, dashes only)
21
+ let wranglerName = projectName
22
+ .toLowerCase()
23
+ .replace(/[^a-z0-9-]/g, "-") // Replace invalid chars with dashes
24
+ .replace(/-+/g, "-") // Replace multiple dashes with single dash
25
+ .replace(/^-|-$/g, ""); // Remove leading/trailing dashes
26
+ // Fallback to prevent empty wrangler names
27
+ if (!wranglerName || wranglerName.length === 0) {
28
+ wranglerName = "worker";
29
+ }
16
30
  try {
17
31
  console.log(chalk.blue("📦 Setting up Cloudflare Worker MCP server..."));
18
32
  // Create package.json
@@ -53,8 +67,8 @@ export async function fetchCloudflareMcpiTemplate(projectPath, options = {}) {
53
67
  "test:coverage": "vitest run --coverage",
54
68
  },
55
69
  dependencies: {
56
- "@kya-os/contracts": "^1.5.1",
57
- "@kya-os/mcp-i-cloudflare": "^1.3.2",
70
+ "@kya-os/contracts": "^1.5.2-canary.0",
71
+ "@kya-os/mcp-i-cloudflare": "^1.4.1-canary.1",
58
72
  "@modelcontextprotocol/sdk": "^1.19.1",
59
73
  agents: "^0.2.8",
60
74
  hono: "^4.9.10",
@@ -228,9 +242,38 @@ async function setup() {
228
242
 
229
243
  log(\`Creating \${ns.name} (\${ns.purpose})...\`, colors.blue);
230
244
 
245
+ // First, check if namespace already exists
246
+ let existingNamespace = null;
247
+ try {
248
+ const listOutput = execSync('wrangler kv namespace list', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
249
+
250
+ try {
251
+ const allNamespaces = JSON.parse(listOutput);
252
+ existingNamespace = allNamespaces.find(n => n.title === ns.binding);
253
+ } catch (parseError) {
254
+ // Fallback to regex if JSON parsing fails
255
+ const existingMatch = listOutput.match(new RegExp(\`"title":\\s*"\${ns.binding}"[^}]*"id":\\s*"([^"]+)"\`));
256
+ if (existingMatch && existingMatch[1]) {
257
+ existingNamespace = { id: existingMatch[1], title: ns.binding };
258
+ }
259
+ }
260
+ } catch (listError) {
261
+ // If we can't list namespaces, continue to try creating
262
+ }
263
+
264
+ if (existingNamespace && existingNamespace.id) {
265
+ kvIds[ns.binding] = existingNamespace.id;
266
+ log(\` ✅ Using existing namespace with ID: \${existingNamespace.id}\`, colors.green);
267
+ continue;
268
+ }
269
+
270
+ // Namespace doesn't exist, try to create it
231
271
  try {
232
- // Create the namespace
233
- const output = execSync(\`wrangler kv namespace create "\${ns.binding}"\`, { encoding: 'utf-8', stderr: 'pipe' });
272
+ // Suppress stderr to avoid noisy error messages
273
+ const output = execSync(\`wrangler kv namespace create "\${ns.binding}"\`, {
274
+ encoding: 'utf-8',
275
+ stdio: ['pipe', 'pipe', 'pipe']
276
+ });
234
277
 
235
278
  // Extract the ID from output
236
279
  const idMatch = output.match(/id = "([^"]+)"/);
@@ -239,36 +282,26 @@ async function setup() {
239
282
  kvIds[ns.binding] = idMatch[1];
240
283
  log(\` ✅ Created with ID: \${idMatch[1]}\`, colors.green);
241
284
  } else {
242
- // Try to get existing namespace
243
- const listOutput = execSync('wrangler kv namespace list', { encoding: 'utf-8' });
244
-
285
+ log(\` ⚠️ Created but could not extract ID. Checking existing namespaces...\`, colors.yellow);
286
+ // Fallback: try to find it in the list
287
+ const listOutput = execSync('wrangler kv namespace list', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
245
288
  try {
246
- const namespaces = JSON.parse(listOutput);
247
- const existingNamespace = namespaces.find(n => n.title === ns.binding);
248
-
249
- if (existingNamespace && existingNamespace.id) {
250
- kvIds[ns.binding] = existingNamespace.id;
251
- log(\` ⚠️ Namespace already exists with ID: \${existingNamespace.id}\`, colors.yellow);
252
- } else {
253
- log(\` ⚠️ Could not extract ID for \${ns.binding}. You may need to add it manually.\`, colors.yellow);
254
- }
255
- } catch (parseError) {
256
- // Fallback to regex if JSON parsing fails
257
- const existingMatch = listOutput.match(new RegExp(\`"title":\\s*"\${ns.binding}"[^}]*"id":\\s*"([^"]+)"\`));
258
-
259
- if (existingMatch && existingMatch[1]) {
260
- kvIds[ns.binding] = existingMatch[1];
261
- log(\` ⚠️ Namespace already exists with ID: \${existingMatch[1]}\`, colors.yellow);
262
- } else {
263
- log(\` ⚠️ Could not extract ID for \${ns.binding}. You may need to add it manually.\`, colors.yellow);
289
+ const allNamespaces = JSON.parse(listOutput);
290
+ const found = allNamespaces.find(n => n.title === ns.binding);
291
+ if (found && found.id) {
292
+ kvIds[ns.binding] = found.id;
293
+ log(\` ✅ Found ID: \${found.id}\`, colors.green);
264
294
  }
295
+ } catch {
296
+ // Ignore parse errors
265
297
  }
266
298
  }
267
299
  } catch (error) {
268
300
  const errorMessage = error.message || error.toString();
301
+ const errorOutput = error.stdout || error.stderr || '';
269
302
 
270
303
  // Check if this is a multiple accounts error
271
- if (errorMessage.includes('More than one account') || errorMessage.includes('multiple accounts')) {
304
+ if (errorMessage.includes('More than one account') || errorMessage.includes('multiple accounts') || errorOutput.includes('More than one account')) {
272
305
  multipleAccountsDetected = true;
273
306
  log('\\n⚠️ Multiple Cloudflare accounts detected!\\n', colors.yellow);
274
307
  log('Wrangler cannot automatically select an account in non-interactive mode.\\n', colors.yellow);
@@ -286,37 +319,39 @@ async function setup() {
286
319
  break;
287
320
  }
288
321
 
289
- // Check if namespace already exists
290
- try {
291
- const listOutput = execSync('wrangler kv namespace list', { encoding: 'utf-8' });
292
-
293
- // Parse JSON output
322
+ // Check if namespace already exists (common case - suppress noisy error)
323
+ if (errorMessage.includes('already exists') || errorOutput.includes('already exists') || errorOutput.includes('code: 10014')) {
324
+ // Try to get the existing namespace ID
294
325
  try {
295
- const namespaces = JSON.parse(listOutput);
296
-
297
- // Look for namespace by title (which matches the binding name)
298
- const existingNamespace = namespaces.find(n => n.title === ns.binding);
299
-
300
- if (existingNamespace && existingNamespace.id) {
301
- kvIds[ns.binding] = existingNamespace.id;
302
- log(\` ⚠️ Found existing namespace with ID: \${existingNamespace.id}\`, colors.yellow);
303
- } else {
304
- log(\` ⚠️ Could not find existing namespace: \${ns.binding}\`, colors.yellow);
305
- log(\` Error: \${errorMessage}\`, colors.yellow);
306
- }
307
- } catch (parseError) {
308
- // If JSON parse fails, try regex as fallback
309
- const existingMatch = listOutput.match(new RegExp(\`"title":\\s*"\${ns.binding}"[^}]*"id":\\s*"([^"]+)"\`));
310
-
311
- if (existingMatch && existingMatch[1]) {
312
- kvIds[ns.binding] = existingMatch[1];
313
- log(\` ⚠️ Found existing namespace with ID: \${existingMatch[1]}\`, colors.yellow);
314
- } else {
315
- log(\` ❌ Failed to create \${ns.binding}: \${errorMessage}\`, colors.red);
326
+ const listOutput = execSync('wrangler kv namespace list', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
327
+
328
+ try {
329
+ const allNamespaces = JSON.parse(listOutput);
330
+ const found = allNamespaces.find(n => n.title === ns.binding);
331
+
332
+ if (found && found.id) {
333
+ kvIds[ns.binding] = found.id;
334
+ log(\` ✅ Using existing namespace with ID: \${found.id}\`, colors.green);
335
+ } else {
336
+ log(\` ⚠️ Namespace exists but could not find ID. You may need to add it manually.\`, colors.yellow);
337
+ }
338
+ } catch (parseError) {
339
+ // Fallback to regex
340
+ const existingMatch = listOutput.match(new RegExp(\`"title":\\s*"\${ns.binding}"[^}]*"id":\\s*"([^"]+)"\`));
341
+ if (existingMatch && existingMatch[1]) {
342
+ kvIds[ns.binding] = existingMatch[1];
343
+ log(\` ✅ Using existing namespace with ID: \${existingMatch[1]}\`, colors.green);
344
+ } else {
345
+ log(\` ⚠️ Namespace exists but could not find ID. You may need to add it manually.\`, colors.yellow);
346
+ }
316
347
  }
348
+ } catch (listError) {
349
+ log(\` ⚠️ Namespace may already exist. Run 'wrangler kv namespace list' to verify.\`, colors.yellow);
317
350
  }
318
- } catch (listError) {
319
- log(\` ❌ Failed to create or find \${ns.binding}\`, colors.red);
351
+ } else {
352
+ // Some other error occurred
353
+ log(\` ❌ Failed to create \${ns.binding}\`, colors.red);
354
+ log(\` Error: \${errorMessage}\`, colors.red);
320
355
  }
321
356
  }
322
357
  }
@@ -422,243 +457,8 @@ setup().catch((error) => {
422
457
  if (process.platform !== "win32") {
423
458
  fs.chmodSync(path.join(scriptsDir, "setup.js"), "755");
424
459
  }
425
- // Create tests directory
426
- const testsDir = path.join(projectPath, "tests");
427
- fs.ensureDirSync(testsDir);
428
- // Create delegation test file
429
- const delegationTestContent = `import { describe, test, expect, vi, beforeEach } from 'vitest';
430
-
431
- /**
432
- * Delegation Management Tests
433
- * Tests delegation verification, caching, and invalidation
434
- */
435
- describe('Delegation Management', () => {
436
- const mockDelegationStorage = {
437
- get: vi.fn(),
438
- put: vi.fn(),
439
- delete: vi.fn()
440
- };
441
-
442
- const mockVerificationCache = {
443
- get: vi.fn(),
444
- put: vi.fn(),
445
- delete: vi.fn()
446
- };
447
-
448
- const mockEnv = {
449
- ${className.toUpperCase()}_DELEGATION_STORAGE: mockDelegationStorage,
450
- TOOL_PROTECTION_KV: mockVerificationCache,
451
- AGENTSHIELD_API_KEY: 'test-key',
452
- AGENTSHIELD_API_URL: 'https://test.agentshield.ai'
453
- };
454
-
455
- beforeEach(() => {
456
- vi.clearAllMocks();
457
- global.fetch = vi.fn();
458
- });
459
-
460
- test('should verify delegation token with AgentShield API', async () => {
461
- const token = 'test-delegation-token';
462
-
463
- // Mock verification cache miss
464
- mockVerificationCache.get.mockResolvedValueOnce(null);
465
-
466
- // Mock API success
467
- global.fetch = vi.fn().mockResolvedValueOnce({
468
- ok: true
469
- });
470
-
471
- // Test verification would happen here
472
- expect(global.fetch).toHaveBeenCalledWith(
473
- expect.stringContaining('/api/v1/bouncer/delegations/verify'),
474
- expect.objectContaining({
475
- method: 'POST',
476
- body: JSON.stringify({ token })
477
- })
478
- );
479
- });
480
-
481
- test('should use 5-minute cache TTL for delegations', async () => {
482
- const token = 'test-token';
483
- const sessionId = 'test-session';
484
-
485
- await mockDelegationStorage.put(
486
- \`session:\${sessionId}\`,
487
- token,
488
- { expirationTtl: 300 } // 5 minutes
489
- );
490
-
491
- expect(mockDelegationStorage.put).toHaveBeenCalledWith(
492
- expect.any(String),
493
- token,
494
- { expirationTtl: 300 }
495
- );
496
- });
497
-
498
- test('should invalidate cache on revocation', async () => {
499
- const sessionId = 'revoked-session';
500
- const token = 'revoked-token';
501
-
502
- // Test invalidation
503
- await Promise.all([
504
- mockDelegationStorage.delete(\`session:\${sessionId}\`),
505
- mockVerificationCache.delete(\`verified:\${token.substring(0, 16)}\`)
506
- ]);
507
-
508
- expect(mockDelegationStorage.delete).toHaveBeenCalled();
509
- expect(mockVerificationCache.delete).toHaveBeenCalled();
510
- });
511
- });
512
- `;
513
- fs.writeFileSync(path.join(testsDir, "delegation.test.ts"), delegationTestContent);
514
- // Create DO routing test file
515
- const doRoutingTestContent = `import { describe, test, expect } from 'vitest';
516
-
517
- /**
518
- * Durable Object Routing Tests
519
- * Tests multi-instance DO routing for horizontal scaling
520
- */
521
- describe('DO Multi-Instance Routing', () => {
522
-
523
- function getDoInstanceId(request: Request, env: { DO_ROUTING_STRATEGY?: string; DO_SHARD_COUNT?: string }): string {
524
- const strategy = env.DO_ROUTING_STRATEGY || 'session';
525
- const headers = request.headers;
526
-
527
- switch (strategy) {
528
- case 'session': {
529
- const sessionId = headers.get('mcp-session-id') ||
530
- headers.get('Mcp-Session-Id') ||
531
- crypto.randomUUID();
532
- return \`session:\${sessionId}\`;
533
- }
534
-
535
- case 'shard': {
536
- const identifier = headers.get('mcp-session-id') || Math.random().toString();
537
- let hash = 0;
538
- for (let i = 0; i < identifier.length; i++) {
539
- hash = ((hash << 5) - hash) + identifier.charCodeAt(i);
540
- hash = hash & hash;
541
- }
542
- const shardCount = parseInt(env.DO_SHARD_COUNT || '10');
543
- // Validate shard count - must be a valid positive number
544
- const validShardCount = (!isNaN(shardCount) && shardCount > 0) ? shardCount : 10;
545
- const shard = Math.abs(hash) % validShardCount;
546
- return \`shard:\${shard}\`;
547
- }
548
-
549
- default:
550
- return 'default';
551
- }
552
- }
553
-
554
- test('should route to different instances for different sessions', () => {
555
- const env = { DO_ROUTING_STRATEGY: 'session' };
556
-
557
- const req1 = new Request('http://test/mcp', {
558
- headers: { 'mcp-session-id': 'session-123' }
559
- });
560
- const req2 = new Request('http://test/mcp', {
561
- headers: { 'mcp-session-id': 'session-456' }
562
- });
563
-
564
- const id1 = getDoInstanceId(req1, env);
565
- const id2 = getDoInstanceId(req2, env);
566
-
567
- expect(id1).toBe('session:session-123');
568
- expect(id2).toBe('session:session-456');
569
- expect(id1).not.toBe(id2);
570
- });
571
-
572
- test('should distribute load across shards', () => {
573
- const env = {
574
- DO_ROUTING_STRATEGY: 'shard',
575
- DO_SHARD_COUNT: '10'
576
- };
577
-
578
- const distribution = new Map<string, number>();
579
-
580
- // Generate 100 requests
581
- for (let i = 0; i < 100; i++) {
582
- const req = new Request('http://test/mcp', {
583
- headers: { 'mcp-session-id': \`session-\${i}\` }
584
- });
585
-
586
- const instanceId = getDoInstanceId(req, env);
587
- const shard = instanceId.split(':')[1];
588
-
589
- distribution.set(shard, (distribution.get(shard) || 0) + 1);
590
- }
591
-
592
- // Should use multiple shards
593
- expect(distribution.size).toBeGreaterThan(5);
594
- });
595
- });
596
- `;
597
- fs.writeFileSync(path.join(testsDir, "do-routing.test.ts"), doRoutingTestContent);
598
- // Create security test file
599
- const securityTestContent = `import { describe, test, expect } from 'vitest';
600
-
601
- /**
602
- * Security Tests
603
- * Tests CORS configuration and API key handling
604
- */
605
- describe('Security Configuration', () => {
606
-
607
- function getCorsOrigin(requestOrigin: string | null, env: { ALLOWED_ORIGINS?: string; MCPI_ENV?: string }): string | null {
608
- const allowedOrigins = env.ALLOWED_ORIGINS?.split(',').map((o: string) => o.trim()) || [
609
- 'https://claude.ai',
610
- 'https://app.anthropic.com'
611
- ];
612
-
613
- if (env.MCPI_ENV !== 'production' && !allowedOrigins.includes('http://localhost:3000')) {
614
- allowedOrigins.push('http://localhost:3000');
615
- }
616
-
617
- const origin = requestOrigin || '';
618
- const isAllowed = allowedOrigins.includes(origin);
619
-
620
- return isAllowed ? origin : allowedOrigins[0];
621
- }
622
-
623
- test('should allow Claude.ai by default', () => {
624
- const env = {};
625
- const origin = 'https://claude.ai';
626
- const result = getCorsOrigin(origin, env);
627
-
628
- expect(result).toBe(origin);
629
- });
630
-
631
- test('should reject unauthorized origins', () => {
632
- const env = { MCPI_ENV: 'production' };
633
- const origin = 'https://evil.com';
634
- const result = getCorsOrigin(origin, env);
635
-
636
- expect(result).toBe('https://claude.ai');
637
- expect(result).not.toBe(origin);
638
- });
639
-
640
- test('should not expose API keys in wrangler.toml', () => {
641
- // This test validates that API keys are only in .dev.vars
642
- const wranglerContent = \`
643
- [vars]
644
- AGENTSHIELD_API_URL = "https://kya.vouched.id"
645
- # AGENTSHIELD_API_KEY - Set securely
646
- \`;
647
-
648
- expect(wranglerContent).not.toContain('sk_');
649
- expect(wranglerContent).toContain('Set securely');
650
- });
651
-
652
- test('should use short TTLs for security', () => {
653
- const DELEGATION_TTL = 300; // 5 minutes
654
- const VERIFICATION_TTL = 60; // 1 minute
655
-
656
- expect(DELEGATION_TTL).toBeLessThanOrEqual(300);
657
- expect(VERIFICATION_TTL).toBeLessThanOrEqual(60);
658
- });
659
- });
660
- `;
661
- fs.writeFileSync(path.join(testsDir, "security.test.ts"), securityTestContent);
460
+ // Note: Tests directory is not created by default
461
+ // Users can add their own tests as needed
662
462
  // Create greet tool
663
463
  const greetToolContent = `import { z } from "zod";
664
464
 
@@ -792,11 +592,26 @@ export default createMCPIApp({
792
592
  `;
793
593
  fs.writeFileSync(path.join(srcDir, "index.ts"), indexContent);
794
594
  const wranglerContent = `#:schema node_modules/wrangler/config-schema.json
795
- name = "${projectName}"
595
+ name = "${wranglerName}"
796
596
  main = "src/index.ts"
797
597
  compatibility_date = "2025-06-18"
798
598
  compatibility_flags = ["nodejs_compat"]
799
599
 
600
+ # Build configuration
601
+ # Exclude native Node.js modules that can't be bundled by esbuild
602
+ [build]
603
+ external = [
604
+ "@swc/core-darwin-arm64",
605
+ "@swc/core-darwin-x64",
606
+ "@swc/core-linux-arm64-gnu",
607
+ "@swc/core-linux-arm64-musl",
608
+ "@swc/core-linux-x64-gnu",
609
+ "@swc/core-linux-x64-musl",
610
+ "@swc/core-win32-arm64-msvc",
611
+ "@swc/core-win32-x64-msvc",
612
+ "@swc/wasm"
613
+ ]
614
+
800
615
  [[durable_objects.bindings]]
801
616
  name = "MCP_OBJECT"
802
617
  class_name = "${pascalClassName}MCP"
@@ -805,6 +620,13 @@ class_name = "${pascalClassName}MCP"
805
620
  tag = "v1"
806
621
  new_sqlite_classes = ["${pascalClassName}MCP"]
807
622
 
623
+ # Cron trigger for proof batch queue flushing
624
+ # Flushes pending proofs every minute to ensure timely submission
625
+ # Adjust schedule as needed (cron format: minute hour day month weekday)
626
+ # Recommended: "*/5 * * * *" for production (every 5 minutes)
627
+ [[triggers.crons]]
628
+ cron = "* * * * *" # Every minute
629
+
808
630
  # KV Namespace for nonce cache (REQUIRED for replay attack prevention)
809
631
  #
810
632
  # RECOMMENDED: Share a single NONCE_CACHE namespace across all MCP-I workers