@tmcp/transport-cli 0.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Paolo Ricciuti
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,136 @@
1
+ # @tmcp/transport-cli
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/).
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add @tmcp/transport-cli tmcp
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```javascript
14
+ import { McpServer } from 'tmcp';
15
+ import { CliTransport } from '@tmcp/transport-cli';
16
+ import { ZodJsonSchemaAdapter } from '@tmcp/adapter-zod';
17
+ import { z } from 'zod';
18
+
19
+ const server = new McpServer(
20
+ {
21
+ name: 'my-cli',
22
+ version: '1.0.0',
23
+ description: 'My CLI tool',
24
+ },
25
+ {
26
+ adapter: new ZodJsonSchemaAdapter(),
27
+ capabilities: { tools: {} },
28
+ },
29
+ );
30
+
31
+ server.tool(
32
+ {
33
+ name: 'greet',
34
+ description: 'Greet someone by name',
35
+ schema: z.object({
36
+ name: z.string().describe('Name of the person to greet'),
37
+ loud: z.boolean().optional().describe('Shout the greeting'),
38
+ }),
39
+ },
40
+ async (input) => {
41
+ const text = `Hello, ${input.name}!`;
42
+ return {
43
+ content: [
44
+ { type: 'text', text: input.loud ? text.toUpperCase() : text },
45
+ ],
46
+ };
47
+ },
48
+ );
49
+
50
+ const cli = new CliTransport(server);
51
+ await cli.run(undefined, process.argv.slice(2));
52
+ ```
53
+
54
+ Running the above:
55
+
56
+ ```bash
57
+ node my-cli.js greet --name Alice
58
+ # {"content":[{"type":"text","text":"Hello, Alice!"}]}
59
+
60
+ node my-cli.js greet --name Alice --loud
61
+ # {"content":[{"type":"text","text":"HELLO, ALICE!"}]}
62
+ ```
63
+
64
+ Output is pretty-printed JSON written to stdout. Errors go to stderr and set `process.exitCode` to 1.
65
+
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.
67
+
68
+ ## Argument types
69
+
70
+ The transport maps JSON Schema types from your tool's input schema to yargs option types:
71
+
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` |
79
+
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`.
81
+
82
+ ## Custom context
83
+
84
+ If your server uses custom context, you can pass it as the first argument to `run()`:
85
+
86
+ ```javascript
87
+ const cli = new CliTransport(server);
88
+ await cli.run({ userId: 'cli-user' }, process.argv.slice(2));
89
+ ```
90
+
91
+ The context is forwarded to the server on every request, so your tool handlers can read it from `server.ctx.custom`.
92
+
93
+ ## API
94
+
95
+ ### `CliTransport`
96
+
97
+ #### Constructor
98
+
99
+ ```typescript
100
+ new CliTransport(server: McpServer)
101
+ ```
102
+
103
+ Creates a new CLI transport. The `server` parameter is the TMCP server instance whose tools will be exposed as commands.
104
+
105
+ #### Methods
106
+
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
115
+
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.
121
+
122
+ ## Related Packages
123
+
124
+ - [`tmcp`](../tmcp) - Core TMCP server implementation
125
+ - [`@tmcp/transport-stdio`](../transport-stdio) - Standard I/O transport
126
+ - [`@tmcp/transport-http`](../transport-http) - HTTP transport
127
+ - [`@tmcp/adapter-zod`](../adapter-zod) - Zod schema adapter
128
+ - [`@tmcp/adapter-valibot`](../adapter-valibot) - Valibot schema adapter
129
+
130
+ ## Acknowledgments
131
+
132
+ Huge thanks to Sean O'Bannon that provided us with the `@tmcp` scope on npm.
133
+
134
+ ## License
135
+
136
+ MIT
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@tmcp/transport-cli",
3
+ "version": "0.0.1",
4
+ "description": "CLI transport for TMCP - exposes MCP tools as CLI commands via yargs",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "types": "src/types/index.d.ts",
8
+ "files": [
9
+ "src"
10
+ ],
11
+ "exports": {
12
+ ".": {
13
+ "types": "./src/types/index.d.ts",
14
+ "default": "./src/index.js"
15
+ },
16
+ "./package.json": "./package.json"
17
+ },
18
+ "peerDependencies": {
19
+ "tmcp": "^1.16.3"
20
+ },
21
+ "dependencies": {
22
+ "yargs": "^17.7.2"
23
+ },
24
+ "keywords": [
25
+ "tmcp",
26
+ "cli",
27
+ "transport",
28
+ "yargs"
29
+ ],
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/paoloricciuti/tmcp.git",
33
+ "directory": "packages/transport-cli"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "^24.0.13",
37
+ "@types/yargs": "^17.0.33",
38
+ "dts-buddy": "^0.6.2",
39
+ "publint": "^0.3.12",
40
+ "valibot": "^1.1.0",
41
+ "vitest": "^4.0.6",
42
+ "@tmcp/adapter-valibot": "^0.1.5",
43
+ "tmcp": "^1.19.3"
44
+ },
45
+ "publishConfig": {
46
+ "access": "public"
47
+ },
48
+ "scripts": {
49
+ "generate:types": "dts-buddy && publint",
50
+ "prepublish": "pnpm generate:types",
51
+ "test": "vitest"
52
+ }
53
+ }
package/src/index.js ADDED
@@ -0,0 +1,277 @@
1
+ /**
2
+ * @import { McpServer } from "tmcp";
3
+ * @import { Options } from "yargs";
4
+ * @import { InputSchema, Tool } from "./internal.js";
5
+ */
6
+ import process from 'node:process';
7
+ import { AsyncLocalStorage } from 'node:async_hooks';
8
+ import Yargs from 'yargs/yargs';
9
+
10
+ /**
11
+ * Maps a JSON Schema type string to a yargs option type.
12
+ * @param {string | undefined} json_schema_type
13
+ * @returns {Options["type"]}
14
+ */
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';
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Converts a JSON Schema inputSchema into a yargs options object.
33
+ * @param {InputSchema} input_schema
34
+ * @returns {Record<string, Options>}
35
+ */
36
+ function json_schema_to_yargs_options(input_schema) {
37
+ /** @type {Record<string, Options>} */
38
+ const options = {};
39
+
40
+ const properties = input_schema.properties ?? {};
41
+ const required = new Set(input_schema.required ?? []);
42
+
43
+ for (const [name, schema] of Object.entries(properties)) {
44
+ /** @type {Options} */
45
+ const option = {};
46
+
47
+ option.type = json_schema_type_to_yargs(schema.type);
48
+ option.demandOption = required.has(name);
49
+
50
+ if (schema.description) {
51
+ option.describe = schema.description;
52
+ }
53
+
54
+ if (schema.enum) {
55
+ option.choices =
56
+ /** @type {Array<string | number | true | undefined>} */ (
57
+ schema.enum
58
+ );
59
+ }
60
+
61
+ if (schema.default !== undefined) {
62
+ option.default = schema.default;
63
+ }
64
+
65
+ options[name] = option;
66
+ }
67
+
68
+ return options;
69
+ }
70
+
71
+ /**
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>}
78
+ */
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;
98
+ }
99
+ } else {
100
+ result[key] = value;
101
+ }
102
+ }
103
+
104
+ return result;
105
+ }
106
+
107
+ /**
108
+ * @template {Record<string, unknown> | undefined} [TCustom=undefined]
109
+ */
110
+ export class CliTransport {
111
+ /**
112
+ * @type {McpServer<any, TCustom>}
113
+ */
114
+ #server;
115
+
116
+ /**
117
+ * @type {AsyncLocalStorage<string | undefined>}
118
+ */
119
+ #session_id_storage = new AsyncLocalStorage();
120
+
121
+ /**
122
+ * @type {number}
123
+ */
124
+ #request_id = 0;
125
+
126
+ /**
127
+ * @type {string}
128
+ */
129
+ #session_id = crypto.randomUUID();
130
+
131
+ /**
132
+ * @param {McpServer<any, TCustom>} server
133
+ */
134
+ constructor(server) {
135
+ this.#server = server;
136
+ }
137
+
138
+ /**
139
+ * Sends a JSON-RPC request to the server and returns the result.
140
+ * @param {string} method
141
+ * @param {Record<string, unknown>} [params]
142
+ * @param {TCustom} [ctx]
143
+ * @returns {Promise<any>}
144
+ */
145
+ async #request(method, params, ctx) {
146
+ const request_id = this.#request_id++;
147
+
148
+ const response = await this.#session_id_storage.run(
149
+ this.#session_id,
150
+ () =>
151
+ this.#server.receive(
152
+ {
153
+ jsonrpc: '2.0',
154
+ id: request_id,
155
+ method,
156
+ ...(params ? { params } : {}),
157
+ },
158
+ {
159
+ sessionId: this.#session_id,
160
+ custom: ctx,
161
+ },
162
+ ),
163
+ );
164
+
165
+ if (response?.error) {
166
+ throw new Error(response.error.message ?? 'Unknown JSON-RPC error');
167
+ }
168
+
169
+ return response?.result;
170
+ }
171
+
172
+ /**
173
+ * Initialize the MCP session with the server.
174
+ * @param {TCustom} [ctx]
175
+ * @returns {Promise<{ serverInfo?: { name?: string } }>}
176
+ */
177
+ async #initialize(ctx) {
178
+ return this.#request(
179
+ 'initialize',
180
+ {
181
+ protocolVersion: '2025-03-26',
182
+ capabilities: {},
183
+ clientInfo: {
184
+ name: 'tmcp-cli',
185
+ version: '0.0.1',
186
+ },
187
+ },
188
+ ctx,
189
+ );
190
+ }
191
+
192
+ /**
193
+ * Fetches the list of tools from the server.
194
+ * @param {TCustom} [ctx]
195
+ * @returns {Promise<Array<Tool>>}
196
+ */
197
+ async #list_tools(ctx) {
198
+ const result = await this.#request('tools/list', undefined, ctx);
199
+ return result?.tools ?? [];
200
+ }
201
+
202
+ /**
203
+ * Calls a tool by name with arguments.
204
+ * @param {string} name
205
+ * @param {Record<string, unknown>} [args]
206
+ * @param {TCustom} [ctx]
207
+ * @returns {Promise<any>}
208
+ */
209
+ async #call_tool(name, args, ctx) {
210
+ return this.#request(
211
+ 'tools/call',
212
+ {
213
+ name,
214
+ arguments: args ?? {},
215
+ },
216
+ ctx,
217
+ );
218
+ }
219
+
220
+ /**
221
+ * Starts the CLI. Initializes the MCP session, lists tools,
222
+ * builds yargs commands from the tool definitions, and parses argv.
223
+ * @param {TCustom} [ctx]
224
+ * @param {Array<string>} [argv]
225
+ */
226
+ 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
+ },
272
+ );
273
+ }
274
+
275
+ await cli.parseAsync();
276
+ }
277
+ }
@@ -0,0 +1,20 @@
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
+ >;
13
+ required?: Array<string>;
14
+ };
15
+
16
+ export type Tool = {
17
+ name: string;
18
+ description?: string;
19
+ inputSchema: InputSchema;
20
+ };
package/src/test.js ADDED
@@ -0,0 +1,5 @@
1
+ import Yargs from 'yargs/yargs';
2
+
3
+ await Yargs(process.argv.slice(2))
4
+ .command('test', 'Run tests', {}, console.log)
5
+ .parseAsync();
@@ -0,0 +1,18 @@
1
+ declare module '@tmcp/transport-cli' {
2
+ import type { McpServer } from 'tmcp';
3
+ export class CliTransport<TCustom extends Record<string, unknown> | undefined = undefined> {
4
+
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
+ */
11
+ run(ctx?: TCustom, argv?: Array<string>): Promise<void>;
12
+ #private;
13
+ }
14
+
15
+ export {};
16
+ }
17
+
18
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,15 @@
1
+ {
2
+ "version": 3,
3
+ "file": "index.d.ts",
4
+ "names": [
5
+ "CliTransport"
6
+ ],
7
+ "sources": [
8
+ "../index.js"
9
+ ],
10
+ "sourcesContent": [
11
+ null
12
+ ],
13
+ "mappings": ";;cA6GaA,YAAYA",
14
+ "ignoreList": []
15
+ }