Skip to content

perf(sdk): load the SSE transport with the first SSE session - #937

Merged
alexmercerpo merged 1 commit into
perf/1.10.x-lazy-optional-depsfrom
perf/1.10.x-sse-transport-on-demand
Oct 21, 2026
Merged

alexmercerpo merged 1 commit into
perf/1.10.x-lazy-optional-depsfrom
perf/1.10.x-sse-transport-on-demand

Conversation

@frontegg-david

@frontegg-david frontegg-david commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Stack 3 of 4, based on #935 (perf/1.10.x-lazy-optional-deps). Merge in order.

What and why

A stdio server serves no HTTP, but its scope still creates the TransportService and with it the transport adapters. TransportSSEAdapter imported RecreateableSSEServerTransport, whose base class reads POST bodies with raw-body and content-type, so every stdio server loaded raw-body, content-type, iconv-lite, http-errors, statuses, depd, bytes and five more small packages: 16 files it never uses.

TransportSSEAdapter now imports the transport as a type and loads the class with loadSseTransport(). TransportService awaits it where it creates the LocalTransporter of an SSE session, which it already did asynchronously. That is what #924 did for the Streamable HTTP transport, so a stdio server now loads neither transport, and an HTTP server loads each with its first session of that type. The adapter throws an InternalMcpError if it creates a transport before the class is loaded, as the Streamable HTTP adapter does.

The HTTP server loads the same modules as before: Express's body-parser requires raw-body and content-type anyway. The stdio server loads 264 modules instead of 280.

Results

Apple M4 Pro (14 cores, 48 GiB), macOS 15.6.1, Node.js 24.18.1, on AC power, with the machine's normal background load (an IDE and other jobs). The server is the benchmark's FrontMCP implementation (frontmcp build --target node), run with node and no flags, against npm packed builds of this branch and its base, installed into scratch copies of the implementation and built with npm run build, measured in one session in randomized, interleaved rounds with the benchmark's own harness functions. Cold start is the time from spawning node to the first tools/call response, initialize included: one discarded start per server, then 8 rounds of 4 starts per transport in random order. The 100-tool startup is spawn to the first full tools/list, 5 rounds of 2 starts, and its RSS is read 1 s after that. Throughput is the benchmark's closed-loop tools/call at concurrency 1, 16 and 64 (3 s warmup, 10 s measured, 5 rounds); RSS after startup is read 1 s after the first call, RSS after load after the last level, median of the 5 rounds. Heap used is process.memoryUsage().heapUsed 700 ms after start (after the first call, no forced GC), median of 10 interleaved starts per transport in a separate run right after. MB is 10^6 bytes. The 1-minute load average was 3.4 to 7.5 during the session (median 4.1), and 3.0 to 5.6 during the heap runs.

perf/1.10.x-lazy-optional-deps This PR
Cold start, stdio, p50 / p90 182.2 / 186.8 ms 178.5 / 184.9 ms
Cold start, HTTP, p50 / p90 242.1 / 258.7 ms 243.6 / 255.3 ms
100 tools, spawn to first full tools/list, p50 259.5 ms 261.5 ms
RSS after startup (HTTP) 140.2 MB 140.8 MB
RSS after startup, 100-tool server 146.3 MB 146.2 MB
Heap used after startup, HTTP 37.1 MB 37.3 MB
Heap used after startup, stdio 28.7 MB 28.5 MB
RSS after startup, stdio (read in-process) 125.2 MB 122.3 MB
RSS after load (HTTP) 481.7 MB 477.2 MB
Throughput, c=1 / 16 / 64 6,027 / 9,116 / 8,849 rps 6,014 / 9,052 / 8,786 rps

The stdio cold start was lower in every one of the 8 rounds, by a median of 1.6 ms (0.5 to 14.3 ms). A separate cold-start run of 16 rounds of 4 starts per transport, at a load average of 6.5 to 7.8, gave stdio 176.9 → 173.4 ms at p50 and 184.4 → 178.8 ms at p90, lower in 14 of 16 rounds by a median of 4.2 ms. The HTTP rows, the 100-tool server and throughput don't run this code and are within their run-to-run spread (c=64: 8,683 to 9,005 rps for the base, 8,645 to 9,063 for this PR).

Tests

  • transport.registry.spec.ts: a new test checks that TransportService loads the SSE transport for SSE sessions only, next to the existing one for the Streamable HTTP transport.
  • New transport.sse.adapter.load.spec.ts: creating a transport before loadSseTransport() throws InternalMcpError and constructs nothing; after it, createTransport and createTransportFromSession construct the transport with the same arguments as before.
  • nx run sdk:test --coverage: 7,142 tests in 559 suites pass.
  • nx run sdk:lint, nx run sdk:typecheck and Prettier on the changed files pass.
  • e2e: testing 21 (among them a server over the legacy HTTP+SSE transport), transport-recreation 40 (SSE session reconnect and event replay), stdio-transport 26, standalone 49, skills 188 (2 skipped), serverless 17, protocol-20260728 145 and unix-socket 20 pass.
  • Loaded modules, recorded with a module.registerHooks preload: the benchmark's stdio server loads 264 modules instead of 280, without raw-body, content-type, iconv-lite, http-errors and their dependencies; its HTTP server loads the same 404 as before.
  • The benchmark's correctness gate (HTTP, stdio and the 100-tool server) passes against this build.

Summary by CodeRabbit

  • Performance
    • SSE transport components now load on demand, reducing unnecessary initial loading for other transport types.
    • Streamable HTTP, stateless HTTP, and standard input/output connections avoid loading unused SSE components.
  • Reliability
    • SSE sessions continue to support transport creation and restoration after the transport is loaded.
    • Added validation to prevent SSE transport creation before loading is complete.

A stdio server initializes the scope's TransportService, and with it
TransportSSEAdapter, which defined RecreateableSSEServerTransport at startup.
Its base class reads POST bodies with raw-body and content-type, so a stdio
server loaded raw-body, content-type, iconv-lite, http-errors and their
dependencies (16 files) that it never uses.

TransportSSEAdapter now imports the transport as a type and loads it with
loadSseTransport(), which TransportService awaits where it creates the
LocalTransporter of an SSE session, as it does for the Streamable HTTP
transport since #924.
@frontegg-david
frontegg-david added this pull request to stack #936 October 21, 2026 15:45
@frontegg-david

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration
  • Configuration used: Repository: agentfront/frontmcp/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Essentials
  • Run ID: 506668e7-abb6-4d9a-86af-e64779129701
📥 Commits

Reviewing files that changed from the base of the PR and between bc56dbf and 795407e.

📒 Files selected for processing: 4
  • libs/sdk/src/transport/__tests__/transport.registry.spec.ts
  • libs/sdk/src/transport/adapters/__tests__/transport.sse.adapter.load.spec.ts
  • libs/sdk/src/transport/adapters/transport.sse.adapter.ts
  • libs/sdk/src/transport/transport.registry.ts
+------------------------------------------+
| 💣 Deploying bug fixes in 3... 2... 1... |
+------------------------------------------+
  \
   \     /)/)
        ( '')
        /  |
      o(___)

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: agentfront/frontmcp/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Essentials
  • Run ID: 11cd1988-e180-42db-a933-2537361defda
📥 Commits

Reviewing files that changed from the base of the PR and between bc56dbf and 795407e.

📒 Files selected for processing: 4
  • libs/sdk/src/transport/__tests__/transport.registry.spec.ts
  • libs/sdk/src/transport/adapters/__tests__/transport.sse.adapter.load.spec.ts
  • libs/sdk/src/transport/adapters/transport.sse.adapter.ts
  • libs/sdk/src/transport/transport.registry.ts

Included review availability: This review used your included allowance. 3 included reviews remain after this review. Your included PR review attempts over the past 7 days set your current allowance at 5 reviews per hour.

📝 Walkthrough

The SSE transport adapter now loads its transport class on demand and caches it. The transport registry invokes this loader for SSE sessions. Tests cover session-type selection, unloaded transport errors, and transport creation and restoration after loading.

Changes: SSE transport loading

Layer / File(s) Summary
Adapter loading and transport creation
libs/sdk/src/transport/adapters/transport.sse.adapter.ts, libs/sdk/src/transport/adapters/__tests__/transport.sse.adapter.load.spec.ts
loadSseTransport() dynamically imports and caches the SSE transport class. Creation and session restoration use the cached class. Tests cover use before loading and creation after loading.
Registry loading by session type
libs/sdk/src/transport/transport.registry.ts, libs/sdk/src/transport/__tests__/transport.registry.spec.ts
The registry loads the SSE transport for SSE sessions. Tests verify that Streamable HTTP and stateless HTTP sessions do not load it.

Priority: ⬇️ Low

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Refactor

Merge Risk: ⚪ Minimal · up to 79540

The SSE loading change is ready to merge after normal checks; no blocking compatibility or runtime issue was established.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 65.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 3 functions across 4 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Title check Passed The title clearly and concisely describes the main change: lazy SSE transport loading on the first SSE session.
Description check Passed The description is complete and relevant. It explains the motivation, implementation, performance impact, and extensive validation. The optional user-facing change section is omitted appropriately bec…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Description check

Explanation

The description is complete and relevant. It explains the motivation, implementation, performance impact, and extensive validation. The optional user-facing change section is omitted appropriately because this is an internal performance change.

✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

Performance Test Results

Status: ✅ All tests passed

Summary

Project Tests Passed Warnings Failed Leaks
✅ demo-e2e-agents 4 4 0 0 0
✅ demo-e2e-cache 11 11 0 0 0
✅ demo-e2e-codecall 4 4 0 0 0
✅ demo-e2e-config 4 4 0 0 0
✅ demo-e2e-direct 3 3 0 0 0
✅ demo-e2e-elicitation 1 1 0 0 0
✅ demo-e2e-errors 4 4 0 0 0
✅ demo-e2e-hooks 3 3 0 0 0
✅ demo-e2e-multiapp 4 4 0 0 0
✅ demo-e2e-notifications 3 3 0 0 0
✅ demo-e2e-openapi 2 2 0 0 0
✅ demo-e2e-providers 4 4 0 0 0
✅ demo-e2e-public 4 4 0 0 0
✅ demo-e2e-redis 15 15 0 0 0
✅ demo-e2e-remember 4 4 0 0 0
✅ demo-e2e-remote 5 5 0 0 0
✅ demo-e2e-serverless 2 2 0 0 0
✅ demo-e2e-skills 15 15 0 0 0
✅ demo-e2e-standalone 2 2 0 0 0
✅ demo-e2e-transport-recreation 3 3 0 0 0
✅ demo-e2e-ui 4 4 0 0 0

Total: 101 tests across 21 projects

📊 View full report in workflow run


Generated at: 2026-10-21T16:00:05.269Z
Commit: 55f369e5

@coderabbitai

coderabbitai Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor
⚠️ Action not completed

Pull request base or head changed.

@alexmercerpo
alexmercerpo merged commit f13b8af into release/1.10.x Oct 21, 2026
43 of 44 checks passed
@alexmercerpo
alexmercerpo deleted the perf/1.10.x-sse-transport-on-demand branch October 21, 2026 16:12
@github-actions

Copy link
Copy Markdown
Contributor

Cherry-pick Created

A cherry-pick PR to main has been automatically created.

Please review and merge if this change should also be in main.

If the cherry-pick is not needed, close the PR.

alexmercerpo pushed a commit that referenced this pull request Oct 21, 2026
* perf(sdk): serve in-memory, direct-client and web-standard MCP with the loaded protocol module (#934)

createInMemoryServer(), the direct client's notification and elicitation
handlers, and runWebStandardMcp() took their classes and schemas from
`await import('@frontmcp/protocol')`. In a CommonJS process that import
resolves the package's `import` condition, so connect(), create() clients and
createFetchHandler() loaded a second, ESM copy of @frontmcp/protocol and,
through it, of the MCP SDK, on top of the CommonJS copy the SDK had already
required.

They now use the statically imported @frontmcp/protocol, as runStdio() does
since #923. In an ESM process the import resolves the same ESM module as before.

(cherry picked from commit 5b54191)

* perf(sdk): load js-yaml, semver and the uipack renderer on first use under require() (#935)

A server loaded js-yaml (25 files), semver (23) and the @frontmcp/uipack root
at startup, for code most servers never run: SKILL.md and YAML config parsing,
the ESM loader's version ranges, and the auth-UI pages.

They now come through three subpath imports, #js-yaml, #semver and
#uipack-render, whose `require` variant reads each function from its package
the first time it is used, like @frontmcp/protocol's #client. The CommonJS
build keeps them external and ships both variants; the ESM and browser builds
resolve them at build time to the static variant, so those bundles import the
packages as before.

(cherry picked from commit b7ee831)

* perf(sdk): load the SSE transport with the first SSE session (#937)

A stdio server initializes the scope's TransportService, and with it
TransportSSEAdapter, which defined RecreateableSSEServerTransport at startup.
Its base class reads POST bodies with raw-body and content-type, so a stdio
server loaded raw-body, content-type, iconv-lite, http-errors and their
dependencies (16 files) that it never uses.

TransportSSEAdapter now imports the transport as a type and loads it with
loadSseTransport(), which TransportService awaits where it creates the
LocalTransporter of an SSE session, as it does for the Streamable HTTP
transport since #924.

(cherry picked from commit f13b8af)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants