cli4ai 1.2.0 → 1.2.1
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 +39 -0
- package/dist/bin.d.ts +6 -0
- package/dist/bin.js +105 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +335 -0
- package/dist/commands/add.d.ts +11 -0
- package/dist/commands/add.js +459 -0
- package/dist/commands/browse.d.ts +4 -0
- package/dist/commands/browse.js +379 -0
- package/dist/commands/config.d.ts +10 -0
- package/dist/commands/config.js +121 -0
- package/dist/commands/info.d.ts +9 -0
- package/dist/commands/info.js +122 -0
- package/dist/commands/init.d.ts +10 -0
- package/dist/commands/init.js +458 -0
- package/dist/commands/list.d.ts +10 -0
- package/dist/commands/list.js +76 -0
- package/dist/commands/mcp-config.d.ts +10 -0
- package/dist/commands/mcp-config.js +49 -0
- package/dist/commands/remotes.d.ts +22 -0
- package/dist/commands/remotes.js +196 -0
- package/dist/commands/remove.d.ts +8 -0
- package/dist/commands/remove.js +61 -0
- package/dist/commands/routines.d.ts +29 -0
- package/dist/commands/routines.js +363 -0
- package/dist/commands/run.d.ts +12 -0
- package/dist/commands/run.js +104 -0
- package/dist/commands/scheduler.d.ts +27 -0
- package/dist/commands/scheduler.js +350 -0
- package/dist/commands/search.d.ts +9 -0
- package/dist/commands/search.js +159 -0
- package/dist/commands/secrets.d.ts +28 -0
- package/dist/commands/secrets.js +236 -0
- package/dist/commands/serve.d.ts +13 -0
- package/dist/commands/serve.js +49 -0
- package/dist/commands/start.d.ts +8 -0
- package/dist/commands/start.js +27 -0
- package/dist/commands/update.d.ts +17 -0
- package/dist/commands/update.js +210 -0
- package/dist/core/config.d.ts +91 -0
- package/dist/core/config.js +738 -0
- package/dist/core/execute.d.ts +51 -0
- package/dist/core/execute.js +475 -0
- package/dist/core/link.d.ts +39 -0
- package/dist/core/link.js +214 -0
- package/dist/core/lockfile.d.ts +63 -0
- package/dist/core/lockfile.js +140 -0
- package/dist/core/manifest.d.ts +96 -0
- package/dist/core/manifest.js +224 -0
- package/dist/core/registry.d.ts +74 -0
- package/dist/core/registry.js +116 -0
- package/dist/core/remote-client.d.ts +98 -0
- package/dist/core/remote-client.js +252 -0
- package/dist/core/remotes.d.ts +88 -0
- package/dist/core/remotes.js +206 -0
- package/dist/core/routine-engine.d.ts +124 -0
- package/dist/core/routine-engine.js +699 -0
- package/dist/core/routines.d.ts +36 -0
- package/dist/core/routines.js +132 -0
- package/dist/core/scheduler-daemon.d.ts +10 -0
- package/dist/core/scheduler-daemon.js +77 -0
- package/dist/core/scheduler.d.ts +131 -0
- package/dist/core/scheduler.js +492 -0
- package/dist/core/secrets.d.ts +48 -0
- package/dist/core/secrets.js +384 -0
- package/dist/lib/cli.d.ts +84 -0
- package/dist/lib/cli.js +216 -0
- package/dist/mcp/adapter.d.ts +35 -0
- package/dist/mcp/adapter.js +94 -0
- package/dist/mcp/config-gen.d.ts +31 -0
- package/dist/mcp/config-gen.js +75 -0
- package/dist/mcp/server.d.ts +41 -0
- package/dist/mcp/server.js +296 -0
- package/dist/server/service.d.ts +85 -0
- package/dist/server/service.js +304 -0
- package/package.json +6 -3
- package/src/bin.ts +0 -118
- package/src/cli.ts +0 -412
- package/src/commands/add.ts +0 -562
- package/src/commands/browse.ts +0 -449
- package/src/commands/config.ts +0 -154
- package/src/commands/info.ts +0 -133
- package/src/commands/init.ts +0 -514
- package/src/commands/list.ts +0 -95
- package/src/commands/mcp-config.ts +0 -69
- package/src/commands/remotes.ts +0 -253
- package/src/commands/remove.ts +0 -78
- package/src/commands/routines.ts +0 -427
- package/src/commands/run.ts +0 -127
- package/src/commands/scheduler.ts +0 -438
- package/src/commands/search.ts +0 -185
- package/src/commands/secrets.ts +0 -292
- package/src/commands/serve.ts +0 -66
- package/src/commands/start.ts +0 -40
- package/src/commands/update.ts +0 -252
- package/src/core/config.ts +0 -845
- package/src/core/execute.ts +0 -569
- package/src/core/link.ts +0 -246
- package/src/core/lockfile.ts +0 -187
- package/src/core/manifest.ts +0 -327
- package/src/core/registry.ts +0 -165
- package/src/core/remote-client.ts +0 -419
- package/src/core/remotes.ts +0 -268
- package/src/core/routine-engine.ts +0 -895
- package/src/core/routines.ts +0 -171
- package/src/core/scheduler-daemon.ts +0 -94
- package/src/core/scheduler.ts +0 -606
- package/src/core/secrets.ts +0 -430
- package/src/lib/cli.ts +0 -261
- package/src/mcp/adapter.ts +0 -131
- package/src/mcp/config-gen.ts +0 -106
- package/src/mcp/server.ts +0 -365
- package/src/server/service.ts +0 -434
package/src/mcp/adapter.ts
DELETED
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MCP Adapter - Converts CLI tools to MCP tools
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { spawn } from 'child_process';
|
|
6
|
-
import { type Manifest, type CommandDef } from '../core/manifest.js';
|
|
7
|
-
|
|
8
|
-
export interface McpTool {
|
|
9
|
-
name: string;
|
|
10
|
-
description: string;
|
|
11
|
-
inputSchema: {
|
|
12
|
-
type: 'object';
|
|
13
|
-
properties: Record<string, { type: string; description?: string }>;
|
|
14
|
-
required: string[];
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface McpToolResult {
|
|
19
|
-
content: Array<{ type: 'text'; text: string }>;
|
|
20
|
-
isError?: boolean;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Convert a CLI command definition to MCP tool format
|
|
25
|
-
*/
|
|
26
|
-
export function commandToMcpTool(manifest: Manifest, cmdName: string, cmd: CommandDef): McpTool {
|
|
27
|
-
const properties: Record<string, { type: string; description?: string }> = {};
|
|
28
|
-
const required: string[] = [];
|
|
29
|
-
|
|
30
|
-
// Convert args to JSON Schema properties
|
|
31
|
-
if (cmd.args) {
|
|
32
|
-
for (const arg of cmd.args) {
|
|
33
|
-
properties[arg.name] = {
|
|
34
|
-
type: 'string',
|
|
35
|
-
description: arg.description
|
|
36
|
-
};
|
|
37
|
-
if (arg.required) {
|
|
38
|
-
required.push(arg.name);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return {
|
|
44
|
-
name: `${manifest.name}_${cmdName}`,
|
|
45
|
-
description: `${manifest.name}: ${cmd.description || cmdName}`,
|
|
46
|
-
inputSchema: {
|
|
47
|
-
type: 'object',
|
|
48
|
-
properties,
|
|
49
|
-
required
|
|
50
|
-
}
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Convert all commands in a manifest to MCP tools
|
|
56
|
-
*/
|
|
57
|
-
export function manifestToMcpTools(manifest: Manifest): McpTool[] {
|
|
58
|
-
const tools: McpTool[] = [];
|
|
59
|
-
|
|
60
|
-
if (manifest.commands) {
|
|
61
|
-
for (const [cmdName, cmdDef] of Object.entries(manifest.commands)) {
|
|
62
|
-
tools.push(commandToMcpTool(manifest, cmdName, cmdDef));
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return tools;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Execute a CLI tool command and return MCP-formatted result
|
|
71
|
-
*/
|
|
72
|
-
export async function executeTool(
|
|
73
|
-
entryPath: string,
|
|
74
|
-
runtime: string,
|
|
75
|
-
command: string,
|
|
76
|
-
args: Record<string, string>,
|
|
77
|
-
argOrder?: string[]
|
|
78
|
-
): Promise<McpToolResult> {
|
|
79
|
-
return new Promise((resolve) => {
|
|
80
|
-
// Build command arguments
|
|
81
|
-
const cmdArgs = [command];
|
|
82
|
-
|
|
83
|
-
// Prefer manifest-defined arg order (positional), fall back to stable ordering.
|
|
84
|
-
const orderedKeys = argOrder ?? Object.keys(args).sort();
|
|
85
|
-
for (const key of orderedKeys) {
|
|
86
|
-
const value = args[key];
|
|
87
|
-
if (value !== undefined && value !== '') cmdArgs.push(value);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const runtimeArgs =
|
|
91
|
-
runtime === 'node'
|
|
92
|
-
? [entryPath, ...cmdArgs]
|
|
93
|
-
: ['run', entryPath, ...cmdArgs];
|
|
94
|
-
|
|
95
|
-
const proc = spawn(runtime, runtimeArgs, {
|
|
96
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
97
|
-
env: { ...process.env }
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
let stdout = '';
|
|
101
|
-
let stderr = '';
|
|
102
|
-
|
|
103
|
-
proc.stdout.on('data', (data) => {
|
|
104
|
-
stdout += data.toString();
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
proc.stderr.on('data', (data) => {
|
|
108
|
-
stderr += data.toString();
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
proc.on('close', (code) => {
|
|
112
|
-
if (code !== 0) {
|
|
113
|
-
resolve({
|
|
114
|
-
content: [{ type: 'text', text: stderr || `Command failed with exit code ${code}` }],
|
|
115
|
-
isError: true
|
|
116
|
-
});
|
|
117
|
-
} else {
|
|
118
|
-
resolve({
|
|
119
|
-
content: [{ type: 'text', text: stdout || 'Command completed successfully' }]
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
proc.on('error', (err) => {
|
|
125
|
-
resolve({
|
|
126
|
-
content: [{ type: 'text', text: `Failed to execute: ${err.message}` }],
|
|
127
|
-
isError: true
|
|
128
|
-
});
|
|
129
|
-
});
|
|
130
|
-
});
|
|
131
|
-
}
|
package/src/mcp/config-gen.ts
DELETED
|
@@ -1,106 +0,0 @@
|
|
|
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
|
-
}
|
package/src/mcp/server.ts
DELETED
|
@@ -1,365 +0,0 @@
|
|
|
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
|
-
import { loadConfig } from '../core/config.js';
|
|
21
|
-
|
|
22
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
23
|
-
// SECURITY: Audit logging and rate limiting
|
|
24
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
25
|
-
|
|
26
|
-
const AUDIT_LOG_DIR = resolve(homedir(), '.cli4ai', 'logs');
|
|
27
|
-
const RATE_LIMIT_WINDOW_MS = 60000; // 1 minute
|
|
28
|
-
const RATE_LIMIT_MAX_CALLS = 100; // Max calls per minute per tool
|
|
29
|
-
|
|
30
|
-
interface RateLimitEntry {
|
|
31
|
-
count: number;
|
|
32
|
-
windowStart: number;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Audit log an MCP tool call for security tracking
|
|
37
|
-
* Can be disabled via `cli4ai config set audit.enabled false`
|
|
38
|
-
*/
|
|
39
|
-
function auditLog(
|
|
40
|
-
packageName: string,
|
|
41
|
-
toolName: string,
|
|
42
|
-
args: Record<string, unknown>,
|
|
43
|
-
result: 'success' | 'error' | 'rate_limited',
|
|
44
|
-
errorMessage?: string
|
|
45
|
-
): void {
|
|
46
|
-
try {
|
|
47
|
-
// Check if audit logging is enabled
|
|
48
|
-
const config = loadConfig();
|
|
49
|
-
if (!config.audit?.enabled) {
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (!existsSync(AUDIT_LOG_DIR)) {
|
|
54
|
-
mkdirSync(AUDIT_LOG_DIR, { recursive: true });
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const logEntry = {
|
|
58
|
-
timestamp: new Date().toISOString(),
|
|
59
|
-
package: packageName,
|
|
60
|
-
tool: toolName,
|
|
61
|
-
// Redact potentially sensitive argument values, keep keys
|
|
62
|
-
argKeys: Object.keys(args),
|
|
63
|
-
result,
|
|
64
|
-
error: errorMessage,
|
|
65
|
-
pid: process.pid
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
const logFile = resolve(AUDIT_LOG_DIR, `mcp-audit-${new Date().toISOString().slice(0, 10)}.log`);
|
|
69
|
-
appendFileSync(logFile, JSON.stringify(logEntry) + '\n');
|
|
70
|
-
} catch {
|
|
71
|
-
// Don't fail tool execution if logging fails
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
interface JsonRpcRequest {
|
|
76
|
-
jsonrpc: '2.0';
|
|
77
|
-
id: number | string;
|
|
78
|
-
method: string;
|
|
79
|
-
params?: Record<string, unknown>;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
interface JsonRpcResponse {
|
|
83
|
-
jsonrpc: '2.0';
|
|
84
|
-
id: number | string;
|
|
85
|
-
result?: unknown;
|
|
86
|
-
error?: { code: number; message: string; data?: unknown };
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* MCP Server that wraps a CLI tool
|
|
91
|
-
*/
|
|
92
|
-
export class McpServer {
|
|
93
|
-
private manifest: Manifest;
|
|
94
|
-
private packagePath: string;
|
|
95
|
-
private tools: McpTool[];
|
|
96
|
-
private rateLimits: Map<string, RateLimitEntry> = new Map();
|
|
97
|
-
private totalCallCount: number = 0;
|
|
98
|
-
|
|
99
|
-
constructor(manifest: Manifest, packagePath: string) {
|
|
100
|
-
this.manifest = manifest;
|
|
101
|
-
this.packagePath = packagePath;
|
|
102
|
-
this.tools = manifestToMcpTools(manifest);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* SECURITY: Check if a tool call should be rate limited
|
|
107
|
-
*/
|
|
108
|
-
private checkRateLimit(toolName: string): { allowed: boolean; retryAfterMs?: number } {
|
|
109
|
-
const now = Date.now();
|
|
110
|
-
const entry = this.rateLimits.get(toolName);
|
|
111
|
-
|
|
112
|
-
if (!entry || now - entry.windowStart >= RATE_LIMIT_WINDOW_MS) {
|
|
113
|
-
// Start new window
|
|
114
|
-
this.rateLimits.set(toolName, { count: 1, windowStart: now });
|
|
115
|
-
return { allowed: true };
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (entry.count >= RATE_LIMIT_MAX_CALLS) {
|
|
119
|
-
const retryAfterMs = RATE_LIMIT_WINDOW_MS - (now - entry.windowStart);
|
|
120
|
-
return { allowed: false, retryAfterMs };
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
entry.count++;
|
|
124
|
-
return { allowed: true };
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Start the MCP server (stdio mode)
|
|
129
|
-
*/
|
|
130
|
-
async start(): Promise<void> {
|
|
131
|
-
process.stdin.setEncoding('utf8');
|
|
132
|
-
|
|
133
|
-
let buffer = '';
|
|
134
|
-
|
|
135
|
-
process.stdin.on('data', (chunk: string) => {
|
|
136
|
-
buffer += chunk;
|
|
137
|
-
|
|
138
|
-
// Try to parse complete JSON-RPC messages
|
|
139
|
-
const lines = buffer.split('\n');
|
|
140
|
-
buffer = lines.pop() || '';
|
|
141
|
-
|
|
142
|
-
for (const line of lines) {
|
|
143
|
-
if (line.trim()) {
|
|
144
|
-
this.handleMessage(line.trim());
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
process.stdin.on('end', () => {
|
|
150
|
-
process.exit(0);
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
private handleMessage(message: string): void {
|
|
155
|
-
try {
|
|
156
|
-
const request = JSON.parse(message) as JsonRpcRequest;
|
|
157
|
-
this.handleRequest(request);
|
|
158
|
-
} catch (err) {
|
|
159
|
-
this.sendError(null, -32700, 'Parse error');
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
private async handleRequest(request: JsonRpcRequest): Promise<void> {
|
|
164
|
-
const { id, method, params } = request;
|
|
165
|
-
|
|
166
|
-
try {
|
|
167
|
-
switch (method) {
|
|
168
|
-
case 'initialize':
|
|
169
|
-
this.sendResult(id, {
|
|
170
|
-
protocolVersion: '2024-11-05',
|
|
171
|
-
capabilities: {
|
|
172
|
-
tools: {}
|
|
173
|
-
},
|
|
174
|
-
serverInfo: {
|
|
175
|
-
name: `cli4ai-${this.manifest.name}`,
|
|
176
|
-
version: this.manifest.version
|
|
177
|
-
}
|
|
178
|
-
});
|
|
179
|
-
break;
|
|
180
|
-
|
|
181
|
-
case 'initialized':
|
|
182
|
-
// No response needed
|
|
183
|
-
break;
|
|
184
|
-
|
|
185
|
-
case 'tools/list':
|
|
186
|
-
this.sendResult(id, {
|
|
187
|
-
tools: this.tools.map(t => ({
|
|
188
|
-
name: t.name,
|
|
189
|
-
description: t.description,
|
|
190
|
-
inputSchema: t.inputSchema
|
|
191
|
-
}))
|
|
192
|
-
});
|
|
193
|
-
break;
|
|
194
|
-
|
|
195
|
-
case 'tools/call':
|
|
196
|
-
await this.handleToolCall(id, params as { name: string; arguments?: Record<string, string> });
|
|
197
|
-
break;
|
|
198
|
-
|
|
199
|
-
case 'ping':
|
|
200
|
-
this.sendResult(id, {});
|
|
201
|
-
break;
|
|
202
|
-
|
|
203
|
-
default:
|
|
204
|
-
this.sendError(id, -32601, `Method not found: ${method}`);
|
|
205
|
-
}
|
|
206
|
-
} catch (err) {
|
|
207
|
-
this.sendError(id, -32603, (err as Error).message);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
private async handleToolCall(
|
|
212
|
-
id: number | string,
|
|
213
|
-
params: { name: string; arguments?: Record<string, string> }
|
|
214
|
-
): Promise<void> {
|
|
215
|
-
const { name, arguments: args = {} } = params;
|
|
216
|
-
|
|
217
|
-
// SECURITY: Rate limiting
|
|
218
|
-
const rateCheck = this.checkRateLimit(name);
|
|
219
|
-
if (!rateCheck.allowed) {
|
|
220
|
-
auditLog(this.manifest.name, name, args, 'rate_limited');
|
|
221
|
-
this.sendError(id, -32000, `Rate limit exceeded. Retry after ${Math.ceil((rateCheck.retryAfterMs || 0) / 1000)}s`);
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// Track total calls for monitoring
|
|
226
|
-
this.totalCallCount++;
|
|
227
|
-
|
|
228
|
-
// Parse tool name: package_command
|
|
229
|
-
const parts = name.split('_');
|
|
230
|
-
if (parts.length < 2 || parts[0] !== this.manifest.name) {
|
|
231
|
-
auditLog(this.manifest.name, name, args, 'error', 'Unknown tool');
|
|
232
|
-
this.sendError(id, -32602, `Unknown tool: ${name}`);
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
const command = parts.slice(1).join('_');
|
|
237
|
-
const cmdDef = this.manifest.commands?.[command];
|
|
238
|
-
|
|
239
|
-
if (!cmdDef) {
|
|
240
|
-
auditLog(this.manifest.name, name, args, 'error', 'Unknown command');
|
|
241
|
-
this.sendError(id, -32602, `Unknown command: ${command}`);
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// Build command arguments in order
|
|
246
|
-
const cmdArgs: string[] = [command];
|
|
247
|
-
if (cmdDef.args) {
|
|
248
|
-
for (const argDef of cmdDef.args) {
|
|
249
|
-
const value = args[argDef.name];
|
|
250
|
-
if (value !== undefined && value !== '') {
|
|
251
|
-
cmdArgs.push(String(value));
|
|
252
|
-
} else if (argDef.required) {
|
|
253
|
-
auditLog(this.manifest.name, name, args, 'error', `Missing required argument: ${argDef.name}`);
|
|
254
|
-
this.sendError(id, -32602, `Missing required argument: ${argDef.name}`);
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// Execute the CLI tool
|
|
261
|
-
const entryPath = resolve(this.packagePath, this.manifest.entry);
|
|
262
|
-
|
|
263
|
-
try {
|
|
264
|
-
const result = await this.executeCommand(entryPath, cmdArgs);
|
|
265
|
-
auditLog(this.manifest.name, name, args, 'success');
|
|
266
|
-
this.sendResult(id, {
|
|
267
|
-
content: [{ type: 'text', text: result }]
|
|
268
|
-
});
|
|
269
|
-
} catch (err) {
|
|
270
|
-
const errorMessage = (err as Error).message;
|
|
271
|
-
auditLog(this.manifest.name, name, args, 'error', errorMessage);
|
|
272
|
-
this.sendResult(id, {
|
|
273
|
-
content: [{ type: 'text', text: errorMessage }],
|
|
274
|
-
isError: true
|
|
275
|
-
});
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
private executeCommand(entryPath: string, args: string[]): Promise<string> {
|
|
280
|
-
return new Promise((resolve, reject) => {
|
|
281
|
-
// Use tsx for TypeScript files, node for JavaScript
|
|
282
|
-
let cmd: string;
|
|
283
|
-
let cmdArgs: string[];
|
|
284
|
-
if (entryPath.endsWith('.ts') || entryPath.endsWith('.tsx')) {
|
|
285
|
-
cmd = 'npx';
|
|
286
|
-
cmdArgs = ['tsx', entryPath, ...args];
|
|
287
|
-
} else {
|
|
288
|
-
cmd = 'node';
|
|
289
|
-
cmdArgs = [entryPath, ...args];
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// Inject secrets from manifest env definitions
|
|
293
|
-
// SECURITY: Use package-scoped secret lookup (tries scoped first, then global)
|
|
294
|
-
const secretsEnv: Record<string, string> = {};
|
|
295
|
-
if (this.manifest.env) {
|
|
296
|
-
for (const key of Object.keys(this.manifest.env)) {
|
|
297
|
-
const value = getSecret(key, this.manifest.name);
|
|
298
|
-
if (value) {
|
|
299
|
-
secretsEnv[key] = value;
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
const proc = spawn(cmd, cmdArgs, {
|
|
305
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
306
|
-
env: { ...process.env, ...secretsEnv }
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
let stdout = '';
|
|
310
|
-
let stderr = '';
|
|
311
|
-
|
|
312
|
-
proc.stdout.on('data', (data) => {
|
|
313
|
-
stdout += data.toString();
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
proc.stderr.on('data', (data) => {
|
|
317
|
-
stderr += data.toString();
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
proc.on('close', (code) => {
|
|
321
|
-
if (code !== 0) {
|
|
322
|
-
reject(new Error(stderr || `Exit code ${code}`));
|
|
323
|
-
} else {
|
|
324
|
-
resolve(stdout);
|
|
325
|
-
}
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
proc.on('error', (err) => {
|
|
329
|
-
reject(err);
|
|
330
|
-
});
|
|
331
|
-
});
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
private sendResult(id: number | string | null, result: unknown): void {
|
|
335
|
-
if (id === null) return;
|
|
336
|
-
|
|
337
|
-
const response: JsonRpcResponse = {
|
|
338
|
-
jsonrpc: '2.0',
|
|
339
|
-
id,
|
|
340
|
-
result
|
|
341
|
-
};
|
|
342
|
-
console.log(JSON.stringify(response));
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
private sendError(id: number | string | null, code: number, message: string): void {
|
|
346
|
-
// Per JSON-RPC 2.0 spec, error responses for parse errors should include id: null
|
|
347
|
-
// For other errors where id is null (shouldn't happen), we skip the response
|
|
348
|
-
if (id === null && code !== -32700) return;
|
|
349
|
-
|
|
350
|
-
const response: JsonRpcResponse = {
|
|
351
|
-
jsonrpc: '2.0',
|
|
352
|
-
id: id as number | string, // For parse errors (-32700), this will be cast from null
|
|
353
|
-
error: { code, message }
|
|
354
|
-
};
|
|
355
|
-
console.log(JSON.stringify(response));
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
/**
|
|
360
|
-
* Start MCP server for a package
|
|
361
|
-
*/
|
|
362
|
-
export async function startMcpServer(manifest: Manifest, packagePath: string): Promise<void> {
|
|
363
|
-
const server = new McpServer(manifest, packagePath);
|
|
364
|
-
await server.start();
|
|
365
|
-
}
|