@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 +76 -39
- package/package.json +8 -5
- package/src/index.js +527 -132
- 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,84 @@ 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
|
+
- 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,
|
|
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
|
|
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
|
-
|
|
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
|
|
153
|
+
```typescript
|
|
154
|
+
run(ctx?: TCustom, argv?: string[]): Promise<void>
|
|
155
|
+
```
|
|
115
156
|
|
|
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.
|
|
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.
|
|
4
|
-
"description": "CLI transport for TMCP
|
|
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
|
-
"
|
|
25
|
+
"sade": "^1.8.1"
|
|
23
26
|
},
|
|
24
27
|
"keywords": [
|
|
25
28
|
"tmcp",
|
|
26
29
|
"cli",
|
|
27
30
|
"transport",
|
|
28
|
-
"
|
|
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 {
|
|
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
|
|
8
|
+
import sade from 'sade';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
*
|
|
12
|
-
* @param {string | undefined} json_schema_type
|
|
13
|
-
* @returns {Options["type"]}
|
|
11
|
+
* @typedef {'full' | 'structured' | 'content' | 'text'} OutputMode
|
|
14
12
|
*/
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
*
|
|
33
|
-
* @
|
|
34
|
-
* @returns {Record<string, Options>}
|
|
56
|
+
* @param {Array<unknown>} args
|
|
57
|
+
* @returns {{ positionals: Array<unknown>; options: Record<string, unknown> }}
|
|
35
58
|
*/
|
|
36
|
-
function
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
41
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
48
|
-
|
|
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 (
|
|
51
|
-
|
|
158
|
+
if (!is_record(current) || !(segment in current)) {
|
|
159
|
+
throw new Error(`Unknown field path: ${path.join('.')}`);
|
|
52
160
|
}
|
|
53
161
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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 (
|
|
62
|
-
|
|
205
|
+
if (is_last) {
|
|
206
|
+
current[segment] = value;
|
|
207
|
+
return;
|
|
63
208
|
}
|
|
64
209
|
|
|
65
|
-
|
|
210
|
+
current[segment] ??= next_value;
|
|
211
|
+
current = /** @type {Record<string, unknown> | Array<unknown>} */ (
|
|
212
|
+
current[segment]
|
|
213
|
+
);
|
|
66
214
|
}
|
|
215
|
+
}
|
|
67
216
|
|
|
68
|
-
|
|
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
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
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
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
100
|
-
|
|
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
|
|
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 =
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
199
|
-
|
|
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<
|
|
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
|
-
*
|
|
222
|
-
*
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
.
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
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
|
|
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": ";;cAsVaA,YAAYA;;;;;;;aA5UgCC,UAAUA;aAIZC,WAAWA",
|
|
14
16
|
"ignoreList": []
|
|
15
17
|
}
|