Skip to content

fix(controller): defer MCP base middleware to first request#5995

Open
elrrrrrrr wants to merge 1 commit into
nextfrom
fix/mcp-base-middleware-lazy
Open

fix(controller): defer MCP base middleware to first request#5995
elrrrrrrr wants to merge 1 commit into
nextfrom
fix/mcp-base-middleware-lazy

Conversation

@elrrrrrrr

@elrrrrrrr elrrrrrrr commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #5994.

Problem

The MCP route-setup methods acquire the base middleware eagerly during controller registration:

let mw = (app.middleware as any).teggCtxLifecycleMiddleware();
mw = self.composeGlobalMiddleware(mw);

app.middleware.teggCtxLifecycleMiddleware is loaded by egg’s loadMiddleware, which runs after controller registration (the tegg load-unit init / postCreate). Calling it at registration time throws:

TypeError: self.app.middleware.teggCtxLifecycleMiddleware is not a function
  at MCPControllerRegister.mcpStatelessStreamServerInit
  at MCPControllerRegister.register
  at AppLoadUnitControllerHook.postCreate

when booting an app that registers MCP controllers.

This is the same class of bug as #5994 (eager middleware access at registration), one layer deeper: #5994 deferred the configured global middlewares; this defers the base middleware too.

Fix

composeGlobalMiddleware now takes a thunk that resolves the base middleware on the first request, alongside the global middlewares. All four call sites (mcpStatelessStreamServerInit, mcpStreamServerInit, sseCtxStorageRun, mcpServerRegister) pass () => app.middleware.teggCtxLifecycleMiddleware() instead of the already-resolved middleware.

Tests

tegg/plugin/controller/test/lib/MCPGlobalMiddleware.test.ts (8 tests, all green):

  • new: defers the base middleware factory to the first request — asserts the base thunk is invoked exactly once, on the first request, not at wrap time.
  • strengthened the route-setup wiring test to assert teggCtxLifecycleMiddleware is not invoked at registration.

Verified end-to-end by patching the published @eggjs/controller-plugin@4.0.2-beta.14 dist into a downstream tegg app: the teggCtxLifecycleMiddleware is not a function boot error is gone.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved request handling by delaying middleware setup until the first request, reducing unnecessary startup work.
    • Ensured middleware chains are built once and reused, keeping behavior consistent across repeated requests.
    • Preserved the expected order of global and base middleware during request processing.

The MCP route-setup methods acquire the base middleware eagerly during
controller registration:

    let mw = app.middleware.teggCtxLifecycleMiddleware();
    mw = self.composeGlobalMiddleware(mw);

But \`app.middleware.teggCtxLifecycleMiddleware\` is loaded by egg's
loadMiddleware, which runs *after* controller registration (the tegg
load-unit init / postCreate). Calling it at registration time throws
\`teggCtxLifecycleMiddleware is not a function\` when booting an app that
registers MCP controllers.

This is the same class of bug as #5994 (eager middleware access at
registration), one layer deeper: that PR deferred the configured global
middlewares; this one defers the base middleware too. \`composeGlobalMiddleware\`
now takes a thunk that resolves the base middleware on the first request,
alongside the global middlewares.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 25, 2026 13:14
@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bae8921b-f96f-463e-933a-834256e1bf2e

📥 Commits

Reviewing files that changed from the base of the PR and between 76cafb1 and 834660a.

📒 Files selected for processing (2)
  • tegg/plugin/controller/src/lib/impl/mcp/MCPControllerRegister.ts
  • tegg/plugin/controller/test/lib/MCPGlobalMiddleware.test.ts

📝 Walkthrough

Walkthrough

MCPControllerRegister now passes the base lifecycle middleware as a thunk, defers global middleware composition until the first request, caches the resulting chain, and updates tests for deferred resolution and reuse.

Changes

Lazy MCP middleware resolution

Layer / File(s) Summary
Deferred middleware wiring and caching
tegg/plugin/controller/src/lib/impl/mcp/MCPControllerRegister.ts
mcpStatelessStreamServerInit, mcpStreamServerInit, sseCtxStorageRun, and mcpServerRegister pass teggCtxLifecycleMiddleware as a thunk, and composeGlobalMiddleware resolves the thunk on first request before composing and caching the middleware chain.
Lazy-resolution tests
tegg/plugin/controller/test/lib/MCPGlobalMiddleware.test.ts
Tests pass the base middleware as a thunk, add first-request and cache assertions, and verify registration-time wiring leaves register.globalMiddlewares undefined and the base factory uncalled.

Sequence Diagram(s)

sequenceDiagram
  participant Request
  participant MCPControllerRegister
  participant composeGlobalMiddleware
  participant teggCtxLifecycleMiddleware
  participant register.globalMiddlewares

  Request->>MCPControllerRegister: first request enters MCP route
  MCPControllerRegister->>composeGlobalMiddleware: call with mwFactory thunk
  composeGlobalMiddleware->>teggCtxLifecycleMiddleware: invoke mwFactory()
  composeGlobalMiddleware->>register.globalMiddlewares: resolve configured global middleware
  composeGlobalMiddleware->>composeGlobalMiddleware: cache composed middleware
  Request->>MCPControllerRegister: later request enters MCP route
  MCPControllerRegister->>composeGlobalMiddleware: reuse cached middleware
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • eggjs/egg#5994: Refactors the same MCP middleware composition path to defer middleware-chain construction and updates the matching tests.

Suggested reviewers

  • gxkl
  • JimmyDaddy
  • killagu

Poem

A bunny hopped through middleware mist,
With a thunk in its paw and a cache to assist.
First hop woke the chain, then the rest stayed still,
Soft lazy requests now bounce by at will. 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: deferring MCP base middleware resolution until the first request.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/mcp-base-middleware-lazy

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed: private package registry requires authentication. Disable ESLint in CodeRabbit settings or use public packages.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying egg with  Cloudflare Pages  Cloudflare Pages

Latest commit: 834660a
Status: ✅  Deploy successful!
Preview URL: https://4eb0de05.egg-cci.pages.dev
Branch Preview URL: https://fix-mcp-base-middleware-lazy.egg-cci.pages.dev

View logs

@codecov

codecov Bot commented Jun 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 85.32%. Comparing base (76cafb1) to head (834660a).

Additional details and impacted files
@@            Coverage Diff             @@
##             next    #5995      +/-   ##
==========================================
+ Coverage   84.89%   85.32%   +0.43%     
==========================================
  Files         669      658      -11     
  Lines       19946    19678     -268     
  Branches     3964     3904      -60     
==========================================
- Hits        16933    16791     -142     
+ Misses       2588     2482     -106     
+ Partials      425      405      -20     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying egg-v3 with  Cloudflare Pages  Cloudflare Pages

Latest commit: 834660a
Status: ✅  Deploy successful!
Preview URL: https://fc714dc0.egg-v3.pages.dev
Branch Preview URL: https://fix-mcp-base-middleware-lazy.egg-v3.pages.dev

View logs

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request defers the resolution of the base middleware (teggCtxLifecycleMiddleware) to the first request by passing it as a factory function (thunk) to composeGlobalMiddleware. This prevents errors during controller registration when the middleware is not yet loaded. The tests have been updated and expanded to verify this deferred behavior. The review feedback suggests introducing a cached helper method, getMcpMiddleware(), to eliminate the duplicated middleware creation logic across several registration methods.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread tegg/plugin/controller/src/lib/impl/mcp/MCPControllerRegister.ts
Comment thread tegg/plugin/controller/src/lib/impl/mcp/MCPControllerRegister.ts
const self = this;
let mw = (this.app.middleware as any).teggCtxLifecycleMiddleware();
mw = self.composeGlobalMiddleware(mw);
const mw = self.composeGlobalMiddleware(() => (this.app.middleware as any).teggCtxLifecycleMiddleware());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Use the cached getMcpMiddleware() helper to avoid duplicate middleware creation logic and prevent recreating the middleware closure per SSE connection.

Suggested change
const mw = self.composeGlobalMiddleware(() => (this.app.middleware as any).teggCtxLifecycleMiddleware());
const mw = self.getMcpMiddleware();

Comment thread tegg/plugin/controller/src/lib/impl/mcp/MCPControllerRegister.ts

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR is a follow-up fix for MCP controller registration timing: it prevents MCP route setup from eagerly calling app.middleware.teggCtxLifecycleMiddleware() during controller registration (which happens before Egg’s loadMiddleware), by deferring acquisition of the base middleware to the first request.

Changes:

  • Change composeGlobalMiddleware to accept a base-middleware factory thunk and resolve it lazily on first request.
  • Update all MCP route setup call sites to pass () => app.middleware.teggCtxLifecycleMiddleware() instead of invoking it during registration.
  • Expand tests to assert the base middleware factory is not invoked at registration and is invoked exactly once on first request.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
tegg/plugin/controller/src/lib/impl/mcp/MCPControllerRegister.ts Defer base middleware acquisition via thunk in MCP route setup; update composeGlobalMiddleware to resolve base + globals lazily.
tegg/plugin/controller/test/lib/MCPGlobalMiddleware.test.ts Add/adjust tests to validate base thunk deferral + caching and ensure registration doesn’t touch app.middlewares or the base factory.

// request.
let resolved = false;
let composed: compose.Middleware<EggContext> = mw;
let composed: compose.Middleware<EggContext>;
@elrrrrrrr elrrrrrrr requested review from gxkl and killagu June 25, 2026 13:43

@gxkl gxkl left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

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.

3 participants