@forinda/kickjs-mcp 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +86 -0
- package/dist/index.d.mts +461 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +552 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +107 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Felix Orinda
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# @forinda/kickjs-mcp
|
|
2
|
+
|
|
3
|
+
[Model Context Protocol](https://modelcontextprotocol.io) server adapter for KickJS. Expose `@Controller` endpoints as callable MCP tools for Claude Code, Cursor, Zed, and other MCP-aware clients — with zero duplicated schemas.
|
|
4
|
+
|
|
5
|
+
## Status
|
|
6
|
+
|
|
7
|
+
**v0 — skeleton.** The decorator and adapter surface exist and compile against the framework. The tool-discovery scan and MCP SDK wiring are still TODOs inside the adapter's lifecycle hooks. This package is part of Workstream 1 of the v3 AI plan and will reach v1 when:
|
|
8
|
+
|
|
9
|
+
- [ ] Tool discovery scans every `@Controller` registered in the DI container
|
|
10
|
+
- [ ] Zod body schemas are converted to JSON Schema via the shared Swagger converter
|
|
11
|
+
- [ ] `stdio`, `sse`, and `http` transports are wired to `@modelcontextprotocol/sdk`
|
|
12
|
+
- [ ] The `kick mcp` CLI command starts a standalone MCP server
|
|
13
|
+
- [ ] Integration test: start a KickJS app, connect an MCP client, call a tool
|
|
14
|
+
- [ ] Example app in `examples/mcp-server-api/`
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pnpm add @forinda/kickjs-mcp @modelcontextprotocol/sdk
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage (planned)
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { bootstrap } from '@forinda/kickjs'
|
|
26
|
+
import { McpAdapter } from '@forinda/kickjs-mcp'
|
|
27
|
+
import { modules } from './modules'
|
|
28
|
+
|
|
29
|
+
export const app = await bootstrap({
|
|
30
|
+
modules,
|
|
31
|
+
adapters: [
|
|
32
|
+
new McpAdapter({
|
|
33
|
+
name: 'task-api',
|
|
34
|
+
version: '1.0.0',
|
|
35
|
+
description: 'Task management MCP server',
|
|
36
|
+
mode: 'explicit', // only @McpTool-decorated methods
|
|
37
|
+
transport: 'sse',
|
|
38
|
+
}),
|
|
39
|
+
],
|
|
40
|
+
})
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Mark controller methods with `@McpTool` to expose them:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { Controller, Post, type Ctx } from '@forinda/kickjs'
|
|
47
|
+
import { McpTool } from '@forinda/kickjs-mcp'
|
|
48
|
+
import { createTaskSchema } from './dtos/create-task.dto'
|
|
49
|
+
|
|
50
|
+
@Controller('/tasks')
|
|
51
|
+
export class TaskController {
|
|
52
|
+
@Post('/', { body: createTaskSchema, name: 'CreateTask' })
|
|
53
|
+
@McpTool({
|
|
54
|
+
description: 'Create a new task with title, priority, and optional assignee',
|
|
55
|
+
examples: [
|
|
56
|
+
{ args: { title: 'Ship v3', priority: 'high' } },
|
|
57
|
+
],
|
|
58
|
+
})
|
|
59
|
+
create(ctx: Ctx<KickRoutes.TaskController['create']>) {
|
|
60
|
+
return this.createTaskUseCase.execute(ctx.body)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The input schema of each tool is derived automatically from the route's Zod `body` schema. Add a field, and both OpenAPI (via the Swagger adapter) and MCP pick it up on the next restart — no duplicated type declarations.
|
|
66
|
+
|
|
67
|
+
## Exposure modes
|
|
68
|
+
|
|
69
|
+
| Mode | Behavior |
|
|
70
|
+
|------|----------|
|
|
71
|
+
| `explicit` (default) | Only methods decorated with `@McpTool` are exposed. |
|
|
72
|
+
| `auto` | Every route is exposed, subject to `include` / `exclude` filters. |
|
|
73
|
+
|
|
74
|
+
Use `explicit` in production unless you've carefully reviewed every route. `auto` is convenient during development but can leak admin or internal endpoints if you forget to filter them out.
|
|
75
|
+
|
|
76
|
+
## Transports
|
|
77
|
+
|
|
78
|
+
| Transport | Use case |
|
|
79
|
+
|-----------|----------|
|
|
80
|
+
| `stdio` | CLI MCP clients (Claude Code, Cursor). Run via `kick mcp` in a separate process. |
|
|
81
|
+
| `sse` | Mounted on the existing Express app. Default for long-running servers. |
|
|
82
|
+
| `http` | Simpler than SSE; gives up live notifications. |
|
|
83
|
+
|
|
84
|
+
## License
|
|
85
|
+
|
|
86
|
+
MIT
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
|
|
2
|
+
import { AdapterContext, AppAdapter, Constructor } from "@forinda/kickjs";
|
|
3
|
+
import { ZodTypeAny } from "zod";
|
|
4
|
+
|
|
5
|
+
//#region src/types.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* Transport modes supported by the MCP adapter.
|
|
8
|
+
*
|
|
9
|
+
* - `stdio` — standard MCP transport for CLI clients (Claude Code, Cursor).
|
|
10
|
+
* The MCP server owns stdin/stdout. Cannot be combined with a normal
|
|
11
|
+
* Express dev server in the same process without care.
|
|
12
|
+
* - `sse` — Server-Sent Events over HTTP. Good fit when KickJS already
|
|
13
|
+
* exposes an HTTP server — the MCP endpoints mount on the same app.
|
|
14
|
+
* - `http` — plain HTTP POST/GET streaming. Simpler than SSE for some
|
|
15
|
+
* clients but gives up live notifications.
|
|
16
|
+
*/
|
|
17
|
+
type McpTransport = 'stdio' | 'sse' | 'http';
|
|
18
|
+
/**
|
|
19
|
+
* How the adapter decides which endpoints become MCP tools.
|
|
20
|
+
*
|
|
21
|
+
* - `explicit` (default) — only methods decorated with `@McpTool` are
|
|
22
|
+
* exposed. Safest default; prevents accidental exposure of internal
|
|
23
|
+
* endpoints or admin routes.
|
|
24
|
+
* - `auto` — every route discovered at startup becomes a tool, subject
|
|
25
|
+
* to the `include` / `exclude` filters. Use with care in production.
|
|
26
|
+
*/
|
|
27
|
+
type McpExposureMode = 'explicit' | 'auto';
|
|
28
|
+
/**
|
|
29
|
+
* Authentication configuration for the MCP transport.
|
|
30
|
+
*
|
|
31
|
+
* For `stdio`, auth is usually not needed (client and server share a
|
|
32
|
+
* process). For `sse` and `http`, set this so the adapter refuses
|
|
33
|
+
* unauthenticated tool calls.
|
|
34
|
+
*/
|
|
35
|
+
interface McpAuthOptions {
|
|
36
|
+
/** Strategy to use. `bearer` reads `Authorization: Bearer <token>`. */
|
|
37
|
+
type: 'bearer' | 'custom';
|
|
38
|
+
/** Called on every tool invocation. Return true (or truthy data) to allow. */
|
|
39
|
+
validate: (token: string) => boolean | Promise<boolean>;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Options for the `McpAdapter` constructor.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```ts
|
|
46
|
+
* new McpAdapter({
|
|
47
|
+
* name: 'task-api',
|
|
48
|
+
* version: '1.0.0',
|
|
49
|
+
* description: 'Task management MCP server',
|
|
50
|
+
* mode: 'explicit',
|
|
51
|
+
* transport: 'sse',
|
|
52
|
+
* })
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
interface McpAdapterOptions {
|
|
56
|
+
/** MCP server name advertised to clients. Usually matches package.json name. */
|
|
57
|
+
name: string;
|
|
58
|
+
/** Server version advertised to clients. Defaults to '0.0.0' if omitted. */
|
|
59
|
+
version?: string;
|
|
60
|
+
/** Human-readable description shown in MCP client UIs. */
|
|
61
|
+
description?: string;
|
|
62
|
+
/** Exposure mode. Defaults to `'explicit'`. */
|
|
63
|
+
mode?: McpExposureMode;
|
|
64
|
+
/** Transport mode. Defaults to `'sse'`. */
|
|
65
|
+
transport?: McpTransport;
|
|
66
|
+
/** HTTP methods to include when `mode === 'auto'`. */
|
|
67
|
+
include?: Array<'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'>;
|
|
68
|
+
/** Glob-style path prefixes to exclude when `mode === 'auto'`. */
|
|
69
|
+
exclude?: string[];
|
|
70
|
+
/** Auth config for `sse` and `http` transports. */
|
|
71
|
+
auth?: McpAuthOptions;
|
|
72
|
+
/** Base path for the MCP endpoint (SSE/HTTP only). Defaults to `/_mcp`. */
|
|
73
|
+
basePath?: string;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Example input/output pair shown alongside a tool description.
|
|
77
|
+
*
|
|
78
|
+
* Models can use examples to learn the expected shape of arguments and
|
|
79
|
+
* what a successful call returns. Keep examples small and representative.
|
|
80
|
+
*/
|
|
81
|
+
interface McpToolExample {
|
|
82
|
+
/** Natural-language description of what this example does. */
|
|
83
|
+
description?: string;
|
|
84
|
+
/** Arguments to pass to the tool. Must match the Zod input schema. */
|
|
85
|
+
args: Record<string, unknown>;
|
|
86
|
+
/** Expected result shape. Used in docs only — not validated. */
|
|
87
|
+
result?: unknown;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Options for the `@McpTool` decorator.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```ts
|
|
94
|
+
* @Post('/', { body: createTaskSchema, name: 'CreateTask' })
|
|
95
|
+
* @McpTool({
|
|
96
|
+
* description: 'Create a new task',
|
|
97
|
+
* examples: [{ args: { title: 'Ship v3', priority: 'high' } }],
|
|
98
|
+
* })
|
|
99
|
+
* create(ctx: Ctx<KickRoutes.TaskController['create']>) {}
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
interface McpToolOptions {
|
|
103
|
+
/**
|
|
104
|
+
* Override the tool name. Defaults to `<ControllerName>.<methodName>`.
|
|
105
|
+
* Tool names must be unique across the entire MCP server.
|
|
106
|
+
*/
|
|
107
|
+
name?: string;
|
|
108
|
+
/**
|
|
109
|
+
* Human-readable description of what the tool does. Shown to the LLM
|
|
110
|
+
* when it decides whether to call this tool. Be specific: "Create a
|
|
111
|
+
* task" is less useful than "Create a new task with the given title,
|
|
112
|
+
* priority, and optional assignee".
|
|
113
|
+
*/
|
|
114
|
+
description: string;
|
|
115
|
+
/**
|
|
116
|
+
* Optional input schema override. If omitted, the adapter derives
|
|
117
|
+
* the input schema from the route's `body` Zod schema (if any).
|
|
118
|
+
*/
|
|
119
|
+
inputSchema?: ZodTypeAny;
|
|
120
|
+
/**
|
|
121
|
+
* Optional output schema for documentation. Not validated at runtime.
|
|
122
|
+
*/
|
|
123
|
+
outputSchema?: ZodTypeAny;
|
|
124
|
+
/** Optional usage examples shown in the tool description. */
|
|
125
|
+
examples?: McpToolExample[];
|
|
126
|
+
/**
|
|
127
|
+
* When set to `true`, exclude this tool from any `auto` exposure mode
|
|
128
|
+
* filter. Useful to mark admin-only routes inside otherwise-exposed
|
|
129
|
+
* controllers.
|
|
130
|
+
*/
|
|
131
|
+
hidden?: boolean;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Resolved tool definition after scanning decorators at startup.
|
|
135
|
+
*
|
|
136
|
+
* This is the shape the adapter hands to the MCP SDK when registering
|
|
137
|
+
* tools. Users don't construct this directly — it's derived from
|
|
138
|
+
* `@McpTool` metadata plus route metadata from `@Controller`.
|
|
139
|
+
*
|
|
140
|
+
* Both the raw Zod schema (`zodInputSchema`) and the converted JSON
|
|
141
|
+
* Schema (`inputSchema`) are kept on the definition. The MCP SDK
|
|
142
|
+
* accepts the Zod form directly, while the JSON Schema is exposed via
|
|
143
|
+
* `getTools()` for inspection, the `kick mcp --list` command, and
|
|
144
|
+
* documentation surfaces.
|
|
145
|
+
*/
|
|
146
|
+
interface McpToolDefinition {
|
|
147
|
+
/** Resolved tool name (either from options.name or derived). */
|
|
148
|
+
name: string;
|
|
149
|
+
/** Human-readable description. */
|
|
150
|
+
description: string;
|
|
151
|
+
/** JSON Schema for tool inputs, derived from the Zod body schema. */
|
|
152
|
+
inputSchema: Record<string, unknown>;
|
|
153
|
+
/**
|
|
154
|
+
* Original Zod schema for the tool input, when one was attached to
|
|
155
|
+
* the route via `body` (or via `@McpTool({ inputSchema })`). The MCP
|
|
156
|
+
* SDK accepts this form directly via `registerTool`. May be undefined
|
|
157
|
+
* for routes without a body schema.
|
|
158
|
+
*/
|
|
159
|
+
zodInputSchema?: unknown;
|
|
160
|
+
/** Optional JSON Schema for tool outputs. */
|
|
161
|
+
outputSchema?: Record<string, unknown>;
|
|
162
|
+
/** HTTP method of the underlying route. */
|
|
163
|
+
httpMethod: string;
|
|
164
|
+
/** Full mount path of the underlying route (after apiPrefix + version). */
|
|
165
|
+
mountPath: string;
|
|
166
|
+
/** Examples for documentation. */
|
|
167
|
+
examples?: McpToolExample[];
|
|
168
|
+
}
|
|
169
|
+
//#endregion
|
|
170
|
+
//#region src/mcp.adapter.d.ts
|
|
171
|
+
/**
|
|
172
|
+
* Expose a KickJS application as a Model Context Protocol (MCP) server.
|
|
173
|
+
*
|
|
174
|
+
* The adapter implements `onRouteMount` to collect every registered
|
|
175
|
+
* controller alongside its mount path. During `beforeStart` (after all
|
|
176
|
+
* modules have finished mounting), it walks the collected controllers,
|
|
177
|
+
* reads route metadata via `getClassMeta(METADATA.ROUTES, ...)`, and
|
|
178
|
+
* builds a `McpToolDefinition[]` that the MCP SDK will register as
|
|
179
|
+
* callable tools.
|
|
180
|
+
*
|
|
181
|
+
* The input schema of each tool is the JSON Schema equivalent of the
|
|
182
|
+
* route's Zod `body` schema, converted via the package's own
|
|
183
|
+
* `zod-to-json-schema` helper. Tools with no body schema get an empty
|
|
184
|
+
* object schema so the model can still call them with no arguments.
|
|
185
|
+
*
|
|
186
|
+
* @example
|
|
187
|
+
* ```ts
|
|
188
|
+
* import { bootstrap } from '@forinda/kickjs'
|
|
189
|
+
* import { McpAdapter } from '@forinda/kickjs-mcp'
|
|
190
|
+
* import { modules } from './modules'
|
|
191
|
+
*
|
|
192
|
+
* export const app = await bootstrap({
|
|
193
|
+
* modules,
|
|
194
|
+
* adapters: [
|
|
195
|
+
* new McpAdapter({
|
|
196
|
+
* name: 'task-api',
|
|
197
|
+
* version: '1.0.0',
|
|
198
|
+
* description: 'Task management MCP server',
|
|
199
|
+
* mode: 'explicit',
|
|
200
|
+
* transport: 'sse',
|
|
201
|
+
* }),
|
|
202
|
+
* ],
|
|
203
|
+
* })
|
|
204
|
+
* ```
|
|
205
|
+
*
|
|
206
|
+
* @remarks
|
|
207
|
+
* Tool discovery is complete. The remaining work for v1 is wiring the
|
|
208
|
+
* generated tool definitions to `@modelcontextprotocol/sdk` in
|
|
209
|
+
* `afterStart` — see the TODO markers in that hook. Until then,
|
|
210
|
+
* `getTools()` returns the discovered definitions so tests can assert
|
|
211
|
+
* the scan produced the expected shape.
|
|
212
|
+
*/
|
|
213
|
+
declare class McpAdapter implements AppAdapter {
|
|
214
|
+
readonly name = "McpAdapter";
|
|
215
|
+
private readonly options;
|
|
216
|
+
/** Controllers collected during the mount phase, in insertion order. */
|
|
217
|
+
private readonly mountedControllers;
|
|
218
|
+
/** Discovered tool definitions, built during `beforeStart`. */
|
|
219
|
+
private readonly tools;
|
|
220
|
+
/** Active MCP server instance, created in `afterStart`. */
|
|
221
|
+
private mcpServer;
|
|
222
|
+
/**
|
|
223
|
+
* Active MCP transport, created in `afterStart`. Can be either a
|
|
224
|
+
* `StreamableHTTPServerTransport` (the default for HTTP-based MCP)
|
|
225
|
+
* or a `StdioServerTransport` (when running via the `kick mcp` CLI
|
|
226
|
+
* or with `KICK_MCP_STDIO=1`).
|
|
227
|
+
*/
|
|
228
|
+
private transport;
|
|
229
|
+
/**
|
|
230
|
+
* Base URL of the running KickJS HTTP server, captured in `afterStart`
|
|
231
|
+
* once the server is listening. Tool dispatch makes internal HTTP
|
|
232
|
+
* requests against this base URL so calls flow through the normal
|
|
233
|
+
* Express pipeline (middleware, validation, auth, logging, error
|
|
234
|
+
* handling) rather than bypassing it.
|
|
235
|
+
*
|
|
236
|
+
* Format: `http://127.0.0.1:<port>`. Set to `null` until afterStart
|
|
237
|
+
* runs and reset to `null` on shutdown.
|
|
238
|
+
*/
|
|
239
|
+
private serverBaseUrl;
|
|
240
|
+
constructor(options: McpAdapterOptions);
|
|
241
|
+
/**
|
|
242
|
+
* Called by the framework each time a module mounts a controller.
|
|
243
|
+
*
|
|
244
|
+
* We don't inspect routes here — we just record the pair and process
|
|
245
|
+
* everything in `beforeStart` once mounting is fully complete. This
|
|
246
|
+
* keeps the scan logic in one place and makes it easier to unit test.
|
|
247
|
+
*/
|
|
248
|
+
onRouteMount(controller: Constructor, mountPath: string): void;
|
|
249
|
+
/**
|
|
250
|
+
* Walk collected controllers, read route metadata, and materialize
|
|
251
|
+
* `McpToolDefinition[]`.
|
|
252
|
+
*
|
|
253
|
+
* Runs after every module has mounted but before the HTTP server
|
|
254
|
+
* starts listening, so the MCP server can be initialized in
|
|
255
|
+
* `afterStart` with a complete tool list.
|
|
256
|
+
*/
|
|
257
|
+
beforeStart(_ctx: AdapterContext): void;
|
|
258
|
+
/**
|
|
259
|
+
* Start the MCP server on the configured transport.
|
|
260
|
+
*
|
|
261
|
+
* - `http` (recommended): mounts a `StreamableHTTPServerTransport` on
|
|
262
|
+
* the existing Express app at `${basePath}/messages`. This is the
|
|
263
|
+
* modern, spec-compliant way to expose MCP over HTTP.
|
|
264
|
+
* - `sse` (deprecated): currently aliases to `http` and emits a warning.
|
|
265
|
+
* The MCP SSE transport class is deprecated upstream in favor of
|
|
266
|
+
* StreamableHTTP, which already supports SSE-style streaming under
|
|
267
|
+
* the hood.
|
|
268
|
+
* - `stdio`: skipped here. The standalone `kick mcp` CLI command
|
|
269
|
+
* instantiates the adapter directly and connects it to a stdio
|
|
270
|
+
* transport so dev logs don't interfere.
|
|
271
|
+
*/
|
|
272
|
+
afterStart(ctx: AdapterContext): Promise<void>;
|
|
273
|
+
/**
|
|
274
|
+
* Decide which transport to actually start.
|
|
275
|
+
*
|
|
276
|
+
* Precedence: an explicit `KICK_MCP_STDIO=1` environment variable
|
|
277
|
+
* always wins, because that's how the `kick mcp` CLI command tells
|
|
278
|
+
* the running process to switch to stdio mode without requiring the
|
|
279
|
+
* user to edit their bootstrap. Otherwise the constructor option is
|
|
280
|
+
* honored as-is.
|
|
281
|
+
*/
|
|
282
|
+
private resolveTransportMode;
|
|
283
|
+
/**
|
|
284
|
+
* Start the MCP server bound to process stdio.
|
|
285
|
+
*
|
|
286
|
+
* Used by the `kick mcp` CLI: the parent process pipes its stdin/
|
|
287
|
+
* stdout to this adapter so MCP clients (Claude Code, Cursor) can
|
|
288
|
+
* speak the protocol over the wire. Logs MUST go to stderr in this
|
|
289
|
+
* mode — anything written to stdout corrupts the JSON-RPC stream.
|
|
290
|
+
*
|
|
291
|
+
* The HTTP server is still running (the framework called start()
|
|
292
|
+
* before afterStart), but we don't mount the MCP routes on it. Tool
|
|
293
|
+
* dispatch routes through fetch against `serverBaseUrl` exactly the
|
|
294
|
+
* same way it does in HTTP mode, so dispatch behavior is uniform
|
|
295
|
+
* across transports.
|
|
296
|
+
*/
|
|
297
|
+
private startStdioTransport;
|
|
298
|
+
/**
|
|
299
|
+
* Tear down the MCP server and any open transports.
|
|
300
|
+
*
|
|
301
|
+
* Called during graceful shutdown. Idempotent — KickJS may invoke
|
|
302
|
+
* `shutdown` more than once under error conditions.
|
|
303
|
+
*/
|
|
304
|
+
shutdown(): Promise<void>;
|
|
305
|
+
/**
|
|
306
|
+
* Return the list of tools discovered during startup.
|
|
307
|
+
*
|
|
308
|
+
* Primary consumers:
|
|
309
|
+
* - the `kick mcp --list` command
|
|
310
|
+
* - unit tests that verify a route was exposed as expected
|
|
311
|
+
*/
|
|
312
|
+
getTools(): readonly McpToolDefinition[];
|
|
313
|
+
/**
|
|
314
|
+
* Build a `McpToolDefinition` for a single route, or return `null`
|
|
315
|
+
* if the route should be skipped under the current exposure mode.
|
|
316
|
+
*
|
|
317
|
+
* Scoping rules:
|
|
318
|
+
* - `explicit` (default): only routes with `@McpTool` are exposed
|
|
319
|
+
* - `auto`: every route is exposed, filtered by `include` /
|
|
320
|
+
* `exclude`; a `hidden: true` on `@McpTool` still drops the route
|
|
321
|
+
*/
|
|
322
|
+
private tryBuildTool;
|
|
323
|
+
/**
|
|
324
|
+
* Derive a default description for routes exposed in `auto` mode
|
|
325
|
+
* without an explicit `@McpTool` decorator. Kept intentionally
|
|
326
|
+
* generic — teams running `auto` should still add `@McpTool` with
|
|
327
|
+
* real descriptions for any tool the model is expected to call
|
|
328
|
+
* reliably.
|
|
329
|
+
*/
|
|
330
|
+
private deriveDescription;
|
|
331
|
+
/**
|
|
332
|
+
* Join a module mount path with the route-level sub-path.
|
|
333
|
+
*
|
|
334
|
+
* Mount path already includes the API prefix + version (e.g.
|
|
335
|
+
* `/api/v1/tasks`); the route-level `path` is relative (e.g. `/:id`).
|
|
336
|
+
* Trailing/leading slashes are normalized so the final URL is stable.
|
|
337
|
+
*/
|
|
338
|
+
private joinMountPath;
|
|
339
|
+
/**
|
|
340
|
+
* Construct the underlying `McpServer` and register every discovered
|
|
341
|
+
* tool against it. The SDK accepts Zod schemas natively, so we pass
|
|
342
|
+
* `zodInputSchema` straight through and skip the JSON Schema form
|
|
343
|
+
* here (the JSON Schema is for inspection / docs).
|
|
344
|
+
*
|
|
345
|
+
* Tool calls dispatch through the Express pipeline via internal HTTP
|
|
346
|
+
* requests against the running server's address (captured in
|
|
347
|
+
* `afterStart`). This preserves middleware, validation, auth, and
|
|
348
|
+
* logging — tool calls behave exactly like external HTTP requests
|
|
349
|
+
* to the same route.
|
|
350
|
+
*/
|
|
351
|
+
private buildMcpServer;
|
|
352
|
+
/**
|
|
353
|
+
* Dispatch a tool call through the Express pipeline.
|
|
354
|
+
*
|
|
355
|
+
* Builds an HTTP request that matches the tool's underlying route
|
|
356
|
+
* (method + path + body or query string from the args) and sends it
|
|
357
|
+
* to the running server's `serverBaseUrl`. The request goes through
|
|
358
|
+
* every middleware the route normally hits — auth, validation,
|
|
359
|
+
* logging, error handling — so tool calls observe exactly the same
|
|
360
|
+
* guarantees as external HTTP clients.
|
|
361
|
+
*
|
|
362
|
+
* Path parameters (e.g. `/:id`) are substituted from `args` before
|
|
363
|
+
* the request fires; matching keys are removed from the body/query
|
|
364
|
+
* to avoid sending them twice.
|
|
365
|
+
*
|
|
366
|
+
* Returns a `CallToolResult` whose `content` contains the response
|
|
367
|
+
* body as text. Non-2xx responses are flagged with `isError: true`
|
|
368
|
+
* so the calling LLM can react.
|
|
369
|
+
*/
|
|
370
|
+
private dispatchTool;
|
|
371
|
+
/**
|
|
372
|
+
* Substitute Express-style path parameters (`:id`) in `mountPath`
|
|
373
|
+
* with values from `args`. Returns the resolved path plus the args
|
|
374
|
+
* that were NOT consumed by parameters, so they can be sent as the
|
|
375
|
+
* request body or query string.
|
|
376
|
+
*
|
|
377
|
+
* If a `:param` is referenced in the path but missing from args,
|
|
378
|
+
* the placeholder is left in place — the request will hit a 404 from
|
|
379
|
+
* the underlying route, which is reported back as an MCP error.
|
|
380
|
+
*/
|
|
381
|
+
private substitutePathParams;
|
|
382
|
+
/**
|
|
383
|
+
* Resolve the running server's base URL from a Node `http.Server`
|
|
384
|
+
* instance. Returns null if the server isn't listening or its
|
|
385
|
+
* address can't be determined (e.g. when the adapter is mounted
|
|
386
|
+
* standalone for testing).
|
|
387
|
+
*
|
|
388
|
+
* IPv6 addresses are wrapped in brackets per RFC 3986. The hostname
|
|
389
|
+
* `0.0.0.0` (Linux default) is rewritten to `127.0.0.1` because the
|
|
390
|
+
* former is not a valid request target on all platforms.
|
|
391
|
+
*/
|
|
392
|
+
private resolveServerBaseUrl;
|
|
393
|
+
/**
|
|
394
|
+
* Mount the StreamableHTTP transport endpoints on the existing
|
|
395
|
+
* Express app. The transport handles three HTTP verbs at a single
|
|
396
|
+
* URL:
|
|
397
|
+
* - POST: client → server messages (initialize, tool calls, etc.)
|
|
398
|
+
* - GET: server → client SSE stream for notifications
|
|
399
|
+
* - DELETE: client tells the server to terminate a session
|
|
400
|
+
*
|
|
401
|
+
* We mount all three on `${basePath}/messages` so a single URL is
|
|
402
|
+
* the entire MCP surface area.
|
|
403
|
+
*/
|
|
404
|
+
private mountHttpRoutes;
|
|
405
|
+
}
|
|
406
|
+
//#endregion
|
|
407
|
+
//#region src/decorators.d.ts
|
|
408
|
+
/**
|
|
409
|
+
* Mark a controller method as an MCP tool.
|
|
410
|
+
*
|
|
411
|
+
* The adapter scans for this decorator at startup and registers the
|
|
412
|
+
* method as a callable tool on the MCP server. The input schema is
|
|
413
|
+
* inferred from the route's `body` Zod schema — you don't repeat it here.
|
|
414
|
+
*
|
|
415
|
+
* @example
|
|
416
|
+
* ```ts
|
|
417
|
+
* import { Controller, Post, type Ctx } from '@forinda/kickjs'
|
|
418
|
+
* import { McpTool } from '@forinda/kickjs-mcp'
|
|
419
|
+
* import { createTaskSchema } from './dtos/create-task.dto'
|
|
420
|
+
*
|
|
421
|
+
* @Controller('/tasks')
|
|
422
|
+
* export class TaskController {
|
|
423
|
+
* @Post('/', { body: createTaskSchema, name: 'CreateTask' })
|
|
424
|
+
* @McpTool({ description: 'Create a new task' })
|
|
425
|
+
* create(ctx: Ctx<KickRoutes.TaskController['create']>) {
|
|
426
|
+
* // ... existing handler ...
|
|
427
|
+
* }
|
|
428
|
+
* }
|
|
429
|
+
* ```
|
|
430
|
+
*
|
|
431
|
+
* In `explicit` mode (the default), only methods with this decorator
|
|
432
|
+
* are exposed as tools. In `auto` mode, it still controls the human-
|
|
433
|
+
* readable description and examples shown to the model.
|
|
434
|
+
*/
|
|
435
|
+
declare function McpTool(options: McpToolOptions): MethodDecorator;
|
|
436
|
+
/**
|
|
437
|
+
* Read the MCP tool metadata attached to a method, if any.
|
|
438
|
+
*
|
|
439
|
+
* Returns `undefined` if the method was not decorated with `@McpTool`.
|
|
440
|
+
* The adapter uses this during the startup scan to decide whether a
|
|
441
|
+
* route should be registered as a tool.
|
|
442
|
+
*/
|
|
443
|
+
declare function getMcpToolMeta(target: object, method: string): McpToolOptions | undefined;
|
|
444
|
+
/** Check whether a method was decorated with `@McpTool`. */
|
|
445
|
+
declare function isMcpTool(target: object, method: string): boolean;
|
|
446
|
+
//#endregion
|
|
447
|
+
//#region src/constants.d.ts
|
|
448
|
+
/**
|
|
449
|
+
* Metadata key for the `@McpTool` decorator.
|
|
450
|
+
*
|
|
451
|
+
* Using `createToken` gives a collision-safe, type-carrying identifier:
|
|
452
|
+
* the phantom type parameter flows through `getMethodMetaOrUndefined`
|
|
453
|
+
* so consumers get `McpToolOptions` back without a manual cast, and
|
|
454
|
+
* reference-equality guarantees that two separate definitions of
|
|
455
|
+
* `MCP_TOOL_METADATA` can never shadow each other even if the package
|
|
456
|
+
* is loaded more than once.
|
|
457
|
+
*/
|
|
458
|
+
declare const MCP_TOOL_METADATA: any;
|
|
459
|
+
//#endregion
|
|
460
|
+
export { MCP_TOOL_METADATA, McpAdapter, type McpAdapterOptions, type McpAuthOptions, type McpExposureMode, McpTool, type McpToolDefinition, type McpToolExample, type McpToolOptions, type McpTransport, getMcpToolMeta, isMcpTool };
|
|
461
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/mcp.adapter.ts","../src/decorators.ts","../src/constants.ts"],"mappings":";;;;;;;;AAaA;;;;;AAWA;;;KAXY,YAAA;;AAoBZ;;;;;;;;KATY,eAAA;;AA8BZ;;;;;;UArBiB,cAAA;EAqCM;EAnCrB,IAAA;EAqBA;EAnBA,QAAA,GAAW,KAAA,uBAA4B,OAAA;AAAA;;;;;;;;;;;;;AA4CzC;;UA3BiB,iBAAA;EA+BH;EA7BZ,IAAA;EA6BA;EA3BA,OAAA;EA6BA;EA3BA,WAAA;EA2BM;EAzBN,IAAA,GAAO,eAAA;EAyCsB;EAvC7B,SAAA,GAAY,YAAA;EAwDE;EAtDd,OAAA,GAAU,KAAA;EA4DC;EA1DX,OAAA;EA0DyB;EAxDzB,IAAA,GAAO,cAAA;EA6CP;EA3CA,QAAA;AAAA;;;;;;;UASe,cAAA;EAmEA;EAjEf,WAAA;;EAEA,IAAA,EAAM,MAAA;EA8ES;EA5Ef,MAAA;AAAA;;;;;;;;;;;;;;UAgBe,cAAA;;;;ACxCjB;ED6CE,IAAA;;;;;;;EAOA,WAAA;ECkKqB;;;;ED7JrB,WAAA,GAAc,UAAA;ECxDL;;;ED4DT,YAAA,GAAe,UAAA;EC3CP;ED6CR,QAAA,GAAW,cAAA;ECzBH;;;;;ED+BR,MAAA;AAAA;;;;;;;;;;;;;;UAgBe,iBAAA;ECgJP;ED9IR,IAAA;EC+MQ;ED7MR,WAAA;ECqRc;EDnRd,WAAA,EAAa,MAAA;ECiYL;;;;;;ED1XR,cAAA;EElIc;EFoId,YAAA,GAAe,MAAA;;EAEf,UAAA;EEtI+B;EFwI/B,SAAA;EExIgD;EF0IhD,QAAA,GAAW,cAAA;AAAA;;;;;AA5Jb;;;;;AAWA;;;;;AASA;;;;;;;;;;AAqBA;;;;;;;;;;;;;;;;;;;;cCSa,UAAA,YAAsB,UAAA;EAAA,SACxB,IAAA;EAAA,iBAEQ,OAAA;EDMT;EAAA,iBCAS,kBAAA;EDSY;EAAA,iBCHZ,KAAA;EDOL;EAAA,QCJJ,SAAA;EDIR;;;;;AAkBF;EAlBE,QCIQ,SAAA;;;;;;;;;;;UAYA,aAAA;cAEI,OAAA,EAAS,iBAAA;EDuBrB;;;;;AAsBF;;EC5BE,YAAA,CAAa,UAAA,EAAY,WAAA,EAAa,SAAA;EDkCzB;;;;;;;;ECtBb,WAAA,CAAY,IAAA,EAAM,cAAA;ED6BlB;;;;;;;;;;;;AClGF;;EAkGQ,UAAA,CAAW,GAAA,EAAK,cAAA,GAAiB,OAAA;EA1DlB;;;;;;;;;EAAA,QAiHb,oBAAA;EAzJyB;;;;;;;;;;;;;;EAAA,QA8KnB,mBAAA;EAzGd;;;;;;EAyHM,QAAA,CAAA,GAAY,OAAA;EArCV;;;;;;;EA6DR,QAAA,CAAA,YAAqB,iBAAA;EAgFb;;;;;;;;;EAAA,QAjEA,YAAA;;ACrQV;;;;;;UD2TU,iBAAA;EC3TuD;;AAajE;;;;;EAbiE,QDsUvD,aAAA;ECzTsD;;;AAKhE;;;;;;;;ACpCA;ED+BgE,QD4UtD,cAAA;;;;;;;;;;;;;;;;;;;UAqDM,YAAA;;;;;;;;;;;UAoFN,oBAAA;;;;;;;;;;;UA0BA,oBAAA;;;;;;;;;;;;UAqBA,eAAA;AAAA;;;;;;ADniBV;;;;;AAWA;;;;;AASA;;;;;;;;;;AAqBA;;;;iBEvBgB,OAAA,CAAQ,OAAA,EAAS,cAAA,GAAiB,eAAA;;;;;;;;iBAalC,cAAA,CAAe,MAAA,UAAgB,MAAA,WAAiB,cAAA;;iBAKhD,SAAA,CAAU,MAAA,UAAgB,MAAA;;;;;;;AFpC1C;;;;;AAWA;cGXa,iBAAA"}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @forinda/kickjs-mcp v2.3.0
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) Felix Orinda
|
|
5
|
+
*
|
|
6
|
+
* This source code is licensed under the MIT license found in the
|
|
7
|
+
* LICENSE file in the root directory of this source tree.
|
|
8
|
+
*
|
|
9
|
+
* @license MIT
|
|
10
|
+
*/
|
|
11
|
+
import { randomUUID } from "node:crypto";
|
|
12
|
+
import { Logger, METADATA, createToken, getClassMeta, getMethodMetaOrUndefined, setMethodMeta } from "@forinda/kickjs";
|
|
13
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
14
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
15
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
16
|
+
//#region src/constants.ts
|
|
17
|
+
/**
|
|
18
|
+
* Metadata key for the `@McpTool` decorator.
|
|
19
|
+
*
|
|
20
|
+
* Using `createToken` gives a collision-safe, type-carrying identifier:
|
|
21
|
+
* the phantom type parameter flows through `getMethodMetaOrUndefined`
|
|
22
|
+
* so consumers get `McpToolOptions` back without a manual cast, and
|
|
23
|
+
* reference-equality guarantees that two separate definitions of
|
|
24
|
+
* `MCP_TOOL_METADATA` can never shadow each other even if the package
|
|
25
|
+
* is loaded more than once.
|
|
26
|
+
*/
|
|
27
|
+
const MCP_TOOL_METADATA = createToken("kickjs.mcp.tool");
|
|
28
|
+
//#endregion
|
|
29
|
+
//#region src/decorators.ts
|
|
30
|
+
/**
|
|
31
|
+
* Mark a controller method as an MCP tool.
|
|
32
|
+
*
|
|
33
|
+
* The adapter scans for this decorator at startup and registers the
|
|
34
|
+
* method as a callable tool on the MCP server. The input schema is
|
|
35
|
+
* inferred from the route's `body` Zod schema — you don't repeat it here.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```ts
|
|
39
|
+
* import { Controller, Post, type Ctx } from '@forinda/kickjs'
|
|
40
|
+
* import { McpTool } from '@forinda/kickjs-mcp'
|
|
41
|
+
* import { createTaskSchema } from './dtos/create-task.dto'
|
|
42
|
+
*
|
|
43
|
+
* @Controller('/tasks')
|
|
44
|
+
* export class TaskController {
|
|
45
|
+
* @Post('/', { body: createTaskSchema, name: 'CreateTask' })
|
|
46
|
+
* @McpTool({ description: 'Create a new task' })
|
|
47
|
+
* create(ctx: Ctx<KickRoutes.TaskController['create']>) {
|
|
48
|
+
* // ... existing handler ...
|
|
49
|
+
* }
|
|
50
|
+
* }
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* In `explicit` mode (the default), only methods with this decorator
|
|
54
|
+
* are exposed as tools. In `auto` mode, it still controls the human-
|
|
55
|
+
* readable description and examples shown to the model.
|
|
56
|
+
*/
|
|
57
|
+
function McpTool(options) {
|
|
58
|
+
return (target, propertyKey) => {
|
|
59
|
+
setMethodMeta(MCP_TOOL_METADATA, options, target, propertyKey);
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Read the MCP tool metadata attached to a method, if any.
|
|
64
|
+
*
|
|
65
|
+
* Returns `undefined` if the method was not decorated with `@McpTool`.
|
|
66
|
+
* The adapter uses this during the startup scan to decide whether a
|
|
67
|
+
* route should be registered as a tool.
|
|
68
|
+
*/
|
|
69
|
+
function getMcpToolMeta(target, method) {
|
|
70
|
+
return getMethodMetaOrUndefined(MCP_TOOL_METADATA, target, method);
|
|
71
|
+
}
|
|
72
|
+
/** Check whether a method was decorated with `@McpTool`. */
|
|
73
|
+
function isMcpTool(target, method) {
|
|
74
|
+
return getMcpToolMeta(target, method) !== void 0;
|
|
75
|
+
}
|
|
76
|
+
//#endregion
|
|
77
|
+
//#region src/zod-to-json-schema.ts
|
|
78
|
+
/**
|
|
79
|
+
* Minimal Zod v4+ schema parser.
|
|
80
|
+
*
|
|
81
|
+
* Mirrors the `zodSchemaParser` in `@forinda/kickjs-swagger`. Zod v4
|
|
82
|
+
* ships with a native `.toJSONSchema()` instance method, so this is
|
|
83
|
+
* just a type guard + a call.
|
|
84
|
+
*
|
|
85
|
+
* Kept in-package (rather than importing from Swagger) so the MCP
|
|
86
|
+
* adapter has no dependency on the Swagger package. If KickJS ever
|
|
87
|
+
* extracts a shared `@forinda/kickjs-schema` utility, both adapters
|
|
88
|
+
* can switch to it in one PR.
|
|
89
|
+
*/
|
|
90
|
+
/**
|
|
91
|
+
* Check whether a value looks like a Zod v4+ schema.
|
|
92
|
+
*
|
|
93
|
+
* Uses structural duck-typing: the object has `safeParse` (all Zod
|
|
94
|
+
* versions) AND `toJSONSchema` (Zod v4+). This avoids importing Zod
|
|
95
|
+
* as a value, which would force it to become a runtime dep.
|
|
96
|
+
*/
|
|
97
|
+
function isZodSchema(schema) {
|
|
98
|
+
return schema != null && typeof schema === "object" && typeof schema.safeParse === "function" && typeof schema.toJSONSchema === "function";
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Convert a Zod v4+ schema to a JSON Schema object, stripping the
|
|
102
|
+
* top-level `$schema` key so the output can be embedded inside an
|
|
103
|
+
* MCP tool definition directly.
|
|
104
|
+
*
|
|
105
|
+
* Returns `null` if the input doesn't look like a Zod schema. Callers
|
|
106
|
+
* should fall back to an empty-object input schema in that case.
|
|
107
|
+
*/
|
|
108
|
+
function zodToJsonSchema(schema) {
|
|
109
|
+
if (!isZodSchema(schema)) return null;
|
|
110
|
+
const { $schema: _ignored, ...rest } = schema.toJSONSchema();
|
|
111
|
+
return rest;
|
|
112
|
+
}
|
|
113
|
+
//#endregion
|
|
114
|
+
//#region src/mcp.adapter.ts
|
|
115
|
+
const log = Logger.for("McpAdapter");
|
|
116
|
+
/**
|
|
117
|
+
* Expose a KickJS application as a Model Context Protocol (MCP) server.
|
|
118
|
+
*
|
|
119
|
+
* The adapter implements `onRouteMount` to collect every registered
|
|
120
|
+
* controller alongside its mount path. During `beforeStart` (after all
|
|
121
|
+
* modules have finished mounting), it walks the collected controllers,
|
|
122
|
+
* reads route metadata via `getClassMeta(METADATA.ROUTES, ...)`, and
|
|
123
|
+
* builds a `McpToolDefinition[]` that the MCP SDK will register as
|
|
124
|
+
* callable tools.
|
|
125
|
+
*
|
|
126
|
+
* The input schema of each tool is the JSON Schema equivalent of the
|
|
127
|
+
* route's Zod `body` schema, converted via the package's own
|
|
128
|
+
* `zod-to-json-schema` helper. Tools with no body schema get an empty
|
|
129
|
+
* object schema so the model can still call them with no arguments.
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* ```ts
|
|
133
|
+
* import { bootstrap } from '@forinda/kickjs'
|
|
134
|
+
* import { McpAdapter } from '@forinda/kickjs-mcp'
|
|
135
|
+
* import { modules } from './modules'
|
|
136
|
+
*
|
|
137
|
+
* export const app = await bootstrap({
|
|
138
|
+
* modules,
|
|
139
|
+
* adapters: [
|
|
140
|
+
* new McpAdapter({
|
|
141
|
+
* name: 'task-api',
|
|
142
|
+
* version: '1.0.0',
|
|
143
|
+
* description: 'Task management MCP server',
|
|
144
|
+
* mode: 'explicit',
|
|
145
|
+
* transport: 'sse',
|
|
146
|
+
* }),
|
|
147
|
+
* ],
|
|
148
|
+
* })
|
|
149
|
+
* ```
|
|
150
|
+
*
|
|
151
|
+
* @remarks
|
|
152
|
+
* Tool discovery is complete. The remaining work for v1 is wiring the
|
|
153
|
+
* generated tool definitions to `@modelcontextprotocol/sdk` in
|
|
154
|
+
* `afterStart` — see the TODO markers in that hook. Until then,
|
|
155
|
+
* `getTools()` returns the discovered definitions so tests can assert
|
|
156
|
+
* the scan produced the expected shape.
|
|
157
|
+
*/
|
|
158
|
+
var McpAdapter = class {
|
|
159
|
+
name = "McpAdapter";
|
|
160
|
+
options;
|
|
161
|
+
/** Controllers collected during the mount phase, in insertion order. */
|
|
162
|
+
mountedControllers = [];
|
|
163
|
+
/** Discovered tool definitions, built during `beforeStart`. */
|
|
164
|
+
tools = [];
|
|
165
|
+
/** Active MCP server instance, created in `afterStart`. */
|
|
166
|
+
mcpServer = null;
|
|
167
|
+
/**
|
|
168
|
+
* Active MCP transport, created in `afterStart`. Can be either a
|
|
169
|
+
* `StreamableHTTPServerTransport` (the default for HTTP-based MCP)
|
|
170
|
+
* or a `StdioServerTransport` (when running via the `kick mcp` CLI
|
|
171
|
+
* or with `KICK_MCP_STDIO=1`).
|
|
172
|
+
*/
|
|
173
|
+
transport = null;
|
|
174
|
+
/**
|
|
175
|
+
* Base URL of the running KickJS HTTP server, captured in `afterStart`
|
|
176
|
+
* once the server is listening. Tool dispatch makes internal HTTP
|
|
177
|
+
* requests against this base URL so calls flow through the normal
|
|
178
|
+
* Express pipeline (middleware, validation, auth, logging, error
|
|
179
|
+
* handling) rather than bypassing it.
|
|
180
|
+
*
|
|
181
|
+
* Format: `http://127.0.0.1:<port>`. Set to `null` until afterStart
|
|
182
|
+
* runs and reset to `null` on shutdown.
|
|
183
|
+
*/
|
|
184
|
+
serverBaseUrl = null;
|
|
185
|
+
constructor(options) {
|
|
186
|
+
this.options = {
|
|
187
|
+
mode: options.mode ?? "explicit",
|
|
188
|
+
transport: options.transport ?? "sse",
|
|
189
|
+
basePath: options.basePath ?? "/_mcp",
|
|
190
|
+
version: options.version ?? "0.0.0",
|
|
191
|
+
...options
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Called by the framework each time a module mounts a controller.
|
|
196
|
+
*
|
|
197
|
+
* We don't inspect routes here — we just record the pair and process
|
|
198
|
+
* everything in `beforeStart` once mounting is fully complete. This
|
|
199
|
+
* keeps the scan logic in one place and makes it easier to unit test.
|
|
200
|
+
*/
|
|
201
|
+
onRouteMount(controller, mountPath) {
|
|
202
|
+
this.mountedControllers.push({
|
|
203
|
+
controller,
|
|
204
|
+
mountPath
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Walk collected controllers, read route metadata, and materialize
|
|
209
|
+
* `McpToolDefinition[]`.
|
|
210
|
+
*
|
|
211
|
+
* Runs after every module has mounted but before the HTTP server
|
|
212
|
+
* starts listening, so the MCP server can be initialized in
|
|
213
|
+
* `afterStart` with a complete tool list.
|
|
214
|
+
*/
|
|
215
|
+
beforeStart(_ctx) {
|
|
216
|
+
for (const { controller, mountPath } of this.mountedControllers) {
|
|
217
|
+
const routes = getClassMeta(METADATA.ROUTES, controller, []);
|
|
218
|
+
for (const route of routes) {
|
|
219
|
+
const tool = this.tryBuildTool(controller, mountPath, route);
|
|
220
|
+
if (tool) this.tools.push(tool);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
log.debug(`MCP adapter discovered ${this.tools.length} tool(s) (mode=${this.options.mode}, transport=${this.options.transport})`);
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Start the MCP server on the configured transport.
|
|
227
|
+
*
|
|
228
|
+
* - `http` (recommended): mounts a `StreamableHTTPServerTransport` on
|
|
229
|
+
* the existing Express app at `${basePath}/messages`. This is the
|
|
230
|
+
* modern, spec-compliant way to expose MCP over HTTP.
|
|
231
|
+
* - `sse` (deprecated): currently aliases to `http` and emits a warning.
|
|
232
|
+
* The MCP SSE transport class is deprecated upstream in favor of
|
|
233
|
+
* StreamableHTTP, which already supports SSE-style streaming under
|
|
234
|
+
* the hood.
|
|
235
|
+
* - `stdio`: skipped here. The standalone `kick mcp` CLI command
|
|
236
|
+
* instantiates the adapter directly and connects it to a stdio
|
|
237
|
+
* transport so dev logs don't interfere.
|
|
238
|
+
*/
|
|
239
|
+
async afterStart(ctx) {
|
|
240
|
+
this.serverBaseUrl = this.resolveServerBaseUrl(ctx.server);
|
|
241
|
+
const effectiveTransport = this.resolveTransportMode();
|
|
242
|
+
if (effectiveTransport === "stdio") {
|
|
243
|
+
await this.startStdioTransport();
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (effectiveTransport === "sse") log.warn("sse transport is deprecated upstream; using StreamableHTTP transport, which supports the same SSE wire format under the hood");
|
|
247
|
+
const expressApp = ctx.app;
|
|
248
|
+
if (!expressApp) {
|
|
249
|
+
log.warn("McpAdapter: AdapterContext.app is unavailable, cannot mount HTTP transport");
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
this.mcpServer = this.buildMcpServer();
|
|
253
|
+
const httpTransport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID() });
|
|
254
|
+
this.transport = httpTransport;
|
|
255
|
+
await this.mcpServer.connect(httpTransport);
|
|
256
|
+
this.mountHttpRoutes(expressApp, httpTransport);
|
|
257
|
+
log.info(`McpAdapter ready — ${this.tools.length} tool(s) registered, listening at ${this.options.basePath}/messages`);
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Decide which transport to actually start.
|
|
261
|
+
*
|
|
262
|
+
* Precedence: an explicit `KICK_MCP_STDIO=1` environment variable
|
|
263
|
+
* always wins, because that's how the `kick mcp` CLI command tells
|
|
264
|
+
* the running process to switch to stdio mode without requiring the
|
|
265
|
+
* user to edit their bootstrap. Otherwise the constructor option is
|
|
266
|
+
* honored as-is.
|
|
267
|
+
*/
|
|
268
|
+
resolveTransportMode() {
|
|
269
|
+
if (process.env.KICK_MCP_STDIO === "1" || process.env.KICK_MCP_STDIO === "true") return "stdio";
|
|
270
|
+
return this.options.transport;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Start the MCP server bound to process stdio.
|
|
274
|
+
*
|
|
275
|
+
* Used by the `kick mcp` CLI: the parent process pipes its stdin/
|
|
276
|
+
* stdout to this adapter so MCP clients (Claude Code, Cursor) can
|
|
277
|
+
* speak the protocol over the wire. Logs MUST go to stderr in this
|
|
278
|
+
* mode — anything written to stdout corrupts the JSON-RPC stream.
|
|
279
|
+
*
|
|
280
|
+
* The HTTP server is still running (the framework called start()
|
|
281
|
+
* before afterStart), but we don't mount the MCP routes on it. Tool
|
|
282
|
+
* dispatch routes through fetch against `serverBaseUrl` exactly the
|
|
283
|
+
* same way it does in HTTP mode, so dispatch behavior is uniform
|
|
284
|
+
* across transports.
|
|
285
|
+
*/
|
|
286
|
+
async startStdioTransport() {
|
|
287
|
+
this.mcpServer = this.buildMcpServer();
|
|
288
|
+
this.transport = new StdioServerTransport();
|
|
289
|
+
await this.mcpServer.connect(this.transport);
|
|
290
|
+
log.info(`McpAdapter ready (stdio) — ${this.tools.length} tool(s) registered, dispatching against ${this.serverBaseUrl ?? "unknown"}`);
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Tear down the MCP server and any open transports.
|
|
294
|
+
*
|
|
295
|
+
* Called during graceful shutdown. Idempotent — KickJS may invoke
|
|
296
|
+
* `shutdown` more than once under error conditions.
|
|
297
|
+
*/
|
|
298
|
+
async shutdown() {
|
|
299
|
+
try {
|
|
300
|
+
await this.transport?.close();
|
|
301
|
+
} catch (err) {
|
|
302
|
+
log.error(err, "McpAdapter: failed to close transport");
|
|
303
|
+
}
|
|
304
|
+
try {
|
|
305
|
+
await this.mcpServer?.close();
|
|
306
|
+
} catch (err) {
|
|
307
|
+
log.error(err, "McpAdapter: failed to close server");
|
|
308
|
+
}
|
|
309
|
+
this.transport = null;
|
|
310
|
+
this.mcpServer = null;
|
|
311
|
+
this.serverBaseUrl = null;
|
|
312
|
+
log.debug("McpAdapter shutdown complete");
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Return the list of tools discovered during startup.
|
|
316
|
+
*
|
|
317
|
+
* Primary consumers:
|
|
318
|
+
* - the `kick mcp --list` command
|
|
319
|
+
* - unit tests that verify a route was exposed as expected
|
|
320
|
+
*/
|
|
321
|
+
getTools() {
|
|
322
|
+
return this.tools;
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Build a `McpToolDefinition` for a single route, or return `null`
|
|
326
|
+
* if the route should be skipped under the current exposure mode.
|
|
327
|
+
*
|
|
328
|
+
* Scoping rules:
|
|
329
|
+
* - `explicit` (default): only routes with `@McpTool` are exposed
|
|
330
|
+
* - `auto`: every route is exposed, filtered by `include` /
|
|
331
|
+
* `exclude`; a `hidden: true` on `@McpTool` still drops the route
|
|
332
|
+
*/
|
|
333
|
+
tryBuildTool(controller, mountPath, route) {
|
|
334
|
+
const meta = getMcpToolMeta(controller.prototype, route.handlerName);
|
|
335
|
+
if (this.options.mode === "explicit" && !meta) return null;
|
|
336
|
+
if (meta?.hidden) return null;
|
|
337
|
+
if (this.options.mode === "auto") {
|
|
338
|
+
const methodUpper = route.method.toUpperCase();
|
|
339
|
+
if (this.options.include && !this.options.include.includes(methodUpper)) return null;
|
|
340
|
+
if (this.options.exclude?.some((prefix) => mountPath.startsWith(prefix))) return null;
|
|
341
|
+
}
|
|
342
|
+
const description = meta?.description ?? this.deriveDescription(controller, route);
|
|
343
|
+
const name = meta?.name ?? `${controller.name}.${route.handlerName}`;
|
|
344
|
+
const candidateSchema = meta?.inputSchema ?? route.validation?.body ?? route.validation?.query;
|
|
345
|
+
return {
|
|
346
|
+
name,
|
|
347
|
+
description,
|
|
348
|
+
inputSchema: zodToJsonSchema(candidateSchema) ?? {
|
|
349
|
+
type: "object",
|
|
350
|
+
properties: {},
|
|
351
|
+
additionalProperties: false
|
|
352
|
+
},
|
|
353
|
+
zodInputSchema: candidateSchema,
|
|
354
|
+
outputSchema: (meta?.outputSchema ? zodToJsonSchema(meta.outputSchema) : void 0) ?? void 0,
|
|
355
|
+
httpMethod: route.method.toUpperCase(),
|
|
356
|
+
mountPath: this.joinMountPath(mountPath, route.path),
|
|
357
|
+
examples: meta?.examples
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Derive a default description for routes exposed in `auto` mode
|
|
362
|
+
* without an explicit `@McpTool` decorator. Kept intentionally
|
|
363
|
+
* generic — teams running `auto` should still add `@McpTool` with
|
|
364
|
+
* real descriptions for any tool the model is expected to call
|
|
365
|
+
* reliably.
|
|
366
|
+
*/
|
|
367
|
+
deriveDescription(controller, route) {
|
|
368
|
+
return `${route.method.toUpperCase()} handler ${controller.name}.${route.handlerName}`;
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Join a module mount path with the route-level sub-path.
|
|
372
|
+
*
|
|
373
|
+
* Mount path already includes the API prefix + version (e.g.
|
|
374
|
+
* `/api/v1/tasks`); the route-level `path` is relative (e.g. `/:id`).
|
|
375
|
+
* Trailing/leading slashes are normalized so the final URL is stable.
|
|
376
|
+
*/
|
|
377
|
+
joinMountPath(mountPath, routePath) {
|
|
378
|
+
const base = mountPath.endsWith("/") ? mountPath.slice(0, -1) : mountPath;
|
|
379
|
+
if (!routePath || routePath === "/") return base;
|
|
380
|
+
return `${base}${routePath.startsWith("/") ? routePath : `/${routePath}`}`;
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Construct the underlying `McpServer` and register every discovered
|
|
384
|
+
* tool against it. The SDK accepts Zod schemas natively, so we pass
|
|
385
|
+
* `zodInputSchema` straight through and skip the JSON Schema form
|
|
386
|
+
* here (the JSON Schema is for inspection / docs).
|
|
387
|
+
*
|
|
388
|
+
* Tool calls dispatch through the Express pipeline via internal HTTP
|
|
389
|
+
* requests against the running server's address (captured in
|
|
390
|
+
* `afterStart`). This preserves middleware, validation, auth, and
|
|
391
|
+
* logging — tool calls behave exactly like external HTTP requests
|
|
392
|
+
* to the same route.
|
|
393
|
+
*/
|
|
394
|
+
buildMcpServer() {
|
|
395
|
+
const server = new McpServer({
|
|
396
|
+
name: this.options.name,
|
|
397
|
+
version: this.options.version,
|
|
398
|
+
...this.options.description ? { description: this.options.description } : {}
|
|
399
|
+
});
|
|
400
|
+
const registerTool = server.registerTool.bind(server);
|
|
401
|
+
for (const tool of this.tools) {
|
|
402
|
+
const config = { description: tool.description };
|
|
403
|
+
if (tool.zodInputSchema) config.inputSchema = tool.zodInputSchema;
|
|
404
|
+
registerTool(tool.name, config, async (args) => this.dispatchTool(tool, args));
|
|
405
|
+
}
|
|
406
|
+
return server;
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Dispatch a tool call through the Express pipeline.
|
|
410
|
+
*
|
|
411
|
+
* Builds an HTTP request that matches the tool's underlying route
|
|
412
|
+
* (method + path + body or query string from the args) and sends it
|
|
413
|
+
* to the running server's `serverBaseUrl`. The request goes through
|
|
414
|
+
* every middleware the route normally hits — auth, validation,
|
|
415
|
+
* logging, error handling — so tool calls observe exactly the same
|
|
416
|
+
* guarantees as external HTTP clients.
|
|
417
|
+
*
|
|
418
|
+
* Path parameters (e.g. `/:id`) are substituted from `args` before
|
|
419
|
+
* the request fires; matching keys are removed from the body/query
|
|
420
|
+
* to avoid sending them twice.
|
|
421
|
+
*
|
|
422
|
+
* Returns a `CallToolResult` whose `content` contains the response
|
|
423
|
+
* body as text. Non-2xx responses are flagged with `isError: true`
|
|
424
|
+
* so the calling LLM can react.
|
|
425
|
+
*/
|
|
426
|
+
async dispatchTool(tool, rawArgs) {
|
|
427
|
+
if (!this.serverBaseUrl) return {
|
|
428
|
+
isError: true,
|
|
429
|
+
content: [{
|
|
430
|
+
type: "text",
|
|
431
|
+
text: `Cannot dispatch ${tool.name}: HTTP server address not yet captured`
|
|
432
|
+
}]
|
|
433
|
+
};
|
|
434
|
+
const args = rawArgs ?? {};
|
|
435
|
+
const { path, remainingArgs } = this.substitutePathParams(tool.mountPath, args);
|
|
436
|
+
const method = tool.httpMethod.toUpperCase();
|
|
437
|
+
const hasBody = method === "POST" || method === "PUT" || method === "PATCH";
|
|
438
|
+
let url = `${this.serverBaseUrl}${path}`;
|
|
439
|
+
const init = {
|
|
440
|
+
method,
|
|
441
|
+
headers: {
|
|
442
|
+
accept: "application/json",
|
|
443
|
+
"x-mcp-tool": tool.name
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
if (hasBody) {
|
|
447
|
+
init.headers["content-type"] = "application/json";
|
|
448
|
+
init.body = JSON.stringify(remainingArgs);
|
|
449
|
+
} else if (Object.keys(remainingArgs).length > 0) {
|
|
450
|
+
const qs = new URLSearchParams();
|
|
451
|
+
for (const [key, value] of Object.entries(remainingArgs)) {
|
|
452
|
+
if (value === void 0 || value === null) continue;
|
|
453
|
+
qs.append(key, typeof value === "string" ? value : JSON.stringify(value));
|
|
454
|
+
}
|
|
455
|
+
const sep = url.includes("?") ? "&" : "?";
|
|
456
|
+
url = `${url}${sep}${qs.toString()}`;
|
|
457
|
+
}
|
|
458
|
+
try {
|
|
459
|
+
const res = await fetch(url, init);
|
|
460
|
+
const text = await res.text();
|
|
461
|
+
return {
|
|
462
|
+
isError: res.status >= 400,
|
|
463
|
+
content: [{
|
|
464
|
+
type: "text",
|
|
465
|
+
text: text || `(${res.status} ${res.statusText})`
|
|
466
|
+
}]
|
|
467
|
+
};
|
|
468
|
+
} catch (err) {
|
|
469
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
470
|
+
log.error(err, `McpAdapter: dispatch failed for ${tool.name}`);
|
|
471
|
+
return {
|
|
472
|
+
isError: true,
|
|
473
|
+
content: [{
|
|
474
|
+
type: "text",
|
|
475
|
+
text: `Tool dispatch error: ${message}`
|
|
476
|
+
}]
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Substitute Express-style path parameters (`:id`) in `mountPath`
|
|
482
|
+
* with values from `args`. Returns the resolved path plus the args
|
|
483
|
+
* that were NOT consumed by parameters, so they can be sent as the
|
|
484
|
+
* request body or query string.
|
|
485
|
+
*
|
|
486
|
+
* If a `:param` is referenced in the path but missing from args,
|
|
487
|
+
* the placeholder is left in place — the request will hit a 404 from
|
|
488
|
+
* the underlying route, which is reported back as an MCP error.
|
|
489
|
+
*/
|
|
490
|
+
substitutePathParams(mountPath, args) {
|
|
491
|
+
const remaining = { ...args };
|
|
492
|
+
return {
|
|
493
|
+
path: mountPath.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, (_match, param) => {
|
|
494
|
+
if (param in remaining) {
|
|
495
|
+
const value = remaining[param];
|
|
496
|
+
delete remaining[param];
|
|
497
|
+
return encodeURIComponent(String(value));
|
|
498
|
+
}
|
|
499
|
+
return `:${param}`;
|
|
500
|
+
}),
|
|
501
|
+
remainingArgs: remaining
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Resolve the running server's base URL from a Node `http.Server`
|
|
506
|
+
* instance. Returns null if the server isn't listening or its
|
|
507
|
+
* address can't be determined (e.g. when the adapter is mounted
|
|
508
|
+
* standalone for testing).
|
|
509
|
+
*
|
|
510
|
+
* IPv6 addresses are wrapped in brackets per RFC 3986. The hostname
|
|
511
|
+
* `0.0.0.0` (Linux default) is rewritten to `127.0.0.1` because the
|
|
512
|
+
* former is not a valid request target on all platforms.
|
|
513
|
+
*/
|
|
514
|
+
resolveServerBaseUrl(server) {
|
|
515
|
+
if (!server) return null;
|
|
516
|
+
const address = server.address();
|
|
517
|
+
if (!address || typeof address === "string") return null;
|
|
518
|
+
let host = address.address;
|
|
519
|
+
if (host === "::" || host === "0.0.0.0" || host === "") host = "127.0.0.1";
|
|
520
|
+
if (host.includes(":") && !host.startsWith("[")) host = `[${host}]`;
|
|
521
|
+
return `http://${host}:${address.port}`;
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Mount the StreamableHTTP transport endpoints on the existing
|
|
525
|
+
* Express app. The transport handles three HTTP verbs at a single
|
|
526
|
+
* URL:
|
|
527
|
+
* - POST: client → server messages (initialize, tool calls, etc.)
|
|
528
|
+
* - GET: server → client SSE stream for notifications
|
|
529
|
+
* - DELETE: client tells the server to terminate a session
|
|
530
|
+
*
|
|
531
|
+
* We mount all three on `${basePath}/messages` so a single URL is
|
|
532
|
+
* the entire MCP surface area.
|
|
533
|
+
*/
|
|
534
|
+
mountHttpRoutes(app, transport) {
|
|
535
|
+
const path = `${this.options.basePath}/messages`;
|
|
536
|
+
const handleRequest = async (req, res) => {
|
|
537
|
+
try {
|
|
538
|
+
await transport.handleRequest(req, res, req.body);
|
|
539
|
+
} catch (err) {
|
|
540
|
+
log.error(err, `McpAdapter: error handling ${req.method} ${path}`);
|
|
541
|
+
if (!res.headersSent) res.status(500).json({ error: "MCP transport error" });
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
app.post(path, handleRequest);
|
|
545
|
+
app.get(path, handleRequest);
|
|
546
|
+
app.delete(path, handleRequest);
|
|
547
|
+
}
|
|
548
|
+
};
|
|
549
|
+
//#endregion
|
|
550
|
+
export { MCP_TOOL_METADATA, McpAdapter, McpTool, getMcpToolMeta, isMcpTool };
|
|
551
|
+
|
|
552
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/constants.ts","../src/decorators.ts","../src/zod-to-json-schema.ts","../src/mcp.adapter.ts"],"sourcesContent":["import { createToken } from '@forinda/kickjs'\nimport type { McpToolOptions } from './types'\n\n/**\n * Metadata key for the `@McpTool` decorator.\n *\n * Using `createToken` gives a collision-safe, type-carrying identifier:\n * the phantom type parameter flows through `getMethodMetaOrUndefined`\n * so consumers get `McpToolOptions` back without a manual cast, and\n * reference-equality guarantees that two separate definitions of\n * `MCP_TOOL_METADATA` can never shadow each other even if the package\n * is loaded more than once.\n */\nexport const MCP_TOOL_METADATA = createToken<McpToolOptions>('kickjs.mcp.tool')\n","import { setMethodMeta, getMethodMetaOrUndefined } from '@forinda/kickjs'\nimport { MCP_TOOL_METADATA } from './constants'\nimport type { McpToolOptions } from './types'\n\n/**\n * Mark a controller method as an MCP tool.\n *\n * The adapter scans for this decorator at startup and registers the\n * method as a callable tool on the MCP server. The input schema is\n * inferred from the route's `body` Zod schema — you don't repeat it here.\n *\n * @example\n * ```ts\n * import { Controller, Post, type Ctx } from '@forinda/kickjs'\n * import { McpTool } from '@forinda/kickjs-mcp'\n * import { createTaskSchema } from './dtos/create-task.dto'\n *\n * @Controller('/tasks')\n * export class TaskController {\n * @Post('/', { body: createTaskSchema, name: 'CreateTask' })\n * @McpTool({ description: 'Create a new task' })\n * create(ctx: Ctx<KickRoutes.TaskController['create']>) {\n * // ... existing handler ...\n * }\n * }\n * ```\n *\n * In `explicit` mode (the default), only methods with this decorator\n * are exposed as tools. In `auto` mode, it still controls the human-\n * readable description and examples shown to the model.\n */\nexport function McpTool(options: McpToolOptions): MethodDecorator {\n return (target, propertyKey) => {\n setMethodMeta(MCP_TOOL_METADATA, options, target, propertyKey as string)\n }\n}\n\n/**\n * Read the MCP tool metadata attached to a method, if any.\n *\n * Returns `undefined` if the method was not decorated with `@McpTool`.\n * The adapter uses this during the startup scan to decide whether a\n * route should be registered as a tool.\n */\nexport function getMcpToolMeta(target: object, method: string): McpToolOptions | undefined {\n return getMethodMetaOrUndefined<McpToolOptions>(MCP_TOOL_METADATA, target, method)\n}\n\n/** Check whether a method was decorated with `@McpTool`. */\nexport function isMcpTool(target: object, method: string): boolean {\n return getMcpToolMeta(target, method) !== undefined\n}\n","/**\n * Minimal Zod v4+ schema parser.\n *\n * Mirrors the `zodSchemaParser` in `@forinda/kickjs-swagger`. Zod v4\n * ships with a native `.toJSONSchema()` instance method, so this is\n * just a type guard + a call.\n *\n * Kept in-package (rather than importing from Swagger) so the MCP\n * adapter has no dependency on the Swagger package. If KickJS ever\n * extracts a shared `@forinda/kickjs-schema` utility, both adapters\n * can switch to it in one PR.\n */\n\n/**\n * Check whether a value looks like a Zod v4+ schema.\n *\n * Uses structural duck-typing: the object has `safeParse` (all Zod\n * versions) AND `toJSONSchema` (Zod v4+). This avoids importing Zod\n * as a value, which would force it to become a runtime dep.\n */\nexport function isZodSchema(schema: unknown): boolean {\n return (\n schema != null &&\n typeof schema === 'object' &&\n typeof (schema as { safeParse?: unknown }).safeParse === 'function' &&\n typeof (schema as { toJSONSchema?: unknown }).toJSONSchema === 'function'\n )\n}\n\n/**\n * Convert a Zod v4+ schema to a JSON Schema object, stripping the\n * top-level `$schema` key so the output can be embedded inside an\n * MCP tool definition directly.\n *\n * Returns `null` if the input doesn't look like a Zod schema. Callers\n * should fall back to an empty-object input schema in that case.\n */\nexport function zodToJsonSchema(schema: unknown): Record<string, unknown> | null {\n if (!isZodSchema(schema)) return null\n const { $schema: _ignored, ...rest } = (\n schema as { toJSONSchema: () => Record<string, unknown> }\n ).toJSONSchema() as Record<string, unknown>\n return rest\n}\n","import { randomUUID } from 'node:crypto'\nimport {\n Logger,\n METADATA,\n getClassMeta,\n type AppAdapter,\n type AdapterContext,\n type Constructor,\n type RouteDefinition,\n} from '@forinda/kickjs'\nimport type { Express } from 'express'\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'\nimport type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'\nimport { getMcpToolMeta } from './decorators'\nimport { zodToJsonSchema } from './zod-to-json-schema'\nimport type { McpAdapterOptions, McpToolDefinition, McpTransport } from './types'\n\nconst log = Logger.for('McpAdapter')\n\n/**\n * Expose a KickJS application as a Model Context Protocol (MCP) server.\n *\n * The adapter implements `onRouteMount` to collect every registered\n * controller alongside its mount path. During `beforeStart` (after all\n * modules have finished mounting), it walks the collected controllers,\n * reads route metadata via `getClassMeta(METADATA.ROUTES, ...)`, and\n * builds a `McpToolDefinition[]` that the MCP SDK will register as\n * callable tools.\n *\n * The input schema of each tool is the JSON Schema equivalent of the\n * route's Zod `body` schema, converted via the package's own\n * `zod-to-json-schema` helper. Tools with no body schema get an empty\n * object schema so the model can still call them with no arguments.\n *\n * @example\n * ```ts\n * import { bootstrap } from '@forinda/kickjs'\n * import { McpAdapter } from '@forinda/kickjs-mcp'\n * import { modules } from './modules'\n *\n * export const app = await bootstrap({\n * modules,\n * adapters: [\n * new McpAdapter({\n * name: 'task-api',\n * version: '1.0.0',\n * description: 'Task management MCP server',\n * mode: 'explicit',\n * transport: 'sse',\n * }),\n * ],\n * })\n * ```\n *\n * @remarks\n * Tool discovery is complete. The remaining work for v1 is wiring the\n * generated tool definitions to `@modelcontextprotocol/sdk` in\n * `afterStart` — see the TODO markers in that hook. Until then,\n * `getTools()` returns the discovered definitions so tests can assert\n * the scan produced the expected shape.\n */\nexport class McpAdapter implements AppAdapter {\n readonly name = 'McpAdapter'\n\n private readonly options: Required<\n Pick<McpAdapterOptions, 'mode' | 'transport' | 'basePath' | 'version'>\n > &\n McpAdapterOptions\n\n /** Controllers collected during the mount phase, in insertion order. */\n private readonly mountedControllers: Array<{\n controller: Constructor\n mountPath: string\n }> = []\n\n /** Discovered tool definitions, built during `beforeStart`. */\n private readonly tools: McpToolDefinition[] = []\n\n /** Active MCP server instance, created in `afterStart`. */\n private mcpServer: McpServer | null = null\n\n /**\n * Active MCP transport, created in `afterStart`. Can be either a\n * `StreamableHTTPServerTransport` (the default for HTTP-based MCP)\n * or a `StdioServerTransport` (when running via the `kick mcp` CLI\n * or with `KICK_MCP_STDIO=1`).\n */\n private transport: Transport | null = null\n\n /**\n * Base URL of the running KickJS HTTP server, captured in `afterStart`\n * once the server is listening. Tool dispatch makes internal HTTP\n * requests against this base URL so calls flow through the normal\n * Express pipeline (middleware, validation, auth, logging, error\n * handling) rather than bypassing it.\n *\n * Format: `http://127.0.0.1:<port>`. Set to `null` until afterStart\n * runs and reset to `null` on shutdown.\n */\n private serverBaseUrl: string | null = null\n\n constructor(options: McpAdapterOptions) {\n this.options = {\n mode: options.mode ?? 'explicit',\n transport: options.transport ?? 'sse',\n basePath: options.basePath ?? '/_mcp',\n version: options.version ?? '0.0.0',\n ...options,\n }\n }\n\n /**\n * Called by the framework each time a module mounts a controller.\n *\n * We don't inspect routes here — we just record the pair and process\n * everything in `beforeStart` once mounting is fully complete. This\n * keeps the scan logic in one place and makes it easier to unit test.\n */\n onRouteMount(controller: Constructor, mountPath: string): void {\n this.mountedControllers.push({ controller, mountPath })\n }\n\n /**\n * Walk collected controllers, read route metadata, and materialize\n * `McpToolDefinition[]`.\n *\n * Runs after every module has mounted but before the HTTP server\n * starts listening, so the MCP server can be initialized in\n * `afterStart` with a complete tool list.\n */\n beforeStart(_ctx: AdapterContext): void {\n for (const { controller, mountPath } of this.mountedControllers) {\n const routes = getClassMeta<RouteDefinition[]>(METADATA.ROUTES, controller, [])\n for (const route of routes) {\n const tool = this.tryBuildTool(controller, mountPath, route)\n if (tool) this.tools.push(tool)\n }\n }\n\n log.debug(\n `MCP adapter discovered ${this.tools.length} tool(s) ` +\n `(mode=${this.options.mode}, transport=${this.options.transport})`,\n )\n }\n\n /**\n * Start the MCP server on the configured transport.\n *\n * - `http` (recommended): mounts a `StreamableHTTPServerTransport` on\n * the existing Express app at `${basePath}/messages`. This is the\n * modern, spec-compliant way to expose MCP over HTTP.\n * - `sse` (deprecated): currently aliases to `http` and emits a warning.\n * The MCP SSE transport class is deprecated upstream in favor of\n * StreamableHTTP, which already supports SSE-style streaming under\n * the hood.\n * - `stdio`: skipped here. The standalone `kick mcp` CLI command\n * instantiates the adapter directly and connects it to a stdio\n * transport so dev logs don't interfere.\n */\n async afterStart(ctx: AdapterContext): Promise<void> {\n // Capture the running server's address so tool dispatch can make\n // internal HTTP requests against the actual port. The framework\n // calls afterStart only once the server is listening, so\n // server.address() returns a real AddressInfo at this point.\n // We capture this regardless of transport mode because dispatch\n // always uses the local HTTP listener — even in stdio mode it's\n // the internal route into the Express pipeline.\n this.serverBaseUrl = this.resolveServerBaseUrl(ctx.server)\n\n const effectiveTransport = this.resolveTransportMode()\n\n if (effectiveTransport === 'stdio') {\n await this.startStdioTransport()\n return\n }\n\n if (effectiveTransport === 'sse') {\n log.warn(\n 'sse transport is deprecated upstream; using StreamableHTTP transport, which supports the same SSE wire format under the hood',\n )\n }\n\n const expressApp = ctx.app as Express | undefined\n if (!expressApp) {\n log.warn('McpAdapter: AdapterContext.app is unavailable, cannot mount HTTP transport')\n return\n }\n\n this.mcpServer = this.buildMcpServer()\n const httpTransport = new StreamableHTTPServerTransport({\n // Stateless mode for v0 — every request gets a fresh session. We can\n // switch to a stateful generator (sessionIdGenerator: randomUUID) once\n // we add session-aware tool dispatch.\n sessionIdGenerator: () => randomUUID(),\n })\n this.transport = httpTransport\n\n await this.mcpServer.connect(httpTransport)\n this.mountHttpRoutes(expressApp, httpTransport)\n\n log.info(\n `McpAdapter ready — ${this.tools.length} tool(s) registered, listening at ${this.options.basePath}/messages`,\n )\n }\n\n /**\n * Decide which transport to actually start.\n *\n * Precedence: an explicit `KICK_MCP_STDIO=1` environment variable\n * always wins, because that's how the `kick mcp` CLI command tells\n * the running process to switch to stdio mode without requiring the\n * user to edit their bootstrap. Otherwise the constructor option is\n * honored as-is.\n */\n private resolveTransportMode(): McpTransport {\n if (process.env.KICK_MCP_STDIO === '1' || process.env.KICK_MCP_STDIO === 'true') {\n return 'stdio'\n }\n return this.options.transport\n }\n\n /**\n * Start the MCP server bound to process stdio.\n *\n * Used by the `kick mcp` CLI: the parent process pipes its stdin/\n * stdout to this adapter so MCP clients (Claude Code, Cursor) can\n * speak the protocol over the wire. Logs MUST go to stderr in this\n * mode — anything written to stdout corrupts the JSON-RPC stream.\n *\n * The HTTP server is still running (the framework called start()\n * before afterStart), but we don't mount the MCP routes on it. Tool\n * dispatch routes through fetch against `serverBaseUrl` exactly the\n * same way it does in HTTP mode, so dispatch behavior is uniform\n * across transports.\n */\n private async startStdioTransport(): Promise<void> {\n this.mcpServer = this.buildMcpServer()\n this.transport = new StdioServerTransport()\n await this.mcpServer.connect(this.transport)\n // Use stderr-friendly log level so we don't break the protocol\n log.info(\n `McpAdapter ready (stdio) — ${this.tools.length} tool(s) registered, dispatching against ${this.serverBaseUrl ?? 'unknown'}`,\n )\n }\n\n /**\n * Tear down the MCP server and any open transports.\n *\n * Called during graceful shutdown. Idempotent — KickJS may invoke\n * `shutdown` more than once under error conditions.\n */\n async shutdown(): Promise<void> {\n try {\n await this.transport?.close()\n } catch (err) {\n log.error(err as Error, 'McpAdapter: failed to close transport')\n }\n try {\n await this.mcpServer?.close()\n } catch (err) {\n log.error(err as Error, 'McpAdapter: failed to close server')\n }\n this.transport = null\n this.mcpServer = null\n this.serverBaseUrl = null\n log.debug('McpAdapter shutdown complete')\n }\n\n /**\n * Return the list of tools discovered during startup.\n *\n * Primary consumers:\n * - the `kick mcp --list` command\n * - unit tests that verify a route was exposed as expected\n */\n getTools(): readonly McpToolDefinition[] {\n return this.tools\n }\n\n // ── Internal helpers ───────────────────────────────────────────────\n\n /**\n * Build a `McpToolDefinition` for a single route, or return `null`\n * if the route should be skipped under the current exposure mode.\n *\n * Scoping rules:\n * - `explicit` (default): only routes with `@McpTool` are exposed\n * - `auto`: every route is exposed, filtered by `include` /\n * `exclude`; a `hidden: true` on `@McpTool` still drops the route\n */\n private tryBuildTool(\n controller: Constructor,\n mountPath: string,\n route: RouteDefinition,\n ): McpToolDefinition | null {\n const meta = getMcpToolMeta(controller.prototype, route.handlerName)\n\n if (this.options.mode === 'explicit' && !meta) return null\n if (meta?.hidden) return null\n\n if (this.options.mode === 'auto') {\n const methodUpper = route.method.toUpperCase() as 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'\n if (this.options.include && !this.options.include.includes(methodUpper)) return null\n if (this.options.exclude?.some((prefix) => mountPath.startsWith(prefix))) return null\n }\n\n const description = meta?.description ?? this.deriveDescription(controller, route)\n const name = meta?.name ?? `${controller.name}.${route.handlerName}`\n\n // Prefer the body schema for POST/PUT/PATCH, query schema for GET/DELETE.\n // In `auto` mode the decorator may be absent entirely, in which case we\n // fall back to whatever schema the route decorator declared.\n const candidateSchema = meta?.inputSchema ?? route.validation?.body ?? route.validation?.query\n\n const inputSchema = zodToJsonSchema(candidateSchema) ?? {\n type: 'object',\n properties: {},\n additionalProperties: false,\n }\n\n const outputSchema = meta?.outputSchema ? zodToJsonSchema(meta.outputSchema) : undefined\n\n return {\n name,\n description,\n inputSchema,\n // Keep the original Zod schema alongside the JSON Schema. The MCP\n // SDK accepts Zod directly via `registerTool`, while `inputSchema`\n // (above) is what `getTools()` and inspection surfaces consume.\n zodInputSchema: candidateSchema,\n outputSchema: outputSchema ?? undefined,\n httpMethod: route.method.toUpperCase(),\n mountPath: this.joinMountPath(mountPath, route.path),\n examples: meta?.examples,\n }\n }\n\n /**\n * Derive a default description for routes exposed in `auto` mode\n * without an explicit `@McpTool` decorator. Kept intentionally\n * generic — teams running `auto` should still add `@McpTool` with\n * real descriptions for any tool the model is expected to call\n * reliably.\n */\n private deriveDescription(controller: Constructor, route: RouteDefinition): string {\n return `${route.method.toUpperCase()} handler ${controller.name}.${route.handlerName}`\n }\n\n /**\n * Join a module mount path with the route-level sub-path.\n *\n * Mount path already includes the API prefix + version (e.g.\n * `/api/v1/tasks`); the route-level `path` is relative (e.g. `/:id`).\n * Trailing/leading slashes are normalized so the final URL is stable.\n */\n private joinMountPath(mountPath: string, routePath: string): string {\n const base = mountPath.endsWith('/') ? mountPath.slice(0, -1) : mountPath\n if (!routePath || routePath === '/') return base\n const sub = routePath.startsWith('/') ? routePath : `/${routePath}`\n return `${base}${sub}`\n }\n\n /**\n * Construct the underlying `McpServer` and register every discovered\n * tool against it. The SDK accepts Zod schemas natively, so we pass\n * `zodInputSchema` straight through and skip the JSON Schema form\n * here (the JSON Schema is for inspection / docs).\n *\n * Tool calls dispatch through the Express pipeline via internal HTTP\n * requests against the running server's address (captured in\n * `afterStart`). This preserves middleware, validation, auth, and\n * logging — tool calls behave exactly like external HTTP requests\n * to the same route.\n */\n private buildMcpServer(): McpServer {\n const server = new McpServer({\n name: this.options.name,\n version: this.options.version,\n ...(this.options.description ? { description: this.options.description } : {}),\n })\n\n // The SDK's `registerTool` is heavily overloaded with deep generic\n // inference over Zod input/output shapes. The McpToolDefinition\n // intentionally types `zodInputSchema` as `unknown` to keep our\n // public surface free of SDK internal types, which makes the\n // overload picker unhappy. Cast through `any` once, here, so the\n // call sites stay clean. The SDK validates the schema at register\n // time anyway, so the `any` is bounded to this loop.\n /* eslint-disable @typescript-eslint/no-explicit-any */\n const registerTool = server.registerTool.bind(server) as (\n name: string,\n config: { description: string; inputSchema?: unknown },\n cb: (args: unknown) => any,\n ) => unknown\n /* eslint-enable @typescript-eslint/no-explicit-any */\n\n for (const tool of this.tools) {\n const config: { description: string; inputSchema?: unknown } = {\n description: tool.description,\n }\n if (tool.zodInputSchema) {\n config.inputSchema = tool.zodInputSchema\n }\n registerTool(tool.name, config, async (args) => this.dispatchTool(tool, args))\n }\n\n return server\n }\n\n /**\n * Dispatch a tool call through the Express pipeline.\n *\n * Builds an HTTP request that matches the tool's underlying route\n * (method + path + body or query string from the args) and sends it\n * to the running server's `serverBaseUrl`. The request goes through\n * every middleware the route normally hits — auth, validation,\n * logging, error handling — so tool calls observe exactly the same\n * guarantees as external HTTP clients.\n *\n * Path parameters (e.g. `/:id`) are substituted from `args` before\n * the request fires; matching keys are removed from the body/query\n * to avoid sending them twice.\n *\n * Returns a `CallToolResult` whose `content` contains the response\n * body as text. Non-2xx responses are flagged with `isError: true`\n * so the calling LLM can react.\n */\n private async dispatchTool(\n tool: McpToolDefinition,\n rawArgs: unknown,\n ): Promise<{\n content: Array<{ type: 'text'; text: string }>\n isError?: boolean\n }> {\n if (!this.serverBaseUrl) {\n return {\n isError: true,\n content: [\n {\n type: 'text' as const,\n text: `Cannot dispatch ${tool.name}: HTTP server address not yet captured`,\n },\n ],\n }\n }\n\n const args = (rawArgs ?? {}) as Record<string, unknown>\n const { path, remainingArgs } = this.substitutePathParams(tool.mountPath, args)\n const method = tool.httpMethod.toUpperCase()\n const hasBody = method === 'POST' || method === 'PUT' || method === 'PATCH'\n\n let url = `${this.serverBaseUrl}${path}`\n const init: RequestInit = {\n method,\n headers: {\n accept: 'application/json',\n 'x-mcp-tool': tool.name,\n },\n }\n\n if (hasBody) {\n ;(init.headers as Record<string, string>)['content-type'] = 'application/json'\n init.body = JSON.stringify(remainingArgs)\n } else if (Object.keys(remainingArgs).length > 0) {\n // GET / DELETE: serialize args as a query string\n const qs = new URLSearchParams()\n for (const [key, value] of Object.entries(remainingArgs)) {\n if (value === undefined || value === null) continue\n qs.append(key, typeof value === 'string' ? value : JSON.stringify(value))\n }\n const sep = url.includes('?') ? '&' : '?'\n url = `${url}${sep}${qs.toString()}`\n }\n\n try {\n const res = await fetch(url, init)\n const text = await res.text()\n return {\n isError: res.status >= 400,\n content: [\n {\n type: 'text' as const,\n text: text || `(${res.status} ${res.statusText})`,\n },\n ],\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n log.error(err as Error, `McpAdapter: dispatch failed for ${tool.name}`)\n return {\n isError: true,\n content: [\n {\n type: 'text' as const,\n text: `Tool dispatch error: ${message}`,\n },\n ],\n }\n }\n }\n\n /**\n * Substitute Express-style path parameters (`:id`) in `mountPath`\n * with values from `args`. Returns the resolved path plus the args\n * that were NOT consumed by parameters, so they can be sent as the\n * request body or query string.\n *\n * If a `:param` is referenced in the path but missing from args,\n * the placeholder is left in place — the request will hit a 404 from\n * the underlying route, which is reported back as an MCP error.\n */\n private substitutePathParams(\n mountPath: string,\n args: Record<string, unknown>,\n ): { path: string; remainingArgs: Record<string, unknown> } {\n const remaining: Record<string, unknown> = { ...args }\n const path = mountPath.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, (_match, param: string) => {\n if (param in remaining) {\n const value = remaining[param]\n delete remaining[param]\n return encodeURIComponent(String(value))\n }\n return `:${param}`\n })\n return { path, remainingArgs: remaining }\n }\n\n /**\n * Resolve the running server's base URL from a Node `http.Server`\n * instance. Returns null if the server isn't listening or its\n * address can't be determined (e.g. when the adapter is mounted\n * standalone for testing).\n *\n * IPv6 addresses are wrapped in brackets per RFC 3986. The hostname\n * `0.0.0.0` (Linux default) is rewritten to `127.0.0.1` because the\n * former is not a valid request target on all platforms.\n */\n private resolveServerBaseUrl(server: AdapterContext['server']): string | null {\n if (!server) return null\n const address = server.address()\n if (!address || typeof address === 'string') return null\n let host = address.address\n if (host === '::' || host === '0.0.0.0' || host === '') host = '127.0.0.1'\n if (host.includes(':') && !host.startsWith('[')) host = `[${host}]`\n return `http://${host}:${address.port}`\n }\n\n /**\n * Mount the StreamableHTTP transport endpoints on the existing\n * Express app. The transport handles three HTTP verbs at a single\n * URL:\n * - POST: client → server messages (initialize, tool calls, etc.)\n * - GET: server → client SSE stream for notifications\n * - DELETE: client tells the server to terminate a session\n *\n * We mount all three on `${basePath}/messages` so a single URL is\n * the entire MCP surface area.\n */\n private mountHttpRoutes(app: Express, transport: StreamableHTTPServerTransport): void {\n const path = `${this.options.basePath}/messages`\n\n /* eslint-disable @typescript-eslint/no-explicit-any */\n const handleRequest = async (req: any, res: any): Promise<void> => {\n try {\n await transport.handleRequest(req, res, req.body)\n } catch (err) {\n log.error(err as Error, `McpAdapter: error handling ${req.method} ${path}`)\n if (!res.headersSent) {\n res.status(500).json({ error: 'MCP transport error' })\n }\n }\n }\n /* eslint-enable @typescript-eslint/no-explicit-any */\n\n app.post(path, handleRequest)\n app.get(path, handleRequest)\n app.delete(path, handleRequest)\n }\n}\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@forinda/kickjs-mcp",
|
|
3
|
+
"version": "2.3.0",
|
|
4
|
+
"description": "Model Context Protocol server adapter for KickJS — expose @Controller endpoints as MCP tools",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"kickjs",
|
|
7
|
+
"nodejs",
|
|
8
|
+
"typescript",
|
|
9
|
+
"express",
|
|
10
|
+
"decorators",
|
|
11
|
+
"dependency-injection",
|
|
12
|
+
"backend",
|
|
13
|
+
"api",
|
|
14
|
+
"framework",
|
|
15
|
+
"mcp",
|
|
16
|
+
"model-context-protocol",
|
|
17
|
+
"ai",
|
|
18
|
+
"claude",
|
|
19
|
+
"anthropic",
|
|
20
|
+
"tool-calling",
|
|
21
|
+
"agent",
|
|
22
|
+
"llm",
|
|
23
|
+
"@forinda/kickjs",
|
|
24
|
+
"@forinda/kickjs-cli",
|
|
25
|
+
"@forinda/kickjs-swagger"
|
|
26
|
+
],
|
|
27
|
+
"type": "module",
|
|
28
|
+
"main": "dist/index.mjs",
|
|
29
|
+
"types": "dist/index.d.mts",
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"import": "./dist/index.mjs",
|
|
33
|
+
"types": "./dist/index.d.mts"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist"
|
|
38
|
+
],
|
|
39
|
+
"wireit": {
|
|
40
|
+
"build": {
|
|
41
|
+
"command": "tsdown",
|
|
42
|
+
"files": [
|
|
43
|
+
"src/**/*.ts",
|
|
44
|
+
"tsdown.config.ts",
|
|
45
|
+
"tsconfig.json",
|
|
46
|
+
"package.json"
|
|
47
|
+
],
|
|
48
|
+
"output": [
|
|
49
|
+
"dist/**"
|
|
50
|
+
],
|
|
51
|
+
"dependencies": [
|
|
52
|
+
"../kickjs:build"
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"reflect-metadata": "^0.2.2",
|
|
58
|
+
"@forinda/kickjs": "2.3.0"
|
|
59
|
+
},
|
|
60
|
+
"peerDependencies": {
|
|
61
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
62
|
+
"zod": "^4.3.6"
|
|
63
|
+
},
|
|
64
|
+
"peerDependenciesMeta": {
|
|
65
|
+
"@modelcontextprotocol/sdk": {
|
|
66
|
+
"optional": false
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"devDependencies": {
|
|
70
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
71
|
+
"@swc/core": "^1.15.21",
|
|
72
|
+
"@types/express": "^5.0.6",
|
|
73
|
+
"@types/node": "^25.0.0",
|
|
74
|
+
"@types/supertest": "^7.2.0",
|
|
75
|
+
"express": "^5.1.0",
|
|
76
|
+
"supertest": "^7.2.2",
|
|
77
|
+
"typescript": "^5.9.2",
|
|
78
|
+
"unplugin-swc": "^1.5.9",
|
|
79
|
+
"vitest": "^4.1.2",
|
|
80
|
+
"zod": "^4.3.6"
|
|
81
|
+
},
|
|
82
|
+
"publishConfig": {
|
|
83
|
+
"access": "public"
|
|
84
|
+
},
|
|
85
|
+
"license": "MIT",
|
|
86
|
+
"author": "Felix Orinda",
|
|
87
|
+
"engines": {
|
|
88
|
+
"node": ">=20.0"
|
|
89
|
+
},
|
|
90
|
+
"homepage": "https://forinda.github.io/kick-js/",
|
|
91
|
+
"repository": {
|
|
92
|
+
"type": "git",
|
|
93
|
+
"url": "https://github.com/forinda/kick-js.git",
|
|
94
|
+
"directory": "packages/mcp"
|
|
95
|
+
},
|
|
96
|
+
"bugs": {
|
|
97
|
+
"url": "https://github.com/forinda/kick-js/issues"
|
|
98
|
+
},
|
|
99
|
+
"scripts": {
|
|
100
|
+
"build": "wireit",
|
|
101
|
+
"dev": "tsdown --watch",
|
|
102
|
+
"test": "vitest run --passWithNoTests",
|
|
103
|
+
"test:watch": "vitest",
|
|
104
|
+
"typecheck": "tsc --noEmit",
|
|
105
|
+
"clean": "rm -rf dist .wireit"
|
|
106
|
+
}
|
|
107
|
+
}
|