cli4ai 1.2.0 → 1.2.2
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 +464 -0
- package/dist/commands/browse.d.ts +4 -0
- package/dist/commands/browse.js +382 -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 +125 -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 +162 -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
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Adapter - Converts CLI tools to MCP tools
|
|
3
|
+
*/
|
|
4
|
+
import { type Manifest, type CommandDef } from '../core/manifest.js';
|
|
5
|
+
export interface McpTool {
|
|
6
|
+
name: string;
|
|
7
|
+
description: string;
|
|
8
|
+
inputSchema: {
|
|
9
|
+
type: 'object';
|
|
10
|
+
properties: Record<string, {
|
|
11
|
+
type: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
}>;
|
|
14
|
+
required: string[];
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export interface McpToolResult {
|
|
18
|
+
content: Array<{
|
|
19
|
+
type: 'text';
|
|
20
|
+
text: string;
|
|
21
|
+
}>;
|
|
22
|
+
isError?: boolean;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Convert a CLI command definition to MCP tool format
|
|
26
|
+
*/
|
|
27
|
+
export declare function commandToMcpTool(manifest: Manifest, cmdName: string, cmd: CommandDef): McpTool;
|
|
28
|
+
/**
|
|
29
|
+
* Convert all commands in a manifest to MCP tools
|
|
30
|
+
*/
|
|
31
|
+
export declare function manifestToMcpTools(manifest: Manifest): McpTool[];
|
|
32
|
+
/**
|
|
33
|
+
* Execute a CLI tool command and return MCP-formatted result
|
|
34
|
+
*/
|
|
35
|
+
export declare function executeTool(entryPath: string, runtime: string, command: string, args: Record<string, string>, argOrder?: string[]): Promise<McpToolResult>;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Adapter - Converts CLI tools to MCP tools
|
|
3
|
+
*/
|
|
4
|
+
import { spawn } from 'child_process';
|
|
5
|
+
/**
|
|
6
|
+
* Convert a CLI command definition to MCP tool format
|
|
7
|
+
*/
|
|
8
|
+
export function commandToMcpTool(manifest, cmdName, cmd) {
|
|
9
|
+
const properties = {};
|
|
10
|
+
const required = [];
|
|
11
|
+
// Convert args to JSON Schema properties
|
|
12
|
+
if (cmd.args) {
|
|
13
|
+
for (const arg of cmd.args) {
|
|
14
|
+
properties[arg.name] = {
|
|
15
|
+
type: 'string',
|
|
16
|
+
description: arg.description
|
|
17
|
+
};
|
|
18
|
+
if (arg.required) {
|
|
19
|
+
required.push(arg.name);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
name: `${manifest.name}_${cmdName}`,
|
|
25
|
+
description: `${manifest.name}: ${cmd.description || cmdName}`,
|
|
26
|
+
inputSchema: {
|
|
27
|
+
type: 'object',
|
|
28
|
+
properties,
|
|
29
|
+
required
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Convert all commands in a manifest to MCP tools
|
|
35
|
+
*/
|
|
36
|
+
export function manifestToMcpTools(manifest) {
|
|
37
|
+
const tools = [];
|
|
38
|
+
if (manifest.commands) {
|
|
39
|
+
for (const [cmdName, cmdDef] of Object.entries(manifest.commands)) {
|
|
40
|
+
tools.push(commandToMcpTool(manifest, cmdName, cmdDef));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return tools;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Execute a CLI tool command and return MCP-formatted result
|
|
47
|
+
*/
|
|
48
|
+
export async function executeTool(entryPath, runtime, command, args, argOrder) {
|
|
49
|
+
return new Promise((resolve) => {
|
|
50
|
+
// Build command arguments
|
|
51
|
+
const cmdArgs = [command];
|
|
52
|
+
// Prefer manifest-defined arg order (positional), fall back to stable ordering.
|
|
53
|
+
const orderedKeys = argOrder ?? Object.keys(args).sort();
|
|
54
|
+
for (const key of orderedKeys) {
|
|
55
|
+
const value = args[key];
|
|
56
|
+
if (value !== undefined && value !== '')
|
|
57
|
+
cmdArgs.push(value);
|
|
58
|
+
}
|
|
59
|
+
const runtimeArgs = runtime === 'node'
|
|
60
|
+
? [entryPath, ...cmdArgs]
|
|
61
|
+
: ['run', entryPath, ...cmdArgs];
|
|
62
|
+
const proc = spawn(runtime, runtimeArgs, {
|
|
63
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
64
|
+
env: { ...process.env }
|
|
65
|
+
});
|
|
66
|
+
let stdout = '';
|
|
67
|
+
let stderr = '';
|
|
68
|
+
proc.stdout.on('data', (data) => {
|
|
69
|
+
stdout += data.toString();
|
|
70
|
+
});
|
|
71
|
+
proc.stderr.on('data', (data) => {
|
|
72
|
+
stderr += data.toString();
|
|
73
|
+
});
|
|
74
|
+
proc.on('close', (code) => {
|
|
75
|
+
if (code !== 0) {
|
|
76
|
+
resolve({
|
|
77
|
+
content: [{ type: 'text', text: stderr || `Command failed with exit code ${code}` }],
|
|
78
|
+
isError: true
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
resolve({
|
|
83
|
+
content: [{ type: 'text', text: stdout || 'Command completed successfully' }]
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
proc.on('error', (err) => {
|
|
88
|
+
resolve({
|
|
89
|
+
content: [{ type: 'text', text: `Failed to execute: ${err.message}` }],
|
|
90
|
+
isError: true
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Config Generator - Generate Claude Code MCP configuration
|
|
3
|
+
*/
|
|
4
|
+
import { type Manifest } from '../core/manifest.js';
|
|
5
|
+
export interface McpServerConfig {
|
|
6
|
+
command: string;
|
|
7
|
+
args: string[];
|
|
8
|
+
env?: Record<string, string>;
|
|
9
|
+
}
|
|
10
|
+
export interface ClaudeCodeConfig {
|
|
11
|
+
mcpServers: Record<string, McpServerConfig>;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Generate MCP server config for a single package
|
|
15
|
+
*/
|
|
16
|
+
export declare function generateServerConfig(manifest: Manifest, _packagePath: string): McpServerConfig;
|
|
17
|
+
/**
|
|
18
|
+
* Generate Claude Code config for installed packages
|
|
19
|
+
*/
|
|
20
|
+
export declare function generateClaudeCodeConfig(cwd: string, options?: {
|
|
21
|
+
global?: boolean;
|
|
22
|
+
packages?: string[];
|
|
23
|
+
}): ClaudeCodeConfig;
|
|
24
|
+
/**
|
|
25
|
+
* Format config as JSON for Claude Code
|
|
26
|
+
*/
|
|
27
|
+
export declare function formatClaudeCodeConfig(config: ClaudeCodeConfig): string;
|
|
28
|
+
/**
|
|
29
|
+
* Generate config snippet for adding to existing claude_desktop_config.json
|
|
30
|
+
*/
|
|
31
|
+
export declare function generateConfigSnippet(manifest: Manifest, packagePath: string): string;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Config Generator - Generate Claude Code MCP configuration
|
|
3
|
+
*/
|
|
4
|
+
import { tryLoadManifest } from '../core/manifest.js';
|
|
5
|
+
import { findPackage, getGlobalPackages, getLocalPackages } from '../core/config.js';
|
|
6
|
+
/**
|
|
7
|
+
* Generate MCP server config for a single package
|
|
8
|
+
*/
|
|
9
|
+
export function generateServerConfig(manifest, _packagePath) {
|
|
10
|
+
// Use cli4ai start command which handles the MCP server
|
|
11
|
+
return {
|
|
12
|
+
command: 'cli4ai',
|
|
13
|
+
args: ['start', manifest.name]
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Load manifest for an installed package
|
|
18
|
+
*/
|
|
19
|
+
function loadPackageWithManifest(pkg) {
|
|
20
|
+
const manifest = tryLoadManifest(pkg.path);
|
|
21
|
+
if (!manifest)
|
|
22
|
+
return null;
|
|
23
|
+
return { name: pkg.name, path: pkg.path, manifest };
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Generate Claude Code config for installed packages
|
|
27
|
+
*/
|
|
28
|
+
export function generateClaudeCodeConfig(cwd, options = {}) {
|
|
29
|
+
const mcpServers = {};
|
|
30
|
+
// Get installed packages
|
|
31
|
+
let installedPackages = [];
|
|
32
|
+
if (options.packages && options.packages.length > 0) {
|
|
33
|
+
// Specific packages requested
|
|
34
|
+
for (const pkgName of options.packages) {
|
|
35
|
+
const pkg = findPackage(pkgName, cwd);
|
|
36
|
+
if (pkg) {
|
|
37
|
+
installedPackages.push(pkg);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
// All installed packages
|
|
43
|
+
if (options.global) {
|
|
44
|
+
installedPackages = getGlobalPackages();
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
installedPackages = [
|
|
48
|
+
...getLocalPackages(cwd),
|
|
49
|
+
...getGlobalPackages()
|
|
50
|
+
];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Load manifests and filter to MCP-enabled packages
|
|
54
|
+
for (const pkg of installedPackages) {
|
|
55
|
+
const pkgWithManifest = loadPackageWithManifest(pkg);
|
|
56
|
+
if (pkgWithManifest && pkgWithManifest.manifest.mcp?.enabled) {
|
|
57
|
+
mcpServers[`cli4ai-${pkgWithManifest.name}`] = generateServerConfig(pkgWithManifest.manifest, pkgWithManifest.path);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return { mcpServers };
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Format config as JSON for Claude Code
|
|
64
|
+
*/
|
|
65
|
+
export function formatClaudeCodeConfig(config) {
|
|
66
|
+
return JSON.stringify(config, null, 2);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Generate config snippet for adding to existing claude_desktop_config.json
|
|
70
|
+
*/
|
|
71
|
+
export function generateConfigSnippet(manifest, packagePath) {
|
|
72
|
+
const serverConfig = generateServerConfig(manifest, packagePath);
|
|
73
|
+
const serverName = `cli4ai-${manifest.name}`;
|
|
74
|
+
return `"${serverName}": ${JSON.stringify(serverConfig, null, 2)}`;
|
|
75
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
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
|
+
import { type Manifest } from '../core/manifest.js';
|
|
13
|
+
/**
|
|
14
|
+
* MCP Server that wraps a CLI tool
|
|
15
|
+
*/
|
|
16
|
+
export declare class McpServer {
|
|
17
|
+
private manifest;
|
|
18
|
+
private packagePath;
|
|
19
|
+
private tools;
|
|
20
|
+
private rateLimits;
|
|
21
|
+
private totalCallCount;
|
|
22
|
+
constructor(manifest: Manifest, packagePath: string);
|
|
23
|
+
/**
|
|
24
|
+
* SECURITY: Check if a tool call should be rate limited
|
|
25
|
+
*/
|
|
26
|
+
private checkRateLimit;
|
|
27
|
+
/**
|
|
28
|
+
* Start the MCP server (stdio mode)
|
|
29
|
+
*/
|
|
30
|
+
start(): Promise<void>;
|
|
31
|
+
private handleMessage;
|
|
32
|
+
private handleRequest;
|
|
33
|
+
private handleToolCall;
|
|
34
|
+
private executeCommand;
|
|
35
|
+
private sendResult;
|
|
36
|
+
private sendError;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Start MCP server for a package
|
|
40
|
+
*/
|
|
41
|
+
export declare function startMcpServer(manifest: Manifest, packagePath: string): Promise<void>;
|
|
@@ -0,0 +1,296 @@
|
|
|
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
|
+
import { spawn } from 'child_process';
|
|
13
|
+
import { resolve } from 'path';
|
|
14
|
+
import { appendFileSync, existsSync, mkdirSync } from 'fs';
|
|
15
|
+
import { homedir } from 'os';
|
|
16
|
+
import { manifestToMcpTools } from './adapter.js';
|
|
17
|
+
import { getSecret } from '../core/secrets.js';
|
|
18
|
+
import { loadConfig } from '../core/config.js';
|
|
19
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
20
|
+
// SECURITY: Audit logging and rate limiting
|
|
21
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
22
|
+
const AUDIT_LOG_DIR = resolve(homedir(), '.cli4ai', 'logs');
|
|
23
|
+
const RATE_LIMIT_WINDOW_MS = 60000; // 1 minute
|
|
24
|
+
const RATE_LIMIT_MAX_CALLS = 100; // Max calls per minute per tool
|
|
25
|
+
/**
|
|
26
|
+
* Audit log an MCP tool call for security tracking
|
|
27
|
+
* Can be disabled via `cli4ai config set audit.enabled false`
|
|
28
|
+
*/
|
|
29
|
+
function auditLog(packageName, toolName, args, result, errorMessage) {
|
|
30
|
+
try {
|
|
31
|
+
// Check if audit logging is enabled
|
|
32
|
+
const config = loadConfig();
|
|
33
|
+
if (!config.audit?.enabled) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (!existsSync(AUDIT_LOG_DIR)) {
|
|
37
|
+
mkdirSync(AUDIT_LOG_DIR, { recursive: true });
|
|
38
|
+
}
|
|
39
|
+
const logEntry = {
|
|
40
|
+
timestamp: new Date().toISOString(),
|
|
41
|
+
package: packageName,
|
|
42
|
+
tool: toolName,
|
|
43
|
+
// Redact potentially sensitive argument values, keep keys
|
|
44
|
+
argKeys: Object.keys(args),
|
|
45
|
+
result,
|
|
46
|
+
error: errorMessage,
|
|
47
|
+
pid: process.pid
|
|
48
|
+
};
|
|
49
|
+
const logFile = resolve(AUDIT_LOG_DIR, `mcp-audit-${new Date().toISOString().slice(0, 10)}.log`);
|
|
50
|
+
appendFileSync(logFile, JSON.stringify(logEntry) + '\n');
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// Don't fail tool execution if logging fails
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* MCP Server that wraps a CLI tool
|
|
58
|
+
*/
|
|
59
|
+
export class McpServer {
|
|
60
|
+
manifest;
|
|
61
|
+
packagePath;
|
|
62
|
+
tools;
|
|
63
|
+
rateLimits = new Map();
|
|
64
|
+
totalCallCount = 0;
|
|
65
|
+
constructor(manifest, packagePath) {
|
|
66
|
+
this.manifest = manifest;
|
|
67
|
+
this.packagePath = packagePath;
|
|
68
|
+
this.tools = manifestToMcpTools(manifest);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* SECURITY: Check if a tool call should be rate limited
|
|
72
|
+
*/
|
|
73
|
+
checkRateLimit(toolName) {
|
|
74
|
+
const now = Date.now();
|
|
75
|
+
const entry = this.rateLimits.get(toolName);
|
|
76
|
+
if (!entry || now - entry.windowStart >= RATE_LIMIT_WINDOW_MS) {
|
|
77
|
+
// Start new window
|
|
78
|
+
this.rateLimits.set(toolName, { count: 1, windowStart: now });
|
|
79
|
+
return { allowed: true };
|
|
80
|
+
}
|
|
81
|
+
if (entry.count >= RATE_LIMIT_MAX_CALLS) {
|
|
82
|
+
const retryAfterMs = RATE_LIMIT_WINDOW_MS - (now - entry.windowStart);
|
|
83
|
+
return { allowed: false, retryAfterMs };
|
|
84
|
+
}
|
|
85
|
+
entry.count++;
|
|
86
|
+
return { allowed: true };
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Start the MCP server (stdio mode)
|
|
90
|
+
*/
|
|
91
|
+
async start() {
|
|
92
|
+
process.stdin.setEncoding('utf8');
|
|
93
|
+
let buffer = '';
|
|
94
|
+
process.stdin.on('data', (chunk) => {
|
|
95
|
+
buffer += chunk;
|
|
96
|
+
// Try to parse complete JSON-RPC messages
|
|
97
|
+
const lines = buffer.split('\n');
|
|
98
|
+
buffer = lines.pop() || '';
|
|
99
|
+
for (const line of lines) {
|
|
100
|
+
if (line.trim()) {
|
|
101
|
+
this.handleMessage(line.trim());
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
process.stdin.on('end', () => {
|
|
106
|
+
process.exit(0);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
handleMessage(message) {
|
|
110
|
+
try {
|
|
111
|
+
const request = JSON.parse(message);
|
|
112
|
+
this.handleRequest(request);
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
this.sendError(null, -32700, 'Parse error');
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async handleRequest(request) {
|
|
119
|
+
const { id, method, params } = request;
|
|
120
|
+
try {
|
|
121
|
+
switch (method) {
|
|
122
|
+
case 'initialize':
|
|
123
|
+
this.sendResult(id, {
|
|
124
|
+
protocolVersion: '2024-11-05',
|
|
125
|
+
capabilities: {
|
|
126
|
+
tools: {}
|
|
127
|
+
},
|
|
128
|
+
serverInfo: {
|
|
129
|
+
name: `cli4ai-${this.manifest.name}`,
|
|
130
|
+
version: this.manifest.version
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
break;
|
|
134
|
+
case 'initialized':
|
|
135
|
+
// No response needed
|
|
136
|
+
break;
|
|
137
|
+
case 'tools/list':
|
|
138
|
+
this.sendResult(id, {
|
|
139
|
+
tools: this.tools.map(t => ({
|
|
140
|
+
name: t.name,
|
|
141
|
+
description: t.description,
|
|
142
|
+
inputSchema: t.inputSchema
|
|
143
|
+
}))
|
|
144
|
+
});
|
|
145
|
+
break;
|
|
146
|
+
case 'tools/call':
|
|
147
|
+
await this.handleToolCall(id, params);
|
|
148
|
+
break;
|
|
149
|
+
case 'ping':
|
|
150
|
+
this.sendResult(id, {});
|
|
151
|
+
break;
|
|
152
|
+
default:
|
|
153
|
+
this.sendError(id, -32601, `Method not found: ${method}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
this.sendError(id, -32603, err.message);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
async handleToolCall(id, params) {
|
|
161
|
+
const { name, arguments: args = {} } = params;
|
|
162
|
+
// SECURITY: Rate limiting
|
|
163
|
+
const rateCheck = this.checkRateLimit(name);
|
|
164
|
+
if (!rateCheck.allowed) {
|
|
165
|
+
auditLog(this.manifest.name, name, args, 'rate_limited');
|
|
166
|
+
this.sendError(id, -32000, `Rate limit exceeded. Retry after ${Math.ceil((rateCheck.retryAfterMs || 0) / 1000)}s`);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
// Track total calls for monitoring
|
|
170
|
+
this.totalCallCount++;
|
|
171
|
+
// Parse tool name: package_command
|
|
172
|
+
const parts = name.split('_');
|
|
173
|
+
if (parts.length < 2 || parts[0] !== this.manifest.name) {
|
|
174
|
+
auditLog(this.manifest.name, name, args, 'error', 'Unknown tool');
|
|
175
|
+
this.sendError(id, -32602, `Unknown tool: ${name}`);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const command = parts.slice(1).join('_');
|
|
179
|
+
const cmdDef = this.manifest.commands?.[command];
|
|
180
|
+
if (!cmdDef) {
|
|
181
|
+
auditLog(this.manifest.name, name, args, 'error', 'Unknown command');
|
|
182
|
+
this.sendError(id, -32602, `Unknown command: ${command}`);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
// Build command arguments in order
|
|
186
|
+
const cmdArgs = [command];
|
|
187
|
+
if (cmdDef.args) {
|
|
188
|
+
for (const argDef of cmdDef.args) {
|
|
189
|
+
const value = args[argDef.name];
|
|
190
|
+
if (value !== undefined && value !== '') {
|
|
191
|
+
cmdArgs.push(String(value));
|
|
192
|
+
}
|
|
193
|
+
else if (argDef.required) {
|
|
194
|
+
auditLog(this.manifest.name, name, args, 'error', `Missing required argument: ${argDef.name}`);
|
|
195
|
+
this.sendError(id, -32602, `Missing required argument: ${argDef.name}`);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// Execute the CLI tool
|
|
201
|
+
const entryPath = resolve(this.packagePath, this.manifest.entry);
|
|
202
|
+
try {
|
|
203
|
+
const result = await this.executeCommand(entryPath, cmdArgs);
|
|
204
|
+
auditLog(this.manifest.name, name, args, 'success');
|
|
205
|
+
this.sendResult(id, {
|
|
206
|
+
content: [{ type: 'text', text: result }]
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
catch (err) {
|
|
210
|
+
const errorMessage = err.message;
|
|
211
|
+
auditLog(this.manifest.name, name, args, 'error', errorMessage);
|
|
212
|
+
this.sendResult(id, {
|
|
213
|
+
content: [{ type: 'text', text: errorMessage }],
|
|
214
|
+
isError: true
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
executeCommand(entryPath, args) {
|
|
219
|
+
return new Promise((resolve, reject) => {
|
|
220
|
+
// Use tsx for TypeScript files, node for JavaScript
|
|
221
|
+
let cmd;
|
|
222
|
+
let cmdArgs;
|
|
223
|
+
if (entryPath.endsWith('.ts') || entryPath.endsWith('.tsx')) {
|
|
224
|
+
cmd = 'npx';
|
|
225
|
+
cmdArgs = ['tsx', entryPath, ...args];
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
cmd = 'node';
|
|
229
|
+
cmdArgs = [entryPath, ...args];
|
|
230
|
+
}
|
|
231
|
+
// Inject secrets from manifest env definitions
|
|
232
|
+
// SECURITY: Use package-scoped secret lookup (tries scoped first, then global)
|
|
233
|
+
const secretsEnv = {};
|
|
234
|
+
if (this.manifest.env) {
|
|
235
|
+
for (const key of Object.keys(this.manifest.env)) {
|
|
236
|
+
const value = getSecret(key, this.manifest.name);
|
|
237
|
+
if (value) {
|
|
238
|
+
secretsEnv[key] = value;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const proc = spawn(cmd, cmdArgs, {
|
|
243
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
244
|
+
env: { ...process.env, ...secretsEnv }
|
|
245
|
+
});
|
|
246
|
+
let stdout = '';
|
|
247
|
+
let stderr = '';
|
|
248
|
+
proc.stdout.on('data', (data) => {
|
|
249
|
+
stdout += data.toString();
|
|
250
|
+
});
|
|
251
|
+
proc.stderr.on('data', (data) => {
|
|
252
|
+
stderr += data.toString();
|
|
253
|
+
});
|
|
254
|
+
proc.on('close', (code) => {
|
|
255
|
+
if (code !== 0) {
|
|
256
|
+
reject(new Error(stderr || `Exit code ${code}`));
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
resolve(stdout);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
proc.on('error', (err) => {
|
|
263
|
+
reject(err);
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
sendResult(id, result) {
|
|
268
|
+
if (id === null)
|
|
269
|
+
return;
|
|
270
|
+
const response = {
|
|
271
|
+
jsonrpc: '2.0',
|
|
272
|
+
id,
|
|
273
|
+
result
|
|
274
|
+
};
|
|
275
|
+
console.log(JSON.stringify(response));
|
|
276
|
+
}
|
|
277
|
+
sendError(id, code, message) {
|
|
278
|
+
// Per JSON-RPC 2.0 spec, error responses for parse errors should include id: null
|
|
279
|
+
// For other errors where id is null (shouldn't happen), we skip the response
|
|
280
|
+
if (id === null && code !== -32700)
|
|
281
|
+
return;
|
|
282
|
+
const response = {
|
|
283
|
+
jsonrpc: '2.0',
|
|
284
|
+
id: id, // For parse errors (-32700), this will be cast from null
|
|
285
|
+
error: { code, message }
|
|
286
|
+
};
|
|
287
|
+
console.log(JSON.stringify(response));
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Start MCP server for a package
|
|
292
|
+
*/
|
|
293
|
+
export async function startMcpServer(manifest, packagePath) {
|
|
294
|
+
const server = new McpServer(manifest, packagePath);
|
|
295
|
+
await server.start();
|
|
296
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli4ai Remote Service
|
|
3
|
+
*
|
|
4
|
+
* HTTP server that exposes cli4ai functionality for remote execution.
|
|
5
|
+
* Run with `cli4ai serve` to start the service.
|
|
6
|
+
*/
|
|
7
|
+
import { createServer } from 'http';
|
|
8
|
+
import { type ScopeLevel } from '../core/execute.js';
|
|
9
|
+
export interface ServiceConfig {
|
|
10
|
+
/** Port to listen on (default: 4100) */
|
|
11
|
+
port: number;
|
|
12
|
+
/** Host to bind to (default: 0.0.0.0) */
|
|
13
|
+
host: string;
|
|
14
|
+
/** API key for authentication (optional but recommended) */
|
|
15
|
+
apiKey?: string;
|
|
16
|
+
/** Working directory for command execution */
|
|
17
|
+
cwd: string;
|
|
18
|
+
/** Allowed scope levels (defaults to ['read', 'write', 'full']) */
|
|
19
|
+
allowedScopes?: ScopeLevel[];
|
|
20
|
+
}
|
|
21
|
+
export interface RunToolRequest {
|
|
22
|
+
/** Package name */
|
|
23
|
+
package: string;
|
|
24
|
+
/** Command within the package */
|
|
25
|
+
command?: string;
|
|
26
|
+
/** Arguments to pass */
|
|
27
|
+
args?: string[];
|
|
28
|
+
/** Environment variables */
|
|
29
|
+
env?: Record<string, string>;
|
|
30
|
+
/** Standard input to pass to the tool */
|
|
31
|
+
stdin?: string;
|
|
32
|
+
/** Timeout in milliseconds */
|
|
33
|
+
timeout?: number;
|
|
34
|
+
/** Scope level for execution */
|
|
35
|
+
scope?: ScopeLevel;
|
|
36
|
+
}
|
|
37
|
+
export interface RunToolResponse {
|
|
38
|
+
success: boolean;
|
|
39
|
+
exitCode: number;
|
|
40
|
+
stdout?: string;
|
|
41
|
+
stderr?: string;
|
|
42
|
+
durationMs: number;
|
|
43
|
+
error?: {
|
|
44
|
+
code: string;
|
|
45
|
+
message: string;
|
|
46
|
+
details?: Record<string, unknown>;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export interface RunRoutineRequest {
|
|
50
|
+
/** Routine name */
|
|
51
|
+
routine: string;
|
|
52
|
+
/** Variables to pass to the routine */
|
|
53
|
+
vars?: Record<string, string>;
|
|
54
|
+
}
|
|
55
|
+
export interface ListPackagesResponse {
|
|
56
|
+
packages: Array<{
|
|
57
|
+
name: string;
|
|
58
|
+
version: string;
|
|
59
|
+
path: string;
|
|
60
|
+
source: 'local' | 'registry';
|
|
61
|
+
}>;
|
|
62
|
+
}
|
|
63
|
+
export interface PackageInfoResponse {
|
|
64
|
+
name: string;
|
|
65
|
+
version: string;
|
|
66
|
+
description?: string;
|
|
67
|
+
commands?: Record<string, {
|
|
68
|
+
description: string;
|
|
69
|
+
}>;
|
|
70
|
+
}
|
|
71
|
+
export interface HealthResponse {
|
|
72
|
+
status: 'ok';
|
|
73
|
+
hostname: string;
|
|
74
|
+
version: string;
|
|
75
|
+
uptime: number;
|
|
76
|
+
}
|
|
77
|
+
export declare function createService(config: ServiceConfig): ReturnType<typeof createServer>;
|
|
78
|
+
export interface StartServiceOptions {
|
|
79
|
+
port?: number;
|
|
80
|
+
host?: string;
|
|
81
|
+
apiKey?: string;
|
|
82
|
+
cwd?: string;
|
|
83
|
+
allowedScopes?: ScopeLevel[];
|
|
84
|
+
}
|
|
85
|
+
export declare function startService(options?: StartServiceOptions): Promise<void>;
|