@goke/mcp 0.0.7 → 0.0.9
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 +86 -31
- package/dist/__test__/add-cli-tools-to-mcp.test.d.ts +5 -0
- package/dist/__test__/add-cli-tools-to-mcp.test.d.ts.map +1 -0
- package/dist/__test__/add-cli-tools-to-mcp.test.js +399 -0
- package/dist/__test__/create-mcp-action.test.d.ts +8 -0
- package/dist/__test__/create-mcp-action.test.d.ts.map +1 -0
- package/dist/__test__/create-mcp-action.test.js +274 -0
- package/dist/cli-to-mcp.d.ts +28 -0
- package/dist/cli-to-mcp.d.ts.map +1 -1
- package/dist/cli-to-mcp.js +44 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +28 -16
- package/src/__test__/add-cli-tools-to-mcp.test.ts +459 -0
- package/src/__test__/create-mcp-action.test.ts +353 -0
- package/src/cli-to-mcp.ts +68 -0
- package/src/index.ts +2 -2
package/README.md
CHANGED
|
@@ -62,60 +62,114 @@ notion-mcp-cli notion-retrieve-page --page_id "abc123"
|
|
|
62
62
|
notion-mcp-cli notion-list-users
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
-
##
|
|
65
|
+
## Expose a CLI as an MCP server
|
|
66
66
|
|
|
67
|
-
`
|
|
68
|
-
|
|
69
|
-
- Command description → MCP tool description
|
|
70
|
-
- Option schema (Zod or any Standard Schema library) → MCP `inputSchema` JSON Schema
|
|
71
|
-
- Command names are sanitized into valid MCP tool names (invalid characters become `_`)
|
|
72
|
-
- Composable with existing MCP tools already registered on the same server
|
|
73
|
-
|
|
74
|
-
### With low-level `Server`
|
|
67
|
+
`createMcpAction()` turns your entire CLI into a stdio MCP server with one line. Every CLI command becomes an MCP tool automatically. The command you attach it to is excluded from the tool list.
|
|
75
68
|
|
|
76
69
|
```ts
|
|
77
70
|
import { goke } from "goke"
|
|
78
71
|
import { z } from "zod"
|
|
79
|
-
import {
|
|
80
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
81
|
-
import { addCliToolsToMcp } from "@goke/mcp"
|
|
72
|
+
import { createMcpAction } from "@goke/mcp"
|
|
82
73
|
|
|
83
74
|
const cli = goke("my-cli")
|
|
84
75
|
|
|
85
76
|
cli
|
|
86
|
-
.command("
|
|
77
|
+
.command("search", "Search pages")
|
|
87
78
|
.option("--query <query>", z.string().describe("Search query"))
|
|
88
|
-
.
|
|
79
|
+
.option("--limit [limit]", z.number().default(10).describe("Max results"))
|
|
80
|
+
.action((options) => {
|
|
81
|
+
return { results: findPages(options.query, options.limit) }
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
cli
|
|
85
|
+
.command("deploy <env>", "Deploy to environment")
|
|
86
|
+
.option("--dry-run", z.boolean().default(false).describe("Simulate"))
|
|
87
|
+
.action((env, options) => {
|
|
88
|
+
return options.dryRun ? `would deploy to ${env}` : deploy(env)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
// Add MCP support — runs a stdio MCP server when the user invokes `my-cli mcp`
|
|
92
|
+
cli.command("mcp", "Start MCP server over stdio")
|
|
93
|
+
.action(createMcpAction({ cli }))
|
|
94
|
+
|
|
95
|
+
cli.help()
|
|
96
|
+
cli.parse()
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Now users can use your CLI directly **or** connect it as an MCP server:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
# Use as a normal CLI
|
|
103
|
+
my-cli search --query "meeting notes"
|
|
104
|
+
my-cli deploy staging --dry-run
|
|
105
|
+
|
|
106
|
+
# Use as an MCP server (e.g. from Claude Desktop, Cursor, etc.)
|
|
107
|
+
my-cli mcp
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
When running as MCP, the server exposes `search` and `deploy` as tools. The `mcp` command itself is excluded. Options with Zod schemas (or any Standard Schema) become typed `inputSchema` properties in the MCP tool definition.
|
|
111
|
+
|
|
112
|
+
### Installing the MCP server in clients
|
|
113
|
+
|
|
114
|
+
Users can install your CLI as an MCP server in any client using [`@playwriter/install-mcp`](https://github.com/nicepkg/install-mcp) — a cross-platform tool that handles config file locations for every major MCP client:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
# Install in Claude Desktop
|
|
118
|
+
npx @playwriter/install-mcp my-cli --client claude-desktop
|
|
119
|
+
|
|
120
|
+
# Install in Cursor
|
|
121
|
+
npx @playwriter/install-mcp my-cli --client cursor
|
|
122
|
+
|
|
123
|
+
# Install in VS Code
|
|
124
|
+
npx @playwriter/install-mcp my-cli --client vscode
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
This works with any client: `claude-desktop`, `cursor`, `vscode`, `windsurf`, `claude-code`, `opencode`, `zed`, `goose`, `cline`, `codex`, `gemini-cli`, and [more](https://github.com/supermemoryai/install-mcp#supported-clients). If the command needs custom arguments, pass the full command string:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
npx @playwriter/install-mcp 'npx my-cli mcp' --client cursor
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
`createMcpAction` accepts the same filtering options as `addCliToolsToMcp`:
|
|
134
|
+
|
|
135
|
+
| Option | Type | Default | Description |
|
|
136
|
+
|--------|------|---------|-------------|
|
|
137
|
+
| `cli` | `Goke` | **required** | The CLI instance to expose |
|
|
138
|
+
| `commandFilter` | `(name) => boolean` | — | Additional filter (MCP command is always excluded) |
|
|
139
|
+
| `sanitizeToolName` | `(name) => string` | — | Custom tool name sanitizer |
|
|
140
|
+
| `serverName` | `string` | CLI name | MCP server name |
|
|
141
|
+
| `serverVersion` | `string` | `'1.0.0'` | MCP server version |
|
|
142
|
+
| `createTransport` | `() => Transport` | stdio | Custom transport factory |
|
|
143
|
+
|
|
144
|
+
### Advanced: `addCliToolsToMcp`
|
|
145
|
+
|
|
146
|
+
For more control (composing with existing MCP tools, using a custom server), use `addCliToolsToMcp()` directly:
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js"
|
|
150
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
151
|
+
import { addCliToolsToMcp } from "@goke/mcp"
|
|
89
152
|
|
|
90
153
|
const server = new Server(
|
|
91
154
|
{ name: "my-cli-mcp", version: "1.0.0" },
|
|
92
155
|
{ capabilities: {} },
|
|
93
156
|
)
|
|
94
157
|
|
|
158
|
+
// Mount CLI commands as tools alongside your own
|
|
95
159
|
addCliToolsToMcp({ cli, server })
|
|
96
160
|
|
|
97
161
|
const transport = new StdioServerTransport()
|
|
98
162
|
await server.connect(transport)
|
|
99
163
|
```
|
|
100
164
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
```bash
|
|
104
|
-
node dist/server.js
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
### With high-level `McpServer`
|
|
165
|
+
Also works with the high-level `McpServer`:
|
|
108
166
|
|
|
109
167
|
```ts
|
|
110
168
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
|
|
111
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
112
|
-
import { addCliToolsToMcp } from "@goke/mcp"
|
|
113
169
|
|
|
114
170
|
const mcp = new McpServer({ name: "my-cli-mcp", version: "1.0.0" })
|
|
171
|
+
mcp.tool("custom-tool", "A tool defined directly", async () => ({ ... }))
|
|
115
172
|
addCliToolsToMcp({ cli, server: mcp })
|
|
116
|
-
|
|
117
|
-
const transport = new StdioServerTransport()
|
|
118
|
-
await mcp.connect(transport)
|
|
119
173
|
```
|
|
120
174
|
|
|
121
175
|
## Full example (with config persistence)
|
|
@@ -220,13 +274,14 @@ Registers MCP tool commands on a goke CLI instance.
|
|
|
220
274
|
### Exports
|
|
221
275
|
|
|
222
276
|
```ts
|
|
223
|
-
//
|
|
277
|
+
// MCP server → CLI (consume MCP tools as CLI commands)
|
|
224
278
|
export { addMcpCommands } from '@goke/mcp'
|
|
225
|
-
|
|
226
|
-
// Types
|
|
227
|
-
export type { AddMcpCommandsOptions } from '@goke/mcp'
|
|
228
|
-
export type { CachedMcpTools } from '@goke/mcp'
|
|
279
|
+
export type { AddMcpCommandsOptions, CachedMcpTools } from '@goke/mcp'
|
|
229
280
|
export type { McpOAuthConfig, McpOAuthState } from '@goke/mcp'
|
|
281
|
+
|
|
282
|
+
// CLI → MCP server (expose CLI commands as MCP tools)
|
|
283
|
+
export { createMcpAction, addCliToolsToMcp } from '@goke/mcp'
|
|
284
|
+
export type { CreateMcpActionOptions, AddCliToolsToMcpOptions } from '@goke/mcp'
|
|
230
285
|
```
|
|
231
286
|
|
|
232
287
|
## OAuth flow
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"add-cli-tools-to-mcp.test.d.ts","sourceRoot":"","sources":["../../src/__test__/add-cli-tools-to-mcp.test.ts"],"names":[],"mappings":"AAAA;;GAEG"}
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* End-to-end tests for exposing a goke CLI as MCP tools.
|
|
3
|
+
*/
|
|
4
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
5
|
+
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
|
6
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
7
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8
|
+
import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError } from "@modelcontextprotocol/sdk/types.js";
|
|
9
|
+
import { goke, wrapJsonSchema } from "goke";
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
import { describe, expect, it } from "vitest";
|
|
12
|
+
import { addCliToolsToMcp } from "../cli-to-mcp.js";
|
|
13
|
+
function createCli() {
|
|
14
|
+
const cli = goke("test-cli");
|
|
15
|
+
cli
|
|
16
|
+
.command("say hi", "Say hello")
|
|
17
|
+
.option("--name <name>", z.string().describe("Person to greet"))
|
|
18
|
+
.option("--caps", z.boolean().default(false).describe("Uppercase output"))
|
|
19
|
+
.action((options) => {
|
|
20
|
+
const message = `Hello ${options.name}!`;
|
|
21
|
+
return options.caps ? message.toUpperCase() : message;
|
|
22
|
+
});
|
|
23
|
+
cli
|
|
24
|
+
.command("sum-values", "Add two numbers")
|
|
25
|
+
.option("--left <left>", wrapJsonSchema({
|
|
26
|
+
type: "number",
|
|
27
|
+
description: "Left operand",
|
|
28
|
+
}))
|
|
29
|
+
.option("--right <right>", wrapJsonSchema({
|
|
30
|
+
type: "number",
|
|
31
|
+
description: "Right operand",
|
|
32
|
+
}))
|
|
33
|
+
.action((options) => ({
|
|
34
|
+
sum: options.left + options.right,
|
|
35
|
+
}));
|
|
36
|
+
cli
|
|
37
|
+
.command("echo <message>", "Echo positional message")
|
|
38
|
+
.option("--repeat [repeat]", wrapJsonSchema({
|
|
39
|
+
type: "integer",
|
|
40
|
+
default: 2,
|
|
41
|
+
description: "Repeat count",
|
|
42
|
+
}))
|
|
43
|
+
.action((message, options) => {
|
|
44
|
+
return message.repeat(options.repeat);
|
|
45
|
+
});
|
|
46
|
+
cli
|
|
47
|
+
.command("string-options", "Infer option types from plain string descriptions")
|
|
48
|
+
.option("--title <title>", "Required title")
|
|
49
|
+
.option("--tag [tag]", "Optional tag")
|
|
50
|
+
.option("--dry-run", "Dry run flag")
|
|
51
|
+
.action((options) => {
|
|
52
|
+
return options;
|
|
53
|
+
});
|
|
54
|
+
return cli;
|
|
55
|
+
}
|
|
56
|
+
function firstTextContent(result) {
|
|
57
|
+
const content = "content" in result ? result.content : [];
|
|
58
|
+
return content.find((entry) => entry.type === "text")?.text ?? "";
|
|
59
|
+
}
|
|
60
|
+
function expectCommandDescriptions(tools) {
|
|
61
|
+
const descriptionByName = Object.fromEntries(tools.map((tool) => [tool.name, tool.description]));
|
|
62
|
+
expect(descriptionByName).toMatchObject({
|
|
63
|
+
say_hi: "Say hello",
|
|
64
|
+
"sum-values": "Add two numbers",
|
|
65
|
+
echo: "Echo positional message",
|
|
66
|
+
"string-options": "Infer option types from plain string descriptions",
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
function addUserLowLevelTools(server) {
|
|
70
|
+
server.registerCapabilities({ tools: { listChanged: true } });
|
|
71
|
+
server.setRequestHandler(ListToolsRequestSchema, () => ({
|
|
72
|
+
tools: [
|
|
73
|
+
{
|
|
74
|
+
name: "user_tool",
|
|
75
|
+
description: "Tool added directly by user",
|
|
76
|
+
inputSchema: {
|
|
77
|
+
type: "object",
|
|
78
|
+
properties: {},
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
}));
|
|
83
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
84
|
+
if (request.params.name !== "user_tool") {
|
|
85
|
+
throw new McpError(ErrorCode.InvalidParams, `Tool ${request.params.name} not found`);
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
content: [{ type: "text", text: "from-user-low-level" }],
|
|
89
|
+
};
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
async function runScenario(mode) {
|
|
93
|
+
const cli = createCli();
|
|
94
|
+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
95
|
+
const server = mode === "low-level-server"
|
|
96
|
+
? new Server({ name: "test-server", version: "1.0.0" }, { capabilities: {} })
|
|
97
|
+
: new McpServer({ name: "test-server", version: "1.0.0" });
|
|
98
|
+
addCliToolsToMcp({ cli, server });
|
|
99
|
+
const client = new Client({ name: "test-client", version: "1.0.0" }, { capabilities: {} });
|
|
100
|
+
try {
|
|
101
|
+
await server.connect(serverTransport);
|
|
102
|
+
await client.connect(clientTransport);
|
|
103
|
+
const toolsResult = await client.listTools();
|
|
104
|
+
const greetingResult = await client.callTool({
|
|
105
|
+
name: "say_hi",
|
|
106
|
+
arguments: { name: "Tommy" },
|
|
107
|
+
});
|
|
108
|
+
const sumResult = await client.callTool({
|
|
109
|
+
name: "sum-values",
|
|
110
|
+
arguments: { left: 2, right: 3 },
|
|
111
|
+
});
|
|
112
|
+
const echoResult = await client.callTool({
|
|
113
|
+
name: "echo",
|
|
114
|
+
arguments: { message: "ha" },
|
|
115
|
+
});
|
|
116
|
+
const stringOptionsResult = await client.callTool({
|
|
117
|
+
name: "string-options",
|
|
118
|
+
arguments: {
|
|
119
|
+
title: "Release notes",
|
|
120
|
+
dryRun: true,
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
return {
|
|
124
|
+
tools: toolsResult.tools,
|
|
125
|
+
greeting: firstTextContent(greetingResult),
|
|
126
|
+
sum: firstTextContent(sumResult),
|
|
127
|
+
echo: firstTextContent(echoResult),
|
|
128
|
+
stringOptions: firstTextContent(stringOptionsResult),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
await client.close();
|
|
133
|
+
await server.close();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
describe("addCliToolsToMcp", () => {
|
|
137
|
+
it("mounts CLI tools and executes calls with low-level Server", async () => {
|
|
138
|
+
const result = await runScenario("low-level-server");
|
|
139
|
+
expect("\n" + JSON.stringify(result.tools, null, 2)).toMatchInlineSnapshot(`
|
|
140
|
+
"
|
|
141
|
+
[
|
|
142
|
+
{
|
|
143
|
+
"name": "say_hi",
|
|
144
|
+
"description": "Say hello",
|
|
145
|
+
"inputSchema": {
|
|
146
|
+
"type": "object",
|
|
147
|
+
"properties": {
|
|
148
|
+
"name": {
|
|
149
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
150
|
+
"type": "string",
|
|
151
|
+
"description": "Person to greet"
|
|
152
|
+
},
|
|
153
|
+
"caps": {
|
|
154
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
155
|
+
"default": false,
|
|
156
|
+
"description": "Uppercase output",
|
|
157
|
+
"type": "boolean"
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
"required": [
|
|
161
|
+
"name"
|
|
162
|
+
]
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
"name": "sum-values",
|
|
167
|
+
"description": "Add two numbers",
|
|
168
|
+
"inputSchema": {
|
|
169
|
+
"type": "object",
|
|
170
|
+
"properties": {
|
|
171
|
+
"left": {
|
|
172
|
+
"type": "number",
|
|
173
|
+
"description": "Left operand"
|
|
174
|
+
},
|
|
175
|
+
"right": {
|
|
176
|
+
"type": "number",
|
|
177
|
+
"description": "Right operand"
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
"required": [
|
|
181
|
+
"left",
|
|
182
|
+
"right"
|
|
183
|
+
]
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
"name": "echo",
|
|
188
|
+
"description": "Echo positional message",
|
|
189
|
+
"inputSchema": {
|
|
190
|
+
"type": "object",
|
|
191
|
+
"properties": {
|
|
192
|
+
"message": {
|
|
193
|
+
"type": "string",
|
|
194
|
+
"description": "Positional argument message"
|
|
195
|
+
},
|
|
196
|
+
"repeat": {
|
|
197
|
+
"type": "integer",
|
|
198
|
+
"default": 2,
|
|
199
|
+
"description": "Repeat count"
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
"required": [
|
|
203
|
+
"message"
|
|
204
|
+
]
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
"name": "string-options",
|
|
209
|
+
"description": "Infer option types from plain string descriptions",
|
|
210
|
+
"inputSchema": {
|
|
211
|
+
"type": "object",
|
|
212
|
+
"properties": {
|
|
213
|
+
"title": {
|
|
214
|
+
"type": "string",
|
|
215
|
+
"description": "Required title"
|
|
216
|
+
},
|
|
217
|
+
"tag": {
|
|
218
|
+
"type": "string",
|
|
219
|
+
"description": "Optional tag"
|
|
220
|
+
},
|
|
221
|
+
"dryRun": {
|
|
222
|
+
"type": "boolean",
|
|
223
|
+
"description": "Dry run flag"
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
"required": [
|
|
227
|
+
"title"
|
|
228
|
+
]
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
]"
|
|
232
|
+
`);
|
|
233
|
+
expectCommandDescriptions(result.tools);
|
|
234
|
+
expect(result.greeting).toBe("Hello Tommy!");
|
|
235
|
+
expect(result.sum).toBe('{\n "sum": 5\n}');
|
|
236
|
+
expect(result.echo).toBe("haha");
|
|
237
|
+
expect(result.stringOptions).toBe('{\n "title": "Release notes",\n "dryRun": true\n}');
|
|
238
|
+
});
|
|
239
|
+
it("mounts CLI tools and executes calls with high-level McpServer", async () => {
|
|
240
|
+
const result = await runScenario("mcp-server");
|
|
241
|
+
expect("\n" + JSON.stringify(result.tools, null, 2)).toMatchInlineSnapshot(`
|
|
242
|
+
"
|
|
243
|
+
[
|
|
244
|
+
{
|
|
245
|
+
"name": "say_hi",
|
|
246
|
+
"description": "Say hello",
|
|
247
|
+
"inputSchema": {
|
|
248
|
+
"type": "object",
|
|
249
|
+
"properties": {
|
|
250
|
+
"name": {
|
|
251
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
252
|
+
"type": "string",
|
|
253
|
+
"description": "Person to greet"
|
|
254
|
+
},
|
|
255
|
+
"caps": {
|
|
256
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
257
|
+
"default": false,
|
|
258
|
+
"description": "Uppercase output",
|
|
259
|
+
"type": "boolean"
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
"required": [
|
|
263
|
+
"name"
|
|
264
|
+
]
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
"name": "sum-values",
|
|
269
|
+
"description": "Add two numbers",
|
|
270
|
+
"inputSchema": {
|
|
271
|
+
"type": "object",
|
|
272
|
+
"properties": {
|
|
273
|
+
"left": {
|
|
274
|
+
"type": "number",
|
|
275
|
+
"description": "Left operand"
|
|
276
|
+
},
|
|
277
|
+
"right": {
|
|
278
|
+
"type": "number",
|
|
279
|
+
"description": "Right operand"
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
"required": [
|
|
283
|
+
"left",
|
|
284
|
+
"right"
|
|
285
|
+
]
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
"name": "echo",
|
|
290
|
+
"description": "Echo positional message",
|
|
291
|
+
"inputSchema": {
|
|
292
|
+
"type": "object",
|
|
293
|
+
"properties": {
|
|
294
|
+
"message": {
|
|
295
|
+
"type": "string",
|
|
296
|
+
"description": "Positional argument message"
|
|
297
|
+
},
|
|
298
|
+
"repeat": {
|
|
299
|
+
"type": "integer",
|
|
300
|
+
"default": 2,
|
|
301
|
+
"description": "Repeat count"
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
"required": [
|
|
305
|
+
"message"
|
|
306
|
+
]
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
"name": "string-options",
|
|
311
|
+
"description": "Infer option types from plain string descriptions",
|
|
312
|
+
"inputSchema": {
|
|
313
|
+
"type": "object",
|
|
314
|
+
"properties": {
|
|
315
|
+
"title": {
|
|
316
|
+
"type": "string",
|
|
317
|
+
"description": "Required title"
|
|
318
|
+
},
|
|
319
|
+
"tag": {
|
|
320
|
+
"type": "string",
|
|
321
|
+
"description": "Optional tag"
|
|
322
|
+
},
|
|
323
|
+
"dryRun": {
|
|
324
|
+
"type": "boolean",
|
|
325
|
+
"description": "Dry run flag"
|
|
326
|
+
}
|
|
327
|
+
},
|
|
328
|
+
"required": [
|
|
329
|
+
"title"
|
|
330
|
+
]
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
]"
|
|
334
|
+
`);
|
|
335
|
+
expectCommandDescriptions(result.tools);
|
|
336
|
+
expect(result.greeting).toBe("Hello Tommy!");
|
|
337
|
+
expect(result.sum).toBe('{\n "sum": 5\n}');
|
|
338
|
+
expect(result.echo).toBe("haha");
|
|
339
|
+
expect(result.stringOptions).toBe('{\n "title": "Release notes",\n "dryRun": true\n}');
|
|
340
|
+
});
|
|
341
|
+
it("composes with user tools already mounted on low-level Server", async () => {
|
|
342
|
+
const cli = createCli();
|
|
343
|
+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
344
|
+
const server = new Server({ name: "test-server", version: "1.0.0" }, { capabilities: {} });
|
|
345
|
+
const client = new Client({ name: "test-client", version: "1.0.0" }, { capabilities: {} });
|
|
346
|
+
addUserLowLevelTools(server);
|
|
347
|
+
addCliToolsToMcp({ cli, server, commandFilter: (name) => name === "say hi" });
|
|
348
|
+
try {
|
|
349
|
+
await server.connect(serverTransport);
|
|
350
|
+
await client.connect(clientTransport);
|
|
351
|
+
const tools = await client.listTools();
|
|
352
|
+
expect(tools.tools.map((tool) => tool.name).sort()).toEqual(["say_hi", "user_tool"]);
|
|
353
|
+
const cliResult = await client.callTool({
|
|
354
|
+
name: "say_hi",
|
|
355
|
+
arguments: { name: "Tommy" },
|
|
356
|
+
});
|
|
357
|
+
const userResult = await client.callTool({
|
|
358
|
+
name: "user_tool",
|
|
359
|
+
arguments: {},
|
|
360
|
+
});
|
|
361
|
+
expect(firstTextContent(cliResult)).toBe("Hello Tommy!");
|
|
362
|
+
expect(firstTextContent(userResult)).toBe("from-user-low-level");
|
|
363
|
+
}
|
|
364
|
+
finally {
|
|
365
|
+
await client.close();
|
|
366
|
+
await server.close();
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
it("composes with user tools already mounted on high-level McpServer", async () => {
|
|
370
|
+
const cli = createCli();
|
|
371
|
+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
372
|
+
const server = new McpServer({ name: "test-server", version: "1.0.0" });
|
|
373
|
+
const client = new Client({ name: "test-client", version: "1.0.0" }, { capabilities: {} });
|
|
374
|
+
server.tool("user_tool", "Tool added directly by user", async () => ({
|
|
375
|
+
content: [{ type: "text", text: "from-user-mcp-server" }],
|
|
376
|
+
}));
|
|
377
|
+
addCliToolsToMcp({ cli, server, commandFilter: (name) => name === "say hi" });
|
|
378
|
+
try {
|
|
379
|
+
await server.connect(serverTransport);
|
|
380
|
+
await client.connect(clientTransport);
|
|
381
|
+
const tools = await client.listTools();
|
|
382
|
+
expect(tools.tools.map((tool) => tool.name).sort()).toEqual(["say_hi", "user_tool"]);
|
|
383
|
+
const cliResult = await client.callTool({
|
|
384
|
+
name: "say_hi",
|
|
385
|
+
arguments: { name: "Tommy" },
|
|
386
|
+
});
|
|
387
|
+
const userResult = await client.callTool({
|
|
388
|
+
name: "user_tool",
|
|
389
|
+
arguments: {},
|
|
390
|
+
});
|
|
391
|
+
expect(firstTextContent(cliResult)).toBe("Hello Tommy!");
|
|
392
|
+
expect(firstTextContent(userResult)).toBe("from-user-mcp-server");
|
|
393
|
+
}
|
|
394
|
+
finally {
|
|
395
|
+
await client.close();
|
|
396
|
+
await server.close();
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for createMcpAction — turning a CLI into a stdio MCP server.
|
|
3
|
+
*
|
|
4
|
+
* Uses InMemoryTransport (via createTransport option) to avoid actual stdio.
|
|
5
|
+
* Simulates the goke runtime by setting matchedCommandName before calling the action.
|
|
6
|
+
*/
|
|
7
|
+
export {};
|
|
8
|
+
//# sourceMappingURL=create-mcp-action.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create-mcp-action.test.d.ts","sourceRoot":"","sources":["../../src/__test__/create-mcp-action.test.ts"],"names":[],"mappings":"AAAA;;;;;GAKG"}
|