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