@zokizuan/satori-mcp 3.8.0 → 4.3.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 +60 -6
- package/dist/cli/args.d.ts +45 -0
- package/dist/cli/args.js +440 -0
- package/dist/cli/client.d.ts +26 -0
- package/dist/cli/client.js +99 -0
- package/dist/cli/errors.d.ts +7 -0
- package/dist/cli/errors.js +18 -0
- package/dist/cli/format.d.ts +15 -0
- package/dist/cli/format.js +100 -0
- package/dist/cli/index.d.ts +16 -0
- package/dist/cli/index.js +283 -0
- package/dist/cli/resolve-server-entry.d.ts +2 -0
- package/dist/cli/resolve-server-entry.js +17 -0
- package/dist/core/call-graph.d.ts +6 -0
- package/dist/core/call-graph.js +34 -8
- package/dist/core/capabilities.d.ts +0 -4
- package/dist/core/capabilities.js +0 -18
- package/dist/core/handlers.d.ts +76 -50
- package/dist/core/handlers.js +2043 -651
- package/dist/core/indexing-recovery.d.ts +17 -0
- package/dist/core/indexing-recovery.js +53 -0
- package/dist/core/manage-types.d.ts +27 -0
- package/dist/core/manage-types.js +2 -0
- package/dist/core/search-constants.d.ts +16 -0
- package/dist/core/search-constants.js +15 -0
- package/dist/core/search-types.d.ts +107 -4
- package/dist/core/snapshot.d.ts +39 -4
- package/dist/core/snapshot.js +535 -51
- package/dist/core/sync.d.ts +1 -1
- package/dist/core/sync.js +4 -0
- package/dist/core/warnings.d.ts +11 -0
- package/dist/core/warnings.js +12 -0
- package/dist/index.js +60 -218
- package/dist/server/start-server.d.ts +48 -0
- package/dist/server/start-server.js +233 -0
- package/dist/server/stdio-safety.d.ts +13 -0
- package/dist/server/stdio-safety.js +124 -0
- package/dist/telemetry/search.d.ts +1 -0
- package/dist/tools/call_graph.js +1 -1
- package/dist/tools/list_codebases.js +136 -8
- package/dist/tools/manage_index.js +2 -2
- package/dist/tools/read_file.js +114 -21
- package/dist/tools/search_codebase.js +9 -3
- package/dist/utils.js +1 -1
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -6,7 +6,9 @@ MCP server for Satori — agent-safe semantic code search and indexing.
|
|
|
6
6
|
|
|
7
7
|
- Capability-driven execution via `CapabilityResolver`
|
|
8
8
|
- Runtime-first `search_codebase` with explicit `scope`, `resultMode`, `groupBy`, and optional `debug` traces
|
|
9
|
-
-
|
|
9
|
+
- Deterministic query-prefix operators in `search_codebase` (`lang:`, `path:`, `-path:`, `must:`, `exclude:`)
|
|
10
|
+
- Default grouped-result diversity and auto changed-files ranking (`rankingMode="auto_changed_first"`)
|
|
11
|
+
- First-class `call_graph` tool with deterministic node/edge sorting and capability-driven language support (currently TS/JS/Python)
|
|
10
12
|
- Sidecar-backed `file_outline` tool for per-file symbol navigation and direct call_graph jump handles
|
|
11
13
|
- Snapshot v3 safety with index fingerprints and strict `requires_reindex` access gates
|
|
12
14
|
- Deterministic train-in-the-error responses for incompatible or legacy index states
|
|
@@ -61,35 +63,36 @@ Tool surface is hard-broken to 6 tools. This keeps routing explicit while exposi
|
|
|
61
63
|
|
|
62
64
|
### `manage_index`
|
|
63
65
|
|
|
64
|
-
Manage index lifecycle operations (create/reindex/sync/status/clear) for a codebase path. Ignore-rule edits in repo-root .satoriignore/.gitignore reconcile automatically in the normal sync path
|
|
66
|
+
Manage index lifecycle operations (create/reindex/sync/status/clear) for a codebase path. Ignore-rule edits in repo-root .satoriignore/.gitignore reconcile automatically in the normal sync path. Use action="sync" for immediate convergence and action="reindex" for full rebuild recovery (preflight may block unnecessary ignore-only reindex churn unless allowUnnecessaryReindex=true).
|
|
65
67
|
|
|
66
68
|
| Parameter | Type | Required | Default | Description |
|
|
67
69
|
|---|---|---|---|---|
|
|
68
70
|
| `action` | enum("create", "reindex", "sync", "status", "clear") | yes | | Required operation to run. |
|
|
69
71
|
| `path` | string | yes | | ABSOLUTE path to the target codebase. |
|
|
70
72
|
| `force` | boolean | no | | Only for action='create'. Force rebuild from scratch. |
|
|
71
|
-
| `
|
|
73
|
+
| `allowUnnecessaryReindex` | boolean | no | | Only for action='reindex'. Override preflight block when reindex is detected as unnecessary ignore-only churn. |
|
|
72
74
|
| `customExtensions` | array<string> | no | | Only for action='create'. Additional file extensions to include. |
|
|
73
75
|
| `ignorePatterns` | array<string> | no | | Only for action='create'. Additional ignore patterns to apply. |
|
|
74
76
|
| `zillizDropCollection` | string | no | | Only for action='create'. Zilliz-only: drop this Satori-managed collection before creating the new index. |
|
|
75
77
|
|
|
76
78
|
### `search_codebase`
|
|
77
79
|
|
|
78
|
-
Unified semantic search with runtime
|
|
80
|
+
Unified semantic search with runtime-first defaults (start with scope="runtime"), grouped/raw output modes, and deterministic ranking/freshness behavior. Operators are parsed from a query prefix block: lang:, path:, -path:, must:, exclude: (escape with \\ to keep literals). Use debug:true for explainability payloads, and rely on response hints for remediation (.satoriignore noise handling, navigation fallback, reindex guidance).
|
|
79
81
|
|
|
80
82
|
| Parameter | Type | Required | Default | Description |
|
|
81
83
|
|---|---|---|---|---|
|
|
82
84
|
| `path` | string | yes | | ABSOLUTE path to an indexed codebase or subdirectory. |
|
|
83
85
|
| `query` | string | yes | | Natural-language query. |
|
|
84
|
-
| `scope` | enum("runtime", "mixed", "docs") | no | `"runtime"` | Search scope policy. runtime excludes docs/tests, docs returns docs/tests only, mixed includes all. |
|
|
86
|
+
| `scope` | enum("runtime", "mixed", "docs") | no | `"runtime"` | Search scope policy. runtime excludes docs/tests, docs returns docs/tests only, mixed includes all. Docs scope skips reranker by policy in the current tool surface. |
|
|
85
87
|
| `resultMode` | enum("grouped", "raw") | no | `"grouped"` | Output mode. grouped returns merged search groups, raw returns chunk hits. |
|
|
86
88
|
| `groupBy` | enum("symbol", "file") | no | `"symbol"` | Grouping strategy in grouped mode. |
|
|
89
|
+
| `rankingMode` | enum("default", "auto_changed_first") | no | `"auto_changed_first"` | Ranking policy. auto_changed_first boosts files changed in the current git working tree when available. |
|
|
87
90
|
| `limit` | integer | no | `50` | Maximum groups (grouped mode) or chunks (raw mode). |
|
|
88
91
|
| `debug` | boolean | no | `false` | Optional debug payload toggle for score and fusion breakdowns. |
|
|
89
92
|
|
|
90
93
|
### `call_graph`
|
|
91
94
|
|
|
92
|
-
Traverse the prebuilt
|
|
95
|
+
Traverse the prebuilt call graph sidecar for callers/callees/bidirectional symbol relationships (language support follows the core callGraphQuery capability set; currently TS/JS/Python).
|
|
93
96
|
|
|
94
97
|
| Parameter | Type | Required | Default | Description |
|
|
95
98
|
|---|---|---|---|---|
|
|
@@ -210,6 +213,57 @@ Never commit real API keys/tokens into repo config files.
|
|
|
210
213
|
pnpm --filter @zokizuan/satori-mcp start
|
|
211
214
|
```
|
|
212
215
|
|
|
216
|
+
## Shell CLI (`satori-cli`)
|
|
217
|
+
|
|
218
|
+
`@zokizuan/satori-mcp` also ships a shell-first client binary that works without an MCP adapter.
|
|
219
|
+
|
|
220
|
+
### Commands
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
satori-cli tools list
|
|
224
|
+
satori-cli tool call <toolName> --args-json '{"path":"/abs/repo","query":"auth"}'
|
|
225
|
+
satori-cli tool call <toolName> --args-file ./args.json
|
|
226
|
+
satori-cli tool call <toolName> --args-json @-
|
|
227
|
+
satori-cli <toolName> [schema-subset flags]
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Global flags (`--startup-timeout-ms`, `--call-timeout-ms`, `--format`, `--debug`) must appear before the command token.
|
|
231
|
+
Example: `satori-cli --debug tools list`.
|
|
232
|
+
|
|
233
|
+
### Output + Exit Contract
|
|
234
|
+
|
|
235
|
+
- `stdout`: JSON only
|
|
236
|
+
- `stderr`: diagnostics and text summaries
|
|
237
|
+
- exit `0`: success
|
|
238
|
+
- exit `1`: tool-level error (`isError=true` or structured envelope `status!="ok"`)
|
|
239
|
+
- exit `2`: usage/argument/schema-subset errors
|
|
240
|
+
- exit `3`: startup/transport/protocol/timeout failures
|
|
241
|
+
|
|
242
|
+
### Wrapper Flag Support
|
|
243
|
+
|
|
244
|
+
Wrapper mode (`satori-cli <toolName> ...`) supports a strict subset from reflected `tools/list` schemas:
|
|
245
|
+
|
|
246
|
+
- primitive properties (`string|number|integer|boolean`)
|
|
247
|
+
- enums of primitives
|
|
248
|
+
- arrays of primitives (repeat flags in insertion order)
|
|
249
|
+
- object properties only via `--<prop>-json '{...}'`
|
|
250
|
+
|
|
251
|
+
Tool-level flags that overlap global names are preserved in wrapper mode once command parsing starts.
|
|
252
|
+
Example: `satori-cli search_codebase --path /repo --query auth --debug` forwards `debug=true` to the tool.
|
|
253
|
+
For boolean wrapper flags, `--flag` implies `true` and `--flag false` is supported.
|
|
254
|
+
|
|
255
|
+
Unsupported schema shapes (for example `oneOf`, `anyOf`, `$ref`, complex arrays, nested expansion) return `E_SCHEMA_UNSUPPORTED` with fallback guidance to `--args-json` / `--args-file`.
|
|
256
|
+
|
|
257
|
+
### Run Mode Semantics
|
|
258
|
+
|
|
259
|
+
When spawned by `satori-cli`, server process mode is `SATORI_RUN_MODE=cli`:
|
|
260
|
+
|
|
261
|
+
- startup background loops are disabled (`verifyCloudState`, watcher mode, background sync)
|
|
262
|
+
- stdio safety hardening is enabled (`stdout` protocol-only, logs to `stderr`)
|
|
263
|
+
- tool behavior stays on-demand and uses the same six MCP tools
|
|
264
|
+
|
|
265
|
+
`SATORI_CLI_STDOUT_GUARD=drop|redirect` controls accidental non-protocol stdout handling (`drop` default).
|
|
266
|
+
|
|
213
267
|
## Development
|
|
214
268
|
|
|
215
269
|
```bash
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export interface GlobalOptions {
|
|
2
|
+
startupTimeoutMs: number;
|
|
3
|
+
callTimeoutMs: number;
|
|
4
|
+
format: "json" | "text";
|
|
5
|
+
debug: boolean;
|
|
6
|
+
}
|
|
7
|
+
export type RawArgsMode = {
|
|
8
|
+
kind: "none";
|
|
9
|
+
} | {
|
|
10
|
+
kind: "json";
|
|
11
|
+
value: string;
|
|
12
|
+
} | {
|
|
13
|
+
kind: "file";
|
|
14
|
+
path: string;
|
|
15
|
+
} | {
|
|
16
|
+
kind: "stdin-json";
|
|
17
|
+
};
|
|
18
|
+
export type ParsedCommand = {
|
|
19
|
+
kind: "help";
|
|
20
|
+
} | {
|
|
21
|
+
kind: "version";
|
|
22
|
+
} | {
|
|
23
|
+
kind: "tools-list";
|
|
24
|
+
} | {
|
|
25
|
+
kind: "tool-call";
|
|
26
|
+
toolName: string;
|
|
27
|
+
rawArgsMode: RawArgsMode;
|
|
28
|
+
} | {
|
|
29
|
+
kind: "wrapper";
|
|
30
|
+
toolName: string;
|
|
31
|
+
rawArgsMode: RawArgsMode;
|
|
32
|
+
wrapperArgs: string[];
|
|
33
|
+
};
|
|
34
|
+
export interface ParsedCliInput {
|
|
35
|
+
globals: GlobalOptions;
|
|
36
|
+
command: ParsedCommand;
|
|
37
|
+
}
|
|
38
|
+
export interface ResolveRawArgsOptions {
|
|
39
|
+
stdin?: NodeJS.ReadStream;
|
|
40
|
+
stdinTimeoutMs: number;
|
|
41
|
+
}
|
|
42
|
+
export declare function parseCliArgs(argv: string[]): ParsedCliInput;
|
|
43
|
+
export declare function resolveRawArguments(rawArgsMode: RawArgsMode, options: ResolveRawArgsOptions): Promise<Record<string, unknown>>;
|
|
44
|
+
export declare function parseWrapperArgumentsFromSchema(toolName: string, inputSchema: unknown, wrapperArgs: string[]): Record<string, unknown>;
|
|
45
|
+
//# sourceMappingURL=args.d.ts.map
|
package/dist/cli/args.js
ADDED
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { CliError } from "./errors.js";
|
|
3
|
+
const RESERVED_SUBCOMMANDS = new Set(["tools", "tool", "help", "version"]);
|
|
4
|
+
const PRIMITIVE_TYPES = new Set(["string", "number", "integer", "boolean"]);
|
|
5
|
+
function parsePositiveInteger(value, flagName) {
|
|
6
|
+
const parsed = Number.parseInt(value, 10);
|
|
7
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
8
|
+
throw new CliError("E_USAGE", `${flagName} must be a positive integer.`, 2);
|
|
9
|
+
}
|
|
10
|
+
return parsed;
|
|
11
|
+
}
|
|
12
|
+
function normalizeFlagToken(token) {
|
|
13
|
+
return token.replace(/^--/, "").replace(/-/g, "_");
|
|
14
|
+
}
|
|
15
|
+
function stripFlagPrefix(token) {
|
|
16
|
+
if (!token.startsWith("--")) {
|
|
17
|
+
throw new CliError("E_USAGE", `Expected a flag but found '${token}'.`, 2);
|
|
18
|
+
}
|
|
19
|
+
return token.slice(2);
|
|
20
|
+
}
|
|
21
|
+
function parseGlobalOptions(argv) {
|
|
22
|
+
const globals = {
|
|
23
|
+
startupTimeoutMs: 180000,
|
|
24
|
+
callTimeoutMs: 600000,
|
|
25
|
+
format: "json",
|
|
26
|
+
debug: false,
|
|
27
|
+
};
|
|
28
|
+
let i = 0;
|
|
29
|
+
while (i < argv.length) {
|
|
30
|
+
const token = argv[i];
|
|
31
|
+
switch (token) {
|
|
32
|
+
case "--startup-timeout-ms": {
|
|
33
|
+
const next = argv[i + 1];
|
|
34
|
+
if (!next) {
|
|
35
|
+
throw new CliError("E_USAGE", "Missing value for --startup-timeout-ms.", 2);
|
|
36
|
+
}
|
|
37
|
+
globals.startupTimeoutMs = parsePositiveInteger(next, "--startup-timeout-ms");
|
|
38
|
+
i += 2;
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
case "--call-timeout-ms": {
|
|
42
|
+
const next = argv[i + 1];
|
|
43
|
+
if (!next) {
|
|
44
|
+
throw new CliError("E_USAGE", "Missing value for --call-timeout-ms.", 2);
|
|
45
|
+
}
|
|
46
|
+
globals.callTimeoutMs = parsePositiveInteger(next, "--call-timeout-ms");
|
|
47
|
+
i += 2;
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
case "--format": {
|
|
51
|
+
const next = argv[i + 1];
|
|
52
|
+
if (!next || (next !== "json" && next !== "text")) {
|
|
53
|
+
throw new CliError("E_USAGE", "--format must be one of: json, text.", 2);
|
|
54
|
+
}
|
|
55
|
+
globals.format = next;
|
|
56
|
+
i += 2;
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
case "--debug": {
|
|
60
|
+
globals.debug = true;
|
|
61
|
+
i += 1;
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
default: {
|
|
65
|
+
return {
|
|
66
|
+
globals,
|
|
67
|
+
rest: argv.slice(i),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return { globals, rest: [] };
|
|
73
|
+
}
|
|
74
|
+
function parseRawArgsMode(args) {
|
|
75
|
+
let rawArgsMode = { kind: "none" };
|
|
76
|
+
const remaining = [];
|
|
77
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
78
|
+
const token = args[i];
|
|
79
|
+
if (token === "--args-json") {
|
|
80
|
+
if (rawArgsMode.kind !== "none") {
|
|
81
|
+
throw new CliError("E_USAGE", "Use only one of --args-json or --args-file.", 2);
|
|
82
|
+
}
|
|
83
|
+
const next = args[i + 1];
|
|
84
|
+
if (!next) {
|
|
85
|
+
throw new CliError("E_USAGE", "Missing value for --args-json.", 2);
|
|
86
|
+
}
|
|
87
|
+
rawArgsMode = next === "@-"
|
|
88
|
+
? { kind: "stdin-json" }
|
|
89
|
+
: { kind: "json", value: next };
|
|
90
|
+
i += 1;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (token === "--args-file") {
|
|
94
|
+
if (rawArgsMode.kind !== "none") {
|
|
95
|
+
throw new CliError("E_USAGE", "Use only one of --args-json or --args-file.", 2);
|
|
96
|
+
}
|
|
97
|
+
const next = args[i + 1];
|
|
98
|
+
if (!next) {
|
|
99
|
+
throw new CliError("E_USAGE", "Missing value for --args-file.", 2);
|
|
100
|
+
}
|
|
101
|
+
rawArgsMode = { kind: "file", path: next };
|
|
102
|
+
i += 1;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
remaining.push(token);
|
|
106
|
+
}
|
|
107
|
+
if (rawArgsMode.kind !== "none" && remaining.length > 0) {
|
|
108
|
+
throw new CliError("E_USAGE", "Tool argument flags cannot be combined with --args-json/--args-file.", 2);
|
|
109
|
+
}
|
|
110
|
+
return { rawArgsMode, remaining };
|
|
111
|
+
}
|
|
112
|
+
export function parseCliArgs(argv) {
|
|
113
|
+
const { globals, rest } = parseGlobalOptions(argv);
|
|
114
|
+
if (rest.length === 0 || rest[0] === "help" || rest.includes("--help") || rest.includes("-h")) {
|
|
115
|
+
return {
|
|
116
|
+
globals,
|
|
117
|
+
command: { kind: "help" }
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
if (rest[0] === "version" || rest.includes("--version") || rest.includes("-v")) {
|
|
121
|
+
return {
|
|
122
|
+
globals,
|
|
123
|
+
command: { kind: "version" }
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
if (rest[0] === "tools") {
|
|
127
|
+
if (rest.length === 2 && rest[1] === "list") {
|
|
128
|
+
return {
|
|
129
|
+
globals,
|
|
130
|
+
command: { kind: "tools-list" }
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
throw new CliError("E_USAGE", "Unsupported tools subcommand. Use: tools list", 2);
|
|
134
|
+
}
|
|
135
|
+
if (rest[0] === "tool") {
|
|
136
|
+
if (rest[1] !== "call") {
|
|
137
|
+
throw new CliError("E_USAGE", "Unsupported tool subcommand. Use: tool call <toolName>", 2);
|
|
138
|
+
}
|
|
139
|
+
const toolName = rest[2];
|
|
140
|
+
if (!toolName) {
|
|
141
|
+
throw new CliError("E_USAGE", "Missing tool name. Use: tool call <toolName>", 2);
|
|
142
|
+
}
|
|
143
|
+
const { rawArgsMode, remaining } = parseRawArgsMode(rest.slice(3));
|
|
144
|
+
if (remaining.length > 0) {
|
|
145
|
+
throw new CliError("E_USAGE", `Unknown arguments for tool call: ${remaining.join(" ")}`, 2);
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
globals,
|
|
149
|
+
command: {
|
|
150
|
+
kind: "tool-call",
|
|
151
|
+
toolName,
|
|
152
|
+
rawArgsMode
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
if (RESERVED_SUBCOMMANDS.has(rest[0])) {
|
|
157
|
+
throw new CliError("E_USAGE", `Unsupported command '${rest[0]}'.`, 2);
|
|
158
|
+
}
|
|
159
|
+
const toolName = rest[0];
|
|
160
|
+
const { rawArgsMode, remaining } = parseRawArgsMode(rest.slice(1));
|
|
161
|
+
return {
|
|
162
|
+
globals,
|
|
163
|
+
command: {
|
|
164
|
+
kind: "wrapper",
|
|
165
|
+
toolName,
|
|
166
|
+
rawArgsMode,
|
|
167
|
+
wrapperArgs: remaining
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function readStdin(stdin, timeoutMs) {
|
|
172
|
+
return new Promise((resolve, reject) => {
|
|
173
|
+
const chunks = [];
|
|
174
|
+
let settled = false;
|
|
175
|
+
const timeout = setTimeout(() => {
|
|
176
|
+
if (settled) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
settled = true;
|
|
180
|
+
reject(new CliError("E_USAGE", "Timed out while reading stdin JSON for --args-json @-.", 2));
|
|
181
|
+
}, timeoutMs);
|
|
182
|
+
timeout.unref();
|
|
183
|
+
stdin.setEncoding("utf8");
|
|
184
|
+
stdin.on("data", (chunk) => {
|
|
185
|
+
chunks.push(String(chunk));
|
|
186
|
+
});
|
|
187
|
+
stdin.on("error", (error) => {
|
|
188
|
+
if (settled) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
settled = true;
|
|
192
|
+
clearTimeout(timeout);
|
|
193
|
+
reject(new CliError("E_USAGE", `Failed to read stdin: ${error.message}`, 2));
|
|
194
|
+
});
|
|
195
|
+
stdin.on("end", () => {
|
|
196
|
+
if (settled) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
settled = true;
|
|
200
|
+
clearTimeout(timeout);
|
|
201
|
+
resolve(chunks.join(""));
|
|
202
|
+
});
|
|
203
|
+
stdin.resume();
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
function parseJsonObject(value, source) {
|
|
207
|
+
let parsed;
|
|
208
|
+
try {
|
|
209
|
+
parsed = JSON.parse(value);
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
213
|
+
throw new CliError("E_USAGE", `Invalid JSON from ${source}: ${message}`, 2);
|
|
214
|
+
}
|
|
215
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
216
|
+
throw new CliError("E_USAGE", `JSON from ${source} must be an object.`, 2);
|
|
217
|
+
}
|
|
218
|
+
return parsed;
|
|
219
|
+
}
|
|
220
|
+
export async function resolveRawArguments(rawArgsMode, options) {
|
|
221
|
+
switch (rawArgsMode.kind) {
|
|
222
|
+
case "none":
|
|
223
|
+
return {};
|
|
224
|
+
case "json":
|
|
225
|
+
return parseJsonObject(rawArgsMode.value, "--args-json");
|
|
226
|
+
case "file": {
|
|
227
|
+
if (!fs.existsSync(rawArgsMode.path)) {
|
|
228
|
+
throw new CliError("E_USAGE", `Arguments file not found: ${rawArgsMode.path}`, 2);
|
|
229
|
+
}
|
|
230
|
+
const content = fs.readFileSync(rawArgsMode.path, "utf8");
|
|
231
|
+
return parseJsonObject(content, "--args-file");
|
|
232
|
+
}
|
|
233
|
+
case "stdin-json": {
|
|
234
|
+
const text = await readStdin(options.stdin || process.stdin, options.stdinTimeoutMs);
|
|
235
|
+
if (text.trim().length === 0) {
|
|
236
|
+
throw new CliError("E_USAGE", "stdin was empty for --args-json @-.", 2);
|
|
237
|
+
}
|
|
238
|
+
return parseJsonObject(text, "--args-json @-");
|
|
239
|
+
}
|
|
240
|
+
default:
|
|
241
|
+
return {};
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
function isPrimitiveEnum(enumValues) {
|
|
245
|
+
return enumValues.every((value) => {
|
|
246
|
+
const valueType = typeof value;
|
|
247
|
+
return valueType === "string" || valueType === "number" || valueType === "boolean";
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
function unsupportedSchemaReason(schema) {
|
|
251
|
+
if (!schema || typeof schema !== "object") {
|
|
252
|
+
return "schema is not an object";
|
|
253
|
+
}
|
|
254
|
+
if ("oneOf" in schema) {
|
|
255
|
+
return "oneOf is not supported in wrapper mode";
|
|
256
|
+
}
|
|
257
|
+
if ("anyOf" in schema) {
|
|
258
|
+
return "anyOf is not supported in wrapper mode";
|
|
259
|
+
}
|
|
260
|
+
if ("allOf" in schema) {
|
|
261
|
+
return "allOf is not supported in wrapper mode";
|
|
262
|
+
}
|
|
263
|
+
if ("$ref" in schema) {
|
|
264
|
+
return "$ref is not supported in wrapper mode";
|
|
265
|
+
}
|
|
266
|
+
if ("patternProperties" in schema) {
|
|
267
|
+
return "patternProperties is not supported in wrapper mode";
|
|
268
|
+
}
|
|
269
|
+
if (Array.isArray(schema.enum) && !isPrimitiveEnum(schema.enum)) {
|
|
270
|
+
return "enum values must be primitive";
|
|
271
|
+
}
|
|
272
|
+
if (schema.type === "array") {
|
|
273
|
+
const itemSchema = schema.items;
|
|
274
|
+
if (!itemSchema || typeof itemSchema !== "object") {
|
|
275
|
+
return "array items schema is missing";
|
|
276
|
+
}
|
|
277
|
+
const itemReason = unsupportedSchemaReason(itemSchema);
|
|
278
|
+
if (itemReason) {
|
|
279
|
+
return `array item schema unsupported: ${itemReason}`;
|
|
280
|
+
}
|
|
281
|
+
if (typeof itemSchema.type === "string" && !PRIMITIVE_TYPES.has(itemSchema.type) && !Array.isArray(itemSchema.enum)) {
|
|
282
|
+
return "array item type must be primitive";
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if (typeof schema.type === "string" && schema.type !== "object" && !PRIMITIVE_TYPES.has(schema.type) && !Array.isArray(schema.enum)) {
|
|
286
|
+
return `schema type '${schema.type}' is not supported`;
|
|
287
|
+
}
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
function parseBooleanValue(value) {
|
|
291
|
+
if (value === "true") {
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
294
|
+
if (value === "false") {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
throw new CliError("E_USAGE", `Invalid boolean value '${value}'. Use true or false.`, 2);
|
|
298
|
+
}
|
|
299
|
+
function parseEnumValue(raw, enumValues) {
|
|
300
|
+
for (const entry of enumValues) {
|
|
301
|
+
if (typeof entry === "string" && entry === raw) {
|
|
302
|
+
return entry;
|
|
303
|
+
}
|
|
304
|
+
if (typeof entry === "number" && Number(raw) === entry) {
|
|
305
|
+
return entry;
|
|
306
|
+
}
|
|
307
|
+
if (typeof entry === "boolean") {
|
|
308
|
+
if (raw === "true" && entry === true) {
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
311
|
+
if (raw === "false" && entry === false) {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
throw new CliError("E_USAGE", `Value '${raw}' is not in enum [${enumValues.map(String).join(", ")}].`, 2);
|
|
317
|
+
}
|
|
318
|
+
function parsePrimitive(schema, raw) {
|
|
319
|
+
if (Array.isArray(schema.enum)) {
|
|
320
|
+
return parseEnumValue(raw, schema.enum);
|
|
321
|
+
}
|
|
322
|
+
switch (schema.type) {
|
|
323
|
+
case "string":
|
|
324
|
+
return raw;
|
|
325
|
+
case "number": {
|
|
326
|
+
const parsed = Number(raw);
|
|
327
|
+
if (!Number.isFinite(parsed)) {
|
|
328
|
+
throw new CliError("E_USAGE", `Value '${raw}' must be a finite number.`, 2);
|
|
329
|
+
}
|
|
330
|
+
return parsed;
|
|
331
|
+
}
|
|
332
|
+
case "integer": {
|
|
333
|
+
const parsed = Number(raw);
|
|
334
|
+
if (!Number.isInteger(parsed)) {
|
|
335
|
+
throw new CliError("E_USAGE", `Value '${raw}' must be an integer.`, 2);
|
|
336
|
+
}
|
|
337
|
+
return parsed;
|
|
338
|
+
}
|
|
339
|
+
case "boolean":
|
|
340
|
+
return parseBooleanValue(raw);
|
|
341
|
+
default:
|
|
342
|
+
throw new CliError("E_SCHEMA_UNSUPPORTED", `Primitive parsing unsupported for schema type '${String(schema.type)}'. Use --args-json/--args-file.`, 2);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
export function parseWrapperArgumentsFromSchema(toolName, inputSchema, wrapperArgs) {
|
|
346
|
+
if (!inputSchema || typeof inputSchema !== "object") {
|
|
347
|
+
throw new CliError("E_SCHEMA_UNSUPPORTED", `${toolName} schema is missing or invalid. Use --args-json/--args-file.`, 2);
|
|
348
|
+
}
|
|
349
|
+
const schema = inputSchema;
|
|
350
|
+
const rootReason = unsupportedSchemaReason(schema);
|
|
351
|
+
if (rootReason) {
|
|
352
|
+
throw new CliError("E_SCHEMA_UNSUPPORTED", `${toolName} schema unsupported (${rootReason}). Use --args-json/--args-file.`, 2);
|
|
353
|
+
}
|
|
354
|
+
const properties = schema.properties;
|
|
355
|
+
if (!properties || typeof properties !== "object") {
|
|
356
|
+
throw new CliError("E_SCHEMA_UNSUPPORTED", `${toolName} schema has no object properties. Use --args-json/--args-file.`, 2);
|
|
357
|
+
}
|
|
358
|
+
const requiredProps = Array.isArray(schema.required)
|
|
359
|
+
? schema.required.filter((entry) => typeof entry === "string")
|
|
360
|
+
: [];
|
|
361
|
+
const normalizedToCanonical = new Map();
|
|
362
|
+
const propertySchemas = new Map();
|
|
363
|
+
for (const [propertyName, propertySchema] of Object.entries(properties)) {
|
|
364
|
+
const unsupportedReason = unsupportedSchemaReason(propertySchema);
|
|
365
|
+
if (unsupportedReason) {
|
|
366
|
+
throw new CliError("E_SCHEMA_UNSUPPORTED", `${toolName}.${propertyName} uses unsupported schema (${unsupportedReason}). Use --args-json/--args-file.`, 2);
|
|
367
|
+
}
|
|
368
|
+
const normalized = normalizeFlagToken(`--${propertyName}`);
|
|
369
|
+
normalizedToCanonical.set(normalized, propertyName);
|
|
370
|
+
propertySchemas.set(propertyName, propertySchema);
|
|
371
|
+
}
|
|
372
|
+
const parsed = {};
|
|
373
|
+
for (let i = 0; i < wrapperArgs.length; i += 1) {
|
|
374
|
+
const token = wrapperArgs[i];
|
|
375
|
+
if (!token.startsWith("--")) {
|
|
376
|
+
throw new CliError("E_USAGE", `Unexpected positional argument '${token}'.`, 2);
|
|
377
|
+
}
|
|
378
|
+
const normalizedFlag = normalizeFlagToken(token);
|
|
379
|
+
const isJsonFlag = normalizedFlag.endsWith("_json");
|
|
380
|
+
const baseNormalized = isJsonFlag ? normalizedFlag.slice(0, -5) : normalizedFlag;
|
|
381
|
+
const canonicalName = normalizedToCanonical.get(baseNormalized);
|
|
382
|
+
if (!canonicalName) {
|
|
383
|
+
throw new CliError("E_USAGE", `Unknown flag '${token}' for tool '${toolName}'.`, 2);
|
|
384
|
+
}
|
|
385
|
+
const propertySchema = propertySchemas.get(canonicalName);
|
|
386
|
+
if (isJsonFlag) {
|
|
387
|
+
const next = wrapperArgs[i + 1];
|
|
388
|
+
if (!next) {
|
|
389
|
+
throw new CliError("E_USAGE", `Missing JSON value for ${token}.`, 2);
|
|
390
|
+
}
|
|
391
|
+
parsed[canonicalName] = parseJsonObject(next, token);
|
|
392
|
+
i += 1;
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
if (propertySchema?.type === "object") {
|
|
396
|
+
throw new CliError("E_USAGE", `Flag '${token}' requires JSON input. Use --${stripFlagPrefix(token)}-json '<json>'.`, 2);
|
|
397
|
+
}
|
|
398
|
+
if (propertySchema?.type === "array") {
|
|
399
|
+
const next = wrapperArgs[i + 1];
|
|
400
|
+
if (!next || next.startsWith("--")) {
|
|
401
|
+
throw new CliError("E_USAGE", `Missing value for ${token}.`, 2);
|
|
402
|
+
}
|
|
403
|
+
const itemSchema = propertySchema.items;
|
|
404
|
+
const parsedValue = parsePrimitive(itemSchema, next);
|
|
405
|
+
const existing = parsed[canonicalName];
|
|
406
|
+
if (!Array.isArray(existing)) {
|
|
407
|
+
parsed[canonicalName] = [parsedValue];
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
existing.push(parsedValue);
|
|
411
|
+
}
|
|
412
|
+
i += 1;
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
if (propertySchema?.type === "boolean") {
|
|
416
|
+
const next = wrapperArgs[i + 1];
|
|
417
|
+
if (!next || next.startsWith("--")) {
|
|
418
|
+
parsed[canonicalName] = true;
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
parsed[canonicalName] = parseBooleanValue(next);
|
|
422
|
+
i += 1;
|
|
423
|
+
}
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
const next = wrapperArgs[i + 1];
|
|
427
|
+
if (!next || next.startsWith("--")) {
|
|
428
|
+
throw new CliError("E_USAGE", `Missing value for ${token}.`, 2);
|
|
429
|
+
}
|
|
430
|
+
parsed[canonicalName] = parsePrimitive(propertySchema, next);
|
|
431
|
+
i += 1;
|
|
432
|
+
}
|
|
433
|
+
for (const requiredProp of requiredProps) {
|
|
434
|
+
if (!(requiredProp in parsed)) {
|
|
435
|
+
throw new CliError("E_USAGE", `Missing required flag for '${toolName}': --${requiredProp}.`, 2);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return parsed;
|
|
439
|
+
}
|
|
440
|
+
//# sourceMappingURL=args.js.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
3
|
+
interface SessionOptions {
|
|
4
|
+
command: string;
|
|
5
|
+
args: string[];
|
|
6
|
+
env: Record<string, string | undefined>;
|
|
7
|
+
cwd?: string;
|
|
8
|
+
startupTimeoutMs: number;
|
|
9
|
+
callTimeoutMs: number;
|
|
10
|
+
writeStderr: (text: string) => void;
|
|
11
|
+
}
|
|
12
|
+
export declare class CliMcpSession {
|
|
13
|
+
private readonly client;
|
|
14
|
+
private readonly transport;
|
|
15
|
+
private readonly callTimeoutMs;
|
|
16
|
+
private readonly writeStderr;
|
|
17
|
+
constructor(client: Client, transport: StdioClientTransport, callTimeoutMs: number, writeStderr: (text: string) => void);
|
|
18
|
+
listTools(): Promise<any>;
|
|
19
|
+
callTool(name: string, args: Record<string, unknown>): Promise<any>;
|
|
20
|
+
close(): Promise<void>;
|
|
21
|
+
logProtocolFailure(error: unknown): never;
|
|
22
|
+
wireStderr(): void;
|
|
23
|
+
}
|
|
24
|
+
export declare function connectCliMcpSession(options: SessionOptions): Promise<CliMcpSession>;
|
|
25
|
+
export {};
|
|
26
|
+
//# sourceMappingURL=client.d.ts.map
|