@kya-os/create-mcpi-app 1.7.39-canary.0 → 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/.turbo/turbo-build.log +2 -2
- package/.turbo/turbo-test$colon$coverage.log +164 -183
- package/.turbo/turbo-test.log +31 -30
- package/README.md +56 -17
- package/dist/helpers/config-builder.d.ts +2 -1
- package/dist/helpers/config-builder.d.ts.map +1 -1
- package/dist/helpers/config-builder.js +3 -2
- package/dist/helpers/config-builder.js.map +1 -1
- package/dist/helpers/fetch-cloudflare-mcpi-template.d.ts.map +1 -1
- package/dist/helpers/fetch-cloudflare-mcpi-template.js +115 -293
- package/dist/helpers/fetch-cloudflare-mcpi-template.js.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/package.json +4 -5
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
|
-
│ ├──
|
|
80
|
-
│
|
|
81
|
-
│
|
|
82
|
-
│ └──
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
- **
|
|
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
|
-
|
|
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
|
-
//
|
|
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;
|
|
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,
|
|
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
|
-
|
|
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.
|
|
57
|
-
"@kya-os/mcp-i-cloudflare": "^1.
|
|
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
|
-
//
|
|
233
|
-
const output = execSync(\`wrangler kv namespace create "\${ns.binding}"\`, {
|
|
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
|
-
|
|
243
|
-
|
|
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
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
291
|
-
|
|
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
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
}
|
|
319
|
-
|
|
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
|
-
//
|
|
426
|
-
|
|
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 = "${
|
|
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
|