@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,153 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
import fs from "fs-extra";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import os from "os";
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
|
|
7
|
+
interface ScaffoldTemplateOptions {
|
|
8
|
+
xmcpVersion?: string;
|
|
9
|
+
xmcpChannel?: string;
|
|
10
|
+
packageManager?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Scaffold XMCP project using the upstream xmcp npm package
|
|
15
|
+
* This follows the requirements specification to use the xmcp package
|
|
16
|
+
* with default caret ^0.3.1 and support for dist-tags
|
|
17
|
+
*/
|
|
18
|
+
export async function fetchXMCPTemplate(
|
|
19
|
+
projectPath: string,
|
|
20
|
+
options: ScaffoldTemplateOptions = {}
|
|
21
|
+
): Promise<void> {
|
|
22
|
+
const { xmcpVersion, xmcpChannel = "latest", packageManager = "npm" } = options;
|
|
23
|
+
|
|
24
|
+
// Get XMCP package configuration from environment or use defaults
|
|
25
|
+
const xmcpPackageName = process.env.XMCP_PACKAGE_NAME || "xmcp";
|
|
26
|
+
const defaultCaret = process.env.XMCP_DEFAULT_CARET || "^0.3.1";
|
|
27
|
+
|
|
28
|
+
// Determine which version to use
|
|
29
|
+
let targetVersion = xmcpVersion || defaultCaret;
|
|
30
|
+
|
|
31
|
+
// If using a channel, check if the dist-tag exists
|
|
32
|
+
if (xmcpChannel === "next") {
|
|
33
|
+
try {
|
|
34
|
+
// Check if 'next' dist-tag exists
|
|
35
|
+
execSync(`npm view ${xmcpPackageName}@next version`, {
|
|
36
|
+
stdio: 'pipe'
|
|
37
|
+
});
|
|
38
|
+
targetVersion = "next";
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.warn(chalk.yellow(`⚠️ dist-tag 'next' not available for ${xmcpPackageName}, falling back to ${defaultCaret}`));
|
|
41
|
+
targetVersion = defaultCaret;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
console.log(
|
|
47
|
+
chalk.blue(`📦 Setting up XMCP project with ${xmcpPackageName}@${targetVersion}...`)
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
// Create package.json with xmcp dependency
|
|
51
|
+
// NOTE: Do NOT add "type": "module" as xmcp framework generates CommonJS code
|
|
52
|
+
const packageJson = {
|
|
53
|
+
name: path.basename(projectPath),
|
|
54
|
+
version: "0.1.0",
|
|
55
|
+
dependencies: {
|
|
56
|
+
[xmcpPackageName]: targetVersion,
|
|
57
|
+
// Other dependencies will be added by apply-identity-preset
|
|
58
|
+
},
|
|
59
|
+
scripts: {
|
|
60
|
+
// Basic scripts - will be enhanced by identity preset
|
|
61
|
+
dev: "xmcp dev",
|
|
62
|
+
build: "xmcp build",
|
|
63
|
+
start: "xmcp start"
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Write package.json
|
|
68
|
+
fs.ensureDirSync(projectPath);
|
|
69
|
+
fs.writeJsonSync(path.join(projectPath, "package.json"), packageJson, { spaces: 2 });
|
|
70
|
+
|
|
71
|
+
// Create basic XMCP project structure
|
|
72
|
+
const srcDir = path.join(projectPath, "src");
|
|
73
|
+
const toolsDir = path.join(srcDir, "tools");
|
|
74
|
+
const promptsDir = path.join(srcDir, "prompts");
|
|
75
|
+
const resourcesDir = path.join(srcDir, "resources");
|
|
76
|
+
|
|
77
|
+
fs.ensureDirSync(toolsDir);
|
|
78
|
+
fs.ensureDirSync(promptsDir);
|
|
79
|
+
fs.ensureDirSync(resourcesDir);
|
|
80
|
+
|
|
81
|
+
// Create a basic greet tool (XMCP default)
|
|
82
|
+
const greetToolContent = `import { z } from "zod";
|
|
83
|
+
import { type InferSchema, type ToolMetadata } from "${xmcpPackageName}";
|
|
84
|
+
|
|
85
|
+
// Define the schema for tool parameters
|
|
86
|
+
export const schema = {
|
|
87
|
+
name: z.string().describe("The name of the user to greet"),
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// Define tool metadata
|
|
91
|
+
export const metadata: ToolMetadata = {
|
|
92
|
+
name: "greet",
|
|
93
|
+
description: "Greet the user",
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// Tool implementation
|
|
97
|
+
export default async function greet({ name }: InferSchema<typeof schema>) {
|
|
98
|
+
return {
|
|
99
|
+
content: [
|
|
100
|
+
{
|
|
101
|
+
type: "text" as const,
|
|
102
|
+
text: \`Hello, \${name}!\`,
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
`;
|
|
108
|
+
|
|
109
|
+
fs.writeFileSync(path.join(toolsDir, "greet.ts"), greetToolContent);
|
|
110
|
+
|
|
111
|
+
// Create tools index (no .js extension needed for TypeScript imports)
|
|
112
|
+
const toolsIndexContent = `export * from "./greet";\n`;
|
|
113
|
+
fs.writeFileSync(path.join(toolsDir, "index.ts"), toolsIndexContent);
|
|
114
|
+
|
|
115
|
+
// Create prompts index (placeholder for xmcp)
|
|
116
|
+
const promptsIndexContent = `// Prompts can be defined here if needed\nexport {};\n`;
|
|
117
|
+
fs.writeFileSync(path.join(promptsDir, "index.ts"), promptsIndexContent);
|
|
118
|
+
|
|
119
|
+
// Create resources index with a comment
|
|
120
|
+
// Note: We don't export anything to avoid "Invalid file path format" warnings
|
|
121
|
+
// Users can add resources here when needed
|
|
122
|
+
const resourcesIndexContent = `// Resources can be defined here if needed
|
|
123
|
+
// Example:
|
|
124
|
+
// export const myResource = {
|
|
125
|
+
// name: "my-resource",
|
|
126
|
+
// description: "A sample resource",
|
|
127
|
+
// uri: "file:///path/to/resource"
|
|
128
|
+
// };
|
|
129
|
+
`;
|
|
130
|
+
fs.writeFileSync(path.join(resourcesDir, "index.ts"), resourcesIndexContent);
|
|
131
|
+
|
|
132
|
+
// Create basic xmcp.config.ts (will be enhanced by generate-config)
|
|
133
|
+
const configContent = `export default {
|
|
134
|
+
tools: "./src/tools",
|
|
135
|
+
transport: "stdio",
|
|
136
|
+
};
|
|
137
|
+
`;
|
|
138
|
+
fs.writeFileSync(path.join(projectPath, "xmcp.config.ts"), configContent);
|
|
139
|
+
|
|
140
|
+
// Create .gitignore
|
|
141
|
+
const gitignoreContent = `node_modules/
|
|
142
|
+
dist/
|
|
143
|
+
.env
|
|
144
|
+
.env.local
|
|
145
|
+
`;
|
|
146
|
+
fs.writeFileSync(path.join(projectPath, ".gitignore"), gitignoreContent);
|
|
147
|
+
|
|
148
|
+
console.log(chalk.green("✅ XMCP project structure created"));
|
|
149
|
+
} catch (error) {
|
|
150
|
+
console.error(chalk.red("Failed to set up XMCP project:"), error);
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import fs from "fs-extra";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import type { NodeRuntimeConfig } from "@kya-os/mcp-i";
|
|
4
|
+
|
|
5
|
+
export function generateConfig(
|
|
6
|
+
projectPath: string,
|
|
7
|
+
transports: string[],
|
|
8
|
+
skipIdentity = false
|
|
9
|
+
): void {
|
|
10
|
+
const hasHttp = transports.includes("http");
|
|
11
|
+
const hasStdio = transports.includes("stdio");
|
|
12
|
+
|
|
13
|
+
let configContent = `import type { XmcpConfig } from "@kya-os/mcp-i";
|
|
14
|
+
|
|
15
|
+
const config: XmcpConfig = {
|
|
16
|
+
// Point to the tools directory
|
|
17
|
+
paths: {
|
|
18
|
+
tools: "./src/tools",
|
|
19
|
+
},`;
|
|
20
|
+
|
|
21
|
+
// Only add identity config if not skipped
|
|
22
|
+
if (!skipIdentity) {
|
|
23
|
+
configContent += `
|
|
24
|
+
|
|
25
|
+
// Enable MCP-I identity features
|
|
26
|
+
identity: {
|
|
27
|
+
enabled: true,
|
|
28
|
+
environment: "development",
|
|
29
|
+
},`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (hasHttp) {
|
|
33
|
+
configContent += `
|
|
34
|
+
|
|
35
|
+
// Enable HTTP transport
|
|
36
|
+
http: true,`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (hasStdio) {
|
|
40
|
+
configContent += `
|
|
41
|
+
|
|
42
|
+
// Enable STDIO transport
|
|
43
|
+
stdio: true,`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
configContent += `
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export default config;
|
|
50
|
+
`;
|
|
51
|
+
|
|
52
|
+
const configPath = path.join(projectPath, "xmcp.config.ts");
|
|
53
|
+
fs.writeFileSync(configPath, configContent);
|
|
54
|
+
|
|
55
|
+
// Generate runtime config for proof submission and delegation
|
|
56
|
+
generateRuntimeConfig(projectPath, skipIdentity);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Generate runtime config file for ProofBatchQueue and delegation
|
|
61
|
+
*/
|
|
62
|
+
function generateRuntimeConfig(projectPath: string, skipIdentity: boolean): void {
|
|
63
|
+
const runtimeConfigContent = `import type { NodeRuntimeConfig } from "@kya-os/mcp-i";
|
|
64
|
+
import { buildBaseConfig } from "@kya-os/create-mcpi-app/config-builder";
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Runtime configuration for MCP-I server
|
|
68
|
+
*
|
|
69
|
+
* This file configures runtime features like proof submission to AgentShield,
|
|
70
|
+
* delegation verification, and audit logging.
|
|
71
|
+
*
|
|
72
|
+
* Note: Environment variables are automatically injected by the MCP-I compiler
|
|
73
|
+
* from wrangler.toml (Cloudflare) or .env (Node.js). Configure them there:
|
|
74
|
+
* - AGENTSHIELD_API_URL: AgentShield API base URL
|
|
75
|
+
* - AGENTSHIELD_API_KEY: Your AgentShield API key
|
|
76
|
+
* - MCPI_ENV: "development" or "production"
|
|
77
|
+
*
|
|
78
|
+
* This config uses the unified MCPIConfig architecture, ensuring the same
|
|
79
|
+
* base configuration shape across all platforms (Cloudflare, Node.js, Vercel).
|
|
80
|
+
*/
|
|
81
|
+
export function getRuntimeConfig(): NodeRuntimeConfig {
|
|
82
|
+
// Access environment variables (works in both Node.js and Cloudflare Workers)
|
|
83
|
+
// The compiler injects these from wrangler.toml or .env
|
|
84
|
+
const env = process.env;
|
|
85
|
+
|
|
86
|
+
// Build base config that works across all platforms
|
|
87
|
+
const baseConfig = buildBaseConfig(env);
|
|
88
|
+
|
|
89
|
+
// Extend with Node.js-specific properties
|
|
90
|
+
return {
|
|
91
|
+
...baseConfig,
|
|
92
|
+
// Node.js-specific server configuration
|
|
93
|
+
server: {
|
|
94
|
+
port: parseInt(env.PORT || "3000", 10),
|
|
95
|
+
host: env.HOST || "0.0.0.0",
|
|
96
|
+
cors: true,
|
|
97
|
+
timeout: 30000
|
|
98
|
+
},
|
|
99
|
+
// Node.js-specific storage configuration
|
|
100
|
+
storage: {
|
|
101
|
+
type: "memory" as const
|
|
102
|
+
},
|
|
103
|
+
// Node.js-specific environment configuration
|
|
104
|
+
nodeEnv: {
|
|
105
|
+
environment: (env.NODE_ENV as "development" | "production" | "test") || "development",
|
|
106
|
+
debug: env.MCPI_ENV === "development"
|
|
107
|
+
}
|
|
108
|
+
} as NodeRuntimeConfig;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export default getRuntimeConfig();
|
|
112
|
+
`;
|
|
113
|
+
|
|
114
|
+
const runtimeConfigPath = path.join(projectPath, "src", "mcpi-runtime-config.ts");
|
|
115
|
+
fs.ensureDirSync(path.join(projectPath, "src"));
|
|
116
|
+
fs.writeFileSync(runtimeConfigPath, runtimeConfigContent);
|
|
117
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identity Generation for MCP-I Applications
|
|
3
|
+
*
|
|
4
|
+
* Generates Ed25519 keypairs and DID:key identifiers
|
|
5
|
+
* for persistent agent identity across deployments.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { webcrypto } from 'crypto';
|
|
9
|
+
import baseX from 'base-x';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Type for Web Crypto API key pair
|
|
13
|
+
*/
|
|
14
|
+
interface WebCryptoKeyPair {
|
|
15
|
+
privateKey: webcrypto.CryptoKey;
|
|
16
|
+
publicKey: webcrypto.CryptoKey;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Generated identity structure
|
|
21
|
+
*/
|
|
22
|
+
export interface GeneratedIdentity {
|
|
23
|
+
did: string;
|
|
24
|
+
kid: string;
|
|
25
|
+
privateKey: string;
|
|
26
|
+
publicKey: string;
|
|
27
|
+
createdAt: string;
|
|
28
|
+
type: 'development' | 'production';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Generate a new Ed25519 identity with DID:key
|
|
33
|
+
*
|
|
34
|
+
* @returns Promise<GeneratedIdentity> - Complete identity with keys and DID
|
|
35
|
+
*/
|
|
36
|
+
export async function generateIdentity(): Promise<GeneratedIdentity> {
|
|
37
|
+
// 1. Generate Ed25519 keypair using Web Crypto API
|
|
38
|
+
const keyPair = (await webcrypto.subtle.generateKey(
|
|
39
|
+
{
|
|
40
|
+
name: 'Ed25519',
|
|
41
|
+
},
|
|
42
|
+
true, // extractable
|
|
43
|
+
['sign', 'verify']
|
|
44
|
+
)) as WebCryptoKeyPair;
|
|
45
|
+
|
|
46
|
+
// 2. Export keys in PKCS#8 (private) and SPKI (public) formats
|
|
47
|
+
const privateKeyPKCS8 = await webcrypto.subtle.exportKey('pkcs8', keyPair.privateKey);
|
|
48
|
+
const publicKeySPKI = await webcrypto.subtle.exportKey('spki', keyPair.publicKey);
|
|
49
|
+
|
|
50
|
+
// 3. Extract RAW keys from PKCS#8/SPKI wrappers
|
|
51
|
+
// This matches WebCryptoProvider.generateKeyPair() behavior
|
|
52
|
+
const privateKeyRaw = extractEd25519PrivateKey(new Uint8Array(privateKeyPKCS8 as ArrayBuffer));
|
|
53
|
+
const publicKeyRaw = extractEd25519PublicKey(Buffer.from(publicKeySPKI as ArrayBuffer).toString('base64'));
|
|
54
|
+
|
|
55
|
+
// 4. Convert to base64 strings for storage
|
|
56
|
+
const privateKey = Buffer.from(privateKeyRaw).toString('base64');
|
|
57
|
+
const publicKey = Buffer.from(publicKeyRaw).toString('base64');
|
|
58
|
+
|
|
59
|
+
// 5. Generate DID:key from raw public key bytes
|
|
60
|
+
const did = generateDIDFromPublicKeyBytes(publicKeyRaw);
|
|
61
|
+
|
|
62
|
+
// 5. Return complete identity
|
|
63
|
+
return {
|
|
64
|
+
did,
|
|
65
|
+
kid: `${did}#key-1`,
|
|
66
|
+
privateKey,
|
|
67
|
+
publicKey,
|
|
68
|
+
createdAt: new Date().toISOString(),
|
|
69
|
+
type: 'development'
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Generate a DID:key identifier from raw Ed25519 public key bytes
|
|
75
|
+
*
|
|
76
|
+
* Following the DID:key specification:
|
|
77
|
+
* https://w3c-ccg.github.io/did-method-key/
|
|
78
|
+
*
|
|
79
|
+
* Format: did:key:z<multibase-base58btc(<multicodec-ed25519-pub><publicKey>)>
|
|
80
|
+
*
|
|
81
|
+
* @param publicKeyBytes - Raw 32-byte Ed25519 public key
|
|
82
|
+
* @returns DID:key string (e.g., "did:key:z6Mk...")
|
|
83
|
+
*/
|
|
84
|
+
function generateDIDFromPublicKeyBytes(publicKeyBytes: Uint8Array): string {
|
|
85
|
+
// 1. Ed25519 multicodec prefix (0xed 0x01)
|
|
86
|
+
const multicodecPrefix = new Uint8Array([0xed, 0x01]);
|
|
87
|
+
|
|
88
|
+
// 2. Combine multicodec prefix + public key
|
|
89
|
+
const multicodecKey = new Uint8Array(multicodecPrefix.length + publicKeyBytes.length);
|
|
90
|
+
multicodecKey.set(multicodecPrefix);
|
|
91
|
+
multicodecKey.set(publicKeyBytes, multicodecPrefix.length);
|
|
92
|
+
|
|
93
|
+
// 3. Encode with base58-btc (Bitcoin alphabet)
|
|
94
|
+
const base58 = baseX('123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz');
|
|
95
|
+
const base58Encoded = base58.encode(multicodecKey);
|
|
96
|
+
|
|
97
|
+
// 4. Add multibase prefix 'z' for base58-btc
|
|
98
|
+
const multibaseEncoded = 'z' + base58Encoded;
|
|
99
|
+
|
|
100
|
+
return `did:key:${multibaseEncoded}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Extract the raw 32-byte Ed25519 private key from PKCS#8 format
|
|
105
|
+
*
|
|
106
|
+
* PKCS#8 format for Ed25519 (48 bytes total):
|
|
107
|
+
* - 16-byte header (algorithm identifier, version, etc.)
|
|
108
|
+
* - 32-byte raw Ed25519 private key
|
|
109
|
+
*
|
|
110
|
+
* @param pkcs8 - PKCS#8 private key as Uint8Array
|
|
111
|
+
* @returns Uint8Array - Raw 32-byte Ed25519 private key
|
|
112
|
+
*/
|
|
113
|
+
function extractEd25519PrivateKey(pkcs8: Uint8Array): Uint8Array {
|
|
114
|
+
// Extract bytes 16-48 (the raw 32-byte key)
|
|
115
|
+
// This matches WebCryptoProvider.extractRawPrivateKey()
|
|
116
|
+
return pkcs8.slice(16, 48);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Extract the raw 32-byte Ed25519 public key from SPKI format
|
|
121
|
+
*
|
|
122
|
+
* SPKI (SubjectPublicKeyInfo) format:
|
|
123
|
+
* - 12-byte header (algorithm identifier)
|
|
124
|
+
* - 32-byte raw Ed25519 public key
|
|
125
|
+
*
|
|
126
|
+
* @param publicKey - Base64-encoded SPKI public key
|
|
127
|
+
* @returns Uint8Array - Raw 32-byte Ed25519 public key
|
|
128
|
+
*/
|
|
129
|
+
function extractEd25519PublicKey(publicKey: string): Uint8Array {
|
|
130
|
+
const spkiBytes = Buffer.from(publicKey, 'base64');
|
|
131
|
+
|
|
132
|
+
// SPKI format: 12-byte header + 32-byte public key
|
|
133
|
+
// We extract the last 32 bytes
|
|
134
|
+
// This matches WebCryptoProvider.extractRawPublicKey()
|
|
135
|
+
return spkiBytes.slice(-32);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Validate that a string is a valid DID:key identifier
|
|
140
|
+
*
|
|
141
|
+
* @param did - String to validate
|
|
142
|
+
* @returns boolean - True if valid DID:key format
|
|
143
|
+
*/
|
|
144
|
+
export function isValidDIDKey(did: string): boolean {
|
|
145
|
+
// DID:key format: did:key:z<base58-encoded-multicodec-key>
|
|
146
|
+
const didKeyRegex = /^did:key:z[1-9A-HJ-NP-Za-km-z]{47,}$/;
|
|
147
|
+
return didKeyRegex.test(did);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Extract the base58-encoded multicodec key from a DID:key
|
|
152
|
+
*
|
|
153
|
+
* @param did - DID:key identifier
|
|
154
|
+
* @returns string - Base58-encoded multicodec key (without 'z' prefix)
|
|
155
|
+
*/
|
|
156
|
+
export function extractMultibaseFromDID(did: string): string | null {
|
|
157
|
+
if (!isValidDIDKey(did)) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Remove "did:key:z" prefix
|
|
162
|
+
return did.slice('did:key:z'.length);
|
|
163
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identity Management Utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides functions for resetting and regenerating MCP-I identities
|
|
5
|
+
* for both Node.js (.mcpi/identity.json) and Cloudflare Workers (wrangler.toml).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'fs-extra';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import chalk from 'chalk';
|
|
11
|
+
import { generateIdentity } from './generate-identity.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Check if a directory is a Node.js MCP-I project
|
|
15
|
+
*/
|
|
16
|
+
export function isNodeMcpiProject(projectPath: string): boolean {
|
|
17
|
+
const identityPath = path.join(projectPath, '.mcpi', 'identity.json');
|
|
18
|
+
const configPath = path.join(projectPath, 'xmcp.config.ts');
|
|
19
|
+
return fs.existsSync(identityPath) || fs.existsSync(configPath);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Check if a directory is a Cloudflare Worker MCP-I project
|
|
24
|
+
*/
|
|
25
|
+
export function isCloudflareProject(projectPath: string): boolean {
|
|
26
|
+
const wranglerPath = path.join(projectPath, 'wrangler.toml');
|
|
27
|
+
if (!fs.existsSync(wranglerPath)) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
// Check if wrangler.toml contains MCP-I or MCP-related content
|
|
31
|
+
const content = fs.readFileSync(wranglerPath, 'utf8');
|
|
32
|
+
return (
|
|
33
|
+
content.includes('MCP_IDENTITY_AGENT_DID') ||
|
|
34
|
+
content.includes('[[durable_objects.bindings]]') ||
|
|
35
|
+
content.includes('AGENTSHIELD_API_KEY')
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Reset identity by deleting existing identity files
|
|
41
|
+
*/
|
|
42
|
+
export async function resetIdentity(projectPath: string = process.cwd()): Promise<void> {
|
|
43
|
+
console.log(chalk.cyan('\n🔄 Resetting identity...'));
|
|
44
|
+
|
|
45
|
+
let identityFound = false;
|
|
46
|
+
|
|
47
|
+
// Check for Node.js project
|
|
48
|
+
const mcpiDir = path.join(projectPath, '.mcpi');
|
|
49
|
+
const identityPath = path.join(mcpiDir, 'identity.json');
|
|
50
|
+
|
|
51
|
+
if (fs.existsSync(identityPath)) {
|
|
52
|
+
fs.removeSync(identityPath);
|
|
53
|
+
console.log(chalk.green('✅ Removed Node.js identity file'));
|
|
54
|
+
console.log(chalk.dim(` Deleted: ${identityPath}`));
|
|
55
|
+
identityFound = true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Check for Cloudflare project
|
|
59
|
+
const wranglerPath = path.join(projectPath, 'wrangler.toml');
|
|
60
|
+
const devVarsPath = path.join(projectPath, '.dev.vars');
|
|
61
|
+
|
|
62
|
+
if (fs.existsSync(wranglerPath)) {
|
|
63
|
+
let content = fs.readFileSync(wranglerPath, 'utf8');
|
|
64
|
+
|
|
65
|
+
if (content.includes('MCP_IDENTITY_AGENT_DID')) {
|
|
66
|
+
// Remove identity variables from wrangler.toml (handles both old and new formats)
|
|
67
|
+
content = content.replace(
|
|
68
|
+
/# Persistent Identity.*?\nMCP_IDENTITY_AGENT_DID = ".*?"\nMCP_IDENTITY_PRIVATE_KEY = ".*?"\nMCP_IDENTITY_PUBLIC_KEY = ".*?"\n/gs,
|
|
69
|
+
''
|
|
70
|
+
);
|
|
71
|
+
content = content.replace(
|
|
72
|
+
/# Persistent Identity.*?\nMCP_IDENTITY_AGENT_DID = ".*?"\n/gs,
|
|
73
|
+
''
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
fs.writeFileSync(wranglerPath, content);
|
|
77
|
+
console.log(chalk.green('✅ Removed Cloudflare Worker identity variables'));
|
|
78
|
+
console.log(chalk.dim(` Updated: ${wranglerPath}`));
|
|
79
|
+
identityFound = true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Remove .dev.vars if it exists
|
|
83
|
+
if (fs.existsSync(devVarsPath)) {
|
|
84
|
+
fs.removeSync(devVarsPath);
|
|
85
|
+
console.log(chalk.green('✅ Removed .dev.vars file'));
|
|
86
|
+
console.log(chalk.dim(` Deleted: ${devVarsPath}`));
|
|
87
|
+
identityFound = true;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Check if any identity was found
|
|
92
|
+
if (!identityFound) {
|
|
93
|
+
console.log(chalk.yellow('⚠️ No identity found to reset'));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
console.log(chalk.green('\n✨ Identity reset complete'));
|
|
98
|
+
console.log(chalk.dim(' Run regenerate command to create a new identity'));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Regenerate identity by creating a new one
|
|
103
|
+
*/
|
|
104
|
+
export async function regenerateIdentity(projectPath: string = process.cwd()): Promise<void> {
|
|
105
|
+
console.log(chalk.cyan('\n🔄 Regenerating identity...'));
|
|
106
|
+
|
|
107
|
+
const isNode = isNodeMcpiProject(projectPath);
|
|
108
|
+
const isCloudflare = isCloudflareProject(projectPath);
|
|
109
|
+
|
|
110
|
+
if (!isNode && !isCloudflare) {
|
|
111
|
+
console.log(chalk.red('❌ Not an MCP-I project'));
|
|
112
|
+
console.log(chalk.dim(' Could not detect .mcpi/identity.json or wrangler.toml with MCP_IDENTITY variables'));
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Generate new identity
|
|
117
|
+
const identity = await generateIdentity();
|
|
118
|
+
|
|
119
|
+
// Node.js project
|
|
120
|
+
if (isNode) {
|
|
121
|
+
const mcpiDir = path.join(projectPath, '.mcpi');
|
|
122
|
+
fs.ensureDirSync(mcpiDir);
|
|
123
|
+
|
|
124
|
+
const identityPath = path.join(mcpiDir, 'identity.json');
|
|
125
|
+
fs.writeJsonSync(identityPath, identity, { spaces: 2 });
|
|
126
|
+
|
|
127
|
+
console.log(chalk.green('✅ Generated new Node.js identity'));
|
|
128
|
+
console.log(chalk.dim(` DID: ${identity.did}`));
|
|
129
|
+
console.log(chalk.dim(` Saved to: ${identityPath}`));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Cloudflare Worker project
|
|
133
|
+
if (isCloudflare) {
|
|
134
|
+
const wranglerPath = path.join(projectPath, 'wrangler.toml');
|
|
135
|
+
const devVarsPath = path.join(projectPath, '.dev.vars');
|
|
136
|
+
let wranglerContent = fs.readFileSync(wranglerPath, 'utf8');
|
|
137
|
+
|
|
138
|
+
// Remove old identity if exists from wrangler.toml
|
|
139
|
+
wranglerContent = wranglerContent.replace(
|
|
140
|
+
/# Persistent Identity.*?\nMCP_IDENTITY_AGENT_DID = ".*?"\nMCP_IDENTITY_PRIVATE_KEY = ".*?"\nMCP_IDENTITY_PUBLIC_KEY = ".*?"\n/gs,
|
|
141
|
+
''
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
// Remove old DID-only format if exists
|
|
145
|
+
wranglerContent = wranglerContent.replace(
|
|
146
|
+
/# Persistent Identity.*?\nMCP_IDENTITY_AGENT_DID = ".*?"\n/gs,
|
|
147
|
+
''
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
// Write private keys to .dev.vars (git-ignored)
|
|
151
|
+
const devVarsContent = `# MCP-I Identity Private Keys (NEVER COMMIT THIS FILE)
|
|
152
|
+
# Generated by create-mcpi-app
|
|
153
|
+
MCP_IDENTITY_PRIVATE_KEY="${identity.privateKey}"
|
|
154
|
+
MCP_IDENTITY_PUBLIC_KEY="${identity.publicKey}"
|
|
155
|
+
`;
|
|
156
|
+
fs.writeFileSync(devVarsPath, devVarsContent);
|
|
157
|
+
|
|
158
|
+
// Add only public DID to wrangler.toml
|
|
159
|
+
const varsMatch = wranglerContent.match(/\[vars\]/);
|
|
160
|
+
if (varsMatch) {
|
|
161
|
+
const insertPosition = varsMatch.index! + varsMatch[0].length;
|
|
162
|
+
const identityVars = `
|
|
163
|
+
# Persistent Identity (generated by create-mcpi-app)
|
|
164
|
+
# Public DID - safe to commit
|
|
165
|
+
MCP_IDENTITY_AGENT_DID = "${identity.did}"
|
|
166
|
+
`;
|
|
167
|
+
|
|
168
|
+
wranglerContent =
|
|
169
|
+
wranglerContent.slice(0, insertPosition) +
|
|
170
|
+
identityVars +
|
|
171
|
+
wranglerContent.slice(insertPosition);
|
|
172
|
+
|
|
173
|
+
fs.writeFileSync(wranglerPath, wranglerContent);
|
|
174
|
+
|
|
175
|
+
console.log(chalk.green('✅ Generated new Cloudflare Worker identity'));
|
|
176
|
+
console.log(chalk.dim(` DID: ${identity.did}`));
|
|
177
|
+
console.log(chalk.dim(` Public DID saved to: ${wranglerPath}`));
|
|
178
|
+
console.log(chalk.dim(` Private keys saved to: ${devVarsPath}`));
|
|
179
|
+
console.log(chalk.yellow(' ⚠️ NEVER commit .dev.vars to version control!'));
|
|
180
|
+
} else {
|
|
181
|
+
console.log(chalk.yellow('⚠️ Could not find [vars] section in wrangler.toml'));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
console.log(chalk.green('\n✨ Identity regeneration complete'));
|
|
186
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
import fs from "fs-extra";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
|
|
6
|
+
export function detectPackageManager(
|
|
7
|
+
projectPath: string
|
|
8
|
+
): "npm" | "yarn" | "pnpm" | null {
|
|
9
|
+
if (fs.existsSync(path.join(projectPath, "pnpm-lock.yaml"))) {
|
|
10
|
+
return "pnpm";
|
|
11
|
+
}
|
|
12
|
+
if (fs.existsSync(path.join(projectPath, "yarn.lock"))) {
|
|
13
|
+
return "yarn";
|
|
14
|
+
}
|
|
15
|
+
if (fs.existsSync(path.join(projectPath, "package-lock.json"))) {
|
|
16
|
+
return "npm";
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function install(
|
|
22
|
+
projectPath: string,
|
|
23
|
+
packageManager: string,
|
|
24
|
+
packageVersion: string
|
|
25
|
+
): void {
|
|
26
|
+
console.log(`\n📦 Installing dependencies with ${packageManager}...`);
|
|
27
|
+
|
|
28
|
+
const command =
|
|
29
|
+
packageManager === "yarn"
|
|
30
|
+
? "yarn install"
|
|
31
|
+
: packageManager === "pnpm"
|
|
32
|
+
? "pnpm install"
|
|
33
|
+
: "npm install";
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
execSync(command, {
|
|
37
|
+
cwd: projectPath,
|
|
38
|
+
stdio: "inherit",
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Check for lockfile and provide guidance
|
|
42
|
+
checkLockfile(projectPath, packageManager);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error(`Failed to install dependencies with ${packageManager}.`);
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function checkLockfile(projectPath: string, packageManager: string): void {
|
|
50
|
+
const lockfiles = {
|
|
51
|
+
npm: "package-lock.json",
|
|
52
|
+
yarn: "yarn.lock",
|
|
53
|
+
pnpm: "pnpm-lock.yaml",
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const expectedLockfile = lockfiles[packageManager as keyof typeof lockfiles];
|
|
57
|
+
|
|
58
|
+
// Handle unknown package managers gracefully
|
|
59
|
+
if (!expectedLockfile) {
|
|
60
|
+
console.warn(
|
|
61
|
+
chalk.yellow(`⚠️ Warning: Unknown package manager "${packageManager}", cannot check lockfile`)
|
|
62
|
+
);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const lockfilePath = path.join(projectPath, expectedLockfile);
|
|
67
|
+
|
|
68
|
+
if (fs.existsSync(lockfilePath)) {
|
|
69
|
+
console.log(
|
|
70
|
+
chalk.gray(
|
|
71
|
+
`✓ Lockfile created (${expectedLockfile}) - remember to commit it`
|
|
72
|
+
)
|
|
73
|
+
);
|
|
74
|
+
} else {
|
|
75
|
+
console.warn(
|
|
76
|
+
chalk.yellow(`⚠️ Warning: No lockfile generated (${expectedLockfile})`)
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|