@silkweave/nestjs 1.12.0 → 2.2.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/README.md +108 -163
- package/build/index.d.mts +250 -267
- package/build/index.d.mts.map +1 -1
- package/build/index.mjs +736 -511
- package/build/index.mjs.map +1 -1
- package/package.json +22 -16
- package/.turbo/turbo-build.log +0 -16
- package/.turbo/turbo-check.log +0 -3
- package/.turbo/turbo-lint.log +0 -2
- package/.turbo/turbo-prepack.log +0 -18
- package/eslint.config.mjs +0 -93
- package/src/adapter/mcp.ts +0 -102
- package/src/adapter/rest.ts +0 -173
- package/src/adapter/trpc.ts +0 -58
- package/src/adapter/typegen.ts +0 -55
- package/src/decorator/action.ts +0 -38
- package/src/decorator/actions.ts +0 -25
- package/src/index.ts +0 -14
- package/src/lib/discovery.ts +0 -131
- package/src/lib/executionContext.ts +0 -76
- package/src/lib/filter.ts +0 -26
- package/src/lib/guards.ts +0 -70
- package/src/lib/metadata.ts +0 -58
- package/src/lib/openapi.ts +0 -116
- package/src/lib/silkweave.module.ts +0 -75
- package/src/lib/swagger.ts +0 -74
- package/src/lib/types.ts +0 -58
- package/tsconfig.json +0 -25
- package/tsconfig.tsbuildinfo +0 -1
- package/tsdown.config.ts +0 -7
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# @silkweave/nestjs
|
|
2
2
|
|
|
3
|
-
NestJS adapter for [Silkweave](https://github.com/silkweave/silkweave).
|
|
3
|
+
NestJS adapter for [Silkweave](https://github.com/silkweave/silkweave). Expose your **existing NestJS controllers** as MCP (Model Context Protocol) tools by adding a single `@Mcp()` decorator to a route handler. The tool's name, description, and input schema are **reflected** from the route, the `@Param`/`@Query`/`@Body` decorators, and any `@nestjs/swagger` / `class-validator` metadata the method already carries - nothing is re-declared. On a tool call the validated input is split back into the method's positional arguments and the handler is invoked directly, with `@UseGuards()` guards applied first.
|
|
4
|
+
|
|
5
|
+
It is **additive**: controllers keep serving HTTP exactly as before, and removing `@Mcp()` fully reverts a method.
|
|
4
6
|
|
|
5
7
|
## Install
|
|
6
8
|
|
|
@@ -8,52 +10,59 @@ NestJS adapter for [Silkweave](https://github.com/silkweave/silkweave). Define a
|
|
|
8
10
|
pnpm add @silkweave/core @silkweave/nestjs @nestjs/common @nestjs/core @nestjs/platform-express reflect-metadata
|
|
9
11
|
```
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
`@nestjs/swagger` and `class-validator` are **optional** peers - install whichever your DTOs already use; the reflector reads both and falls back to TypeScript `design:type` when neither is present.
|
|
14
|
+
|
|
15
|
+
> NestJS needs decorator-aware transforms with `emitDecoratorMetadata`. Use `@swc-node/register` (`node --import @swc-node/register/esm-register src/main.ts`) or the `nest start` CLI; plain `tsx` does not emit decorator metadata.
|
|
12
16
|
|
|
13
17
|
## Usage
|
|
14
18
|
|
|
15
19
|
```ts
|
|
16
|
-
// users.
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import
|
|
20
|
+
// users.controller.ts
|
|
21
|
+
import { Body, Controller, Get, NotFoundException, Param, Post, Query, UseGuards } from '@nestjs/common'
|
|
22
|
+
import { ApiOperation, ApiParam, ApiProperty, ApiQuery } from '@nestjs/swagger'
|
|
23
|
+
import { Mcp } from '@silkweave/nestjs'
|
|
24
|
+
import { IsBoolean, IsOptional, IsString, MinLength } from 'class-validator'
|
|
21
25
|
import { AdminGuard } from './admin.guard.js'
|
|
22
26
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
class BanUserDto {
|
|
28
|
+
@ApiProperty({ description: 'Reason for the ban' })
|
|
29
|
+
@IsString() @MinLength(3)
|
|
30
|
+
reason!: string
|
|
26
31
|
|
|
27
|
-
@
|
|
28
|
-
@
|
|
29
|
-
|
|
32
|
+
@ApiProperty({ description: 'Whether the ban is permanent', required: false })
|
|
33
|
+
@IsOptional() @IsBoolean()
|
|
34
|
+
permanent?: boolean
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@Controller('users')
|
|
38
|
+
export class UsersController {
|
|
30
39
|
constructor(private readonly db: DbService) {}
|
|
31
40
|
|
|
32
|
-
@
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
return this.db.listUsers(input.activeOnly)
|
|
41
|
+
@Get()
|
|
42
|
+
@ApiOperation({ summary: 'List users' })
|
|
43
|
+
@ApiQuery({ name: 'activeOnly', required: false, type: Boolean, description: 'Only active users' })
|
|
44
|
+
@Mcp()
|
|
45
|
+
list(@Query('activeOnly') activeOnly?: boolean) {
|
|
46
|
+
return this.db.listUsers(activeOnly)
|
|
39
47
|
}
|
|
40
48
|
|
|
41
|
-
@
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
49
|
+
@Get(':id')
|
|
50
|
+
@ApiOperation({ summary: 'Get a single user by ID' })
|
|
51
|
+
@ApiParam({ name: 'id', description: 'User ID' })
|
|
52
|
+
@Mcp()
|
|
53
|
+
get(@Param('id') id: string) {
|
|
54
|
+
const user = this.db.getUser(id)
|
|
55
|
+
if (!user) { throw new NotFoundException('user not found') }
|
|
56
|
+
return user
|
|
48
57
|
}
|
|
49
58
|
|
|
50
|
-
@
|
|
51
|
-
@
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
})
|
|
55
|
-
ban(
|
|
56
|
-
return this.db.banUser(
|
|
59
|
+
@Post(':id/ban')
|
|
60
|
+
@ApiOperation({ summary: 'Ban a user' })
|
|
61
|
+
@ApiParam({ name: 'id', description: 'User ID' })
|
|
62
|
+
@UseGuards(AdminGuard) // runs on every transport, MCP included
|
|
63
|
+
@Mcp({ description: 'Ban a user (admin only).' })
|
|
64
|
+
ban(@Param('id') id: string, @Body() body: BanUserDto) {
|
|
65
|
+
return this.db.banUser(id, body.reason, body.permanent ?? false)
|
|
57
66
|
}
|
|
58
67
|
}
|
|
59
68
|
```
|
|
@@ -61,22 +70,19 @@ export class UserActions {
|
|
|
61
70
|
```ts
|
|
62
71
|
// app.module.ts
|
|
63
72
|
import { Module } from '@nestjs/common'
|
|
64
|
-
import { mcp,
|
|
73
|
+
import { mcp, SilkweaveModule } from '@silkweave/nestjs'
|
|
65
74
|
import { AdminGuard } from './admin.guard.js'
|
|
66
|
-
import {
|
|
75
|
+
import { UsersController } from './users.controller.js'
|
|
67
76
|
|
|
68
77
|
@Module({
|
|
69
78
|
imports: [
|
|
70
79
|
SilkweaveModule.forRoot({
|
|
71
80
|
silkweave: { name: 'my-app', description: 'My App', version: '1.0.0' },
|
|
72
|
-
adapters: [
|
|
73
|
-
rest({ basePath: '/api' }),
|
|
74
|
-
trpc({ basePath: '/trpc' }),
|
|
75
|
-
mcp({ basePath: '/mcp' })
|
|
76
|
-
]
|
|
81
|
+
adapters: [mcp({ basePath: '/mcp' })]
|
|
77
82
|
})
|
|
78
83
|
],
|
|
79
|
-
|
|
84
|
+
controllers: [UsersController],
|
|
85
|
+
providers: [AdminGuard]
|
|
80
86
|
})
|
|
81
87
|
export class AppModule {}
|
|
82
88
|
```
|
|
@@ -91,120 +97,64 @@ const app = await NestFactory.create(AppModule)
|
|
|
91
97
|
await app.listen(8080)
|
|
92
98
|
```
|
|
93
99
|
|
|
94
|
-
The `
|
|
95
|
-
|
|
96
|
-
- **REST:** `GET /api/users/list?activeOnly=true` (query param) and `GET /api/users/1` (path param, via `path: 'users/:id'`)
|
|
97
|
-
- **tRPC:** `client.usersList.query({ activeOnly: true })` and `client.usersGet.query({ id: '1' })`
|
|
98
|
-
- **MCP:** tools `UsersList` and `UsersGet`
|
|
99
|
-
|
|
100
|
-
`users.ban` is guarded by `@UseGuards(AdminGuard)` on **every** transport - REST, tRPC, **and** MCP. The guard reads its credential from the request header (`switchToHttp().getRequest().headers`); over MCP the inbound tool-call headers are surfaced the same way (see [Guards & DI](#guards--di)).
|
|
101
|
-
|
|
102
|
-
## Decorators
|
|
103
|
-
|
|
104
|
-
### `@Action(options)` (method decorator)
|
|
105
|
-
|
|
106
|
-
| Option | Type | Default | Description |
|
|
107
|
-
|--------|------|---------|-------------|
|
|
108
|
-
| `name` | `string` | kebab-cased method name | Override the action name. Joined with the class prefix via `.` |
|
|
109
|
-
| `description` | `string` | *required* | Human-readable summary. Used as MCP tool description |
|
|
110
|
-
| `input` | `z.ZodObject` | *required* | Zod object schema for the action's input |
|
|
111
|
-
| `output` | `z.ZodObject` | - | Optional output schema (used by tRPC type inference) |
|
|
112
|
-
| `chunk` | `z.ZodType` | - | Schema for chunks yielded by a streaming (`async function*`) method. **Required** when the method is an async generator |
|
|
113
|
-
| `kind` | `'query' \| 'mutation'` | `'mutation'` | `'query'` → GET in REST, `.query()` in tRPC. `'mutation'` → POST / `.mutation()` |
|
|
114
|
-
| `method` | `'GET' \| 'POST' \| 'PUT' \| 'DELETE'` | `POST` (or `GET` when `kind: 'query'`) | REST HTTP verb. Overrides the `kind`-derived default |
|
|
115
|
-
| `path` | `string` | action name with dots as slashes | REST route, optionally with `:param` placeholders (e.g. `'spaces/:spaceId/users'`). Each placeholder must be a key of `input` and is resolved from the URL path |
|
|
116
|
-
| `queryParams` | `(keyof input)[]` | - | Input fields read from the URL query string instead of the body (e.g. `['offset', 'limit']`). On a bodyless GET every non-path field is read from the query string automatically |
|
|
117
|
-
| `transports` | `('rest' \| 'trpc' \| 'mcp')[]` | all | Allowlist of transports that expose this action |
|
|
118
|
-
| `isEnabled` | `(ctx) => boolean` | - | Dynamic gate (AND-combined with `transports`) |
|
|
119
|
-
| `toolResult` | `(response, ctx) => CallToolResult` | - | Custom MCP `CallToolResult` formatter |
|
|
120
|
-
|
|
121
|
-
### `@Actions(prefix?)` (class decorator)
|
|
122
|
-
|
|
123
|
-
Groups a class's actions under a common prefix. Joined to method-level names with a dot.
|
|
100
|
+
The MCP endpoint at `/mcp` now exposes three tools - `UsersList`, `UsersGet`, `UsersBan` - whose input schemas are reflected from the controllers:
|
|
124
101
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
@Action({ ... }) list(...) {} // action name: 'users.list'
|
|
129
|
-
@Action({ name: 'top' }) ... {} // action name: 'users.top'
|
|
130
|
-
}
|
|
131
|
-
```
|
|
102
|
+
- `UsersGet` → `{ id: string }` (`id` required, described "User ID" from `@ApiParam`)
|
|
103
|
+
- `UsersList` → `{ activeOnly?: boolean }` (optional, typed/described from `@ApiQuery`)
|
|
104
|
+
- `UsersBan` → `{ id: string, reason: string (minLength 3), permanent?: boolean }` (flattened from the path param + `BanUserDto`, with `@ApiProperty` + `class-validator` merged)
|
|
132
105
|
|
|
133
|
-
|
|
106
|
+
`UsersBan` is guarded by `@UseGuards(AdminGuard)`; over MCP the guard reads the inbound tool-call headers (see [Guards & DI](#guards--di)).
|
|
134
107
|
|
|
135
|
-
##
|
|
108
|
+
## Decorator
|
|
136
109
|
|
|
137
|
-
###
|
|
110
|
+
### `@Mcp(options?)` (method decorator)
|
|
138
111
|
|
|
139
|
-
|
|
112
|
+
Exposes the decorated controller route as an MCP tool. Every option is optional.
|
|
140
113
|
|
|
141
114
|
| Option | Type | Default | Description |
|
|
142
115
|
|--------|------|---------|-------------|
|
|
143
|
-
| `
|
|
144
|
-
| `
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
- `users.list` (query) → `GET /api/users/list`
|
|
149
|
-
- `users.ban` (mutation) → `POST /api/users/ban`
|
|
150
|
-
|
|
151
|
-
The action's `method`, `path`, and `queryParams` fields override this (see the `@Action` options table above):
|
|
152
|
-
|
|
153
|
-
- `path: 'users/:id'`, `kind: 'query'` → `GET /api/users/:id` (`id` from the path)
|
|
154
|
-
- `queryParams: ['activeOnly']`, `kind: 'query'` → `GET /api/users/list?activeOnly=true`
|
|
116
|
+
| `name` | `string` | `${ControllerBase}${MethodName}` (e.g. `UsersGet`) | MCP tool name override |
|
|
117
|
+
| `description` | `string` | `@ApiOperation` summary/description, else generated | Tool description |
|
|
118
|
+
| `input` | `Record<string, z.ZodType>` | - | Zod raw-shape override merged over the reflected fields (per-field). The escape hatch for shapes reflection can't express - discriminated unions, custom validators, `@Transform` |
|
|
119
|
+
| `pipes` | `'apply' \| 'skip'` | `'apply'` | Whether to run parameter-bound pipes (`@Param('id', ParseIntPipe)`) when re-binding |
|
|
155
120
|
|
|
156
|
-
|
|
121
|
+
## How reflection works
|
|
157
122
|
|
|
158
|
-
|
|
123
|
+
For each `@Mcp` method the adapter builds **one flat Zod input object** by merging, per field, in increasing precedence:
|
|
159
124
|
|
|
160
|
-
|
|
125
|
+
1. TypeScript `design:type` (the parameter/property constructor)
|
|
126
|
+
2. `class-validator` decorators (`@IsString`, `@MinLength`, `@IsOptional`, `@IsEnum`, ...) - **optional** peer
|
|
127
|
+
3. `@nestjs/swagger` decorators - `@ApiParam`/`@ApiQuery` for scalar path/query params, `@ApiProperty` for whole-DTO properties - **optional** peer
|
|
128
|
+
4. An ingested OpenAPI document, matched by HTTP verb + path (`SilkweaveModule.forRoot({ openapi })`)
|
|
129
|
+
5. `@Mcp({ input })` raw-shape override
|
|
161
130
|
|
|
162
|
-
|
|
163
|
-
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'
|
|
164
|
-
import { addSilkweaveActions } from '@silkweave/nestjs'
|
|
165
|
-
|
|
166
|
-
const config = new DocumentBuilder().setTitle('My API').setVersion('1.0.0').build()
|
|
167
|
-
const document = SwaggerModule.createDocument(app, config) // your controllers
|
|
168
|
-
addSilkweaveActions(app, document) // + silkweave actions
|
|
169
|
-
SwaggerModule.setup('api/docs', app, document)
|
|
170
|
-
```
|
|
131
|
+
Field sources are derived from the parameter decorators:
|
|
171
132
|
|
|
172
|
-
|
|
|
173
|
-
|
|
174
|
-
|
|
|
175
|
-
| `
|
|
176
|
-
|
|
|
133
|
+
| Controller parameter | Tool input |
|
|
134
|
+
|----------------------|-----------|
|
|
135
|
+
| `@Param('id') id` | scalar field `id` |
|
|
136
|
+
| `@Param() params` | one field per `:param` in the route |
|
|
137
|
+
| `@Query('limit') limit` | scalar field `limit` |
|
|
138
|
+
| `@Query() dto: ListDto` | each property of `ListDto`, flattened to top level |
|
|
139
|
+
| `@Body('x') x` | scalar field `x` |
|
|
140
|
+
| `@Body() dto: CreateDto` | each property of `CreateDto`, flattened to top level |
|
|
141
|
+
| `@Req`/`@Res`/`@Headers`/`@Ip`/`@Session`/files | not exposed; bound at call time (headers/req from the MCP request stand-in, the rest `undefined`) |
|
|
177
142
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
### `trpc(options?)`
|
|
181
|
-
|
|
182
|
-
Mounts a tRPC HTTP handler built from `@silkweave/trpc`'s `buildRouter`.
|
|
183
|
-
|
|
184
|
-
| Option | Type | Default | Description |
|
|
185
|
-
|--------|------|---------|-------------|
|
|
186
|
-
| `basePath` | `string` | `'/trpc'` | URL prefix the tRPC handler listens on |
|
|
187
|
-
| `auth` | `AuthConfig` | - | `@silkweave/auth` bearer-token config |
|
|
143
|
+
On a tool call the validated input is split back into the handler's positional arguments per the same parameter map, parameter-bound pipes run (unless `pipes: 'skip'`), and the method is invoked directly.
|
|
188
144
|
|
|
189
|
-
|
|
145
|
+
### Optional OpenAPI ingestion
|
|
190
146
|
|
|
191
|
-
|
|
147
|
+
Pass a pre-built OpenAPI document (e.g. a committed `openapi.json`, or one built in a two-phase bootstrap) to use it as the authoritative schema source. It is matched to each `@Mcp` method by verb + path and overrides decorator reflection for the fields it covers; unmatched operations/fields fall back to reflection.
|
|
192
148
|
|
|
193
149
|
```ts
|
|
194
|
-
|
|
195
|
-
description: '
|
|
196
|
-
|
|
197
|
-
|
|
150
|
+
SilkweaveModule.forRoot({
|
|
151
|
+
silkweave: { name: 'my-app', description: 'My App', version: '1.0.0' },
|
|
152
|
+
adapters: [mcp()],
|
|
153
|
+
openapi: openApiDocument
|
|
198
154
|
})
|
|
199
|
-
async *countdown(input: { from: number }) {
|
|
200
|
-
for (let n = input.from; n >= 0; n -= 1) {
|
|
201
|
-
await new Promise((r) => setTimeout(r, 200))
|
|
202
|
-
yield { n }
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
155
|
```
|
|
206
156
|
|
|
207
|
-
|
|
157
|
+
## Adapter
|
|
208
158
|
|
|
209
159
|
### `mcp(options?)`
|
|
210
160
|
|
|
@@ -220,46 +170,41 @@ Mounts the MCP Streamable HTTP transport directly at `basePath`, with sideload (
|
|
|
220
170
|
|
|
221
171
|
## Guards & DI
|
|
222
172
|
|
|
223
|
-
Native NestJS `@UseGuards()`
|
|
173
|
+
Native NestJS `@UseGuards()` on `@Mcp` methods run before the handler is invoked. Guards receive an `ExecutionContext` with the request in `switchToHttp().getRequest()`, and the `Reflector` is wired up so guards can read custom metadata.
|
|
224
174
|
|
|
225
|
-
**Headers over MCP.**
|
|
175
|
+
**Headers over MCP.** The inbound tool-call request is surfaced as a stand-in `{ headers, url, params, query }` object built from the MCP SDK's `extra.requestInfo`, so a header-based guard - e.g. one reading `getRequest().headers['x-api-key']` - works unchanged. `ExecutionContext.getType()` is `'http'` whenever a request is available (and `'rpc'` for transports with none, e.g. MCP stdio). Caveats:
|
|
226
176
|
|
|
227
|
-
-
|
|
177
|
+
- **Headers**, the request `url`, and **URL path params** cross the MCP boundary. `getRequest().params` is populated from the route's reflected `@Param` fields (as raw strings, like Express), so a path-scoped guard - e.g. one reading `getRequest().params['id']` to fence a key to one resource - works the same over MCP as over REST. `getRequest().query` is still empty (no query string over MCP).
|
|
228
178
|
- A guard that denies (returns `false` or throws) produces a clean MCP tool error (`ForbiddenException`), not an HTTP 500.
|
|
229
|
-
- For OAuth 2.1 / bearer-token MCP auth, prefer `mcp({ auth })` and read the resolved identity from the silkweave context (`ctx.get('auth')`)
|
|
179
|
+
- For OAuth 2.1 / bearer-token MCP auth, prefer `mcp({ auth })` and read the resolved identity from the silkweave context (`ctx.get('auth')`); the header stand-in is for custom request-reading guards.
|
|
230
180
|
|
|
231
|
-
|
|
181
|
+
**Global guards (opt-in).** App-global guards - registered via `app.useGlobalGuards(new X())` or `{ provide: APP_GUARD, useClass }` - do **not** run on tool calls by default. Opt them in by class with `globalGuards`:
|
|
232
182
|
|
|
233
183
|
```ts
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
) {}
|
|
240
|
-
|
|
241
|
-
@UseGuards(AuthGuard, RateLimitGuard)
|
|
242
|
-
@Action({ description: '...', input: z.object({...}) })
|
|
243
|
-
async myAction(input, ctx) {
|
|
244
|
-
return this.db.query(...)
|
|
245
|
-
}
|
|
246
|
-
}
|
|
184
|
+
SilkweaveModule.forRoot({
|
|
185
|
+
silkweave: { name: 'app', description: 'My App', version: '1.0.0' },
|
|
186
|
+
adapters: [mcp({ basePath: '/mcp' })],
|
|
187
|
+
globalGuards: [ApiKeyGuard] // runs before each method/class @UseGuards; throttler etc. deliberately excluded
|
|
188
|
+
})
|
|
247
189
|
```
|
|
248
190
|
|
|
249
|
-
|
|
191
|
+
The allow-list is explicit-by-class on purpose - a blanket "run every global" would also fire unrelated globals (e.g. a `ThrottlerGuard`, which assumes a writable response MCP doesn't provide). Listed guards run **before** the method/class `@UseGuards`, mirroring Nest's request pipeline. They see the same request stand-in as method guards: headers, `url`, and path `params` (populated from reflected `@Param` fields); only `query`/IP-derived logic won't apply over MCP.
|
|
192
|
+
|
|
193
|
+
Controllers are normal Nest providers - inject services via the constructor as usual.
|
|
194
|
+
|
|
195
|
+
## What does *not* run
|
|
196
|
+
|
|
197
|
+
Because the handler is invoked directly (not through Nest's HTTP request pipeline), the following do **not** apply on a tool call - only `@UseGuards()` (plus any opted-in `globalGuards`) and parameter-bound pipes do:
|
|
250
198
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
| `users.list` | `/users/list` | `usersList` | `UsersList` |
|
|
255
|
-
| `posts.get-by-id` | `/posts/get-by-id` | `postsGetById` | `PostsGetById` |
|
|
199
|
+
- Globally-registered `ValidationPipe` / interceptors / exception filters (MCP input is instead validated against the reflected Zod schema).
|
|
200
|
+
- DTO class instantiation - whole-DTO `@Body()`/`@Query()` arguments arrive as plain objects, so `@Transform` and DTO methods do not fire.
|
|
201
|
+
- Discriminated-union / `@ValidateIf` XOR constraints can't be expressed as hard MCP schema constraints - document them in the tool `description` or supply `@Mcp({ input })`.
|
|
256
202
|
|
|
257
203
|
## Notes
|
|
258
204
|
|
|
259
|
-
- **
|
|
260
|
-
- **Express only by default.** `@nestjs/platform-fastify` requires `@fastify/express` registered upfront
|
|
261
|
-
-
|
|
262
|
-
- **`reflect-metadata`** must be imported once at the top of your entry file (typical Nest requirement).
|
|
205
|
+
- **Opt-in.** Only methods carrying `@Mcp()` are exposed - controllers are never auto-published.
|
|
206
|
+
- **Express only by default.** `@nestjs/platform-fastify` requires `@fastify/express` registered upfront.
|
|
207
|
+
- **`reflect-metadata`** must be imported once at the top of your entry file.
|
|
263
208
|
|
|
264
209
|
## License
|
|
265
210
|
|