Skip to content

perf(sdk): serve in-memory, direct-client and web-standard MCP with the loaded protocol module - #934

Merged
alexmercerpo merged 1 commit into
release/1.10.xfrom
perf/1.10.x-protocol-static-imports
Oct 21, 2026
Merged

alexmercerpo merged 1 commit into
release/1.10.xfrom
perf/1.10.x-protocol-static-imports

Conversation

@frontegg-david

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

Copy link
Copy Markdown
Contributor

Stack 1 of 5, based on release/1.10.x. Merge in order.

What and why

#923 made runStdio() use the statically imported @frontmcp/protocol. Three other places still took classes and schemas from await importWithRequireFallback(() => import('@frontmcp/protocol'), ...):

  • createInMemoryServer(): McpServer, InMemoryTransport, McpError, ErrorCode and ElicitResultSchema. It serves connect(), create()'s clients and every other direct client.
  • The direct client's notification and elicitation handlers: ResourceUpdatedNotificationSchema and ElicitRequestSchema.
  • runWebStandardMcp() (the handleWebFetch stage of http:request, behind createFetchHandler() and the Worker builds): McpServer and WebStandardStreamableHTTPServerTransport.

The SDK's CommonJS build keeps that import() as a native dynamic import, which resolves @frontmcp/protocol's import condition. So a CommonJS process loaded the package a second time as ESM, and through it a second copy of the MCP SDK: its ESM types.js with every protocol schema, shared/protocol.js, the client and its transports, and the ESM builds of zod and zod-to-json-schema. That second copy was on the path to the first answer.

All three now use the statically imported @frontmcp/protocol, which the SDK already requires at startup. A CommonJS process uses one copy of @frontmcp/protocol and of the MCP SDK, the CommonJS one; the benchmark's connect() script loads 311 modules instead of 513, and none of them from an esm directory. An ESM process resolves the same ESM module as before. The browser and Worker builds keep @frontmcp/protocol external, as before.

What changes besides startup: in a CommonJS process these classes and schemas now come from the MCP SDK's CommonJS build instead of its ESM build (same package version), so an McpError they raise is an instance of the same class the rest of a CommonJS FrontMCP server uses.

The HTTP and stdio servers of the benchmark don't run this code: their loaded modules are the same before and after.

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). Two CommonJS scripts, run with node and NODE_ENV=production against npm packed builds of this branch and of release/1.10.x (the same code as the published 1.10.9-rc.3), installed into scratch copies of the benchmark's FrontMCP implementation. Both serve the benchmark's add tool:

  • connect(): connect(config), then client.callTool('add', ...).
  • createFetchHandler(): FrontMcpInstance.createFetchHandler(config), then initialize, notifications/initialized and tools/call as Web Requests.

Each script reports, from inside the process: the time from process start to @frontmcp/sdk being loaded, the time from there to the tools/call result, the total, and process.memoryUsage() 700 ms after the result (no forced GC). 12 rounds, the four (branch, script) pairs in random order in each round; medians. MB is 10^6 bytes. The 1-minute load average was 5.5 to 7.7.

release/1.10.x This PR
connect(): connect() to connected 81.0 ms 15.1 ms
connect(): process start to the tools/call result 245.9 ms 176.7 ms
connect(): heap used 40.6 MB 27.5 MB
connect(): RSS 146.4 MB 123.1 MB
connect(): modules loaded 513 311
createFetchHandler(): SDK loaded to the tools/call response 101.9 ms 47.1 ms
createFetchHandler(): process start to the tools/call response 262.3 ms 205.9 ms
createFetchHandler(): heap used 46.5 MB 32.1 MB
createFetchHandler(): RSS 149.0 MB 132.9 MB
createFetchHandler(): modules loaded 527 330

Loading the SDK takes the same time on both (162.3 and 157.9 ms for connect(), 155.2 and 158.3 ms for createFetchHandler()); the difference is all after it.

Tests

  • nx run sdk:test --coverage: 7,133 tests in 555 suites pass, among them the direct-client, connect(), create(), in-memory server and web fetch handler suites. The direct-client and connect() specs mock Client through jest.mock('@frontmcp/protocol') with the actual module for the rest, which the static import picks up the same way.
  • nx run sdk:lint, nx run sdk:typecheck and Prettier on the changed files pass.
  • e2e: direct 77, esm 26, cloudflare 16 (5 skipped), serverless 17, elicitation 38, testing 21 and stdio-transport 26 pass.
  • Loaded modules, recorded with a module.registerHooks preload: the two scripts above load 311 and 330 modules instead of 513 and 527, and no .mjs file or file under an esm directory. The benchmark's HTTP and stdio servers load the same 450 and 326 modules as before.
  • The benchmark's correctness gate (HTTP, stdio and the 100-tool server) passes against this build.

Summary by CodeRabbit

  • Refactor
    • Simplified protocol and transport loading across SDK client and server components.
    • Removed runtime module-loading fallbacks in favor of standard static loading.
    • Preserved existing public APIs and behavior.

…he loaded protocol module

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.
@coderabbitai

coderabbitai Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Important

Review skipped

Review was skipped as selected files did not have any reviewable changes.

⚙️ Run configuration
  • Configuration used: Repository: agentfront/frontmcp/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Essentials
  • Run ID: beb8682d-6597-428f-a768-b863a8a21a21
📥 Commits

Reviewing files that changed from the base of the PR and between 9aed2a5 and 9aed2a5.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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: 8c29d1a2-de96-4717-b13c-db9ca0d7620c
📥 Commits

Reviewing files that changed from the base of the PR and between 9db76e3 and 9aed2a5.

📒 Files selected for processing: 3
  • libs/sdk/src/direct/direct-client.ts
  • libs/sdk/src/transport/in-memory-server.ts
  • libs/sdk/src/transport/web-standard-mcp.ts

Included review availability: This review used your included allowance. 4 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

SDK protocol dependencies are now loaded through static imports in the direct client and transport modules. The dynamic ESM/CJS fallback paths were removed. DirectClientImpl.create also passes clientTransport to connect without an any cast.

Changes: SDK Protocol Imports

Layer / File(s) Summary
Static protocol loading
libs/sdk/src/direct/direct-client.ts, libs/sdk/src/transport/in-memory-server.ts, libs/sdk/src/transport/web-standard-mcp.ts
The modules statically import their protocol schemas, types, and implementations instead of using runtime fallback loading. DirectClientImpl.create passes clientTransport to connect without an any cast.

Priority: ⬇️ Low

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

Change: Refactor

Merge Risk: ⚪ Minimal · up to 9aed2

No actionable merge-blocking risk is established for this change.

Pre-merge checks: Passed 5
Check name Status Explanation
Title check Passed The title clearly identifies the SDK performance change and the three affected MCP paths. It is concise and directly related to the main change.
Description check Passed The description provides a detailed rationale, implementation scope, benchmark results, compatibility notes, and extensive test evidence. It omits the User-facing change section, but that section is o…
Docstring Coverage Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 65.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 2 functions across 3 files.
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 provides a detailed rationale, implementation scope, benchmark results, compatibility notes, and extensive test evidence. It omits the User-facing change section, but that section is optional when no public API change exists.

✨ 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-21T15:02:58.662Z
Commit: c2948865

@alexmercerpo
alexmercerpo merged commit 5b54191 into release/1.10.x Oct 21, 2026
44 checks passed
@alexmercerpo
alexmercerpo deleted the perf/1.10.x-protocol-static-imports 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