@tmcp/transport-cli 0.0.1 → 0.0.3

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @tmcp/transport-cli
2
2
 
3
- A CLI transport for TMCP that turns your MCP server's tools into command-line commands. Each registered tool becomes a subcommand with flags derived from its JSON Schema input, powered by [yargs](https://yargs.js.org/).
3
+ A CLI transport for TMCP that exposes your MCP tools as static, JSON-first commands. It is designed for agent-driven usage: tools are invoked with JSON input, schemas can be inspected directly, and output can be narrowed before it leaves stdout.
4
4
 
5
5
  ## Installation
6
6
 
@@ -33,16 +33,24 @@ server.tool(
33
33
  name: 'greet',
34
34
  description: 'Greet someone by name',
35
35
  schema: z.object({
36
- name: z.string().describe('Name of the person to greet'),
37
- loud: z.boolean().optional().describe('Shout the greeting'),
36
+ name: z.string(),
37
+ loud: z.boolean().optional(),
38
+ }),
39
+ outputSchema: z.object({
40
+ message: z.string(),
38
41
  }),
39
42
  },
40
43
  async (input) => {
41
- const text = `Hello, ${input.name}!`;
44
+ const message = `Hello, ${input.name}!`;
45
+
42
46
  return {
43
47
  content: [
44
- { type: 'text', text: input.loud ? text.toUpperCase() : text },
48
+ {
49
+ type: 'text',
50
+ text: input.loud ? message.toUpperCase() : message,
51
+ },
45
52
  ],
53
+ structuredContent: { message },
46
54
  };
47
55
  },
48
56
  );
@@ -51,44 +59,84 @@ const cli = new CliTransport(server);
51
59
  await cli.run(undefined, process.argv.slice(2));
52
60
  ```
53
61
 
54
- Running the above:
62
+ ## Commands
63
+
64
+ ### `tools`
65
+
66
+ Prints the available tools as pretty JSON.
55
67
 
56
68
  ```bash
57
- node my-cli.js greet --name Alice
58
- # {"content":[{"type":"text","text":"Hello, Alice!"}]}
69
+ node my-cli.js tools
70
+ ```
59
71
 
60
- node my-cli.js greet --name Alice --loud
61
- # {"content":[{"type":"text","text":"HELLO, ALICE!"}]}
72
+ ### `schema <tool>`
73
+
74
+ Prints the tool metadata plus its input and output schemas.
75
+
76
+ ```bash
77
+ node my-cli.js schema greet
62
78
  ```
63
79
 
64
- Output is pretty-printed JSON written to stdout. Errors go to stderr and set `process.exitCode` to 1.
80
+ ### `call <tool> [input]`
81
+
82
+ Calls a tool with a JSON object. The first positional argument is the primary input source.
83
+
84
+ ```bash
85
+ node my-cli.js call greet '{"name":"Alice"}'
86
+ ```
87
+
88
+ ### `<tool> [input]`
89
+
90
+ Each tool name is also registered directly as an alias for `call <tool>`, unless the tool name would collide with a reserved command (`tools`, `schema`, or `call`).
91
+
92
+ ```bash
93
+ node my-cli.js greet '{"name":"Alice","loud":true}'
94
+ ```
65
95
 
66
- The help output uses the server's `name` (from the `McpServer` config) as the CLI program name. In the example above, `--help` would show `my-cli` as the command name.
96
+ ## Input sources
67
97
 
68
- ## Argument types
98
+ Tool calls accept exactly one input source:
69
99
 
70
- The transport maps JSON Schema types from your tool's input schema to yargs option types:
100
+ - Positional JSON: `greet '{"name":"Alice"}'`
71
101
 
72
- | JSON Schema type | yargs type | Example |
73
- | ---------------- | ---------- | ---------------------------- |
74
- | `string` | `string` | `--name Alice` |
75
- | `number` | `number` | `--count 5` |
76
- | `integer` | `number` | `--port 8080` |
77
- | `boolean` | `boolean` | `--verbose` / `--no-verbose` |
78
- | `array` | `array` | `--items foo --items bar` |
102
+ All inputs must parse to a JSON object. If no input is provided, the transport sends `{}`.
79
103
 
80
- Properties marked as required in the schema are enforced by yargs (`demandOption`). Optional properties can be omitted. Enum values (e.g. from `z.enum()` or `v.picklist()`) are passed through as `choices`.
104
+ ## Output controls
105
+
106
+ Tool calls support two static output flags:
107
+
108
+ - `--output full|structured|content|text`
109
+ - `--fields path1,path2`
110
+
111
+ Examples:
112
+
113
+ ```bash
114
+ node my-cli.js greet '{"name":"Alice"}' --output text
115
+
116
+ node my-cli.js get-user '{"id":"1"}' --output structured --fields user.name,user.email
117
+ ```
118
+
119
+ `--fields` uses comma-separated dot paths and is applied after the output mode is selected. It is not available with `--output text`.
120
+
121
+ ## Behavior
122
+
123
+ - Output is written to stdout.
124
+ - JSON outputs are pretty-printed.
125
+ - Errors are written to stderr and set `process.exitCode` to `1`.
126
+ - Running the CLI without a command prints help output.
127
+ - The CLI initializes an MCP session, sends `notifications/initialized`, and paginates through `tools/list` automatically.
128
+ - The program name shown in help output comes from `McpServer`'s `name`.
81
129
 
82
130
  ## Custom context
83
131
 
84
- If your server uses custom context, you can pass it as the first argument to `run()`:
132
+ If your server uses custom context, pass it as the first argument to `run()`:
85
133
 
86
134
  ```javascript
87
135
  const cli = new CliTransport(server);
88
136
  await cli.run({ userId: 'cli-user' }, process.argv.slice(2));
89
137
  ```
90
138
 
91
- The context is forwarded to the server on every request, so your tool handlers can read it from `server.ctx.custom`.
139
+ The context is forwarded on every request, so handlers can read it from `server.ctx.custom`.
92
140
 
93
141
  ## API
94
142
 
@@ -100,24 +148,13 @@ The context is forwarded to the server on every request, so your tool handlers c
100
148
  new CliTransport(server: McpServer)
101
149
  ```
102
150
 
103
- Creates a new CLI transport. The `server` parameter is the TMCP server instance whose tools will be exposed as commands.
104
-
105
151
  #### Methods
106
152
 
107
- ##### `run(ctx?: TCustom, argv?: string[]): Promise<void>`
108
-
109
- Initializes an MCP session, fetches the tool list from the server, builds yargs commands from the tool definitions, and parses the given argv (or `process.argv.slice(2)` if omitted).
110
-
111
- - `ctx` - Optional custom context passed to the server for this invocation.
112
- - `argv` - Optional array of CLI arguments. Defaults to `process.argv.slice(2)`.
113
-
114
- ## How it works
153
+ ```typescript
154
+ run(ctx?: TCustom, argv?: string[]): Promise<void>
155
+ ```
115
156
 
116
- 1. The transport sends an `initialize` JSON-RPC request to the server to start a session.
117
- 2. It calls `tools/list` to get all registered tools and their input schemas.
118
- 3. For each tool, it registers a yargs command using the tool name and converts the tool's `inputSchema` properties into yargs options.
119
- 4. When the user invokes a command, the parsed arguments are coerced back to their schema types and sent as a `tools/call` request.
120
- 5. The result is written to stdout as pretty-printed JSON.
157
+ Starts the CLI, initializes a session, discovers tools, and executes the requested static command or tool alias.
121
158
 
122
159
  ## Related Packages
123
160
 
package/package.json CHANGED
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "name": "@tmcp/transport-cli",
3
- "version": "0.0.1",
4
- "description": "CLI transport for TMCP - exposes MCP tools as CLI commands via yargs",
3
+ "version": "0.0.3",
4
+ "description": "CLI transport for TMCP with static JSON-first tool commands",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
7
7
  "types": "src/types/index.d.ts",
8
+ "engines": {
9
+ "node": ">=18.0.0"
10
+ },
8
11
  "files": [
9
12
  "src"
10
13
  ],
@@ -19,13 +22,14 @@
19
22
  "tmcp": "^1.16.3"
20
23
  },
21
24
  "dependencies": {
22
- "yargs": "^17.7.2"
25
+ "sade": "^1.8.1"
23
26
  },
24
27
  "keywords": [
25
28
  "tmcp",
26
29
  "cli",
27
30
  "transport",
28
- "yargs"
31
+ "mcp",
32
+ "json"
29
33
  ],
30
34
  "repository": {
31
35
  "type": "git",
@@ -34,7 +38,6 @@
34
38
  },
35
39
  "devDependencies": {
36
40
  "@types/node": "^24.0.13",
37
- "@types/yargs": "^17.0.33",
38
41
  "dts-buddy": "^0.6.2",
39
42
  "publint": "^0.3.12",
40
43
  "valibot": "^1.1.0",
package/src/index.js CHANGED
@@ -1,107 +1,340 @@
1
1
  /**
2
2
  * @import { McpServer } from "tmcp";
3
- * @import { Options } from "yargs";
4
- * @import { InputSchema, Tool } from "./internal.js";
3
+ * @import { ListToolsResult, Tool } from "./internal.js";
5
4
  */
6
5
  import process from 'node:process';
6
+ import { randomUUID } from 'node:crypto';
7
7
  import { AsyncLocalStorage } from 'node:async_hooks';
8
- import Yargs from 'yargs/yargs';
8
+ import sade from 'sade';
9
9
 
10
10
  /**
11
- * Maps a JSON Schema type string to a yargs option type.
12
- * @param {string | undefined} json_schema_type
13
- * @returns {Options["type"]}
11
+ * @typedef {'full' | 'structured' | 'content' | 'text'} OutputMode
14
12
  */
15
- function json_schema_type_to_yargs(json_schema_type) {
16
- switch (json_schema_type) {
17
- case 'string':
18
- return 'string';
19
- case 'number':
20
- case 'integer':
21
- return 'number';
22
- case 'boolean':
23
- return 'boolean';
24
- case 'array':
25
- return 'array';
26
- default:
27
- return 'string';
13
+
14
+ /**
15
+ * @typedef {{ output?: OutputMode; fields?: string }} ToolOptions
16
+ */
17
+
18
+ const CLIENT_INFO = {
19
+ name: 'tmcp-cli',
20
+ version: '0.0.1',
21
+ };
22
+
23
+ const RESERVED_COMMANDS = new Set(['call', 'schema', 'tools']);
24
+ const UNSAFE_ALIAS_NAME = /[<>[\]\s]/;
25
+
26
+ /**
27
+ * @param {unknown} value
28
+ * @returns {value is Record<string, unknown>}
29
+ */
30
+ function is_record(value) {
31
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
32
+ }
33
+
34
+ /**
35
+ * @param {unknown} value
36
+ * @param {string} source
37
+ * @returns {Record<string, unknown>}
38
+ */
39
+ function parse_input_json(value, source) {
40
+ let parsed;
41
+
42
+ try {
43
+ parsed = JSON.parse(String(value));
44
+ } catch {
45
+ throw new Error(`Invalid JSON in ${source}`);
46
+ }
47
+
48
+ if (!is_record(parsed)) {
49
+ throw new Error(`Input from ${source} must be a JSON object`);
28
50
  }
51
+
52
+ return parsed;
29
53
  }
30
54
 
31
55
  /**
32
- * Converts a JSON Schema inputSchema into a yargs options object.
33
- * @param {InputSchema} input_schema
34
- * @returns {Record<string, Options>}
56
+ * @param {Array<unknown>} args
57
+ * @returns {{ positionals: Array<unknown>; options: Record<string, unknown> }}
35
58
  */
36
- function json_schema_to_yargs_options(input_schema) {
37
- /** @type {Record<string, Options>} */
38
- const options = {};
59
+ function extract_command_args(args) {
60
+ const last_arg = args.at(-1);
61
+
62
+ if (is_record(last_arg)) {
63
+ return {
64
+ positionals: args.slice(0, -1),
65
+ options: last_arg,
66
+ };
67
+ }
39
68
 
40
- const properties = input_schema.properties ?? {};
41
- const required = new Set(input_schema.required ?? []);
69
+ return {
70
+ positionals: args,
71
+ options: {},
72
+ };
73
+ }
74
+
75
+ /**
76
+ * @param {Record<string, unknown>} options
77
+ * @returns {ToolOptions}
78
+ */
79
+ function normalize_tool_options(options) {
80
+ return {
81
+ output:
82
+ typeof options.output === 'string'
83
+ ? /** @type {OutputMode} */ (options.output)
84
+ : 'full',
85
+ fields: typeof options.fields === 'string' ? options.fields : undefined,
86
+ };
87
+ }
88
+
89
+ /**
90
+ * @param {string | undefined} input
91
+ * @returns {Promise<Record<string, unknown>>}
92
+ */
93
+ async function resolve_tool_input(input) {
94
+ if (typeof input === 'string') {
95
+ return parse_input_json(input, 'positional input');
96
+ }
97
+
98
+ return {};
99
+ }
100
+
101
+ /**
102
+ * @param {unknown} value
103
+ * @returns {string}
104
+ */
105
+ function format_json(value) {
106
+ return `${JSON.stringify(value === undefined ? null : value, null, 2)}\n`;
107
+ }
108
+
109
+ /**
110
+ * @param {string | undefined} fields
111
+ * @returns {Array<string>}
112
+ */
113
+ function parse_fields(fields) {
114
+ if (!fields) return [];
115
+
116
+ const paths = fields
117
+ .split(',')
118
+ .map((field) => field.trim())
119
+ .filter(Boolean);
120
+
121
+ if (paths.length === 0) {
122
+ throw new Error('`--fields` must include at least one dot path');
123
+ }
124
+
125
+ return paths;
126
+ }
42
127
 
43
- for (const [name, schema] of Object.entries(properties)) {
44
- /** @type {Options} */
45
- const option = {};
128
+ /**
129
+ * @param {string} path
130
+ * @returns {Array<string>}
131
+ */
132
+ function split_path(path) {
133
+ return path.split('.').filter(Boolean);
134
+ }
46
135
 
47
- option.type = json_schema_type_to_yargs(schema.type);
48
- option.demandOption = required.has(name);
136
+ /**
137
+ * @param {unknown} value
138
+ * @param {Array<string>} path
139
+ * @returns {unknown}
140
+ */
141
+ function get_path_value(value, path) {
142
+ let current = value;
143
+
144
+ for (const segment of path) {
145
+ if (Array.isArray(current)) {
146
+ const index = Number(segment);
147
+ if (
148
+ !Number.isInteger(index) ||
149
+ index < 0 ||
150
+ index >= current.length
151
+ ) {
152
+ throw new Error(`Unknown field path: ${path.join('.')}`);
153
+ }
154
+ current = current[index];
155
+ continue;
156
+ }
49
157
 
50
- if (schema.description) {
51
- option.describe = schema.description;
158
+ if (!is_record(current) || !(segment in current)) {
159
+ throw new Error(`Unknown field path: ${path.join('.')}`);
52
160
  }
53
161
 
54
- if (schema.enum) {
55
- option.choices =
56
- /** @type {Array<string | number | true | undefined>} */ (
57
- schema.enum
162
+ current = current[segment];
163
+ }
164
+
165
+ return current;
166
+ }
167
+
168
+ /**
169
+ * @param {Record<string, unknown> | Array<unknown>} target
170
+ * @param {Array<string>} path
171
+ * @param {unknown} value
172
+ */
173
+ function set_path_value(target, path, value) {
174
+ let current = target;
175
+
176
+ for (let index = 0; index < path.length; index += 1) {
177
+ const segment = path[index];
178
+ const is_last = index === path.length - 1;
179
+ const next_segment = path[index + 1];
180
+ const next_value =
181
+ next_segment !== undefined && Number.isInteger(Number(next_segment))
182
+ ? []
183
+ : {};
184
+
185
+ if (Array.isArray(current)) {
186
+ const array_index = Number(segment);
187
+ if (!Number.isInteger(array_index) || array_index < 0) {
188
+ throw new Error(
189
+ `Invalid array index in field path: ${path.join('.')}`,
58
190
  );
191
+ }
192
+
193
+ if (is_last) {
194
+ current[array_index] = value;
195
+ return;
196
+ }
197
+
198
+ current[array_index] ??= next_value;
199
+ current = /** @type {Record<string, unknown> | Array<unknown>} */ (
200
+ current[array_index]
201
+ );
202
+ continue;
59
203
  }
60
204
 
61
- if (schema.default !== undefined) {
62
- option.default = schema.default;
205
+ if (is_last) {
206
+ current[segment] = value;
207
+ return;
63
208
  }
64
209
 
65
- options[name] = option;
210
+ current[segment] ??= next_value;
211
+ current = /** @type {Record<string, unknown> | Array<unknown>} */ (
212
+ current[segment]
213
+ );
66
214
  }
215
+ }
67
216
 
68
- return options;
217
+ /**
218
+ * @param {unknown} value
219
+ * @param {Array<string>} paths
220
+ * @returns {unknown}
221
+ */
222
+ function filter_fields(value, paths) {
223
+ if (paths.length === 0) return value;
224
+
225
+ if (!is_record(value) && !Array.isArray(value)) {
226
+ throw new Error(
227
+ '`--fields` can only be used with object or array output',
228
+ );
229
+ }
230
+
231
+ const result = Array.isArray(value) ? [] : {};
232
+
233
+ for (const path of paths) {
234
+ const segments = split_path(path);
235
+ if (segments.length === 0) {
236
+ throw new Error('`--fields` only supports dot-separated paths');
237
+ }
238
+ set_path_value(
239
+ result,
240
+ segments,
241
+ structuredClone(get_path_value(value, segments)),
242
+ );
243
+ }
244
+
245
+ return result;
69
246
  }
70
247
 
71
248
  /**
72
- * Coerces parsed yargs arguments back to their JSON Schema types.
73
- * Yargs parses everything from argv as strings by default for some types,
74
- * so we need to coerce values based on the schema.
75
- * @param {Record<string, unknown>} args
76
- * @param {InputSchema} input_schema
77
- * @returns {Record<string, unknown>}
249
+ * @param {Record<string, unknown>} result
250
+ * @param {OutputMode} output
251
+ * @returns {unknown}
78
252
  */
79
- function coerce_args(args, input_schema) {
80
- /** @type {Record<string, unknown>} */
81
- const result = {};
82
- const properties = input_schema.properties ?? {};
83
-
84
- for (const [key, schema] of Object.entries(properties)) {
85
- const value = args[key];
86
-
87
- if (value === undefined) continue;
88
-
89
- if (schema?.type === 'integer' && typeof value === 'string') {
90
- result[key] = parseInt(value, 10);
91
- } else if (schema?.type === 'number' && typeof value === 'string') {
92
- result[key] = parseFloat(value);
93
- } else if (schema?.type === 'object' && typeof value === 'string') {
94
- try {
95
- result[key] = JSON.parse(value);
96
- } catch {
97
- result[key] = value;
253
+ function select_output(result, output) {
254
+ if (output === 'full') return result;
255
+ if (output === 'structured') {
256
+ return result.structuredContent ?? null;
257
+ }
258
+ if (output === 'content') {
259
+ return result.content ?? [];
260
+ }
261
+ if (output === 'text') {
262
+ const content = result.content;
263
+
264
+ if (!Array.isArray(content)) {
265
+ throw new Error(
266
+ '`--output text` requires a tool result with content',
267
+ );
268
+ }
269
+
270
+ const lines = content.map((item) => {
271
+ if (
272
+ !is_record(item) ||
273
+ item.type !== 'text' ||
274
+ typeof item.text !== 'string'
275
+ ) {
276
+ throw new Error(
277
+ '`--output text` only supports text content blocks',
278
+ );
98
279
  }
99
- } else {
100
- result[key] = value;
280
+
281
+ return item.text;
282
+ });
283
+
284
+ return `${lines.join('\n')}\n`;
285
+ }
286
+
287
+ throw new Error(
288
+ `Invalid output mode: ${output}. Expected one of full, structured, content, text`,
289
+ );
290
+ }
291
+
292
+ /**
293
+ * @param {Record<string, unknown>} result
294
+ * @param {ToolOptions} options
295
+ * @returns {string}
296
+ */
297
+ function format_tool_result(result, options) {
298
+ const selected = select_output(result, options.output ?? 'full');
299
+
300
+ if (typeof selected === 'string') {
301
+ if (options.fields) {
302
+ throw new Error('`--fields` cannot be used with `--output text`');
101
303
  }
304
+ return selected;
102
305
  }
103
306
 
104
- return result;
307
+ return format_json(filter_fields(selected, parse_fields(options.fields)));
308
+ }
309
+
310
+ /**
311
+ * @param {Record<string, unknown>} result
312
+ * @returns {string}
313
+ */
314
+ function format_tool_error(result) {
315
+ const content = result.content;
316
+
317
+ if (Array.isArray(content)) {
318
+ const text_blocks = content
319
+ .map((item) => {
320
+ if (
321
+ is_record(item) &&
322
+ item.type === 'text' &&
323
+ typeof item.text === 'string'
324
+ ) {
325
+ return item.text;
326
+ }
327
+
328
+ return undefined;
329
+ })
330
+ .filter((item) => typeof item === 'string');
331
+
332
+ if (text_blocks.length > 0) {
333
+ return text_blocks.join('\n');
334
+ }
335
+ }
336
+
337
+ return JSON.stringify(result, null, 2);
105
338
  }
106
339
 
107
340
  /**
@@ -126,7 +359,7 @@ export class CliTransport {
126
359
  /**
127
360
  * @type {string}
128
361
  */
129
- #session_id = crypto.randomUUID();
362
+ #session_id = randomUUID();
130
363
 
131
364
  /**
132
365
  * @param {McpServer<any, TCustom>} server
@@ -135,8 +368,11 @@ export class CliTransport {
135
368
  this.#server = server;
136
369
  }
137
370
 
371
+ #exit() {
372
+ process.exit(process.exitCode ?? 0);
373
+ }
374
+
138
375
  /**
139
- * Sends a JSON-RPC request to the server and returns the result.
140
376
  * @param {string} method
141
377
  * @param {Record<string, unknown>} [params]
142
378
  * @param {TCustom} [ctx]
@@ -170,41 +406,76 @@ export class CliTransport {
170
406
  }
171
407
 
172
408
  /**
173
- * Initialize the MCP session with the server.
409
+ * @param {string} method
410
+ * @param {Record<string, unknown>} [params]
411
+ * @param {TCustom} [ctx]
412
+ */
413
+ async #notify(method, params, ctx) {
414
+ await this.#session_id_storage.run(this.#session_id, () =>
415
+ this.#server.receive(
416
+ {
417
+ jsonrpc: '2.0',
418
+ method,
419
+ ...(params ? { params } : {}),
420
+ },
421
+ {
422
+ sessionId: this.#session_id,
423
+ custom: ctx,
424
+ },
425
+ ),
426
+ );
427
+ }
428
+
429
+ /**
174
430
  * @param {TCustom} [ctx]
175
431
  * @returns {Promise<{ serverInfo?: { name?: string } }>}
176
432
  */
177
433
  async #initialize(ctx) {
178
- return this.#request(
434
+ const result = await this.#request(
179
435
  'initialize',
180
436
  {
181
437
  protocolVersion: '2025-03-26',
182
438
  capabilities: {},
183
- clientInfo: {
184
- name: 'tmcp-cli',
185
- version: '0.0.1',
186
- },
439
+ clientInfo: CLIENT_INFO,
187
440
  },
188
441
  ctx,
189
442
  );
443
+
444
+ await this.#notify('notifications/initialized', undefined, ctx);
445
+
446
+ return result;
190
447
  }
191
448
 
192
449
  /**
193
- * Fetches the list of tools from the server.
194
450
  * @param {TCustom} [ctx]
195
451
  * @returns {Promise<Array<Tool>>}
196
452
  */
197
453
  async #list_tools(ctx) {
198
- const result = await this.#request('tools/list', undefined, ctx);
199
- return result?.tools ?? [];
454
+ /** @type {Array<Tool>} */
455
+ const tools = [];
456
+ let cursor = undefined;
457
+
458
+ do {
459
+ const result = /** @type {ListToolsResult | undefined} */ (
460
+ await this.#request(
461
+ 'tools/list',
462
+ cursor ? { cursor } : undefined,
463
+ ctx,
464
+ )
465
+ );
466
+
467
+ tools.push(...(result?.tools ?? []));
468
+ cursor = result?.nextCursor;
469
+ } while (cursor);
470
+
471
+ return tools;
200
472
  }
201
473
 
202
474
  /**
203
- * Calls a tool by name with arguments.
204
475
  * @param {string} name
205
476
  * @param {Record<string, unknown>} [args]
206
477
  * @param {TCustom} [ctx]
207
- * @returns {Promise<any>}
478
+ * @returns {Promise<Record<string, unknown>>}
208
479
  */
209
480
  async #call_tool(name, args, ctx) {
210
481
  return this.#request(
@@ -218,60 +489,184 @@ export class CliTransport {
218
489
  }
219
490
 
220
491
  /**
221
- * Starts the CLI. Initializes the MCP session, lists tools,
222
- * builds yargs commands from the tool definitions, and parses argv.
492
+ * @param {Map<string, Tool>} tool_map
493
+ * @param {string} name
494
+ * @returns {Tool}
495
+ */
496
+ #get_tool(tool_map, name) {
497
+ const tool = tool_map.get(name);
498
+ if (!tool) {
499
+ throw new Error(`Unknown tool: ${name}`);
500
+ }
501
+ return tool;
502
+ }
503
+
504
+ /**
505
+ * @param {Map<string, Tool>} tool_map
506
+ * @param {string} name
507
+ * @param {string | undefined} input
508
+ * @param {ToolOptions} options
509
+ * @param {TCustom} [ctx]
510
+ */
511
+ async #run_tool(tool_map, name, input, options, ctx) {
512
+ const tool = this.#get_tool(tool_map, name);
513
+ const args = await resolve_tool_input(input);
514
+ const result = await this.#call_tool(tool.name, args, ctx);
515
+
516
+ if (result?.isError) {
517
+ process.stderr.write(`Error: ${format_tool_error(result)}\n`);
518
+ process.exitCode = 1;
519
+ return;
520
+ }
521
+
522
+ process.stdout.write(format_tool_result(result, options));
523
+ }
524
+
525
+ /**
223
526
  * @param {TCustom} [ctx]
224
527
  * @param {Array<string>} [argv]
225
528
  */
226
529
  async run(ctx, argv) {
227
- const init_result = await this.#initialize(ctx);
228
-
229
- const tools = await this.#list_tools(ctx);
230
-
231
- const script_name = init_result?.serverInfo?.name ?? 'tmcp';
232
-
233
- const cli = Yargs(argv ?? process.argv.slice(2))
234
- .scriptName(script_name)
235
- .strict()
236
- .demandCommand(
237
- 1,
238
- 'You need to specify a tool command to run. Use --help to see available tools.',
239
- )
240
- .help();
241
-
242
- for (const tool of tools) {
243
- const options = json_schema_to_yargs_options(tool.inputSchema);
244
-
245
- cli.command(
246
- tool.name,
247
- tool.description ?? '',
248
- (yargs) => yargs.options(options),
249
- async (args) => {
250
- try {
251
- const coerced = coerce_args(
252
- /** @type {Record<string, unknown>} */ (args),
253
- tool.inputSchema,
254
- );
255
-
256
- const result = await this.#call_tool(
257
- tool.name,
258
- coerced,
259
- ctx,
260
- );
261
-
262
- process.stdout.write(
263
- JSON.stringify(result, null, 2) + '\n',
264
- );
265
- } catch (err) {
266
- process.stderr.write(
267
- `Error: ${err instanceof Error ? err.message : String(err)}\n`,
268
- );
269
- process.exitCode = 1;
270
- }
271
- },
530
+ try {
531
+ const init_result = await this.#initialize(ctx);
532
+ const tools = await this.#list_tools(ctx);
533
+ const script_name = init_result?.serverInfo?.name ?? 'tmcp';
534
+ const prog = sade(script_name);
535
+
536
+ /** @type {Map<string, Tool>} */
537
+ const tool_map = new Map();
538
+
539
+ for (const tool of tools) {
540
+ tool_map.set(tool.name, tool);
541
+ }
542
+
543
+ prog.command('tools', 'List available tools').action(() => {});
544
+ prog.command('schema <tool>', 'Print a tool schema').action(
545
+ () => {},
272
546
  );
273
- }
274
547
 
275
- await cli.parseAsync();
548
+ prog.command('call <tool> [input]', 'Call a tool with JSON input')
549
+ .option(
550
+ '--output',
551
+ 'Select full, structured, content, or text output',
552
+ 'full',
553
+ )
554
+ .option(
555
+ '--fields',
556
+ 'Select comma-separated dot paths from the chosen output',
557
+ )
558
+ .action(() => {});
559
+
560
+ for (const tool of tools) {
561
+ if (RESERVED_COMMANDS.has(tool.name)) {
562
+ process.stderr.write(
563
+ `Warning: skipping bare alias for tool "${tool.name}" because its name conflicts with a built-in command. Use \`call ${tool.name}\` instead.\n`,
564
+ );
565
+ continue;
566
+ }
567
+
568
+ if (UNSAFE_ALIAS_NAME.test(tool.name)) {
569
+ process.stderr.write(
570
+ `Warning: skipping bare alias for tool "${tool.name}" because its name contains CLI syntax characters. Use \`call ${tool.name}\` instead.\n`,
571
+ );
572
+ continue;
573
+ }
574
+
575
+ prog.command(`${tool.name} [input]`, tool.description ?? '')
576
+ .option(
577
+ '--output',
578
+ 'Select full, structured, content, or text output',
579
+ 'full',
580
+ )
581
+ .option(
582
+ '--fields',
583
+ 'Select comma-separated dot paths from the chosen output',
584
+ )
585
+ .action(() => {});
586
+ }
587
+
588
+ const command_args = argv ?? process.argv.slice(2);
589
+ const args = [
590
+ 'node',
591
+ script_name,
592
+ ...(command_args.length === 0 ? ['--help'] : command_args),
593
+ ];
594
+ const parsed = prog.parse(args, { lazy: true });
595
+
596
+ if (!parsed) {
597
+ this.#exit();
598
+ return;
599
+ }
600
+
601
+ const { name, args: handler_args } = parsed;
602
+ const { positionals, options } = extract_command_args(handler_args);
603
+
604
+ if (name === 'tools') {
605
+ process.stdout.write(format_json(tools));
606
+ this.#exit();
607
+ return;
608
+ }
609
+
610
+ if (name === 'schema') {
611
+ const tool_name = /** @type {string | undefined} */ (
612
+ positionals[0]
613
+ );
614
+ if (!tool_name) {
615
+ throw new Error('Missing tool name for `schema`');
616
+ }
617
+
618
+ process.stdout.write(
619
+ format_json(this.#get_tool(tool_map, tool_name)),
620
+ );
621
+ this.#exit();
622
+ return;
623
+ }
624
+
625
+ if (name === 'call') {
626
+ const tool_name = /** @type {string | undefined} */ (
627
+ positionals[0]
628
+ );
629
+ const input = /** @type {string | undefined} */ (
630
+ positionals[1]
631
+ );
632
+
633
+ if (!tool_name) {
634
+ throw new Error('Missing tool name for `call`');
635
+ }
636
+
637
+ await this.#run_tool(
638
+ tool_map,
639
+ tool_name,
640
+ input,
641
+ normalize_tool_options(options),
642
+ ctx,
643
+ );
644
+ this.#exit();
645
+ return;
646
+ }
647
+
648
+ if (tool_map.has(name)) {
649
+ const input = /** @type {string | undefined} */ (
650
+ positionals[0]
651
+ );
652
+ await this.#run_tool(
653
+ tool_map,
654
+ name,
655
+ input,
656
+ normalize_tool_options(options),
657
+ ctx,
658
+ );
659
+ this.#exit();
660
+ return;
661
+ }
662
+
663
+ this.#exit();
664
+ } catch (err) {
665
+ process.stderr.write(
666
+ `Error: ${err instanceof Error ? err.message : String(err)}\n`,
667
+ );
668
+ process.exitCode = 1;
669
+ this.#exit();
670
+ }
276
671
  }
277
672
  }
package/src/internal.d.ts CHANGED
@@ -1,20 +1,35 @@
1
- export type InputSchema = {
2
- type: 'object';
3
- properties?: Record<
4
- string,
5
- {
6
- type?: string;
7
- description?: string;
8
- enum?: Array<unknown>;
9
- default?: unknown;
10
- items?: { type?: string };
11
- }
12
- >;
1
+ export type JsonSchema = {
2
+ type?: string | Array<string>;
3
+ title?: string;
4
+ description?: string;
5
+ default?: unknown;
6
+ enum?: Array<unknown>;
7
+ properties?: Record<string, JsonSchema>;
13
8
  required?: Array<string>;
9
+ items?: JsonSchema | Array<JsonSchema>;
10
+ additionalProperties?: boolean | JsonSchema;
11
+ oneOf?: Array<JsonSchema>;
12
+ anyOf?: Array<JsonSchema>;
13
+ allOf?: Array<JsonSchema>;
14
+ [key: string]: unknown;
15
+ };
16
+
17
+ export type InputSchema = JsonSchema & {
18
+ type: 'object';
14
19
  };
15
20
 
16
21
  export type Tool = {
17
22
  name: string;
23
+ title?: string;
18
24
  description?: string;
25
+ icons?: Array<unknown>;
26
+ annotations?: Record<string, unknown>;
27
+ _meta?: Record<string, unknown>;
19
28
  inputSchema: InputSchema;
29
+ outputSchema?: JsonSchema;
30
+ };
31
+
32
+ export type ListToolsResult = {
33
+ tools: Array<Tool>;
34
+ nextCursor?: string;
20
35
  };
@@ -3,14 +3,15 @@ declare module '@tmcp/transport-cli' {
3
3
  export class CliTransport<TCustom extends Record<string, unknown> | undefined = undefined> {
4
4
 
5
5
  constructor(server: McpServer<any, TCustom>);
6
- /**
7
- * Starts the CLI. Initializes the MCP session, lists tools,
8
- * builds yargs commands from the tool definitions, and parses argv.
9
- *
10
- */
6
+
11
7
  run(ctx?: TCustom, argv?: Array<string>): Promise<void>;
12
8
  #private;
13
9
  }
10
+ export type OutputMode = "full" | "structured" | "content" | "text";
11
+ export type ToolOptions = {
12
+ output?: OutputMode;
13
+ fields?: string;
14
+ };
14
15
 
15
16
  export {};
16
17
  }
@@ -2,7 +2,9 @@
2
2
  "version": 3,
3
3
  "file": "index.d.ts",
4
4
  "names": [
5
- "CliTransport"
5
+ "CliTransport",
6
+ "OutputMode",
7
+ "ToolOptions"
6
8
  ],
7
9
  "sources": [
8
10
  "../index.js"
@@ -10,6 +12,6 @@
10
12
  "sourcesContent": [
11
13
  null
12
14
  ],
13
- "mappings": ";;cA6GaA,YAAYA",
15
+ "mappings": ";;cAsVaA,YAAYA;;;;;;;aA5UgCC,UAAUA;aAIZC,WAAWA",
14
16
  "ignoreList": []
15
17
  }
package/src/test.js DELETED
@@ -1,5 +0,0 @@
1
- import Yargs from 'yargs/yargs';
2
-
3
- await Yargs(process.argv.slice(2))
4
- .command('test', 'Run tests', {}, console.log)
5
- .parseAsync();