cli4ai 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +275 -0
- package/package.json +49 -0
- package/src/bin.ts +120 -0
- package/src/cli.ts +256 -0
- package/src/commands/add.ts +530 -0
- package/src/commands/browse.ts +449 -0
- package/src/commands/config.ts +126 -0
- package/src/commands/info.ts +102 -0
- package/src/commands/init.test.ts +163 -0
- package/src/commands/init.ts +560 -0
- package/src/commands/list.ts +89 -0
- package/src/commands/mcp-config.ts +59 -0
- package/src/commands/remove.ts +72 -0
- package/src/commands/routines.ts +393 -0
- package/src/commands/run.ts +45 -0
- package/src/commands/search.ts +148 -0
- package/src/commands/secrets.ts +273 -0
- package/src/commands/start.ts +40 -0
- package/src/commands/update.ts +218 -0
- package/src/core/config.test.ts +188 -0
- package/src/core/config.ts +649 -0
- package/src/core/execute.ts +507 -0
- package/src/core/link.test.ts +238 -0
- package/src/core/link.ts +190 -0
- package/src/core/lockfile.test.ts +337 -0
- package/src/core/lockfile.ts +308 -0
- package/src/core/manifest.test.ts +327 -0
- package/src/core/manifest.ts +319 -0
- package/src/core/routine-engine.test.ts +139 -0
- package/src/core/routine-engine.ts +725 -0
- package/src/core/routines.ts +111 -0
- package/src/core/secrets.test.ts +79 -0
- package/src/core/secrets.ts +430 -0
- package/src/lib/cli.ts +234 -0
- package/src/mcp/adapter.test.ts +132 -0
- package/src/mcp/adapter.ts +123 -0
- package/src/mcp/config-gen.test.ts +214 -0
- package/src/mcp/config-gen.ts +106 -0
- package/src/mcp/server.ts +363 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Config Generator - Generate Claude Code MCP configuration
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { resolve } from 'path';
|
|
6
|
+
import { type Manifest, tryLoadManifest } from '../core/manifest.js';
|
|
7
|
+
import { findPackage, getGlobalPackages, getLocalPackages, type InstalledPackage } from '../core/config.js';
|
|
8
|
+
|
|
9
|
+
export interface McpServerConfig {
|
|
10
|
+
command: string;
|
|
11
|
+
args: string[];
|
|
12
|
+
env?: Record<string, string>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ClaudeCodeConfig {
|
|
16
|
+
mcpServers: Record<string, McpServerConfig>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Generate MCP server config for a single package
|
|
21
|
+
*/
|
|
22
|
+
export function generateServerConfig(
|
|
23
|
+
manifest: Manifest,
|
|
24
|
+
_packagePath: string
|
|
25
|
+
): McpServerConfig {
|
|
26
|
+
// Use cli4ai start command which handles the MCP server
|
|
27
|
+
return {
|
|
28
|
+
command: 'cli4ai',
|
|
29
|
+
args: ['start', manifest.name]
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Load manifest for an installed package
|
|
35
|
+
*/
|
|
36
|
+
function loadPackageWithManifest(pkg: InstalledPackage): { name: string; path: string; manifest: Manifest } | null {
|
|
37
|
+
const manifest = tryLoadManifest(pkg.path);
|
|
38
|
+
if (!manifest) return null;
|
|
39
|
+
return { name: pkg.name, path: pkg.path, manifest };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Generate Claude Code config for installed packages
|
|
44
|
+
*/
|
|
45
|
+
export function generateClaudeCodeConfig(
|
|
46
|
+
cwd: string,
|
|
47
|
+
options: { global?: boolean; packages?: string[] } = {}
|
|
48
|
+
): ClaudeCodeConfig {
|
|
49
|
+
const mcpServers: Record<string, McpServerConfig> = {};
|
|
50
|
+
|
|
51
|
+
// Get installed packages
|
|
52
|
+
let installedPackages: InstalledPackage[] = [];
|
|
53
|
+
|
|
54
|
+
if (options.packages && options.packages.length > 0) {
|
|
55
|
+
// Specific packages requested
|
|
56
|
+
for (const pkgName of options.packages) {
|
|
57
|
+
const pkg = findPackage(pkgName, cwd);
|
|
58
|
+
if (pkg) {
|
|
59
|
+
installedPackages.push(pkg);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
// All installed packages
|
|
64
|
+
if (options.global) {
|
|
65
|
+
installedPackages = getGlobalPackages();
|
|
66
|
+
} else {
|
|
67
|
+
installedPackages = [
|
|
68
|
+
...getLocalPackages(cwd),
|
|
69
|
+
...getGlobalPackages()
|
|
70
|
+
];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Load manifests and filter to MCP-enabled packages
|
|
75
|
+
for (const pkg of installedPackages) {
|
|
76
|
+
const pkgWithManifest = loadPackageWithManifest(pkg);
|
|
77
|
+
if (pkgWithManifest && pkgWithManifest.manifest.mcp?.enabled) {
|
|
78
|
+
mcpServers[`cli4ai-${pkgWithManifest.name}`] = generateServerConfig(
|
|
79
|
+
pkgWithManifest.manifest,
|
|
80
|
+
pkgWithManifest.path
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return { mcpServers };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Format config as JSON for Claude Code
|
|
90
|
+
*/
|
|
91
|
+
export function formatClaudeCodeConfig(config: ClaudeCodeConfig): string {
|
|
92
|
+
return JSON.stringify(config, null, 2);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Generate config snippet for adding to existing claude_desktop_config.json
|
|
97
|
+
*/
|
|
98
|
+
export function generateConfigSnippet(
|
|
99
|
+
manifest: Manifest,
|
|
100
|
+
packagePath: string
|
|
101
|
+
): string {
|
|
102
|
+
const serverConfig = generateServerConfig(manifest, packagePath);
|
|
103
|
+
const serverName = `cli4ai-${manifest.name}`;
|
|
104
|
+
|
|
105
|
+
return `"${serverName}": ${JSON.stringify(serverConfig, null, 2)}`;
|
|
106
|
+
}
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Server - Expose CLI tools as MCP tools
|
|
3
|
+
*
|
|
4
|
+
* Implements the Model Context Protocol (MCP) over stdio
|
|
5
|
+
* https://modelcontextprotocol.io/
|
|
6
|
+
*
|
|
7
|
+
* SECURITY: Includes execution safeguards:
|
|
8
|
+
* - Audit logging of all tool calls
|
|
9
|
+
* - Rate limiting to prevent abuse
|
|
10
|
+
* - Trust level tracking for packages
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { spawn } from 'child_process';
|
|
14
|
+
import { resolve } from 'path';
|
|
15
|
+
import { appendFileSync, existsSync, mkdirSync } from 'fs';
|
|
16
|
+
import { homedir } from 'os';
|
|
17
|
+
import { type Manifest } from '../core/manifest.js';
|
|
18
|
+
import { manifestToMcpTools, type McpTool } from './adapter.js';
|
|
19
|
+
import { getSecret } from '../core/secrets.js';
|
|
20
|
+
|
|
21
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
22
|
+
// SECURITY: Audit logging and rate limiting
|
|
23
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
24
|
+
|
|
25
|
+
const AUDIT_LOG_DIR = resolve(homedir(), '.cli4ai', 'logs');
|
|
26
|
+
const RATE_LIMIT_WINDOW_MS = 60000; // 1 minute
|
|
27
|
+
const RATE_LIMIT_MAX_CALLS = 100; // Max calls per minute per tool
|
|
28
|
+
|
|
29
|
+
interface RateLimitEntry {
|
|
30
|
+
count: number;
|
|
31
|
+
windowStart: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Audit log an MCP tool call for security tracking
|
|
36
|
+
*/
|
|
37
|
+
function auditLog(
|
|
38
|
+
packageName: string,
|
|
39
|
+
toolName: string,
|
|
40
|
+
args: Record<string, unknown>,
|
|
41
|
+
result: 'success' | 'error' | 'rate_limited',
|
|
42
|
+
errorMessage?: string
|
|
43
|
+
): void {
|
|
44
|
+
try {
|
|
45
|
+
if (!existsSync(AUDIT_LOG_DIR)) {
|
|
46
|
+
mkdirSync(AUDIT_LOG_DIR, { recursive: true });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const logEntry = {
|
|
50
|
+
timestamp: new Date().toISOString(),
|
|
51
|
+
package: packageName,
|
|
52
|
+
tool: toolName,
|
|
53
|
+
// Redact potentially sensitive argument values, keep keys
|
|
54
|
+
argKeys: Object.keys(args),
|
|
55
|
+
result,
|
|
56
|
+
error: errorMessage,
|
|
57
|
+
pid: process.pid
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const logFile = resolve(AUDIT_LOG_DIR, `mcp-audit-${new Date().toISOString().slice(0, 10)}.log`);
|
|
61
|
+
appendFileSync(logFile, JSON.stringify(logEntry) + '\n');
|
|
62
|
+
} catch {
|
|
63
|
+
// Don't fail tool execution if logging fails
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface JsonRpcRequest {
|
|
68
|
+
jsonrpc: '2.0';
|
|
69
|
+
id: number | string;
|
|
70
|
+
method: string;
|
|
71
|
+
params?: Record<string, unknown>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface JsonRpcResponse {
|
|
75
|
+
jsonrpc: '2.0';
|
|
76
|
+
id: number | string;
|
|
77
|
+
result?: unknown;
|
|
78
|
+
error?: { code: number; message: string; data?: unknown };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* MCP Server that wraps a CLI tool
|
|
83
|
+
*/
|
|
84
|
+
export class McpServer {
|
|
85
|
+
private manifest: Manifest;
|
|
86
|
+
private packagePath: string;
|
|
87
|
+
private tools: McpTool[];
|
|
88
|
+
private rateLimits: Map<string, RateLimitEntry> = new Map();
|
|
89
|
+
private totalCallCount: number = 0;
|
|
90
|
+
|
|
91
|
+
constructor(manifest: Manifest, packagePath: string) {
|
|
92
|
+
this.manifest = manifest;
|
|
93
|
+
this.packagePath = packagePath;
|
|
94
|
+
this.tools = manifestToMcpTools(manifest);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* SECURITY: Check if a tool call should be rate limited
|
|
99
|
+
*/
|
|
100
|
+
private checkRateLimit(toolName: string): { allowed: boolean; retryAfterMs?: number } {
|
|
101
|
+
const now = Date.now();
|
|
102
|
+
const entry = this.rateLimits.get(toolName);
|
|
103
|
+
|
|
104
|
+
if (!entry || now - entry.windowStart >= RATE_LIMIT_WINDOW_MS) {
|
|
105
|
+
// Start new window
|
|
106
|
+
this.rateLimits.set(toolName, { count: 1, windowStart: now });
|
|
107
|
+
return { allowed: true };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (entry.count >= RATE_LIMIT_MAX_CALLS) {
|
|
111
|
+
const retryAfterMs = RATE_LIMIT_WINDOW_MS - (now - entry.windowStart);
|
|
112
|
+
return { allowed: false, retryAfterMs };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
entry.count++;
|
|
116
|
+
return { allowed: true };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Start the MCP server (stdio mode)
|
|
121
|
+
*/
|
|
122
|
+
async start(): Promise<void> {
|
|
123
|
+
process.stdin.setEncoding('utf8');
|
|
124
|
+
|
|
125
|
+
let buffer = '';
|
|
126
|
+
|
|
127
|
+
process.stdin.on('data', (chunk: string) => {
|
|
128
|
+
buffer += chunk;
|
|
129
|
+
|
|
130
|
+
// Try to parse complete JSON-RPC messages
|
|
131
|
+
const lines = buffer.split('\n');
|
|
132
|
+
buffer = lines.pop() || '';
|
|
133
|
+
|
|
134
|
+
for (const line of lines) {
|
|
135
|
+
if (line.trim()) {
|
|
136
|
+
this.handleMessage(line.trim());
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
process.stdin.on('end', () => {
|
|
142
|
+
process.exit(0);
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private handleMessage(message: string): void {
|
|
147
|
+
try {
|
|
148
|
+
const request = JSON.parse(message) as JsonRpcRequest;
|
|
149
|
+
this.handleRequest(request);
|
|
150
|
+
} catch (err) {
|
|
151
|
+
this.sendError(null, -32700, 'Parse error');
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private async handleRequest(request: JsonRpcRequest): Promise<void> {
|
|
156
|
+
const { id, method, params } = request;
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
switch (method) {
|
|
160
|
+
case 'initialize':
|
|
161
|
+
this.sendResult(id, {
|
|
162
|
+
protocolVersion: '2024-11-05',
|
|
163
|
+
capabilities: {
|
|
164
|
+
tools: {}
|
|
165
|
+
},
|
|
166
|
+
serverInfo: {
|
|
167
|
+
name: `cli4ai-${this.manifest.name}`,
|
|
168
|
+
version: this.manifest.version
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
break;
|
|
172
|
+
|
|
173
|
+
case 'initialized':
|
|
174
|
+
// No response needed
|
|
175
|
+
break;
|
|
176
|
+
|
|
177
|
+
case 'tools/list':
|
|
178
|
+
this.sendResult(id, {
|
|
179
|
+
tools: this.tools.map(t => ({
|
|
180
|
+
name: t.name,
|
|
181
|
+
description: t.description,
|
|
182
|
+
inputSchema: t.inputSchema
|
|
183
|
+
}))
|
|
184
|
+
});
|
|
185
|
+
break;
|
|
186
|
+
|
|
187
|
+
case 'tools/call':
|
|
188
|
+
await this.handleToolCall(id, params as { name: string; arguments?: Record<string, string> });
|
|
189
|
+
break;
|
|
190
|
+
|
|
191
|
+
case 'ping':
|
|
192
|
+
this.sendResult(id, {});
|
|
193
|
+
break;
|
|
194
|
+
|
|
195
|
+
default:
|
|
196
|
+
this.sendError(id, -32601, `Method not found: ${method}`);
|
|
197
|
+
}
|
|
198
|
+
} catch (err) {
|
|
199
|
+
this.sendError(id, -32603, (err as Error).message);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private async handleToolCall(
|
|
204
|
+
id: number | string,
|
|
205
|
+
params: { name: string; arguments?: Record<string, string> }
|
|
206
|
+
): Promise<void> {
|
|
207
|
+
const { name, arguments: args = {} } = params;
|
|
208
|
+
|
|
209
|
+
// SECURITY: Rate limiting
|
|
210
|
+
const rateCheck = this.checkRateLimit(name);
|
|
211
|
+
if (!rateCheck.allowed) {
|
|
212
|
+
auditLog(this.manifest.name, name, args, 'rate_limited');
|
|
213
|
+
this.sendError(id, -32000, `Rate limit exceeded. Retry after ${Math.ceil((rateCheck.retryAfterMs || 0) / 1000)}s`);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Track total calls for monitoring
|
|
218
|
+
this.totalCallCount++;
|
|
219
|
+
|
|
220
|
+
// Parse tool name: package_command
|
|
221
|
+
const parts = name.split('_');
|
|
222
|
+
if (parts.length < 2 || parts[0] !== this.manifest.name) {
|
|
223
|
+
auditLog(this.manifest.name, name, args, 'error', 'Unknown tool');
|
|
224
|
+
this.sendError(id, -32602, `Unknown tool: ${name}`);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const command = parts.slice(1).join('_');
|
|
229
|
+
const cmdDef = this.manifest.commands?.[command];
|
|
230
|
+
|
|
231
|
+
if (!cmdDef) {
|
|
232
|
+
auditLog(this.manifest.name, name, args, 'error', 'Unknown command');
|
|
233
|
+
this.sendError(id, -32602, `Unknown command: ${command}`);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Build command arguments in order
|
|
238
|
+
const cmdArgs: string[] = [command];
|
|
239
|
+
if (cmdDef.args) {
|
|
240
|
+
for (const argDef of cmdDef.args) {
|
|
241
|
+
const value = args[argDef.name];
|
|
242
|
+
if (value !== undefined && value !== '') {
|
|
243
|
+
cmdArgs.push(String(value));
|
|
244
|
+
} else if (argDef.required) {
|
|
245
|
+
auditLog(this.manifest.name, name, args, 'error', `Missing required argument: ${argDef.name}`);
|
|
246
|
+
this.sendError(id, -32602, `Missing required argument: ${argDef.name}`);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Execute the CLI tool
|
|
253
|
+
const entryPath = resolve(this.packagePath, this.manifest.entry);
|
|
254
|
+
const runtime = this.manifest.runtime || 'bun';
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
const result = await this.executeCommand(runtime, entryPath, cmdArgs);
|
|
258
|
+
auditLog(this.manifest.name, name, args, 'success');
|
|
259
|
+
this.sendResult(id, {
|
|
260
|
+
content: [{ type: 'text', text: result }]
|
|
261
|
+
});
|
|
262
|
+
} catch (err) {
|
|
263
|
+
const errorMessage = (err as Error).message;
|
|
264
|
+
auditLog(this.manifest.name, name, args, 'error', errorMessage);
|
|
265
|
+
this.sendResult(id, {
|
|
266
|
+
content: [{ type: 'text', text: errorMessage }],
|
|
267
|
+
isError: true
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private executeCommand(runtime: string, entryPath: string, args: string[]): Promise<string> {
|
|
273
|
+
return new Promise((resolve, reject) => {
|
|
274
|
+
// Build runtime-specific command and arguments
|
|
275
|
+
// - bun: bun run <file> [args]
|
|
276
|
+
// - node: node <file> [args]
|
|
277
|
+
let cmd: string;
|
|
278
|
+
let cmdArgs: string[];
|
|
279
|
+
switch (runtime) {
|
|
280
|
+
case 'node':
|
|
281
|
+
cmd = 'node';
|
|
282
|
+
cmdArgs = [entryPath, ...args];
|
|
283
|
+
break;
|
|
284
|
+
case 'bun':
|
|
285
|
+
default:
|
|
286
|
+
cmd = 'bun';
|
|
287
|
+
cmdArgs = ['run', entryPath, ...args];
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Inject secrets from manifest env definitions
|
|
292
|
+
const secretsEnv: Record<string, string> = {};
|
|
293
|
+
if (this.manifest.env) {
|
|
294
|
+
for (const key of Object.keys(this.manifest.env)) {
|
|
295
|
+
const value = getSecret(key);
|
|
296
|
+
if (value) {
|
|
297
|
+
secretsEnv[key] = value;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const proc = spawn(cmd, cmdArgs, {
|
|
303
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
304
|
+
env: { ...process.env, ...secretsEnv }
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
let stdout = '';
|
|
308
|
+
let stderr = '';
|
|
309
|
+
|
|
310
|
+
proc.stdout.on('data', (data) => {
|
|
311
|
+
stdout += data.toString();
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
proc.stderr.on('data', (data) => {
|
|
315
|
+
stderr += data.toString();
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
proc.on('close', (code) => {
|
|
319
|
+
if (code !== 0) {
|
|
320
|
+
reject(new Error(stderr || `Exit code ${code}`));
|
|
321
|
+
} else {
|
|
322
|
+
resolve(stdout);
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
proc.on('error', (err) => {
|
|
327
|
+
reject(err);
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
private sendResult(id: number | string | null, result: unknown): void {
|
|
333
|
+
if (id === null) return;
|
|
334
|
+
|
|
335
|
+
const response: JsonRpcResponse = {
|
|
336
|
+
jsonrpc: '2.0',
|
|
337
|
+
id,
|
|
338
|
+
result
|
|
339
|
+
};
|
|
340
|
+
console.log(JSON.stringify(response));
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
private sendError(id: number | string | null, code: number, message: string): void {
|
|
344
|
+
// Per JSON-RPC 2.0 spec, error responses for parse errors should include id: null
|
|
345
|
+
// For other errors where id is null (shouldn't happen), we skip the response
|
|
346
|
+
if (id === null && code !== -32700) return;
|
|
347
|
+
|
|
348
|
+
const response: JsonRpcResponse = {
|
|
349
|
+
jsonrpc: '2.0',
|
|
350
|
+
id: id as number | string, // For parse errors (-32700), this will be cast from null
|
|
351
|
+
error: { code, message }
|
|
352
|
+
};
|
|
353
|
+
console.log(JSON.stringify(response));
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Start MCP server for a package
|
|
359
|
+
*/
|
|
360
|
+
export async function startMcpServer(manifest: Manifest, packagePath: string): Promise<void> {
|
|
361
|
+
const server = new McpServer(manifest, packagePath);
|
|
362
|
+
await server.start();
|
|
363
|
+
}
|