@lenne.tech/nest-server 11.42.4 → 11.42.6

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.
@@ -569,7 +569,7 @@ Both use body-parser's default limit of **100 kB** unless `bodyParser` says othe
569
569
  bodyParser: { json: { limit: '2mb' } },
570
570
  ```
571
571
 
572
- `CoreBodyParserInitializer` applies it during module init by replacing the handle of the global parser layers NestJS created — in place, because NestJS registers them before any init hook runs and a parser appended later would sit behind the routes. Running during module init is what makes the limit hold under `Test.createTestingModule()`, which never runs `main.ts`. A path-scoped parser (`server.use('/upload', json({ limit }))` in `main.ts`) runs before the global one and is left untouched — that remains the way to give one path a different limit.
572
+ `CoreBodyParserInitializer` applies it during module init by replacing the handle of the global parser layers NestJS created — in place, because NestJS registers them before any init hook runs and a parser appended later would sit behind the routes. Running during module init is what makes the limit hold under `Test.createTestingModule()`, which never runs `main.ts`. A path-scoped parser (`server.use('/upload', json({ limit }))` in `main.ts`) runs before the global one and is left untouched — that remains the way to give one path a different limit. Keep `server.useBodyParser('json')` (or `'urlencoded'`) in `main.ts` next to it: NestJS registers its global parser only when no layer with the same function name exists, so a path-scoped one registered before `app.init()` suppresses it, and every other path receives an empty body. Since 11.42.6 `CoreBodyParserInitializer` warns at boot in that case.
573
573
 
574
574
  The parser runs **before authentication and routing**. Over the limit, the whole request is refused:
575
575
 
@@ -19,7 +19,8 @@ pnpm update --depth Infinity proxy-addr
19
19
  ```
20
20
 
21
21
  Then add the `@graphql-tools/utils` override from §3 to `pnpm-workspace.yaml`, run `pnpm install`,
22
- and check §3 for an older `@graphql-tools/utils` 11.x copy, which the override does not reach.
22
+ refresh `source-map-js` (§3), check §3 for an older `@graphql-tools/utils` 11.x copy, which the
23
+ override does not reach, and commit `pnpm-workspace.yaml` together with `pnpm-lock.yaml`.
23
24
  Vendor-mode projects pick up the core update and need the same two dependency steps. The body limit
24
25
  needs no configuration: unset, both parsers keep the 100 kB they had.
25
26
 
@@ -62,13 +63,28 @@ server.useBodyParser('json', { limit: '2mb' });
62
63
  bodyParser: { json: { limit: '2mb' } },
63
64
  ```
64
65
 
66
+ **Exception: keep `useBodyParser()` when `main.ts` also registers a path-scoped parser** such as
67
+ `server.use('/upload', json({ limit: '20mb' }))`. NestJS registers its own global parser only if no
68
+ layer with the same function name exists yet, and a path-scoped `json()` registered before
69
+ `app.init()` counts as one. Removing `useBodyParser()` then leaves the app without a global JSON
70
+ parser: every request outside that path reaches its handler with an empty body, and a sign-in fails
71
+ with a validation error. Keep `server.useBodyParser('json')` (the configured limit is applied to it)
72
+ and set the limit in the config as above. Since 11.42.6 the server warns at boot when it finds this.
73
+
74
+ **Starter-based projects:** `config.env.ts` builds `local` / `e2e` / `ci` from `localConfig()` and the
75
+ deployed environments from `deployedConfig()`. Put the limit where BOTH see it — the starter ships a
76
+ shared `PROJECT_BODY_PARSER` constant for this since its 11.42.4 update — or set
77
+ `NSC__BODY_PARSER__JSON__LIMIT` in the environment. A value only in the deployed block leaves the e2e
78
+ suite and `lt dev up` at 100 kB.
79
+
65
80
  The config is applied during module init, so it holds in every test that builds the app with
66
81
  `Test.createTestingModule(...).createNestApplication()` — which never runs `main.ts`, so a
67
82
  `useBodyParser()` call there never reached the e2e suite. Once configured, the value also replaces a
68
83
  global parser your `main.ts` registered; keeping both is harmless, but the config wins.
69
84
 
70
85
  A **path-scoped** parser is left alone: `server.use('/upload', json({ limit: '20mb' }))` in `main.ts`
71
- still gives that one path its own limit. The parser runs before routing, so a per-route limit can only
86
+ still gives that one path its own limit, as long as the global parser stays registered next to it
87
+ (see the exception above). The parser runs before routing, so a per-route limit can only
72
88
  key on the path — and all GraphQL operations share one.
73
89
 
74
90
  **`rawBody: true`:** `req.rawBody` is kept for apps created with `NestFactory.create(…, { rawBody:
@@ -101,7 +117,7 @@ refused request, with method, path (without query string) and the config key to
101
117
  The status code is unchanged. Only clients that compare the message text against
102
118
  `'request entity too large'` need adjusting — branch on the status or on `LTNS_0304` instead.
103
119
 
104
- ## 3. Two dependency advisories your project must fix itself
120
+ ## 3. Dependency advisories your project must fix itself
105
121
 
106
122
  `pnpm` overrides apply only to the project that runs the install, so the framework's own fixes do not
107
123
  reach your tree — see [docs/security-overrides.md](../docs/security-overrides.md).
@@ -110,6 +126,7 @@ reach your tree — see [docs/security-overrides.md](../docs/security-overrides.
110
126
  | ------- | -------- | ----------------------- |
111
127
  | `proxy-addr` | [GHSA-jqcg-44mw-7w3h](https://github.com/advisories/GHSA-jqcg-44mw-7w3h) — **critical**: IP spoofing through an IPv4-mapped IPv6 address in a trusted subnet. Patched `>=2.0.8` | A lockfile refresh. `express` requests `^2.0.7`, so `2.0.8` is in range, but an existing lockfile stays on `2.0.7` |
112
128
  | `@graphql-tools/utils` | [GHSA-7mx3-vvmw-hjmv](https://github.com/advisories/GHSA-7mx3-vvmw-hjmv) — high: prototype pollution in `mergeDeep` (schema assembly). Covers `<=12.0.0` — **11.x included, with no 11.x fix**. Patched `>=12.0.1` | An override for the copy `@nestjs/graphql` 13.4.5 exact-pins (`12.0.0`), and — in an older lockfile — a refresh of `@graphql-tools/schema` / `merge`, which can still pull in 11.x (see below) |
129
+ | `source-map-js` | [GHSA-68fv-2mgg-jv7q](https://github.com/advisories/GHSA-68fv-2mgg-jv7q) — high: event-loop DoS through indexed source-map section offsets. Patched `>=1.2.2`. **Dev tooling only** (vitest > vite > postcss), but `pnpm audit` — and therefore `check` — reports it | A lockfile refresh: `pnpm update --depth Infinity source-map-js`. If `postcss` keeps a `1.2.1` copy, re-resolve it the same way rather than with `pnpm dedupe`, which moves unrelated peers too |
113
130
 
114
131
  `proxy-addr` is what Express uses to compute `request.ip` from `X-Forwarded-For` as far as
115
132
  `trustProxy` allows. With `trustProxy` set, every IP-keyed rate limit — the framework's own and any of
@@ -146,7 +163,13 @@ The current releases (schema 10.1.3, merge 9.2.6) request utils `^12.0.3`, which
146
163
  (22.22) and Node 24 satisfy that; if your `engines.node` still declares a lower floor, raise it — the
147
164
  override above cannot hold this copy back, because `^12.0.3` does not intersect its key.
148
165
 
149
- Then `pnpm install` and `pnpm audit` — both advisories must be gone.
166
+ Then `pnpm install` and `pnpm audit` — the advisories must be gone.
167
+
168
+ **Commit `pnpm-workspace.yaml` and `pnpm-lock.yaml` together.** Every step above leaves a consistent
169
+ lockfile once it has run to the end, but the override only reaches the lockfile through the
170
+ `pnpm install` that follows it. A `pnpm-workspace.yaml` committed with the override and without the
171
+ re-installed lockfile fails every `pnpm install --frozen-lockfile` — typically the Docker image build.
172
+ Check with `pnpm install --frozen-lockfile` locally before you push.
150
173
 
151
174
  ## 4. Native Better-Auth endpoints accept the session when the secret is auto-generated
152
175
 
@@ -0,0 +1,104 @@
1
+ # Migration Guide: 11.42.4 → 11.42.5
2
+
3
+ ## Overview
4
+
5
+ | Category | Details |
6
+ | -------------------- | ----------------------------------------------------------------------------------------------------------- |
7
+ | **New feature** | `TestHelper.mcpSession()` / `TestHelper.mcp()` — MCP tests without hand-written SSE parsing (§1) |
8
+ | **Tooling** | `check:overrides` accepts a declared "fix exists, but this consumer cannot use it" suppression that expires by itself (§2) |
9
+ | **Docs** | The 11.42.3 → 11.42.4 guide gained three steps; check them if you already applied it (§3) |
10
+ | **Breaking Changes** | None |
11
+ | **Migration Effort** | None required — `pnpm update`. Adopting the helpers is optional |
12
+
13
+ ## Quick Migration
14
+
15
+ ```bash
16
+ pnpm update @lenne.tech/nest-server
17
+ ```
18
+
19
+ Vendor-mode projects pick up the core update; the new helpers arrive with `src/test/`. No configuration
20
+ change is required.
21
+
22
+ ## 1. TestHelper: MCP tests without SSE parsing
23
+
24
+ Projects that enable the AI module's MCP server (`ai.mcp` in `config.env.ts`) should test their tools over
25
+ MCP as well as over REST/GraphQL: MCP has its own role filter and sessions, and a refused tool comes back as
26
+ a result with `isError: true`, not as an HTTP error. Until now such a test had to set the right `Accept`
27
+ header, carry the `mcp-session-id` header, and parse the SSE stream by hand. `TestHelper` now does that:
28
+
29
+ ```typescript
30
+ const session = await testHelper.mcpSession({ token: userToken });
31
+
32
+ expect((await session.listTools()).map((tool) => tool.name)).not.toContain('delete_user');
33
+
34
+ const refused = await session.callTool('delete_user', { id });
35
+ expect(refused.isError).toBe(true);
36
+
37
+ const result = await session.callTool('find_users', { query: '@test.com' });
38
+ expect(result.json.data.length).toBeGreaterThan(0);
39
+
40
+ await session.close();
41
+ ```
42
+
43
+ `testHelper.mcp(message, { statusCode, sessionId, token })` sends a single JSON-RPC message for the
44
+ HTTP-level cases (401 without a token, 404 for another user's session). Full reference: `src/test/README.md`
45
+ → "MCP Testing".
46
+
47
+ The helpers are additive and build on `testHelper.rest()`, so `token` and `cookies` behave as there. They add
48
+ no dependency, so vendor-mode projects receive them with `src/test/`. A project with its own MCP module (not
49
+ the AI module's `/ai/mcp`) passes its endpoint as `path`.
50
+
51
+ ## 2. `check:overrides`: a suppression for a fix the consumer cannot use
52
+
53
+ `auditConfig.ignoreGhsas` used to be usable only for advisories with no fix at all — the moment any fix
54
+ appeared, `check:overrides` failed with `FIX AVAILABLE`. That is right in the common case and a dead end in
55
+ one: the fix exists, but the package that pulls in the vulnerable version cannot use it (for example, a
56
+ patched major removed an export the consumer imports).
57
+
58
+ That case can now be declared next to the suppression:
59
+
60
+ ```yaml
61
+ auditConfig:
62
+ ignoreGhsas:
63
+ - GHSA-x6jw-m9v5-85vh
64
+ unusableFixConsumers:
65
+ GHSA-x6jw-m9v5-85vh: '@nuxt/devtools@3.4.1 cannot use 4.0.1'
66
+ ```
67
+
68
+ The declaration expires by itself: `check:overrides` fails with `RE-TEST` as soon as the consumer leaves the
69
+ tree or changes version, the advisory offers a patched version nobody assessed (a backport), or anything other
70
+ than exactly that consumer version pulls the vulnerable package — including a direct dependency of your own
71
+ project. An undeclared suppression of a fixed advisory still fails on sight.
72
+
73
+ This is a change to the guard script, not to the published runtime: projects created from
74
+ nest-server-starter or lt-monorepo receive it with their template update. Exhaust the remedies first (raise
75
+ the vulnerable package, raise or patch the consumer); the rules and the reasoning are in
76
+ `.claude/rules/package-management.md` → "When the fix exists and the consumer cannot use it".
77
+
78
+ ## 3. Three steps added to the 11.42.3 → 11.42.4 guide
79
+
80
+ If you already applied [11.42.3 → 11.42.4](./11.42.3-to-11.42.4.md), check these — they were added after its
81
+ release:
82
+
83
+ 1. **`source-map-js` < 1.2.2** ([GHSA-68fv-2mgg-jv7q](https://github.com/advisories/GHSA-68fv-2mgg-jv7q),
84
+ high, dev tooling only) is also reported by `pnpm audit`: `pnpm update --depth Infinity source-map-js`.
85
+ 2. **Commit `pnpm-workspace.yaml` and `pnpm-lock.yaml` together.** An override only reaches the lockfile through
86
+ the `pnpm install` that follows it; a workspace file committed without the re-installed lockfile fails every
87
+ `pnpm install --frozen-lockfile`, typically the Docker image build.
88
+ 3. **Starter-based projects:** set `bodyParser` where both `localConfig()` and `deployedConfig()` see it (the
89
+ starter ships a shared `PROJECT_BODY_PARSER` constant), or via `NSC__BODY_PARSER__JSON__LIMIT`. A value only
90
+ in the deployed block leaves the e2e suite and `lt dev up` at 100 kB.
91
+
92
+ ## Troubleshooting
93
+
94
+ | Symptom | Cause | Fix |
95
+ | ------- | ----- | --- |
96
+ | `check:overrides` fails with `RE-TEST` on a declared suppression | The consumer moved, a new patched version appeared, or another package (or your project directly) now pulls the vulnerable package | Re-assess: take the fix if it is now usable, otherwise update the declaration with what you re-checked (§2) |
97
+ | `check:overrides` fails with `FIX AVAILABLE` | A suppressed advisory gained a fix, and no declaration explains why it cannot be used | Take the fix; declare only after the remedies are exhausted (§2) |
98
+ | `session.callTool()` throws instead of returning `isError` | The JSON-RPC call itself failed (protocol error), not the tool | Assert on `session.request(...)` for protocol-level errors (§1) |
99
+
100
+ ## Module Documentation
101
+
102
+ - [TestHelper README — MCP Testing](../src/test/README.md)
103
+ - [AI module README](../src/core/modules/ai/README.md)
104
+ - `.claude/rules/package-management.md` — suppressions, overrides and the `check:overrides` guard
@@ -0,0 +1,97 @@
1
+ # Migration Guide: 11.42.5 → 11.42.6
2
+
3
+ ## Overview
4
+
5
+ | Category | Details |
6
+ | -------------------- | ------------------------------------------------------------------------------------------------------------ |
7
+ | **Correction** | The 11.42.3 → 11.42.4 guide told every project to drop `useBodyParser()` from `main.ts`. With a path-scoped parser in `main.ts` that removes JSON parsing everywhere else (§1) |
8
+ | **Improvement** | The server warns at boot when NestJS skipped its global body parser because of such a path-scoped one (§1) |
9
+ | **Tooling** | `check:overrides` no longer reports a failed audit as clean, and its probe follows `pnpm_config_registry` (§2) |
10
+ | **Breaking Changes** | None |
11
+ | **Migration Effort** | None for most projects. Projects that removed `useBodyParser()` while registering a path-scoped parser in `main.ts` must add it back (§1) |
12
+
13
+ ## Quick Migration
14
+
15
+ ```bash
16
+ pnpm update @lenne.tech/nest-server
17
+ ```
18
+
19
+ Then check `main.ts` as described in §1. Vendor-mode projects pick up the core update and need the
20
+ same check.
21
+
22
+ ## 1. Keep `useBodyParser()` next to a path-scoped parser
23
+
24
+ §1 of the [11.42.3 → 11.42.4 guide](./11.42.3-to-11.42.4.md) recommended moving the body limit from
25
+ `main.ts` into `bodyParser.json.limit` and deleting the `useBodyParser()` call. That is right, with one
26
+ exception the guide did not name.
27
+
28
+ NestJS registers its own global JSON and URL-encoded parsers inside `app.init()`, but only when no
29
+ layer with the same function name exists yet. It compares the NAME and nothing else. A path-scoped
30
+ parser registered in `main.ts` before `listen()` carries that name:
31
+
32
+ ```typescript
33
+ // main.ts
34
+ server.use('/diagnostics', json({ limit: '256kb' })); // a layer named `jsonParser`
35
+ ```
36
+
37
+ With `useBodyParser()` removed, NestJS treats the JSON parser as already applied and registers none.
38
+ Every request outside `/diagnostics` then reaches its handler with an empty body. A sign-in fails with
39
+ a validation error such as "Missing input", and before this release nothing was logged.
40
+
41
+ **Check `main.ts`.** If it registers `json(...)` or `urlencoded(...)` on a path, keep or restore the
42
+ global parser next to it:
43
+
44
+ ```typescript
45
+ // main.ts
46
+ server.use('/diagnostics', json({ limit: '256kb' }));
47
+ server.useBodyParser('json'); // the configured bodyParser.json.limit is applied to it
48
+ ```
49
+
50
+ Keep the limit itself in the config (`bodyParser.json.limit`), where it also holds under
51
+ `Test.createTestingModule()`. The e2e suite cannot catch the missing parser: the testing module never
52
+ runs `main.ts`, so it never registers the path-scoped parser either.
53
+
54
+ Since 11.42.6 the server logs a warning at boot in exactly this situation:
55
+
56
+ ```
57
+ No global json body parser is registered: NestJS skips its own when a layer named `jsonParser`
58
+ already exists, and a path-scoped `json()` parser registered before `app.init()` counts as one. …
59
+ ```
60
+
61
+ The framework only reports it. It does not register the missing parser itself: NestJS would have put
62
+ it after everything `main.ts` registered, and that position is no longer known during module init. A
63
+ raw-body webhook or a proxy registered after the path-scoped parser must keep receiving the unread
64
+ request stream.
65
+
66
+ ## 2. `check:overrides`: a failed audit is no longer reported as clean
67
+
68
+ Two fixes in `scripts/check-overrides.mjs` (and the shared registry block in `scripts/check.mjs`):
69
+
70
+ - When pnpm 11 cannot reach the registry, `pnpm audit --json` prints
71
+ `{"error": {"message": "fetch failed"}}` instead of a report. The guard read that as "0 advisories"
72
+ and printed `ok — N override(s) checked … none is failing`. It now prints
73
+ `could not obtain an audit report` (exit 0) on a live run, and fails (exit 1) when the failed report
74
+ was passed in with `--audit-file`, which is the CI path. The suppression checks still run in that
75
+ case, because they ask the GitHub Advisory API rather than the audit: a suppressed advisory that
76
+ gained a fix still fails with `FIX AVAILABLE`.
77
+ - The clean-tree probe reads `pnpm_config_registry`, the variable pnpm 11 audits against. It used to
78
+ read `npm_config_registry`, which pnpm 11 ignores, and could then ask a registry the audit never used.
79
+
80
+ `scripts/` is not part of the npm package. Projects created from nest-server-starter or lt-monorepo
81
+ receive these fixes with their template update.
82
+
83
+ ## Troubleshooting
84
+
85
+ | Symptom | Cause | Fix |
86
+ | ------- | ----- | --- |
87
+ | Boot warning "No global json body parser is registered" | `main.ts` registers a path-scoped `json()` and no global parser | Add `server.useBodyParser('json')` to `main.ts` (§1) |
88
+ | Requests arrive with an empty body outside one path; sign-in reports "Missing input" | Same cause, on a version before 11.42.6 | Same fix (§1) |
89
+ | `check:overrides` prints `could not obtain an audit report (fetch failed)` | The npm registry was not reachable during the audit | Re-run with network access, or pass a captured report with `--audit-file` |
90
+ | `check:overrides` fails with `records a failed audit, not a result` | The report passed with `--audit-file` is a pnpm error, not an audit | Capture the report again once the registry answers |
91
+
92
+ ## Module Documentation
93
+
94
+ - [REQUEST-LIFECYCLE.md — 0d. Body parsers](../docs/REQUEST-LIFECYCLE.md)
95
+ - [11.42.3 → 11.42.4 guide §1](./11.42.3-to-11.42.4.md)
96
+ - `.claude/rules/configurable-features.md` — Body Parser Limits row
97
+ - `.claude/rules/package-management.md` — the `check:overrides` guard
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lenne.tech/nest-server",
3
- "version": "11.42.4",
3
+ "version": "11.42.6",
4
4
  "description": "Modern, fast, powerful Node.js web framework in TypeScript based on Nest with a GraphQL API and a connection to MongoDB (or other databases).",
5
5
  "keywords": [
6
6
  "node",
@@ -2335,6 +2335,11 @@ export interface IServerOptions {
2335
2335
  * (`server.use('/upload', json({ limit }))`) is left alone, and stays the way to give ONE path a
2336
2336
  * different limit, since the parser runs before routing and GraphQL shares a single path anyway.
2337
2337
  *
2338
+ * Keep `server.useBodyParser('json')` in `main.ts` next to such a path-scoped parser. NestJS
2339
+ * registers its own global parser only if no layer with the same function name exists yet, so a
2340
+ * path-scoped `json()` registered before `app.init()` makes it skip the global one, and requests
2341
+ * outside that path arrive with an empty body. The initializer warns at boot when it finds that.
2342
+ *
2338
2343
  * One caveat: under the testing module the options passed to `createNestApplication()` are not
2339
2344
  * visible to providers, so a test app created with `{ rawBody: true }` gets no `req.rawBody` from
2340
2345
  * a CONFIGURED parser. Applications created with `NestFactory.create()` are not affected.
@@ -90,11 +90,8 @@ export class CoreBodyParserInitializer implements OnModuleInit {
90
90
  const limit = this.resolveLimit(type, config?.[type]?.limit);
91
91
  const layers = stack.filter((layer) => this.isGlobalParserLayer(layer, type));
92
92
 
93
- if (limit !== undefined && !layers.length) {
94
- this.logger.warn(
95
- `bodyParser.${type}.limit is configured, but no global ${type} body parser is registered ` +
96
- '(was the app created with `bodyParser: false`?) — the limit has no effect.',
97
- );
93
+ if (!layers.length) {
94
+ this.warnMissingGlobalParser(type, stack, limit);
98
95
  continue;
99
96
  }
100
97
 
@@ -147,6 +144,18 @@ export class CoreBodyParserInitializer implements OnModuleInit {
147
144
  return layer?.slash === true && typeof layer.handle === 'function' && layer.handle.name === PARSER_NAMES[type];
148
145
  }
149
146
 
147
+ /**
148
+ * Whether the application was created without `bodyParser: false`.
149
+ *
150
+ * Read from the same container options as `rawBody`. Under `Test.createTestingModule()` those
151
+ * options never reach the container, so the answer there is always "enabled".
152
+ */
153
+ protected isBodyParserEnabled(): boolean {
154
+ const container = (this.moduleRef as unknown as { container?: { contextOptions?: { bodyParser?: boolean } } })
155
+ ?.container;
156
+ return container?.contextOptions?.bodyParser !== false;
157
+ }
158
+
150
159
  /**
151
160
  * Whether the application was created with `rawBody: true`.
152
161
  *
@@ -210,6 +219,39 @@ export class CoreBodyParserInitializer implements OnModuleInit {
210
219
  );
211
220
  }
212
221
 
222
+ /**
223
+ * Explain why there is no global parser of this type, when there should be one.
224
+ *
225
+ * NestJS registers its own parsers inside `app.init()` only if no layer with the parser's function
226
+ * name exists yet (`ExpressAdapter.isMiddlewareApplied()` compares the NAME and nothing else). A
227
+ * path-scoped `server.use('/upload', json({ limit }))` in `main.ts` therefore counts as "applied",
228
+ * NestJS skips its global parser, and every request outside that path reaches its handler with an
229
+ * empty body — a sign-in fails with a validation error, and nothing says why. Restoring the parser
230
+ * from here is not safe: NestJS would have placed it after everything `main.ts` registered, and that
231
+ * position is no longer knowable during module init (a raw-body webhook or a proxy registered after
232
+ * the path-scoped parser must keep seeing the unread stream). So this reports, with the remedy.
233
+ */
234
+ protected warnMissingGlobalParser(type: CoreBodyParserType, stack: ExpressLayer[], limit: number | undefined): void {
235
+ const name = PARSER_NAMES[type];
236
+ const shadowed = stack.some((layer) => layer?.slash !== true && layer?.handle?.name === name);
237
+ if (shadowed && this.isBodyParserEnabled()) {
238
+ this.logger.warn(
239
+ `No global ${type} body parser is registered: NestJS skips its own when a layer named \`${name}\` ` +
240
+ `already exists, and a path-scoped \`${type}()\` parser registered before \`app.init()\` counts as one. ` +
241
+ `Every request outside that path reaches its handler with an EMPTY body. Register the global parser ` +
242
+ `in main.ts as well — \`server.useBodyParser('${type}')\` — a configured \`bodyParser.${type}.limit\` ` +
243
+ 'is applied to it.',
244
+ );
245
+ return;
246
+ }
247
+ if (limit !== undefined) {
248
+ this.logger.warn(
249
+ `bodyParser.${type}.limit is configured, but no global ${type} body parser is registered ` +
250
+ '(was the app created with `bodyParser: false`?) — the limit has no effect.',
251
+ );
252
+ }
253
+ }
254
+
213
255
  /**
214
256
  * Wrap a parser so its 413 is translated, keeping the name NestJS identifies the layer by.
215
257
  *
@@ -243,4 +243,82 @@ const result = await testHelper.graphQl(
243
243
  );
244
244
  ```
245
245
 
246
- See `TestGraphQLConfig` and `TestGraphQLOptions` interfaces in `test.helper.ts` for full configuration options.
246
+ See `TestGraphQLConfig` and `TestGraphQLOptions` interfaces in `test.helper.ts` for full configuration options.
247
+
248
+ ## MCP Testing (`testHelper.mcp()` / `testHelper.mcpSession()`)
249
+
250
+ An MCP client reaches the same services as REST and GraphQL through its own door: the MCP server of the AI module
251
+ (`/ai/mcp`, enabled with `ai.mcp` in `config.env.ts`) has its own role filter, its own sessions, and reports a refused
252
+ or failing tool as a result with `isError: true` instead of an HTTP error. A project that enables MCP therefore tests
253
+ its tools over MCP too, next to its API tests.
254
+
255
+ The transport answers requests as an SSE stream. These helpers handle the stream, the session header and the
256
+ handshake, so a test reads results directly.
257
+
258
+ ### `mcpSession(options?)`
259
+
260
+ Opens a session the way every MCP client does (`initialize`, then `notifications/initialized`) for the user of `token`
261
+ or `cookies`:
262
+
263
+ ```typescript
264
+ const session = await testHelper.mcpSession({ token: userToken });
265
+
266
+ // tools/list — filtered by the user's roles
267
+ const names = (await session.listTools()).map((tool) => tool.name);
268
+ expect(names).toContain('find_users');
269
+
270
+ // tools/call — `json` is the first text content parsed as JSON (the tool's own result)
271
+ const result = await session.callTool('find_users', { query: '@test.com' });
272
+ expect(result.isError).toBeFalsy();
273
+ expect(result.json.data.length).toBeGreaterThan(0);
274
+
275
+ // A tool outside the user's roles is refused, not run
276
+ const refused = await session.callTool('delete_user', { id });
277
+ expect(refused.isError).toBe(true);
278
+
279
+ // Any other JSON-RPC request; returns the whole message, protocol errors included
280
+ const reply = await session.request('prompts/list');
281
+
282
+ // End the session server-side
283
+ await session.close();
284
+ ```
285
+
286
+ | Member | Description |
287
+ | ------------------------------------ | ------------------------------------------------------------------------ |
288
+ | `sessionId` | `mcp-session-id` the server assigned |
289
+ | `initializeResult` | Result of `initialize` (`serverInfo`, `capabilities`, `protocolVersion`) |
290
+ | `listTools()` | `tools/list`, returns the tools array |
291
+ | `callTool(name, args?)` | `tools/call`, returns the `CallToolResult` plus `json` |
292
+ | `request(method, params?, options?)` | Any request in the session, returns the JSON-RPC message |
293
+ | `close()` | `DELETE` with the session id |
294
+
295
+ ### `mcp(message, options?)`
296
+
297
+ Sends a single JSON-RPC message and returns `{ message, response, sessionId }`. Use it for the HTTP-level cases a
298
+ session hides:
299
+
300
+ ```typescript
301
+ // No token → 401
302
+ await testHelper.mcp({ id: 1, method: 'initialize', params: {} }, { statusCode: 401 });
303
+
304
+ // Another user's session id → 404 (not 403: confirming the id exists would be an oracle)
305
+ await testHelper.mcp(
306
+ { id: 2, method: 'tools/list' },
307
+ { sessionId: adminSession.sessionId, statusCode: 404, token: userToken },
308
+ );
309
+ ```
310
+
311
+ ### TestMcpOptions
312
+
313
+ | Option | Type | Default | Description |
314
+ | ------------------ | ---------------------------------- | ------------------------------- | ----------------------------------------------- |
315
+ | `token` | `string` | - | Bearer token, the same one `rest()` takes |
316
+ | `cookies` | `string \| Record<string, string>` | - | Cookie authentication, same modes as `rest()` |
317
+ | `sessionId` | `string` | - | `mcp-session-id` header (`mcp()` only) |
318
+ | `statusCode` | `number` | `200`, `202` for a notification | Expected HTTP status (`mcp()` only) |
319
+ | `path` | `string` | `'/ai/mcp'` | Endpoint, for a project with its own MCP module |
320
+ | `headers` | `Record<string, string>` | - | Additional headers |
321
+ | `log` / `logError` | `boolean` | `false` | Same as `rest()` |
322
+
323
+ `mcpSession()` additionally takes `clientInfo` and `protocolVersion` for the `initialize` request.
324
+ `TestHelper.parseMcpMessage(response)` is the parser behind both, for tests that send requests themselves.