pandora-cli-skills 1.1.33 → 1.1.34
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/SKILL.md +1 -0
- package/cli/lib/cli_output_service.cjs +25 -1
- package/cli/lib/command_executor_service.cjs +123 -0
- package/cli/lib/command_router.cjs +4 -0
- package/cli/lib/error_recovery_service.cjs +108 -0
- package/cli/lib/fork_runtime_service.cjs +79 -0
- package/cli/lib/mcp_server_service.cjs +147 -0
- package/cli/lib/mcp_tool_registry.cjs +344 -0
- package/cli/lib/parsers/core_command_flags.cjs +1450 -0
- package/cli/lib/parsers/lp_flags.cjs +33 -1
- package/cli/lib/parsers/polymarket_flags.cjs +50 -0
- package/cli/lib/parsers/resolve_flags.cjs +33 -1
- package/cli/lib/parsers/trade_flags.cjs +27 -0
- package/cli/lib/polymarket_command_service.cjs +2 -8
- package/cli/lib/schema_command_service.cjs +27 -0
- package/cli/lib/shared/parse_primitives.cjs +324 -0
- package/cli/pandora.cjs +233 -1702
- package/package.json +5 -3
- package/tests/cli/cli.integration.test.cjs +22 -0
- package/tests/cli/mcp.integration.test.cjs +104 -0
package/SKILL.md
CHANGED
|
@@ -9,6 +9,7 @@ function createCliOutputService(options = {}) {
|
|
|
9
9
|
? options.defaultSchemaVersion.trim()
|
|
10
10
|
: '1.0.0';
|
|
11
11
|
const CliError = options.CliError;
|
|
12
|
+
const getRecoveryForError = typeof options.getRecoveryForError === 'function' ? options.getRecoveryForError : null;
|
|
12
13
|
|
|
13
14
|
if (typeof CliError !== 'function') {
|
|
14
15
|
throw new Error('createCliOutputService requires CliError class.');
|
|
@@ -43,16 +44,36 @@ function createCliOutputService(options = {}) {
|
|
|
43
44
|
if (error.details !== undefined) {
|
|
44
45
|
envelope.error.details = error.details;
|
|
45
46
|
}
|
|
47
|
+
if (getRecoveryForError) {
|
|
48
|
+
const recovery = getRecoveryForError({
|
|
49
|
+
code: error.code,
|
|
50
|
+
message: error.message,
|
|
51
|
+
details: error.details,
|
|
52
|
+
});
|
|
53
|
+
if (recovery) {
|
|
54
|
+
envelope.error.recovery = recovery;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
46
57
|
return envelope;
|
|
47
58
|
}
|
|
48
59
|
|
|
49
|
-
|
|
60
|
+
const fallback = {
|
|
50
61
|
ok: false,
|
|
51
62
|
error: {
|
|
52
63
|
code: 'UNEXPECTED_ERROR',
|
|
53
64
|
message: formatErrorValue(error && error.message ? error.message : error),
|
|
54
65
|
},
|
|
55
66
|
};
|
|
67
|
+
if (getRecoveryForError) {
|
|
68
|
+
const recovery = getRecoveryForError({
|
|
69
|
+
code: 'UNEXPECTED_ERROR',
|
|
70
|
+
message: fallback.error.message,
|
|
71
|
+
});
|
|
72
|
+
if (recovery) {
|
|
73
|
+
fallback.error.recovery = recovery;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return fallback;
|
|
56
77
|
}
|
|
57
78
|
|
|
58
79
|
function attachJsonMetadata(data) {
|
|
@@ -100,6 +121,9 @@ function createCliOutputService(options = {}) {
|
|
|
100
121
|
console.error(`Details: ${String(envelope.error.details)}`);
|
|
101
122
|
}
|
|
102
123
|
}
|
|
124
|
+
if (envelope.error.recovery && envelope.error.recovery.command) {
|
|
125
|
+
console.error(`Next: ${envelope.error.recovery.command}`);
|
|
126
|
+
}
|
|
103
127
|
}
|
|
104
128
|
|
|
105
129
|
process.exit(error instanceof CliError ? error.exitCode : 1);
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const { spawnSync } = require('child_process');
|
|
3
|
+
|
|
4
|
+
function coerceErrorMessage(value) {
|
|
5
|
+
if (typeof value === 'string') return value;
|
|
6
|
+
if (value && typeof value.message === 'string') return value.message;
|
|
7
|
+
try {
|
|
8
|
+
return JSON.stringify(value);
|
|
9
|
+
} catch {
|
|
10
|
+
return String(value);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function parseEnvelopeFromOutput(stdout, stderr) {
|
|
15
|
+
const candidates = [stdout, stderr, `${stdout || ''}\n${stderr || ''}`]
|
|
16
|
+
.map((value) => String(value || '').trim())
|
|
17
|
+
.filter(Boolean);
|
|
18
|
+
|
|
19
|
+
for (const candidate of candidates) {
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(candidate);
|
|
22
|
+
} catch {
|
|
23
|
+
const start = candidate.indexOf('{');
|
|
24
|
+
const end = candidate.lastIndexOf('}');
|
|
25
|
+
if (start >= 0 && end > start) {
|
|
26
|
+
const slice = candidate.slice(start, end + 1);
|
|
27
|
+
try {
|
|
28
|
+
return JSON.parse(slice);
|
|
29
|
+
} catch {
|
|
30
|
+
// continue
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Build a child-process command executor for JSON-mode CLI invocations.
|
|
41
|
+
* @param {{cliPath?: string, defaultTimeoutMs?: number, env?: object}} [options]
|
|
42
|
+
* @returns {{executeJsonCommand: (commandArgs: string[], runtime?: {timeoutMs?: number, env?: object}) => {ok: boolean, envelope: object, exitCode: number, stdout: string, stderr: string}}}
|
|
43
|
+
*/
|
|
44
|
+
function createCommandExecutorService(options = {}) {
|
|
45
|
+
const cliPath =
|
|
46
|
+
typeof options.cliPath === 'string' && options.cliPath.trim()
|
|
47
|
+
? options.cliPath.trim()
|
|
48
|
+
: path.resolve(__dirname, '..', 'pandora.cjs');
|
|
49
|
+
const defaultTimeoutMs = Number.isFinite(options.defaultTimeoutMs) ? Math.max(1_000, Math.trunc(options.defaultTimeoutMs)) : 60_000;
|
|
50
|
+
const baseEnv = options.env && typeof options.env === 'object' ? options.env : process.env;
|
|
51
|
+
|
|
52
|
+
function executeJsonCommand(commandArgs, runtime = {}) {
|
|
53
|
+
const timeoutMs = Number.isFinite(runtime.timeoutMs)
|
|
54
|
+
? Math.max(1_000, Math.trunc(runtime.timeoutMs))
|
|
55
|
+
: defaultTimeoutMs;
|
|
56
|
+
const env = runtime.env && typeof runtime.env === 'object' ? runtime.env : baseEnv;
|
|
57
|
+
const argv = ['--output', 'json', ...commandArgs];
|
|
58
|
+
|
|
59
|
+
const result = spawnSync(process.execPath, [cliPath, ...argv], {
|
|
60
|
+
encoding: 'utf8',
|
|
61
|
+
env,
|
|
62
|
+
timeout: timeoutMs,
|
|
63
|
+
killSignal: 'SIGKILL',
|
|
64
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
if (result.error && result.error.code === 'ETIMEDOUT') {
|
|
68
|
+
return {
|
|
69
|
+
ok: false,
|
|
70
|
+
exitCode: 1,
|
|
71
|
+
stdout: result.stdout || '',
|
|
72
|
+
stderr: result.stderr || '',
|
|
73
|
+
envelope: {
|
|
74
|
+
ok: false,
|
|
75
|
+
error: {
|
|
76
|
+
code: 'COMMAND_TIMEOUT',
|
|
77
|
+
message: `Command timed out after ${timeoutMs}ms.`,
|
|
78
|
+
details: { commandArgs, timeoutMs },
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const envelope = parseEnvelopeFromOutput(result.stdout, result.stderr);
|
|
85
|
+
if (!envelope) {
|
|
86
|
+
return {
|
|
87
|
+
ok: false,
|
|
88
|
+
exitCode: typeof result.status === 'number' ? result.status : 1,
|
|
89
|
+
stdout: result.stdout || '',
|
|
90
|
+
stderr: result.stderr || '',
|
|
91
|
+
envelope: {
|
|
92
|
+
ok: false,
|
|
93
|
+
error: {
|
|
94
|
+
code: 'COMMAND_OUTPUT_PARSE_FAILED',
|
|
95
|
+
message: 'CLI command returned non-JSON output.',
|
|
96
|
+
details: {
|
|
97
|
+
commandArgs,
|
|
98
|
+
stdout: String(result.stdout || '').slice(0, 10_000),
|
|
99
|
+
stderr: String(result.stderr || '').slice(0, 10_000),
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
ok: envelope && envelope.ok === true,
|
|
108
|
+
exitCode: typeof result.status === 'number' ? result.status : 1,
|
|
109
|
+
stdout: result.stdout || '',
|
|
110
|
+
stderr: result.stderr || '',
|
|
111
|
+
envelope,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
executeJsonCommand,
|
|
117
|
+
coerceErrorMessage,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
module.exports = {
|
|
122
|
+
createCommandExecutorService,
|
|
123
|
+
};
|
|
@@ -37,6 +37,8 @@ function createCommandRouter(deps = {}) {
|
|
|
37
37
|
runSuggestCommand,
|
|
38
38
|
runResolveCommand,
|
|
39
39
|
runLpCommand,
|
|
40
|
+
runMcpCommand,
|
|
41
|
+
runStreamCommand,
|
|
40
42
|
runScriptCommand,
|
|
41
43
|
} = deps;
|
|
42
44
|
|
|
@@ -131,6 +133,8 @@ function createCommandRouter(deps = {}) {
|
|
|
131
133
|
suggest: async (handlerArgs, handlerContext) => runSuggestCommand(handlerArgs, handlerContext),
|
|
132
134
|
resolve: async (handlerArgs, handlerContext) => runResolveCommand(handlerArgs, handlerContext),
|
|
133
135
|
lp: async (handlerArgs, handlerContext) => runLpCommand(handlerArgs, handlerContext),
|
|
136
|
+
mcp: async (handlerArgs, handlerContext) => runMcpCommand(handlerArgs, handlerContext),
|
|
137
|
+
stream: async (handlerArgs, handlerContext) => runStreamCommand(handlerArgs, handlerContext),
|
|
134
138
|
schema: async (handlerArgs, handlerContext) => deps.runSchemaCommand(handlerArgs, handlerContext),
|
|
135
139
|
launch: async (handlerArgs, handlerContext) => {
|
|
136
140
|
if (handlerContext.outputMode === 'json') {
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
function cleanToken(value, fallback) {
|
|
2
|
+
const normalized = String(value === undefined || value === null ? '' : value).trim();
|
|
3
|
+
return normalized || fallback;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function toAddressOrPlaceholder(value, placeholder = '<address>') {
|
|
7
|
+
const normalized = cleanToken(value, '');
|
|
8
|
+
if (/^0x[a-fA-F0-9]{40}$/.test(normalized)) {
|
|
9
|
+
return normalized;
|
|
10
|
+
}
|
|
11
|
+
return placeholder;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function toSideOrPlaceholder(value) {
|
|
15
|
+
const normalized = cleanToken(value, '').toLowerCase();
|
|
16
|
+
return normalized === 'yes' || normalized === 'no' ? normalized : 'yes';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function toPositiveNumberOrPlaceholder(value, placeholder = '<amount>') {
|
|
20
|
+
const numeric = Number(value);
|
|
21
|
+
if (Number.isFinite(numeric) && numeric > 0) {
|
|
22
|
+
return String(numeric);
|
|
23
|
+
}
|
|
24
|
+
return placeholder;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function buildTradeRetryCommand(cliName, details) {
|
|
28
|
+
const marketAddress = toAddressOrPlaceholder(details && details.marketAddress);
|
|
29
|
+
const side = toSideOrPlaceholder(details && details.side);
|
|
30
|
+
const amountUsdc = toPositiveNumberOrPlaceholder(details && details.amountUsdc);
|
|
31
|
+
return `${cliName} trade --dry-run --market-address ${marketAddress} --side ${side} --amount-usdc ${amountUsdc}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function buildPolymarketApproveCommand(cliName) {
|
|
35
|
+
return `${cliName} polymarket approve --dry-run`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function buildPolymarketPreflightCommand(cliName) {
|
|
39
|
+
return `${cliName} polymarket preflight`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Build deterministic Next-Best-Action recovery hints for JSON errors.
|
|
44
|
+
* @param {{cliName?: string}} [options]
|
|
45
|
+
* @returns {{getRecoveryForError: (errorLike: any) => (null|{action: string, command: string, retryable: boolean})}}
|
|
46
|
+
*/
|
|
47
|
+
function createErrorRecoveryService(options = {}) {
|
|
48
|
+
const cliName = cleanToken(options.cliName, 'pandora');
|
|
49
|
+
|
|
50
|
+
function getRecoveryForError(errorLike) {
|
|
51
|
+
const code = cleanToken(errorLike && errorLike.code, '');
|
|
52
|
+
const details = errorLike && typeof errorLike.details === 'object' && errorLike.details ? errorLike.details : {};
|
|
53
|
+
|
|
54
|
+
switch (code) {
|
|
55
|
+
case 'TRADE_RISK_GUARD':
|
|
56
|
+
return {
|
|
57
|
+
action: 'Adjust risk guard inputs',
|
|
58
|
+
command: buildTradeRetryCommand(cliName, details),
|
|
59
|
+
retryable: true,
|
|
60
|
+
};
|
|
61
|
+
case 'ALLOWANCE_READ_FAILED':
|
|
62
|
+
case 'APPROVE_SIMULATION_FAILED':
|
|
63
|
+
case 'APPROVE_EXECUTION_FAILED':
|
|
64
|
+
case 'TRADE_EXECUTION_FAILED':
|
|
65
|
+
return {
|
|
66
|
+
action: 'Re-run trade planning before execute',
|
|
67
|
+
command: buildTradeRetryCommand(cliName, details),
|
|
68
|
+
retryable: true,
|
|
69
|
+
};
|
|
70
|
+
case 'POLYMARKET_APPROVE_FAILED':
|
|
71
|
+
case 'POLYMARKET_PROXY_APPROVAL_REQUIRES_MANUAL_EXECUTION':
|
|
72
|
+
return {
|
|
73
|
+
action: 'Run Polymarket approve flow first',
|
|
74
|
+
command: buildPolymarketApproveCommand(cliName),
|
|
75
|
+
retryable: true,
|
|
76
|
+
};
|
|
77
|
+
case 'POLYMARKET_TRADE_FAILED':
|
|
78
|
+
case 'POLYMARKET_PREFLIGHT_FAILED':
|
|
79
|
+
case 'POLYMARKET_CHECK_FAILED':
|
|
80
|
+
case 'POLYMARKET_MARKET_RESOLUTION_FAILED':
|
|
81
|
+
return {
|
|
82
|
+
action: 'Run Polymarket preflight diagnostics',
|
|
83
|
+
command: buildPolymarketPreflightCommand(cliName),
|
|
84
|
+
retryable: true,
|
|
85
|
+
};
|
|
86
|
+
case 'MISSING_REQUIRED_FLAG':
|
|
87
|
+
case 'MISSING_FLAG_VALUE':
|
|
88
|
+
case 'INVALID_FLAG_VALUE':
|
|
89
|
+
case 'UNKNOWN_FLAG':
|
|
90
|
+
case 'INVALID_ARGS':
|
|
91
|
+
return {
|
|
92
|
+
action: 'Inspect command help and retry',
|
|
93
|
+
command: `${cliName} help`,
|
|
94
|
+
retryable: true,
|
|
95
|
+
};
|
|
96
|
+
default:
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
getRecoveryForError,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = {
|
|
107
|
+
createErrorRecoveryService,
|
|
108
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
function toServiceError(code, message, details = undefined) {
|
|
2
|
+
const error = new Error(message);
|
|
3
|
+
error.code = code;
|
|
4
|
+
if (details !== undefined) {
|
|
5
|
+
error.details = details;
|
|
6
|
+
}
|
|
7
|
+
return error;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function toIntegerOrNull(value) {
|
|
11
|
+
if (value === null || value === undefined || value === '') return null;
|
|
12
|
+
const parsed = Number(value);
|
|
13
|
+
if (!Number.isInteger(parsed)) return null;
|
|
14
|
+
return parsed;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Resolve attach-only fork runtime settings.
|
|
19
|
+
* Precedence in fork mode: --fork-rpc-url > FORK_RPC_URL (when --fork).
|
|
20
|
+
* @param {object} [options]
|
|
21
|
+
* @param {{env?: object, isSecureHttpUrlOrLocal?: (url: string) => boolean, defaultChainId?: number}} [runtime]
|
|
22
|
+
* @returns {{mode: 'live'|'fork', rpcUrl: (string|null), chainId: (number|null)}}
|
|
23
|
+
*/
|
|
24
|
+
function resolveForkRuntime(options = {}, runtime = {}) {
|
|
25
|
+
const env = runtime.env && typeof runtime.env === 'object' ? runtime.env : process.env;
|
|
26
|
+
const isSecureHttpUrlOrLocal =
|
|
27
|
+
typeof runtime.isSecureHttpUrlOrLocal === 'function' ? runtime.isSecureHttpUrlOrLocal : () => true;
|
|
28
|
+
const defaultChainId = Number.isInteger(runtime.defaultChainId) ? runtime.defaultChainId : 1;
|
|
29
|
+
|
|
30
|
+
const forkExplicit = options.fork === true;
|
|
31
|
+
const forkRpcUrlFromFlag = String(options.forkRpcUrl || '').trim();
|
|
32
|
+
const forkRequested = forkExplicit || Boolean(forkRpcUrlFromFlag);
|
|
33
|
+
|
|
34
|
+
if (!forkRequested) {
|
|
35
|
+
const chainId = toIntegerOrNull(options.chainId) ?? toIntegerOrNull(env.CHAIN_ID) ?? defaultChainId;
|
|
36
|
+
return {
|
|
37
|
+
mode: 'live',
|
|
38
|
+
rpcUrl: null,
|
|
39
|
+
chainId,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const envForkRpcUrl = String(env.FORK_RPC_URL || '').trim();
|
|
44
|
+
let rpcUrl = forkRpcUrlFromFlag || (forkExplicit ? envForkRpcUrl : '');
|
|
45
|
+
|
|
46
|
+
if (!rpcUrl) {
|
|
47
|
+
throw toServiceError(
|
|
48
|
+
'MISSING_REQUIRED_FLAG',
|
|
49
|
+
'--fork requires FORK_RPC_URL env var or explicit --fork-rpc-url <url>.',
|
|
50
|
+
{
|
|
51
|
+
hints: ['Set FORK_RPC_URL=http://127.0.0.1:8545 and rerun with --fork.'],
|
|
52
|
+
},
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!isSecureHttpUrlOrLocal(rpcUrl)) {
|
|
57
|
+
throw toServiceError(
|
|
58
|
+
'INVALID_FLAG_VALUE',
|
|
59
|
+
'--fork-rpc-url must use https:// (or http://localhost/127.0.0.1 for local testing).',
|
|
60
|
+
{ rpcUrl },
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const chainId =
|
|
65
|
+
toIntegerOrNull(options.forkChainId)
|
|
66
|
+
?? toIntegerOrNull(options.chainId)
|
|
67
|
+
?? toIntegerOrNull(env.CHAIN_ID)
|
|
68
|
+
?? defaultChainId;
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
mode: 'fork',
|
|
72
|
+
rpcUrl,
|
|
73
|
+
chainId,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = {
|
|
78
|
+
resolveForkRuntime,
|
|
79
|
+
};
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
|
|
2
|
+
const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
|
|
3
|
+
const {
|
|
4
|
+
ListToolsRequestSchema,
|
|
5
|
+
CallToolRequestSchema,
|
|
6
|
+
McpError,
|
|
7
|
+
ErrorCode,
|
|
8
|
+
} = require('@modelcontextprotocol/sdk/types.js');
|
|
9
|
+
|
|
10
|
+
const { createMcpToolRegistry } = require('./mcp_tool_registry.cjs');
|
|
11
|
+
const { createCommandExecutorService } = require('./command_executor_service.cjs');
|
|
12
|
+
|
|
13
|
+
function coerceErrorMessage(value) {
|
|
14
|
+
if (typeof value === 'string') return value;
|
|
15
|
+
if (value && typeof value.message === 'string') return value.message;
|
|
16
|
+
try {
|
|
17
|
+
return JSON.stringify(value);
|
|
18
|
+
} catch {
|
|
19
|
+
return String(value);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function asCliErrorEnvelope(error) {
|
|
24
|
+
const code = error && error.code ? String(error.code) : 'MCP_TOOL_FAILED';
|
|
25
|
+
const envelope = {
|
|
26
|
+
ok: false,
|
|
27
|
+
error: {
|
|
28
|
+
code,
|
|
29
|
+
message: coerceErrorMessage(error),
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
if (error && error.details !== undefined) {
|
|
33
|
+
envelope.error.details = error.details;
|
|
34
|
+
}
|
|
35
|
+
return envelope;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function asToolResult(envelope) {
|
|
39
|
+
return {
|
|
40
|
+
content: [
|
|
41
|
+
{
|
|
42
|
+
type: 'text',
|
|
43
|
+
text: JSON.stringify(envelope, null, 2),
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
structuredContent: envelope,
|
|
47
|
+
isError: envelope && envelope.ok === false,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Create the MCP stdio server runner for `pandora mcp`.
|
|
53
|
+
* @param {{packageVersion?: string, cliPath?: string}} [options]
|
|
54
|
+
* @returns {{runMcpServer: (args: string[], context: {outputMode: 'table'|'json'}) => Promise<void>}}
|
|
55
|
+
*/
|
|
56
|
+
function createRunMcpServer(options = {}) {
|
|
57
|
+
const packageVersion =
|
|
58
|
+
typeof options.packageVersion === 'string' && options.packageVersion.trim()
|
|
59
|
+
? options.packageVersion.trim()
|
|
60
|
+
: '0.0.0';
|
|
61
|
+
const registry = createMcpToolRegistry();
|
|
62
|
+
const executor = createCommandExecutorService({
|
|
63
|
+
cliPath: options.cliPath,
|
|
64
|
+
defaultTimeoutMs: 60_000,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
async function runMcpServer(args, context) {
|
|
68
|
+
const mcpArgs = Array.isArray(args) ? args : [];
|
|
69
|
+
if (mcpArgs.includes('--help') || mcpArgs.includes('-h')) {
|
|
70
|
+
if (context && context.outputMode === 'json') {
|
|
71
|
+
const usageEnvelope = {
|
|
72
|
+
ok: true,
|
|
73
|
+
command: 'mcp.help',
|
|
74
|
+
data: {
|
|
75
|
+
usage: 'pandora mcp',
|
|
76
|
+
notes: [
|
|
77
|
+
'Runs an MCP stdio server.',
|
|
78
|
+
'Do not pass --output json to this command in normal MCP operation.',
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
process.stdout.write(`${JSON.stringify(usageEnvelope, null, 2)}\n`);
|
|
83
|
+
} else {
|
|
84
|
+
// eslint-disable-next-line no-console
|
|
85
|
+
console.log('Usage: pandora mcp');
|
|
86
|
+
// eslint-disable-next-line no-console
|
|
87
|
+
console.log('Runs Pandora as an MCP stdio server.');
|
|
88
|
+
}
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (context && context.outputMode === 'json') {
|
|
93
|
+
throw new Error('pandora mcp must be run without --output json because MCP uses raw stdio transport.');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const server = new Server(
|
|
97
|
+
{
|
|
98
|
+
name: 'pandora-cli-skills',
|
|
99
|
+
version: packageVersion,
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
capabilities: {
|
|
103
|
+
tools: { listChanged: false },
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
109
|
+
tools: registry.listTools(),
|
|
110
|
+
}));
|
|
111
|
+
|
|
112
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
113
|
+
try {
|
|
114
|
+
const params = request && request.params ? request.params : {};
|
|
115
|
+
const toolName = String(params.name || '').trim();
|
|
116
|
+
if (!toolName) {
|
|
117
|
+
throw new McpError(ErrorCode.InvalidParams, 'tools/call requires params.name.');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const toolArgs = params.arguments && typeof params.arguments === 'object' ? params.arguments : {};
|
|
121
|
+
const invocation = registry.prepareInvocation(toolName, toolArgs);
|
|
122
|
+
const execution = executor.executeJsonCommand(invocation.argv);
|
|
123
|
+
return asToolResult(execution.envelope);
|
|
124
|
+
} catch (err) {
|
|
125
|
+
if (err instanceof McpError) throw err;
|
|
126
|
+
return asToolResult(asCliErrorEnvelope(err));
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const transport = new StdioServerTransport(process.stdin, process.stdout);
|
|
131
|
+
await server.connect(transport);
|
|
132
|
+
|
|
133
|
+
await new Promise((resolve, reject) => {
|
|
134
|
+
transport.onclose = () => resolve();
|
|
135
|
+
transport.onerror = (error) => reject(error);
|
|
136
|
+
server.onclose = () => resolve();
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
runMcpServer,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
module.exports = {
|
|
146
|
+
createRunMcpServer,
|
|
147
|
+
};
|