@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 +75 -39
- package/package.json +5 -5
- package/src/index.js +445 -124
- package/src/internal.d.ts +27 -12
- package/src/types/index.d.ts +6 -5
- package/src/types/index.d.ts.map +4 -2
- package/src/test.js +0 -5
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @tmcp/transport-cli
|
|
2
2
|
|
|
3
|
-
A CLI transport for TMCP that
|
|
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()
|
|
37
|
-
loud: z.boolean().optional()
|
|
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
|
|
44
|
+
const message = `Hello, ${input.name}!`;
|
|
45
|
+
|
|
42
46
|
return {
|
|
43
47
|
content: [
|
|
44
|
-
{
|
|
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
|
-
|
|
62
|
+
## Commands
|
|
63
|
+
|
|
64
|
+
### `tools`
|
|
65
|
+
|
|
66
|
+
Prints the available tools as pretty JSON.
|
|
55
67
|
|
|
56
68
|
```bash
|
|
57
|
-
node my-cli.js
|
|
58
|
-
|
|
69
|
+
node my-cli.js tools
|
|
70
|
+
```
|
|
59
71
|
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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
|
-
|
|
96
|
+
## Input sources
|
|
67
97
|
|
|
68
|
-
|
|
98
|
+
Tool calls accept exactly one input source:
|
|
69
99
|
|
|
70
|
-
|
|
100
|
+
- Positional JSON: `greet '{"name":"Alice"}'`
|
|
71
101
|
|
|
72
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
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.
|
|
4
|
-
"description": "CLI transport for TMCP
|
|
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
|
-
"
|
|
22
|
+
"sade": "^1.8.1"
|
|
23
23
|
},
|
|
24
24
|
"keywords": [
|
|
25
25
|
"tmcp",
|
|
26
26
|
"cli",
|
|
27
27
|
"transport",
|
|
28
|
-
"
|
|
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 {
|
|
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
|
|
7
|
+
import sade from 'sade';
|
|
9
8
|
|
|
10
9
|
/**
|
|
11
|
-
*
|
|
12
|
-
* @param {string | undefined} json_schema_type
|
|
13
|
-
* @returns {Options["type"]}
|
|
10
|
+
* @typedef {'full' | 'structured' | 'content' | 'text'} OutputMode
|
|
14
11
|
*/
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
*
|
|
33
|
-
* @
|
|
34
|
-
* @returns {Record<string, Options>}
|
|
54
|
+
* @param {Array<unknown>} args
|
|
55
|
+
* @returns {{ positionals: Array<unknown>; options: Record<string, unknown> }}
|
|
35
56
|
*/
|
|
36
|
-
function
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
41
|
-
|
|
67
|
+
return {
|
|
68
|
+
positionals: args,
|
|
69
|
+
options: {},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
42
72
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
48
|
-
|
|
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
|
-
|
|
51
|
-
|
|
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 (
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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 (
|
|
62
|
-
|
|
203
|
+
if (is_last) {
|
|
204
|
+
current[segment] = value;
|
|
205
|
+
return;
|
|
63
206
|
}
|
|
64
207
|
|
|
65
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
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
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
100
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
199
|
-
|
|
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<
|
|
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
|
-
*
|
|
222
|
-
*
|
|
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
|
-
|
|
234
|
-
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
-
|
|
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
|
|
2
|
-
type
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
};
|
package/src/types/index.d.ts
CHANGED
|
@@ -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
|
}
|
package/src/types/index.d.ts.map
CHANGED
|
@@ -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": ";;
|
|
15
|
+
"mappings": ";;cAkTaA,YAAYA;;;;;;;aAzSgCC,UAAUA;aAIZC,WAAWA",
|
|
14
16
|
"ignoreList": []
|
|
15
17
|
}
|