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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/.turbo/turbo-test$colon$coverage.log +315 -0
  3. package/.turbo/turbo-test.log +95 -0
  4. package/CHANGELOG.md +372 -0
  5. package/IMPLEMENTATION_SUMMARY.md +108 -0
  6. package/REMEDIATION_PLAN.md +99 -0
  7. package/coverage/base.css +224 -0
  8. package/coverage/block-navigation.js +87 -0
  9. package/coverage/clover.xml +252 -0
  10. package/coverage/config-builder.ts.html +580 -0
  11. package/coverage/coverage-final.json +7 -0
  12. package/coverage/favicon.png +0 -0
  13. package/coverage/fetch-cloudflare-mcpi-template.ts.html +7006 -0
  14. package/coverage/generate-config.ts.html +436 -0
  15. package/coverage/generate-identity.ts.html +574 -0
  16. package/coverage/index.html +191 -0
  17. package/coverage/install.ts.html +322 -0
  18. package/coverage/prettify.css +1 -0
  19. package/coverage/prettify.js +2 -0
  20. package/coverage/sort-arrow-sprite.png +0 -0
  21. package/coverage/sorter.js +210 -0
  22. package/coverage/validate-project-structure.ts.html +466 -0
  23. package/package.json +14 -7
  24. package/scripts/prepare-pack.js +47 -0
  25. package/scripts/validate-no-workspace.js +79 -0
  26. package/src/__tests__/cloudflare-template.test.ts +488 -0
  27. package/src/__tests__/helpers/fetch-cloudflare-mcpi-template.test.ts +337 -0
  28. package/src/__tests__/helpers/generate-config.test.ts +312 -0
  29. package/src/__tests__/helpers/generate-identity.test.ts +271 -0
  30. package/src/__tests__/helpers/install.test.ts +362 -0
  31. package/src/__tests__/helpers/validate-project-structure.test.ts +467 -0
  32. package/src/__tests__.bak/regression.test.ts +434 -0
  33. package/src/effects/index.ts +80 -0
  34. package/src/helpers/__tests__/config-builder.spec.ts +231 -0
  35. package/src/helpers/apply-identity-preset.ts +209 -0
  36. package/src/helpers/config-builder.ts +165 -0
  37. package/src/helpers/copy-template.ts +11 -0
  38. package/src/helpers/create.ts +239 -0
  39. package/src/helpers/fetch-cloudflare-mcpi-template.ts +2311 -0
  40. package/src/helpers/fetch-cloudflare-template.ts +361 -0
  41. package/src/helpers/fetch-mcpi-template.ts +236 -0
  42. package/src/helpers/fetch-xmcp-template.ts +153 -0
  43. package/src/helpers/generate-config.ts +117 -0
  44. package/src/helpers/generate-identity.ts +163 -0
  45. package/src/helpers/identity-manager.ts +186 -0
  46. package/src/helpers/install.ts +79 -0
  47. package/src/helpers/rename.ts +17 -0
  48. package/src/helpers/validate-project-structure.ts +127 -0
  49. package/src/index.ts +480 -0
  50. package/src/utils/check-node.ts +17 -0
  51. package/src/utils/is-folder-empty.ts +60 -0
  52. package/src/utils/validate-project-name.ts +132 -0
  53. package/test-cloudflare/README.md +164 -0
  54. package/test-cloudflare/package.json +28 -0
  55. package/test-cloudflare/src/index.ts +340 -0
  56. package/test-cloudflare/src/tools/greet.ts +19 -0
  57. package/test-cloudflare/tests/cache-invalidation.test.ts +410 -0
  58. package/test-cloudflare/tests/cors-security.test.ts +349 -0
  59. package/test-cloudflare/tests/delegation.test.ts +335 -0
  60. package/test-cloudflare/tests/do-routing.test.ts +314 -0
  61. package/test-cloudflare/tests/integration.test.ts +205 -0
  62. package/test-cloudflare/tests/session-management.test.ts +359 -0
  63. package/test-cloudflare/tsconfig.json +22 -0
  64. package/test-cloudflare/vitest.config.ts +9 -0
  65. package/test-cloudflare/wrangler.toml +37 -0
  66. package/test-node/README.md +44 -0
  67. package/test-node/package.json +23 -0
  68. package/test-node/src/tools/greet.ts +25 -0
  69. package/test-node/xmcp.config.ts +20 -0
  70. package/tsconfig.json +26 -0
  71. package/vitest.config.ts +14 -0
package/package.json CHANGED
@@ -1,15 +1,22 @@
1
1
  {
2
2
  "name": "@kya-os/create-mcpi-app",
3
- "version": "1.7.19",
3
+ "version": "1.7.20",
4
4
  "description": "Bootstrap MCP applications with identity features",
5
5
  "type": "module",
6
- "bin": {
7
- "create-mcpi-app": "index.js"
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "require": "./dist/index.js",
12
+ "types": "./dist/index.d.ts"
13
+ },
14
+ "./config-builder": {
15
+ "import": "./dist/helpers/config-builder.js",
16
+ "require": "./dist/helpers/config-builder.js",
17
+ "types": "./dist/helpers/config-builder.d.ts"
18
+ }
8
19
  },
9
- "files": [
10
- "index.js",
11
- "dist/**/*"
12
- ],
13
20
  "scripts": {
14
21
  "build": "tsc --build && chmod +x dist/index.js",
15
22
  "dev": "tsc --watch",
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Prepare package for npm pack by ensuring no workspace: references
5
+ * This script is run before npm pack in CI to ensure clean dependencies
6
+ */
7
+
8
+ import fs from 'fs';
9
+ import path from 'path';
10
+ import { fileURLToPath } from 'url';
11
+
12
+ const __filename = fileURLToPath(import.meta.url);
13
+ const __dirname = path.dirname(__filename);
14
+
15
+ const packagePath = path.join(__dirname, '..', 'package.json');
16
+ const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf-8'));
17
+
18
+ // Map of workspace dependencies to their published versions
19
+ const workspaceMap = {
20
+ '@kya-os/contracts': '^1.2.1',
21
+ '@kya-os/cli': '^1.2.4',
22
+ '@kya-os/cli-effects': '^1.0.9',
23
+ '@kya-os/mcp-i': '^1.2.3'
24
+ };
25
+
26
+ // Replace any workspace:* dependencies with published versions
27
+ if (packageJson.dependencies) {
28
+ for (const [dep, version] of Object.entries(packageJson.dependencies)) {
29
+ if (version === 'workspace:*' && workspaceMap[dep]) {
30
+ console.log(`Replacing ${dep}: workspace:* → ${workspaceMap[dep]}`);
31
+ packageJson.dependencies[dep] = workspaceMap[dep];
32
+ }
33
+ }
34
+ }
35
+
36
+ if (packageJson.devDependencies) {
37
+ for (const [dep, version] of Object.entries(packageJson.devDependencies)) {
38
+ if (version === 'workspace:*' && workspaceMap[dep]) {
39
+ console.log(`Replacing ${dep}: workspace:* → ${workspaceMap[dep]}`);
40
+ packageJson.devDependencies[dep] = workspaceMap[dep];
41
+ }
42
+ }
43
+ }
44
+
45
+ // Write the updated package.json
46
+ fs.writeFileSync(packagePath, JSON.stringify(packageJson, null, 2) + '\n');
47
+ console.log('✅ Package prepared for packing');
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Validate that package.json has no workspace: references before publishing
5
+ * This prevents publishing broken packages to npm
6
+ */
7
+
8
+ import fs from 'fs';
9
+ import path from 'path';
10
+ import { fileURLToPath } from 'url';
11
+
12
+ const __filename = fileURLToPath(import.meta.url);
13
+ const __dirname = path.dirname(__filename);
14
+
15
+ // Find package.json (supports being run from any package)
16
+ let packagePath;
17
+ if (fs.existsSync(path.join(process.cwd(), 'package.json'))) {
18
+ packagePath = path.join(process.cwd(), 'package.json');
19
+ } else if (fs.existsSync(path.join(__dirname, '..', 'package.json'))) {
20
+ packagePath = path.join(__dirname, '..', 'package.json');
21
+ } else {
22
+ console.error('❌ Cannot find package.json');
23
+ process.exit(1);
24
+ }
25
+
26
+ const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf-8'));
27
+ const packageName = packageJson.name || 'unknown';
28
+
29
+ // Skip root monorepo package - it's not published
30
+ if (packageName === 'mcp-monorepo') {
31
+ console.log(`⏭️ Skipping root monorepo package (not published)`);
32
+ process.exit(0);
33
+ }
34
+
35
+ console.log(`🔍 Validating ${packageName} has no workspace: references...`);
36
+
37
+ let hasWorkspaceRefs = false;
38
+
39
+ // Check dependencies
40
+ if (packageJson.dependencies) {
41
+ for (const [dep, version] of Object.entries(packageJson.dependencies)) {
42
+ if (typeof version === 'string' && version.includes('workspace:')) {
43
+ console.error(`❌ Found workspace: reference in dependencies: ${dep}: ${version}`);
44
+ hasWorkspaceRefs = true;
45
+ }
46
+ }
47
+ }
48
+
49
+ // Check devDependencies
50
+ if (packageJson.devDependencies) {
51
+ for (const [dep, version] of Object.entries(packageJson.devDependencies)) {
52
+ if (typeof version === 'string' && version.includes('workspace:')) {
53
+ console.error(`❌ Found workspace: reference in devDependencies: ${dep}: ${version}`);
54
+ hasWorkspaceRefs = true;
55
+ }
56
+ }
57
+ }
58
+
59
+ // Check peerDependencies
60
+ if (packageJson.peerDependencies) {
61
+ for (const [dep, version] of Object.entries(packageJson.peerDependencies)) {
62
+ if (typeof version === 'string' && version.includes('workspace:')) {
63
+ console.error(`❌ Found workspace: reference in peerDependencies: ${dep}: ${version}`);
64
+ hasWorkspaceRefs = true;
65
+ }
66
+ }
67
+ }
68
+
69
+ if (hasWorkspaceRefs) {
70
+ console.error('');
71
+ console.error('❌ PUBLISH BLOCKED: workspace: references found in package.json');
72
+ console.error('');
73
+ console.error('Please replace all workspace: references with actual version numbers.');
74
+ console.error('Example: "workspace:*" → "^1.0.0"');
75
+ console.error('');
76
+ process.exit(1);
77
+ }
78
+
79
+ console.log(`✅ ${packageName} validation passed - no workspace: references`);
@@ -0,0 +1,488 @@
1
+ /**
2
+ * Cloudflare Template Tests
3
+ *
4
+ * Tests for Cloudflare Worker template generation, including file structure,
5
+ * configuration, KV namespace setup, and tool protection configuration.
6
+ */
7
+
8
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
9
+ import fs from "fs-extra";
10
+ import path from "path";
11
+ import { fetchCloudflareMcpiTemplate } from "../helpers/fetch-cloudflare-mcpi-template";
12
+
13
+ describe("Cloudflare Template Generation", () => {
14
+ let tempDir: string;
15
+ const originalConsoleLog = console.log;
16
+ const originalConsoleError = console.error;
17
+
18
+ beforeEach(() => {
19
+ // Create temporary directory for each test
20
+ tempDir = path.join(
21
+ process.cwd(),
22
+ "test-temp",
23
+ `test-${Date.now()}-${Math.random().toString(36).substring(7)}`
24
+ );
25
+ fs.ensureDirSync(tempDir);
26
+
27
+ // Suppress console output during tests
28
+ console.log = vi.fn();
29
+ console.error = vi.fn();
30
+ });
31
+
32
+ afterEach(async () => {
33
+ // Clean up temporary directory
34
+ if (fs.existsSync(tempDir)) {
35
+ try {
36
+ fs.removeSync(tempDir);
37
+ } catch (error) {
38
+ // Ignore errors if directory is already being cleaned up by another test
39
+ }
40
+ }
41
+
42
+ // Only try to remove parent directory if it exists and is empty
43
+ const testTempDir = path.join(process.cwd(), "test-temp");
44
+ if (fs.existsSync(testTempDir)) {
45
+ try {
46
+ const testTempContents = fs.readdirSync(testTempDir);
47
+ if (testTempContents.length === 0) {
48
+ fs.removeSync(testTempDir);
49
+ }
50
+ } catch (error) {
51
+ // Ignore errors if directory is being modified by another test
52
+ }
53
+ }
54
+
55
+ // Restore console
56
+ console.log = originalConsoleLog;
57
+ console.error = originalConsoleError;
58
+ });
59
+
60
+ describe("Basic template structure", () => {
61
+ it("should create all required files and directories", async () => {
62
+ await fetchCloudflareMcpiTemplate(tempDir, {
63
+ packageManager: "npm",
64
+ projectName: "test-project",
65
+ skipIdentity: false,
66
+ });
67
+
68
+ // Check package.json exists
69
+ expect(fs.existsSync(path.join(tempDir, "package.json"))).toBe(true);
70
+
71
+ // Check src directory structure
72
+ expect(fs.existsSync(path.join(tempDir, "src"))).toBe(true);
73
+ expect(fs.existsSync(path.join(tempDir, "src", "tools"))).toBe(true);
74
+ expect(
75
+ fs.existsSync(path.join(tempDir, "src", "tools", "greet.ts"))
76
+ ).toBe(true);
77
+
78
+ // Check scripts directory
79
+ expect(fs.existsSync(path.join(tempDir, "scripts"))).toBe(true);
80
+ expect(fs.existsSync(path.join(tempDir, "scripts", "setup.js"))).toBe(
81
+ true
82
+ );
83
+
84
+ // Check configuration files
85
+ expect(fs.existsSync(path.join(tempDir, "wrangler.toml"))).toBe(true);
86
+ expect(fs.existsSync(path.join(tempDir, "tsconfig.json"))).toBe(true);
87
+ expect(fs.existsSync(path.join(tempDir, ".gitignore"))).toBe(true);
88
+ expect(fs.existsSync(path.join(tempDir, "README.md"))).toBe(true);
89
+ expect(fs.existsSync(path.join(tempDir, "vitest.config.ts"))).toBe(true);
90
+ });
91
+
92
+ it("should create mcpi-runtime-config.ts with correct structure", async () => {
93
+ await fetchCloudflareMcpiTemplate(tempDir, {
94
+ packageManager: "npm",
95
+ projectName: "test-project",
96
+ });
97
+
98
+ const configPath = path.join(tempDir, "src", "mcpi-runtime-config.ts");
99
+ expect(fs.existsSync(configPath)).toBe(true);
100
+
101
+ const configContent = fs.readFileSync(configPath, "utf-8");
102
+ expect(configContent).toContain("getRuntimeConfig");
103
+ expect(configContent).toContain("buildBaseConfig");
104
+ expect(configContent).toContain("CloudflareRuntimeConfig");
105
+ });
106
+
107
+ it("should create index.ts with correct structure", async () => {
108
+ await fetchCloudflareMcpiTemplate(tempDir, {
109
+ packageManager: "npm",
110
+ projectName: "test-project",
111
+ });
112
+
113
+ const indexPath = path.join(tempDir, "src", "index.ts");
114
+ expect(fs.existsSync(indexPath)).toBe(true);
115
+
116
+ const indexContent = fs.readFileSync(indexPath, "utf-8");
117
+ expect(indexContent).toContain("McpAgent");
118
+ expect(indexContent).toContain("createCloudflareRuntime");
119
+ expect(indexContent).toContain("class");
120
+ });
121
+ });
122
+
123
+ describe("MCP_SERVER_URL configuration", () => {
124
+ it("should include MCP_SERVER_URL placeholder in wrangler.toml", async () => {
125
+ await fetchCloudflareMcpiTemplate(tempDir, {
126
+ packageManager: "npm",
127
+ projectName: "test-project",
128
+ });
129
+
130
+ const wranglerPath = path.join(tempDir, "wrangler.toml");
131
+ const wranglerContent = fs.readFileSync(wranglerPath, "utf-8");
132
+
133
+ expect(wranglerContent).toContain("MCP_SERVER_URL");
134
+ expect(wranglerContent).toContain(
135
+ "Optional: MCP Server URL for tool discovery"
136
+ );
137
+ });
138
+
139
+ it("should log warning when MCP_SERVER_URL not configured", async () => {
140
+ await fetchCloudflareMcpiTemplate(tempDir, {
141
+ packageManager: "npm",
142
+ projectName: "test-project",
143
+ });
144
+
145
+ const indexPath = path.join(tempDir, "src", "index.ts");
146
+ const indexContent = fs.readFileSync(indexPath, "utf-8");
147
+
148
+ // Check that warning is logged in index.ts
149
+ expect(indexContent).toContain("Warning: MCP_SERVER_URL not configured");
150
+ });
151
+
152
+ it("should store MCP_SERVER_URL in class property", async () => {
153
+ await fetchCloudflareMcpiTemplate(tempDir, {
154
+ packageManager: "npm",
155
+ projectName: "test-project",
156
+ });
157
+
158
+ const indexPath = path.join(tempDir, "src", "index.ts");
159
+ const indexContent = fs.readFileSync(indexPath, "utf-8");
160
+
161
+ expect(indexContent).toContain("this.mcpServerUrl");
162
+ expect(indexContent).toContain("env.MCP_SERVER_URL");
163
+ });
164
+ });
165
+
166
+ describe("KV namespace setup", () => {
167
+ it("should include all required KV namespace bindings in wrangler.toml", async () => {
168
+ await fetchCloudflareMcpiTemplate(tempDir, {
169
+ packageManager: "npm",
170
+ projectName: "TestProject",
171
+ });
172
+
173
+ const wranglerPath = path.join(tempDir, "wrangler.toml");
174
+ const wranglerContent = fs.readFileSync(wranglerPath, "utf-8");
175
+
176
+ // Check for all KV namespace bindings (using className TESTPROJECT)
177
+ expect(wranglerContent).toContain("TESTPROJECT_NONCE_CACHE");
178
+ expect(wranglerContent).toContain("TESTPROJECT_PROOF_ARCHIVE");
179
+ expect(wranglerContent).toContain("TESTPROJECT_IDENTITY_STORAGE");
180
+ expect(wranglerContent).toContain("TESTPROJECT_DELEGATION_STORAGE");
181
+ expect(wranglerContent).toContain("TESTPROJECT_TOOL_PROTECTION_KV");
182
+ });
183
+
184
+ it("should create KV namespace creation scripts in package.json", async () => {
185
+ await fetchCloudflareMcpiTemplate(tempDir, {
186
+ packageManager: "npm",
187
+ projectName: "test-project",
188
+ });
189
+
190
+ const packageJsonPath = path.join(tempDir, "package.json");
191
+ const packageJson = fs.readJsonSync(packageJsonPath);
192
+
193
+ expect(packageJson.scripts).toHaveProperty("kv:create");
194
+ expect(packageJson.scripts).toHaveProperty("kv:create-nonce");
195
+ expect(packageJson.scripts).toHaveProperty("kv:create-proof");
196
+ expect(packageJson.scripts).toHaveProperty("kv:create-identity");
197
+ expect(packageJson.scripts).toHaveProperty("kv:create-delegation");
198
+ expect(packageJson.scripts).toHaveProperty("kv:create-tool-protection");
199
+ });
200
+
201
+ it("should create KV namespace listing scripts", async () => {
202
+ await fetchCloudflareMcpiTemplate(tempDir, {
203
+ packageManager: "npm",
204
+ projectName: "test-project",
205
+ });
206
+
207
+ const packageJsonPath = path.join(tempDir, "package.json");
208
+ const packageJson = fs.readJsonSync(packageJsonPath);
209
+
210
+ expect(packageJson.scripts).toHaveProperty("kv:list");
211
+ expect(packageJson.scripts).toHaveProperty("kv:keys-nonce");
212
+ expect(packageJson.scripts).toHaveProperty("kv:keys-proof");
213
+ expect(packageJson.scripts).toHaveProperty("kv:keys-identity");
214
+ expect(packageJson.scripts).toHaveProperty("kv:keys-delegation");
215
+ expect(packageJson.scripts).toHaveProperty("kv:keys-tool-protection");
216
+ });
217
+ });
218
+
219
+ describe("Tool protection setup", () => {
220
+ it("should include tool protection configuration in runtime config", async () => {
221
+ await fetchCloudflareMcpiTemplate(tempDir, {
222
+ packageManager: "npm",
223
+ projectName: "test-project",
224
+ apikey: "test-api-key",
225
+ });
226
+
227
+ const configPath = path.join(tempDir, "src", "mcpi-runtime-config.ts");
228
+ const configContent = fs.readFileSync(configPath, "utf-8");
229
+
230
+ expect(configContent).toContain("toolProtection");
231
+ expect(configContent).toContain("CloudflareRuntime");
232
+ });
233
+
234
+ it("should include CloudflareRuntime import for tool protection", async () => {
235
+ await fetchCloudflareMcpiTemplate(tempDir, {
236
+ packageManager: "npm",
237
+ projectName: "test-project",
238
+ });
239
+
240
+ const configPath = path.join(tempDir, "src", "mcpi-runtime-config.ts");
241
+ const configContent = fs.readFileSync(configPath, "utf-8");
242
+
243
+ expect(configContent).toContain("CloudflareRuntime");
244
+ expect(configContent).toContain('from "@kya-os/mcp-i-cloudflare"');
245
+ });
246
+
247
+ it("should conditionally enable tool protection KV when API key provided", async () => {
248
+ await fetchCloudflareMcpiTemplate(tempDir, {
249
+ packageManager: "npm",
250
+ projectName: "test-project",
251
+ apikey: "test-api-key",
252
+ });
253
+
254
+ const wranglerPath = path.join(tempDir, "wrangler.toml");
255
+ const wranglerContent = fs.readFileSync(wranglerPath, "utf-8");
256
+
257
+ // When API key is provided, TOOL_PROTECTION_KV should not be commented out
258
+ const lines = wranglerContent.split("\n");
259
+ const toolProtectionLines = lines.filter((line) =>
260
+ line.includes("TOOL_PROTECTION_KV")
261
+ );
262
+ const commentedLines = toolProtectionLines.filter((line) =>
263
+ line.trim().startsWith("#")
264
+ );
265
+
266
+ // Should have some non-commented lines for TOOL_PROTECTION_KV
267
+ expect(toolProtectionLines.length).toBeGreaterThan(0);
268
+ });
269
+
270
+ it("should comment out tool protection KV when no API key provided", async () => {
271
+ await fetchCloudflareMcpiTemplate(tempDir, {
272
+ packageManager: "npm",
273
+ projectName: "test-project",
274
+ });
275
+
276
+ const wranglerPath = path.join(tempDir, "wrangler.toml");
277
+ const wranglerContent = fs.readFileSync(wranglerPath, "utf-8");
278
+
279
+ // Tool protection KV is always included (not conditionally commented)
280
+ // The namespace is automatically created by the setup script
281
+ const lines = wranglerContent.split("\n");
282
+ const bindingLine = lines.find(
283
+ (line) =>
284
+ line.includes("binding =") && line.includes("TOOL_PROTECTION_KV")
285
+ );
286
+
287
+ // Should be present and not commented out
288
+ expect(bindingLine).toBeDefined();
289
+ expect(bindingLine?.trim().startsWith("#")).toBe(false);
290
+ });
291
+ });
292
+
293
+ describe("Identity provider configuration", () => {
294
+ it("should generate identity when skipIdentity is false", async () => {
295
+ const generateIdentitySpy = vi.spyOn(
296
+ await import("../helpers/generate-identity"),
297
+ "generateIdentity"
298
+ );
299
+ generateIdentitySpy.mockResolvedValue({
300
+ did: "did:key:ztest123",
301
+ kid: "did:key:ztest123#key-1",
302
+ privateKey: "test-private-key",
303
+ publicKey: "test-public-key",
304
+ createdAt: new Date().toISOString(),
305
+ type: "development",
306
+ });
307
+
308
+ await fetchCloudflareMcpiTemplate(tempDir, {
309
+ packageManager: "npm",
310
+ projectName: "test-project",
311
+ skipIdentity: false,
312
+ });
313
+
314
+ // Check that identity was generated (would be in .dev.vars)
315
+ const devVarsPath = path.join(tempDir, ".dev.vars");
316
+ if (fs.existsSync(devVarsPath)) {
317
+ const devVarsContent = fs.readFileSync(devVarsPath, "utf-8");
318
+ expect(devVarsContent).toContain("MCP_IDENTITY_PRIVATE_KEY");
319
+ expect(devVarsContent).toContain("MCP_IDENTITY_PUBLIC_KEY");
320
+ }
321
+
322
+ generateIdentitySpy.mockRestore();
323
+ });
324
+
325
+ it("should skip identity generation when skipIdentity is true", async () => {
326
+ await fetchCloudflareMcpiTemplate(tempDir, {
327
+ packageManager: "npm",
328
+ projectName: "test-project",
329
+ skipIdentity: true,
330
+ });
331
+
332
+ // Identity should not be generated
333
+ const devVarsPath = path.join(tempDir, ".dev.vars");
334
+ // When skipIdentity is true, .dev.vars might not exist or might not have identity
335
+ // This is expected behavior
336
+ });
337
+
338
+ it("should include identity variables in wrangler.toml", async () => {
339
+ await fetchCloudflareMcpiTemplate(tempDir, {
340
+ packageManager: "npm",
341
+ projectName: "test-project",
342
+ });
343
+
344
+ const wranglerPath = path.join(tempDir, "wrangler.toml");
345
+ const wranglerContent = fs.readFileSync(wranglerPath, "utf-8");
346
+
347
+ expect(wranglerContent).toContain("MCP_IDENTITY_AGENT_DID");
348
+ expect(wranglerContent).toContain("MCP_IDENTITY_PRIVATE_KEY");
349
+ expect(wranglerContent).toContain("MCP_IDENTITY_PUBLIC_KEY");
350
+ });
351
+
352
+ it("should create .dev.vars.example template", async () => {
353
+ await fetchCloudflareMcpiTemplate(tempDir, {
354
+ packageManager: "npm",
355
+ projectName: "test-project",
356
+ });
357
+
358
+ const examplePath = path.join(tempDir, ".dev.vars.example");
359
+ expect(fs.existsSync(examplePath)).toBe(true);
360
+
361
+ const exampleContent = fs.readFileSync(examplePath, "utf-8");
362
+ expect(exampleContent).toContain("MCP_IDENTITY_PRIVATE_KEY");
363
+ expect(exampleContent).toContain("your-private-key-here");
364
+ });
365
+ });
366
+
367
+ describe("Error handling", () => {
368
+ it("should handle file system errors gracefully", async () => {
369
+ // Create a read-only directory to simulate permission errors
370
+ const readOnlyDir = path.join(tempDir, "readonly");
371
+ fs.ensureDirSync(readOnlyDir);
372
+ fs.chmodSync(readOnlyDir, 0o444); // Read-only
373
+
374
+ try {
375
+ await fetchCloudflareMcpiTemplate(readOnlyDir, {
376
+ packageManager: "npm",
377
+ projectName: "test-project",
378
+ });
379
+ // Should throw or handle error
380
+ } catch (error) {
381
+ expect(error).toBeDefined();
382
+ } finally {
383
+ // Restore permissions for cleanup
384
+ fs.chmodSync(readOnlyDir, 0o755);
385
+ }
386
+ });
387
+
388
+ it("should handle invalid project names", async () => {
389
+ await fetchCloudflareMcpiTemplate(tempDir, {
390
+ packageManager: "npm",
391
+ projectName: "123invalid-name!!!",
392
+ skipIdentity: true,
393
+ });
394
+
395
+ // Should sanitize project name for class names
396
+ const indexPath = path.join(tempDir, "src", "index.ts");
397
+ if (fs.existsSync(indexPath)) {
398
+ const indexContent = fs.readFileSync(indexPath, "utf-8");
399
+ // Class name should be sanitized
400
+ expect(indexContent).toMatch(/class\s+\w+MCP/);
401
+ }
402
+ });
403
+ });
404
+
405
+ describe("Package manager configuration", () => {
406
+ it("should use correct package manager in scripts", async () => {
407
+ await fetchCloudflareMcpiTemplate(tempDir, {
408
+ packageManager: "pnpm",
409
+ projectName: "test-project",
410
+ });
411
+
412
+ const packageJsonPath = path.join(tempDir, "package.json");
413
+ const packageJson = fs.readJsonSync(packageJsonPath);
414
+
415
+ // Scripts should use pnpm commands
416
+ expect(packageJson.scripts.setup).toContain("node");
417
+ // Other scripts should use pnpm
418
+ expect(packageJson.scripts.dev).toBe("wrangler dev");
419
+ });
420
+
421
+ it("should create correct package.json structure", async () => {
422
+ await fetchCloudflareMcpiTemplate(tempDir, {
423
+ packageManager: "npm",
424
+ projectName: "test-project",
425
+ });
426
+
427
+ const packageJsonPath = path.join(tempDir, "package.json");
428
+ const packageJson = fs.readJsonSync(packageJsonPath);
429
+
430
+ expect(packageJson.name).toBe("test-project");
431
+ expect(packageJson.version).toBe("0.1.0");
432
+ expect(packageJson.private).toBe(true);
433
+ expect(packageJson.dependencies).toBeDefined();
434
+ expect(packageJson.devDependencies).toBeDefined();
435
+ });
436
+ });
437
+
438
+ describe("Template files content", () => {
439
+ it("should create greet tool with correct structure", async () => {
440
+ await fetchCloudflareMcpiTemplate(tempDir, {
441
+ packageManager: "npm",
442
+ projectName: "test-project",
443
+ });
444
+
445
+ const greetPath = path.join(tempDir, "src", "tools", "greet.ts");
446
+ const greetContent = fs.readFileSync(greetPath, "utf-8");
447
+
448
+ expect(greetContent).toContain("greetTool");
449
+ expect(greetContent).toContain('name: "greet"');
450
+ expect(greetContent).toContain("inputSchema");
451
+ expect(greetContent).toContain("handler");
452
+ });
453
+
454
+ it("should create test files with correct structure", async () => {
455
+ await fetchCloudflareMcpiTemplate(tempDir, {
456
+ packageManager: "npm",
457
+ projectName: "test-project",
458
+ });
459
+
460
+ const testsDir = path.join(tempDir, "tests");
461
+ expect(fs.existsSync(testsDir)).toBe(true);
462
+
463
+ const delegationTestPath = path.join(testsDir, "delegation.test.ts");
464
+ expect(fs.existsSync(delegationTestPath)).toBe(true);
465
+
466
+ const delegationTestContent = fs.readFileSync(
467
+ delegationTestPath,
468
+ "utf-8"
469
+ );
470
+ expect(delegationTestContent).toContain("Delegation Management");
471
+ expect(delegationTestContent).toContain("describe");
472
+ });
473
+
474
+ it("should create README with project name", async () => {
475
+ await fetchCloudflareMcpiTemplate(tempDir, {
476
+ packageManager: "npm",
477
+ projectName: "my-awesome-project",
478
+ });
479
+
480
+ const readmePath = path.join(tempDir, "README.md");
481
+ const readmeContent = fs.readFileSync(readmePath, "utf-8");
482
+
483
+ expect(readmeContent).toContain("my-awesome-project");
484
+ expect(readmeContent).toContain("Quick Start");
485
+ expect(readmeContent).toContain("Features");
486
+ });
487
+ });
488
+ });