Skip to content

Commit b78fcad

Browse files
authored
Merge pull request #24 from nestm-dev/codex/mcp-upstream-gaps-alpha7
feat: streaming SSRF transport, bounded capture + catalog digest, runtime snapshot schemas
2 parents 229b58b + 40502ee commit b78fcad

71 files changed

Lines changed: 5980 additions & 1357 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
"@nestm/mcp-conformance": minor
3+
---
4+
5+
Added bounded hostile-value capture and canonical catalog digests, so hosts stop hand-rolling both
6+
around the canonicalizer. `canonicalizeMcpConformanceValue` enforces only its own fixed ceilings and
7+
walks a hostile shape on the way there; `captureMcpConformanceValue(value, limits)` refuses the
8+
shape instead, copying an untrusted value into deep-frozen, null-prototype JSON data under
9+
caller-supplied bounds and rejecting proxies, accessor and non-enumerable properties, symbol keys,
10+
sparse or subclassed arrays, exotic prototypes, cycles, and non-finite numbers.
11+
`captureMcpToolArguments` layers a bounded argument record on it with predictive byte accounting
12+
that is cross-checked against the canonicalizer's exact output byte length. Bounds resolve through
13+
`resolveMcpConformanceCaptureLimits` against `MCP_CONFORMANCE_DEFAULT_CAPTURE_LIMITS` and
14+
`MCP_CONFORMANCE_HARD_CAPTURE_LIMITS`, and a refusal is an `McpConformanceCaptureError` carrying a
15+
structural `code` that never quotes the rejected value.
16+
17+
`digestMcpRuntimeCatalog(snapshot, { domain, toolSchemaDomain, limits })` returns one
18+
`catalogFingerprint` plus a per-tool `schemaDigest` so a management path and a serving path can
19+
compare the same surface. The snapshot is typed structurally by its `tools`, `resources`,
20+
`resourceTemplates`, and `prompts` collections, keeping the kernel free of a runtime dependency.
21+
Discovery order never reaches a digest — each collection sorts on its identity with a canonical-form
22+
tiebreak — and a repeated identity is refused rather than silently collapsed. `fingerprintMcpConformanceValue`'s
23+
domain rule now also validates both caller-supplied digest domains, and the new
24+
`toMcpConformanceFingerprintHex` renders a `sha256:<base64url>` fingerprint as 64 lowercase
25+
hexadecimal characters for digest columns that check a fixed-width hexadecimal form.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
"@nestm/mcp-conformance": minor
3+
---
4+
5+
Scoped the package to what it actually does and dropped the dead CI-regression half. The manifest
6+
description and keywords now state runtime integrity and safety probing for third-party MCP servers
7+
instead of conformance testing, and the README opens by disclaiming any relationship to the official
8+
`@modelcontextprotocol/conformance` spec-compliance suite: this kernel runs bounded, read-only
9+
probes against a server you do not control and supplies the canonicalization, bounded-capture, and
10+
catalog-fingerprinting primitives that detect catalog drift and refuse hostile payloads. It asserts
11+
nothing about the MCP specification.
12+
13+
`compareMcpConformanceReports` and `toMcpConformanceJUnit` are removed with their types
14+
(`McpConformanceReportComparison`, `McpConformanceComparableReportComparison`,
15+
`McpConformanceIncomparableReportComparison`, `McpConformanceComparisonVerdict`,
16+
`McpConformanceCheckChange`), along with the now-unreachable `maxJunitBytes` entry in
17+
`MCP_CONFORMANCE_DEFAULT_LIMITS` and `MCP_CONFORMANCE_HARD_LIMITS`. Baseline-versus-candidate
18+
comparison and JUnit projection belong to a release pipeline, not to a runtime integrity kernel, and
19+
no caller in this repository used either. Plan execution, immutable reports, fingerprints, capture,
20+
catalog digests, and bounded JSON parsing and serialization are unchanged.
21+
22+
`descriptor.subject` keeps its name — the control-plane API response contract and its web client
23+
both read it — and now carries a schema doc comment stating what it records: the build of the
24+
observing client that produced the report, never the observed target, which is `descriptor.target`.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
"@nestm/mcp-manager": minor
3+
"@nestm/mcp": minor
4+
---
5+
6+
Publish `MCP_RUNTIME_PHASES` and `MCP_RUNTIME_PROTOCOL_ERAS` as frozen tuples that mirror the
7+
`McpRuntimePhase` and `McpRuntimeProtocolEra` unions through a compile-time exhaustiveness fence,
8+
together with `mcpRuntimeStateSnapshotSchema`, `mcpRuntimeProbeSnapshotSchema`, and
9+
`mcpRuntimeCapabilitiesSnapshotSchema`. The validators implement Standard Schema v1 without adding
10+
a runtime dependency: they accept exactly what the manager emits, reject unknown properties and
11+
unpublished phases, eras, or state error codes, and return a frozen normalized snapshot so hosts
12+
can validate persisted projections on the way back in.
13+
14+
Accept an `McpRuntimeToolCallOptions` object as the fourth `callTool` argument. A positional
15+
`AbortSignal` stays source compatible as the cancellation-only form, and pinning `toolDefinition`
16+
threads that exact definition to the managed client runtime so structured output is validated
17+
against it instead of a cached `tools/list` view. `McpManagerService` from `@nestm/mcp/manager`
18+
inherits the widened call surface.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
"@nestm/mcp-auth": minor
3+
---
4+
5+
Add a transport-grade streaming SSRF-guarded fetch and give the existing guarded transports an
6+
exact host allowlist and a loopback-http door.
7+
8+
`createStreamingSsrfGuardedFetch` is a complete `FetchLike` — it drops into
9+
`McpHttpClientTransportDefinition.fetch` with no cast — that keeps the connect-time DNS pinning,
10+
blocked-range predicate, SNI pinning, and forced `accept-encoding: identity` of
11+
`createSsrfGuardedFetch` while handing back a live `ReadableStream`, which is what a long-lived
12+
`text/event-stream` MCP session needs and the buffering fetch structurally cannot serve. Redirects
13+
are manual by construction and any 3xx is rejected. `maxResponseBytes` (4 MiB) meters ordinary
14+
bodies as a running total and against a declared `Content-Length`; `text/event-stream` swaps it for
15+
a per-event `maxSseEventBytes` budget (1 MiB, CR/LF/CRLF framing, reset at each blank line) with no
16+
total cap, plus an `idleTimeoutMs` gap between bytes. A violation errors the stream and destroys the
17+
connection. Zero new runtime dependencies — it is built on `node:https`/`node:http` like the
18+
existing guarded fetch.
19+
20+
`admitMcpHttpEndpoint` and `openGuardedFetch` split admission from the connection so a host can
21+
reject an endpoint before decrypting the credential it would have sent; the lease replays only the
22+
answers pinned at admission and confines every request to the admitted origin.
23+
24+
`createSsrfGuardedFetch` and `createNodeDocumentFetcher` gain the shared `allowedHosts` (exact match
25+
after normalization) and `allowLoopbackHttp` (permits `http:` only to a host whose every resolved
26+
address is loopback) options; both are fail-closed, so existing callers see no behavior change.
27+
Also exported: `normalizeGuardedHost`, `isLoopbackAddress`, `McpGuardedHostPolicyOptions`,
28+
`McpResolvedAddress`, and the `MCP_STREAM_*` fence constants. `McpDocumentFetchFailure` gains a
29+
`host-not-allowed` reason, which CIMD resolution maps to the existing `host-not-allowed` failure.

.github/workflows/ci.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,21 @@ jobs:
7171
- run: pnpm run verify:apps
7272
- run: pnpm --filter "./packages/*" exec publint --strict
7373
- run: pnpm run verify:attw
74+
75+
conformance:
76+
name: Official MCP conformance suite
77+
runs-on: ubuntu-latest
78+
timeout-minutes: 15
79+
steps:
80+
- uses: actions/checkout@v7
81+
- uses: pnpm/setup@v2
82+
with:
83+
install: false
84+
- uses: actions/setup-node@v7
85+
with:
86+
node-version: 24
87+
cache: pnpm
88+
- run: pnpm install --frozen-lockfile
89+
# The fixture imports @nestm/mcp-server through its published export map.
90+
- run: pnpm --filter "./packages/*" run build
91+
- run: pnpm --filter @nestm/mcp-conformance-harness run conformance

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ The primary use case is an artifact or agent runtime that needs to expose truste
1414
| `@nestm/mcp-core` | Framework-neutral operation context, middleware, authorization decisions, and lifecycle observation | Implemented |
1515
| `@nestm/mcp-client` | Named v2 client runtime, strict outbound OAuth, isolated leases, transports, typed requests, managed listening, and middleware | Implemented |
1616
| `@nestm/mcp-manager` | Framework-neutral bounded ownership and lifecycle for opaque, dynamically resolved MCP client generations | Implemented |
17-
| `@nestm/mcp-conformance` | Framework-neutral bounded plan execution, immutable reports, semantic regression comparison, fingerprints, and JSON/JUnit export | Alpha |
17+
| `@nestm/mcp-conformance` | Framework-neutral bounded probe execution, immutable reports, hostile-value capture, fingerprints, and catalog-drift digests | Alpha |
1818
| `@nestm/mcp-server` | Framework-neutral per-request server runtime, feature registry, web-standard/Node/stdio serving, and OAuth resource-server wrapper | Implemented |
1919
| `@nestm/mcp-apps` | Stable MCP Apps server metadata, validation, decorator/native fragments, capability advertisement, and direct-server feature composition | Alpha |
2020
| `@nestm/mcp-gateway` | Tool, prompt, resource, resource-template, and completion projection with policy enforcement and auth-scoped discovery caching | Implemented |

apps/control-plane-api/README.md

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,9 @@ digests—not endpoints, credentials, runtime names, raw catalogs or schemas, re
8989
caught error details.
9090

9191
Dashboard history is operational convenience, not a durable release baseline. Cross-release
92-
regression checks must persist baseline and candidate JSON/JUnit artifacts outside this process and
93-
compare reports produced by separate library builds. This API intentionally provides neither
94-
baseline approval nor durable artifact storage.
92+
regression checks must persist bounded report JSON outside this process and compare reports produced
93+
by separate library builds. This API intentionally provides neither baseline approval nor durable
94+
artifact storage.
9595

9696
The conformance routes share this validation host's loopback/private deployment boundary. They do
9797
not add caller authentication or make the host safe to expose through a routable bind or public
@@ -116,8 +116,20 @@ limits before broadening that boundary.
116116
- Conformance requests select only the host-owned passive plan, pin the full run to one managed
117117
generation lease, enforce time/page/item/schema/concurrency bounds, and never infer permission
118118
from upstream annotations.
119-
- This exact-host fetch guard demonstrates the admission seam; an internet-facing product still
120-
needs DNS pinning/rebinding protection, response-body limits, authentication, and rate limiting.
119+
- Outbound MCP transports run on NestM's streaming SSRF-guarded fetch, so this host inherits
120+
connect-time DNS pinning, blocked private/link-local ranges, refused redirects, and response
121+
fences (a total byte cap for ordinary responses, a per-event cap for SSE) without owning that
122+
code. The application supplies only policy: the exact host allowlist and the loopback switch.
123+
An internet-facing product still needs authentication, rate limiting, and egress authorization.
124+
- OAuth discovery, registration, and token requests run on the same package's buffered guarded
125+
fetch, which is pinned to HTTPS here because the interactive redirect is a browser navigation.
126+
This host adds only its own endpoint policy: a GET may reach the resource origin or an
127+
allow-listed authorization host, and a credential-bearing POST must exactly equal the discovered
128+
token or registration endpoint.
129+
- Runtime OAuth credentials are held as revisioned generations behind the client package's
130+
credential-store port. Refresh, single-flight coalescing across concurrent 401s, claim/commit
131+
fencing, and terminal invalidation belong to that package; this host owns only the projected
132+
status, the process-local storage, and the fenced-generation lifecycle.
121133

122134
## Run
123135

apps/control-plane-api/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"@nestjs/platform-fastify": "12.0.0-alpha.5",
3131
"@nestjs/swagger": "12.0.0-alpha.2",
3232
"@nestm/mcp": "workspace:^",
33+
"@nestm/mcp-auth": "workspace:^",
3334
"@nestm/mcp-client": "workspace:^",
3435
"@nestm/mcp-conformance": "workspace:^",
3536
"@nestm/mcp-gateway": "workspace:^",

0 commit comments

Comments
 (0)