@tmcp/transport-cli 0.0.1 → 0.0.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 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,83 @@ 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
+ - The CLI initializes an MCP session, sends `notifications/initialized`, and paginates through `tools/list` automatically.
127
+ - The program name shown in help output comes from `McpServer`'s `name`.
81
128
 
82
129
  ## Custom context
83
130
 
84
- If your server uses custom context, you can pass it as the first argument to `run()`:
131
+ If your server uses custom context, pass it as the first argument to `run()`:
85
132
 
86
133
  ```javascript
87
134
  const cli = new CliTransport(server);
88
135
  await cli.run({ userId: 'cli-user' }, process.argv.slice(2));
89
136
  ```
90
137
 
91
- The context is forwarded to the server on every request, so your tool handlers can read it from `server.ctx.custom`.
138
+ The context is forwarded on every request, so handlers can read it from `server.ctx.custom`.
92
139
 
93
140
  ## API
94
141
 
@@ -100,24 +147,13 @@ The context is forwarded to the server on every request, so your tool handlers c
100
147
  new CliTransport(server: McpServer)
101
148
  ```
102
149
 
103
- Creates a new CLI transport. The `server` parameter is the TMCP server instance whose tools will be exposed as commands.
104
-
105
150
  #### Methods
106
151
 
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
152
+ ```typescript
153
+ run(ctx?: TCustom, argv?: string[]): Promise<void>
154
+ ```
115
155
 
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.
156
+ Starts the CLI, initializes a session, discovers tools, and executes the requested static command or tool alias.
121
157
 
122
158
  ## Related Packages
123
159
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
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.2",
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",
@@ -19,13 +19,14 @@
19
19
  "tmcp": "^1.16.3"
20
20
  },
21
21
  "dependencies": {
22
- "yargs": "^17.7.2"
22
+ "sade": "^1.8.1"
23
23
  },
24
24
  "keywords": [
25
25
  "tmcp",
26
26
  "cli",
27
27
  "transport",
28
- "yargs"
28
+ "mcp",
29
+ "json"
29
30
  ],
30
31
  "repository": {
31
32
  "type": "git",
@@ -34,7 +35,6 @@
34
35
  },
35
36
  "devDependencies": {
36
37
  "@types/node": "^24.0.13",
37
- "@types/yargs": "^17.0.33",
38
38
  "dts-buddy": "^0.6.2",
39
39
  "publint": "^0.3.12",
40
40
  "valibot": "^1.1.0",
package/src/index.js CHANGED
@@ -1,107 +1,304 @@
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';
7
6
  import { AsyncLocalStorage } from 'node:async_hooks';
8
- import Yargs from 'yargs/yargs';
7
+ import sade from 'sade';
9
8
 
10
9
  /**
11
- * Maps a JSON Schema type string to a yargs option type.
12
- * @param {string | undefined} json_schema_type
13
- * @returns {Options["type"]}
10
+ * @typedef {'full' | 'structured' | 'content' | 'text'} OutputMode
14
11
  */
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';
12
+
13
+ /**
14
+ * @typedef {{ output?: OutputMode; fields?: string }} ToolOptions
15
+ */
16
+
17
+ const CLIENT_INFO = {
18
+ name: 'tmcp-cli',
19
+ version: '0.0.1',
20
+ };
21
+
22
+ const RESERVED_COMMANDS = new Set(['call', 'schema', 'tools']);
23
+
24
+ /**
25
+ * @param {unknown} value
26
+ * @returns {value is Record<string, unknown>}
27
+ */
28
+ function is_record(value) {
29
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
30
+ }
31
+
32
+ /**
33
+ * @param {unknown} value
34
+ * @param {string} source
35
+ * @returns {Record<string, unknown>}
36
+ */
37
+ function parse_input_json(value, source) {
38
+ let parsed;
39
+
40
+ try {
41
+ parsed = JSON.parse(String(value));
42
+ } catch {
43
+ throw new Error(`Invalid JSON in ${source}`);
44
+ }
45
+
46
+ if (!is_record(parsed)) {
47
+ throw new Error(`Input from ${source} must be a JSON object`);
28
48
  }
49
+
50
+ return parsed;
29
51
  }
30
52
 
31
53
  /**
32
- * Converts a JSON Schema inputSchema into a yargs options object.
33
- * @param {InputSchema} input_schema
34
- * @returns {Record<string, Options>}
54
+ * @param {Array<unknown>} args
55
+ * @returns {{ positionals: Array<unknown>; options: Record<string, unknown> }}
35
56
  */
36
- function json_schema_to_yargs_options(input_schema) {
37
- /** @type {Record<string, Options>} */
38
- const options = {};
57
+ function extract_command_args(args) {
58
+ const last_arg = args.at(-1);
59
+
60
+ if (is_record(last_arg)) {
61
+ return {
62
+ positionals: args.slice(0, -1),
63
+ options: last_arg,
64
+ };
65
+ }
39
66
 
40
- const properties = input_schema.properties ?? {};
41
- const required = new Set(input_schema.required ?? []);
67
+ return {
68
+ positionals: args,
69
+ options: {},
70
+ };
71
+ }
42
72
 
43
- for (const [name, schema] of Object.entries(properties)) {
44
- /** @type {Options} */
45
- const option = {};
73
+ /**
74
+ * @param {Record<string, unknown>} options
75
+ * @returns {ToolOptions}
76
+ */
77
+ function normalize_tool_options(options) {
78
+ return {
79
+ output:
80
+ typeof options.output === 'string'
81
+ ? /** @type {OutputMode} */ (options.output)
82
+ : 'full',
83
+ fields: typeof options.fields === 'string' ? options.fields : undefined,
84
+ };
85
+ }
86
+
87
+ /**
88
+ * @param {string | undefined} input
89
+ * @returns {Promise<Record<string, unknown>>}
90
+ */
91
+ async function resolve_tool_input(input) {
92
+ if (typeof input === 'string') {
93
+ return parse_input_json(input, 'positional input');
94
+ }
95
+
96
+ return {};
97
+ }
46
98
 
47
- option.type = json_schema_type_to_yargs(schema.type);
48
- option.demandOption = required.has(name);
99
+ /**
100
+ * @param {unknown} value
101
+ * @returns {string}
102
+ */
103
+ function format_json(value) {
104
+ return `${JSON.stringify(value === undefined ? null : value, null, 2)}\n`;
105
+ }
106
+
107
+ /**
108
+ * @param {string | undefined} fields
109
+ * @returns {Array<string>}
110
+ */
111
+ function parse_fields(fields) {
112
+ if (!fields) return [];
113
+
114
+ const paths = fields
115
+ .split(',')
116
+ .map((field) => field.trim())
117
+ .filter(Boolean);
118
+
119
+ if (paths.length === 0) {
120
+ throw new Error('`--fields` must include at least one dot path');
121
+ }
122
+
123
+ return paths;
124
+ }
49
125
 
50
- if (schema.description) {
51
- option.describe = schema.description;
126
+ /**
127
+ * @param {string} path
128
+ * @returns {Array<string>}
129
+ */
130
+ function split_path(path) {
131
+ return path.split('.').filter(Boolean);
132
+ }
133
+
134
+ /**
135
+ * @param {unknown} value
136
+ * @param {Array<string>} path
137
+ * @returns {unknown}
138
+ */
139
+ function get_path_value(value, path) {
140
+ let current = value;
141
+
142
+ for (const segment of path) {
143
+ if (Array.isArray(current)) {
144
+ const index = Number(segment);
145
+ if (
146
+ !Number.isInteger(index) ||
147
+ index < 0 ||
148
+ index >= current.length
149
+ ) {
150
+ throw new Error(`Unknown field path: ${path.join('.')}`);
151
+ }
152
+ current = current[index];
153
+ continue;
52
154
  }
53
155
 
54
- if (schema.enum) {
55
- option.choices =
56
- /** @type {Array<string | number | true | undefined>} */ (
57
- schema.enum
156
+ if (!is_record(current) || !(segment in current)) {
157
+ throw new Error(`Unknown field path: ${path.join('.')}`);
158
+ }
159
+
160
+ current = current[segment];
161
+ }
162
+
163
+ return current;
164
+ }
165
+
166
+ /**
167
+ * @param {Record<string, unknown> | Array<unknown>} target
168
+ * @param {Array<string>} path
169
+ * @param {unknown} value
170
+ */
171
+ function set_path_value(target, path, value) {
172
+ let current = target;
173
+
174
+ for (let index = 0; index < path.length; index += 1) {
175
+ const segment = path[index];
176
+ const is_last = index === path.length - 1;
177
+ const next_segment = path[index + 1];
178
+ const next_value =
179
+ next_segment !== undefined && Number.isInteger(Number(next_segment))
180
+ ? []
181
+ : {};
182
+
183
+ if (Array.isArray(current)) {
184
+ const array_index = Number(segment);
185
+ if (!Number.isInteger(array_index) || array_index < 0) {
186
+ throw new Error(
187
+ `Invalid array index in field path: ${path.join('.')}`,
58
188
  );
189
+ }
190
+
191
+ if (is_last) {
192
+ current[array_index] = value;
193
+ return;
194
+ }
195
+
196
+ current[array_index] ??= next_value;
197
+ current = /** @type {Record<string, unknown> | Array<unknown>} */ (
198
+ current[array_index]
199
+ );
200
+ continue;
59
201
  }
60
202
 
61
- if (schema.default !== undefined) {
62
- option.default = schema.default;
203
+ if (is_last) {
204
+ current[segment] = value;
205
+ return;
63
206
  }
64
207
 
65
- options[name] = option;
208
+ current[segment] ??= next_value;
209
+ current = /** @type {Record<string, unknown> | Array<unknown>} */ (
210
+ current[segment]
211
+ );
212
+ }
213
+ }
214
+
215
+ /**
216
+ * @param {unknown} value
217
+ * @param {Array<string>} paths
218
+ * @returns {unknown}
219
+ */
220
+ function filter_fields(value, paths) {
221
+ if (paths.length === 0) return value;
222
+
223
+ if (!is_record(value) && !Array.isArray(value)) {
224
+ throw new Error(
225
+ '`--fields` can only be used with object or array output',
226
+ );
66
227
  }
67
228
 
68
- return options;
229
+ const result = Array.isArray(value) ? [] : {};
230
+
231
+ for (const path of paths) {
232
+ const segments = split_path(path);
233
+ if (segments.length === 0) {
234
+ throw new Error('`--fields` only supports dot-separated paths');
235
+ }
236
+ set_path_value(result, segments, get_path_value(value, segments));
237
+ }
238
+
239
+ return result;
69
240
  }
70
241
 
71
242
  /**
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>}
243
+ * @param {Record<string, unknown>} result
244
+ * @param {OutputMode} output
245
+ * @returns {unknown}
78
246
  */
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;
247
+ function select_output(result, output) {
248
+ if (output === 'full') return result;
249
+ if (output === 'structured') {
250
+ return result.structuredContent ?? null;
251
+ }
252
+ if (output === 'content') {
253
+ return result.content ?? [];
254
+ }
255
+ if (output === 'text') {
256
+ const content = result.content;
257
+
258
+ if (!Array.isArray(content)) {
259
+ throw new Error(
260
+ '`--output text` requires a tool result with content',
261
+ );
262
+ }
263
+
264
+ const lines = content.map((item) => {
265
+ if (
266
+ !is_record(item) ||
267
+ item.type !== 'text' ||
268
+ typeof item.text !== 'string'
269
+ ) {
270
+ throw new Error(
271
+ '`--output text` only supports text content blocks',
272
+ );
98
273
  }
99
- } else {
100
- result[key] = value;
274
+
275
+ return item.text;
276
+ });
277
+
278
+ return `${lines.join('\n')}\n`;
279
+ }
280
+
281
+ throw new Error(
282
+ `Invalid output mode: ${output}. Expected one of full, structured, content, text`,
283
+ );
284
+ }
285
+
286
+ /**
287
+ * @param {Record<string, unknown>} result
288
+ * @param {ToolOptions} options
289
+ * @returns {string}
290
+ */
291
+ function format_tool_result(result, options) {
292
+ const selected = select_output(result, options.output ?? 'full');
293
+
294
+ if (typeof selected === 'string') {
295
+ if (options.fields) {
296
+ throw new Error('`--fields` cannot be used with `--output text`');
101
297
  }
298
+ return selected;
102
299
  }
103
300
 
104
- return result;
301
+ return format_json(filter_fields(selected, parse_fields(options.fields)));
105
302
  }
106
303
 
107
304
  /**
@@ -136,7 +333,6 @@ export class CliTransport {
136
333
  }
137
334
 
138
335
  /**
139
- * Sends a JSON-RPC request to the server and returns the result.
140
336
  * @param {string} method
141
337
  * @param {Record<string, unknown>} [params]
142
338
  * @param {TCustom} [ctx]
@@ -170,41 +366,76 @@ export class CliTransport {
170
366
  }
171
367
 
172
368
  /**
173
- * Initialize the MCP session with the server.
369
+ * @param {string} method
370
+ * @param {Record<string, unknown>} [params]
371
+ * @param {TCustom} [ctx]
372
+ */
373
+ async #notify(method, params, ctx) {
374
+ await this.#session_id_storage.run(this.#session_id, () =>
375
+ this.#server.receive(
376
+ {
377
+ jsonrpc: '2.0',
378
+ method,
379
+ ...(params ? { params } : {}),
380
+ },
381
+ {
382
+ sessionId: this.#session_id,
383
+ custom: ctx,
384
+ },
385
+ ),
386
+ );
387
+ }
388
+
389
+ /**
174
390
  * @param {TCustom} [ctx]
175
391
  * @returns {Promise<{ serverInfo?: { name?: string } }>}
176
392
  */
177
393
  async #initialize(ctx) {
178
- return this.#request(
394
+ const result = await this.#request(
179
395
  'initialize',
180
396
  {
181
397
  protocolVersion: '2025-03-26',
182
398
  capabilities: {},
183
- clientInfo: {
184
- name: 'tmcp-cli',
185
- version: '0.0.1',
186
- },
399
+ clientInfo: CLIENT_INFO,
187
400
  },
188
401
  ctx,
189
402
  );
403
+
404
+ await this.#notify('notifications/initialized', undefined, ctx);
405
+
406
+ return result;
190
407
  }
191
408
 
192
409
  /**
193
- * Fetches the list of tools from the server.
194
410
  * @param {TCustom} [ctx]
195
411
  * @returns {Promise<Array<Tool>>}
196
412
  */
197
413
  async #list_tools(ctx) {
198
- const result = await this.#request('tools/list', undefined, ctx);
199
- return result?.tools ?? [];
414
+ /** @type {Array<Tool>} */
415
+ const tools = [];
416
+ let cursor = undefined;
417
+
418
+ do {
419
+ const result = /** @type {ListToolsResult | undefined} */ (
420
+ await this.#request(
421
+ 'tools/list',
422
+ cursor ? { cursor } : undefined,
423
+ ctx,
424
+ )
425
+ );
426
+
427
+ tools.push(...(result?.tools ?? []));
428
+ cursor = result?.nextCursor;
429
+ } while (cursor);
430
+
431
+ return tools;
200
432
  }
201
433
 
202
434
  /**
203
- * Calls a tool by name with arguments.
204
435
  * @param {string} name
205
436
  * @param {Record<string, unknown>} [args]
206
437
  * @param {TCustom} [ctx]
207
- * @returns {Promise<any>}
438
+ * @returns {Promise<Record<string, unknown>>}
208
439
  */
209
440
  async #call_tool(name, args, ctx) {
210
441
  return this.#request(
@@ -218,60 +449,150 @@ export class CliTransport {
218
449
  }
219
450
 
220
451
  /**
221
- * Starts the CLI. Initializes the MCP session, lists tools,
222
- * builds yargs commands from the tool definitions, and parses argv.
452
+ * @param {Map<string, Tool>} tool_map
453
+ * @param {string} name
454
+ * @returns {Tool}
455
+ */
456
+ #get_tool(tool_map, name) {
457
+ const tool = tool_map.get(name);
458
+ if (!tool) {
459
+ throw new Error(`Unknown tool: ${name}`);
460
+ }
461
+ return tool;
462
+ }
463
+
464
+ /**
465
+ * @param {Map<string, Tool>} tool_map
466
+ * @param {string} name
467
+ * @param {string | undefined} input
468
+ * @param {ToolOptions} options
469
+ * @param {TCustom} [ctx]
470
+ */
471
+ async #run_tool(tool_map, name, input, options, ctx) {
472
+ const tool = this.#get_tool(tool_map, name);
473
+ const args = await resolve_tool_input(input);
474
+ const result = await this.#call_tool(tool.name, args, ctx);
475
+
476
+ process.stdout.write(format_tool_result(result, options));
477
+ }
478
+
479
+ /**
223
480
  * @param {TCustom} [ctx]
224
481
  * @param {Array<string>} [argv]
225
482
  */
226
483
  async run(ctx, argv) {
227
484
  const init_result = await this.#initialize(ctx);
228
-
229
485
  const tools = await this.#list_tools(ctx);
230
-
231
486
  const script_name = init_result?.serverInfo?.name ?? 'tmcp';
487
+ const prog = sade(script_name);
232
488
 
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();
489
+ /** @type {Map<string, Tool>} */
490
+ const tool_map = new Map();
241
491
 
242
492
  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
- },
493
+ tool_map.set(tool.name, tool);
494
+ }
495
+
496
+ prog.command('tools', 'List available tools').action(() => {});
497
+ prog.command('schema <tool>', 'Print a tool schema').action(() => {});
498
+
499
+ const call_command = prog
500
+ .command('call <tool> [input]', 'Call a tool with JSON input')
501
+ .option(
502
+ '--output',
503
+ 'Select full, structured, content, or text output',
504
+ 'full',
505
+ )
506
+ .option(
507
+ '--fields',
508
+ 'Select comma-separated dot paths from the chosen output',
272
509
  );
510
+
511
+ call_command.action(() => {});
512
+
513
+ for (const tool of tools) {
514
+ if (RESERVED_COMMANDS.has(tool.name)) continue;
515
+
516
+ prog.command(`${tool.name} [input]`, tool.description ?? '')
517
+ .option(
518
+ '--output',
519
+ 'Select full, structured, content, or text output',
520
+ 'full',
521
+ )
522
+ .option(
523
+ '--fields',
524
+ 'Select comma-separated dot paths from the chosen output',
525
+ )
526
+ .action(() => {});
273
527
  }
274
528
 
275
- await cli.parseAsync();
529
+ const args = argv ? ['node', script_name, ...argv] : process.argv;
530
+ const parsed = prog.parse(args, { lazy: true });
531
+
532
+ if (!parsed) return;
533
+
534
+ const { name, args: handler_args } = parsed;
535
+ const { positionals, options } = extract_command_args(handler_args);
536
+
537
+ try {
538
+ if (name === 'tools') {
539
+ process.stdout.write(format_json(tools));
540
+ return;
541
+ }
542
+
543
+ if (name === 'schema') {
544
+ const tool_name = /** @type {string | undefined} */ (
545
+ positionals[0]
546
+ );
547
+ if (!tool_name) {
548
+ throw new Error('Missing tool name for `schema`');
549
+ }
550
+
551
+ process.stdout.write(
552
+ format_json(this.#get_tool(tool_map, tool_name)),
553
+ );
554
+ return;
555
+ }
556
+
557
+ if (name === 'call') {
558
+ const tool_name = /** @type {string | undefined} */ (
559
+ positionals[0]
560
+ );
561
+ const input = /** @type {string | undefined} */ (
562
+ positionals[1]
563
+ );
564
+
565
+ if (!tool_name) {
566
+ throw new Error('Missing tool name for `call`');
567
+ }
568
+
569
+ await this.#run_tool(
570
+ tool_map,
571
+ tool_name,
572
+ input,
573
+ normalize_tool_options(options),
574
+ ctx,
575
+ );
576
+ return;
577
+ }
578
+
579
+ if (tool_map.has(name)) {
580
+ const input = /** @type {string | undefined} */ (
581
+ positionals[0]
582
+ );
583
+ await this.#run_tool(
584
+ tool_map,
585
+ name,
586
+ input,
587
+ normalize_tool_options(options),
588
+ ctx,
589
+ );
590
+ }
591
+ } catch (err) {
592
+ process.stderr.write(
593
+ `Error: ${err instanceof Error ? err.message : String(err)}\n`,
594
+ );
595
+ process.exitCode = 1;
596
+ }
276
597
  }
277
598
  }
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": ";;cAkTaA,YAAYA;;;;;;;aAzSgCC,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();