@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 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
- ## Turn a goke CLI into an MCP server
65
+ ## Expose a CLI as an MCP server
66
66
 
67
- `addCliToolsToMcp()` does the inverse mapping: every CLI command becomes an MCP tool.
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 { Server } from "@modelcontextprotocol/sdk/server/index.js"
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("notion search", "Search Notion pages")
77
+ .command("search", "Search pages")
87
78
  .option("--query <query>", z.string().describe("Search query"))
88
- .action((options) => ({ query: options.query }))
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
- Run it with Node:
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
- // Main function
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,5 @@
1
+ /**
2
+ * End-to-end tests for exposing a goke CLI as MCP tools.
3
+ */
4
+ export {};
5
+ //# sourceMappingURL=add-cli-tools-to-mcp.test.d.ts.map
@@ -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"}