Skip to content

feat: add mcp#307

Merged
akitaSummer merged 2 commits intoeggjs:masterfrom
akitaSummer:feat/mcp
Apr 28, 2025
Merged

feat: add mcp#307
akitaSummer merged 2 commits intoeggjs:masterfrom
akitaSummer:feat/mcp

Conversation

@akitaSummer
Copy link
Contributor

@akitaSummer akitaSummer commented Apr 25, 2025

Checklist
  • npm test passes
  • tests and/or benchmarks are included
  • documentation is changed or added
  • commit message follows commit guidelines
Affected core subsystem(s)
Description of change

Summary by CodeRabbit

  • New Features

    • Introduced comprehensive support for the Model Context Protocol (MCP) with new decorators for controllers, prompts, tools, resources, and extra parameters, alongside detailed metadata classes and utilities.
    • Added MCP controller registration with lifecycle hooks, integrated into Egg.js routing, container factories, and middleware for MCP request management.
    • Enabled MCP message handling over SSE and streamable HTTP transports, including session management and proxy capabilities.
    • Integrated an MCP proxy plugin supporting multiplexed communication, client registration, message forwarding, and cluster-aware HTTP server handling.
    • Provided Egg.js application extensions exposing MCP proxy clients for seamless integration.
    • Added in-memory event store and transport implementations supporting MCP communication, event replay, and notification streaming.
    • Added tools supporting notification streams, resource listing, and prompt handling with schema validation and Zod integration.
  • Bug Fixes

    • Ensured correct middleware ordering to prevent premature request body consumption on MCP endpoints.
  • Documentation

    • Added changelogs and README files for new MCP and proxy plugins.
  • Tests

    • Added extensive test suites and fixtures validating MCP controllers, proxy functionality, and client-server interactions over multiple transport protocols.
  • Chores

    • Updated dependencies and configuration files to support MCP and proxy features.

@coderabbitai
Copy link

coderabbitai bot commented Apr 25, 2025

Walkthrough

This change introduces comprehensive support for the Model Context Protocol (MCP) as a new controller type within the Egg.js framework. It adds MCP-specific decorators, metadata builders, and utility functions for defining MCP controllers, prompts, tools, and resources. The plugin infrastructure is extended to register and manage MCP controllers, including their lifecycle, routing, and transport mechanisms (SSE and streamable HTTP). Middleware and configuration updates ensure correct request handling and CSRF bypass for MCP routes. Extensive type definitions and test suites are included to validate MCP integration. Additionally, a new @eggjs/mcp-proxy plugin is implemented to facilitate proxying MCP protocol messages between clients and local servers, including client registration, message forwarding, and transport protocol handling.

Changes

File(s) / Path(s) Change Summary
.eslintrc Disabled the import/no-unresolved ESLint rule.
core/controller-decorator/index.ts, core/controller-decorator/package.json Added MCP-related exports and dependency on @modelcontextprotocol/sdk.
core/controller-decorator/src/decorator/mcp/MCPController.ts, MCPPrompt.ts, MCPResource.ts, MCPTool.ts, Extra.ts Introduced decorators for MCP controllers, prompts, resources, tools, and extra parameter marking.
core/controller-decorator/src/impl/mcp/MCPControllerMetaBuilder.ts, MCPControllerPromptMetaBuilder.ts, MCPControllerResourceMetaBuilder.ts, MCPControllerToolMetaBuilder.ts Implemented metadata builders for MCP controllers, prompts, resources, and tools.
core/controller-decorator/src/model/MCPControllerMeta.ts, MCPPromptMeta.ts, MCPResourceMeta.ts, MCPToolMeta.ts, core/controller-decorator/src/model/index.ts Added metadata classes and exports for MCP entities.
core/controller-decorator/src/util/MCPInfoUtil.ts Added utility class for managing MCP metadata on controller classes.
core/controller-decorator/test/MCPMeta.test.ts, core/controller-decorator/test/fixtures/MCPController.ts Added tests and fixtures for MCP controller metadata.
core/types/controller-decorator/MCPController.ts, MCPPromptParams.ts, MCPResourceParams.ts, MCPToolParams.ts Introduced type definitions for MCP controller, prompt, resource, and tool parameters and hooks.
core/types/controller-decorator/MetadataKey.ts Added new metadata keys for MCP-related controller metadata.
core/types/controller-decorator/index.ts, core/types/controller-decorator/model/types.ts Re-exported MCP types and extended ControllerType enum with MCP.
core/types/package.json Added MCP and related packages as dev dependencies.
plugin/controller/app.ts, plugin/controller/lib/impl/mcp/MCPControllerRegister.ts Registered MCP controller type, added MCP controller register, and lifecycle hooks.
plugin/controller/app/middleware/mcp_body_middleware.ts Introduced middleware to disable body parser for MCP routes.
plugin/controller/config/config.default.ts Added MCP paths to CSRF ignore configuration.
plugin/controller/package.json Added MCP and related dependencies.
plugin/controller/test/fixtures/apps/mcp-app/app/controller/AppController.ts, McpController.ts, plugin/controller/test/fixtures/apps/mcp-app/config/config.default.js, plugin/controller/test/fixtures/apps/mcp-app/config/plugin.js, plugin/controller/test/fixtures/apps/mcp-app/package.json, plugin/controller/test/mcp/mcp.test.ts Added test app, configuration, and tests for MCP plugin controller.
plugin/controller/typings/index.d.ts Extended app typings to include MCP proxy.
plugin/mcp-proxy/CHANGELOG.md, README.md, app/extend/agent.ts, app/extend/application.ts, index.ts, lib/MCPProxyDataClient.ts, package.json, test/fixtures/apps/mcp-proxy/..., test/proxy.test.ts, tsconfig.json, tsconfig.pub.json, typings/index.d.ts, agent.ts, app.ts Introduced the new @eggjs/mcp-proxy plugin: proxy client, data client, app/agent hooks, typings, configuration, test fixtures, and documentation.
plugin/mcp-proxy/test/fixtures/apps/mcp-proxy/app/controller/app.ts Added MCP server controller supporting SSE and streamable HTTP transports with event store and proxy handling.
plugin/mcp-proxy/test/fixtures/apps/mcp-proxy/app/router.ts Added routes for MCP proxy endpoints /stream, /init, and /message.
plugin/mcp-proxy/test/fixtures/apps/mcp-proxy/config/config.default.js, plugin.js Added test fixture configurations disabling CSRF and body parser, and enabling plugins.
plugin/mcp-proxy/test/proxy.test.ts Added integration tests for MCP proxy client using SSE and streamable HTTP transports.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant MCPProxyApiClient
    participant EggApp
    participant MCPControllerRegister
    participant MCPServer

    Client->>MCPProxyApiClient: Sends MCP message (SSE/STREAM/STDIO)
    MCPProxyApiClient->>EggApp: Proxies HTTP request (with protocol/session)
    EggApp->>MCPControllerRegister: Handles MCP route (init/message/stream)
    MCPControllerRegister->>MCPServer: Registers/dispatches prompt/tool/resource
    MCPServer-->>MCPControllerRegister: Invokes controller method
    MCPControllerRegister-->>EggApp: Returns response
    EggApp-->>MCPProxyApiClient: Forwards response
    MCPProxyApiClient-->>Client: Delivers response/event
Loading

Suggested reviewers

  • killagu

Poem

🐇
In burrows deep, new code does grow,
MCP hops in, with a bright new show!
Decorators bloom, and proxies stream,
Controllers now can prompt and dream.
With tests and types, the garden’s neat—
A protocol patchwork, quite the feat!
Hooray for changes, crisp and clean—
The cleverest code I’ve ever seen!
🥕

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.

core/controller-decorator/index.ts

Oops! Something went wrong! :(

ESLint: 8.57.1

ESLint couldn't find the plugin "eslint-plugin-eggache".

(The package "eslint-plugin-eggache" was not found when loaded as a Node module from the directory "".)

It's likely that the plugin isn't installed correctly. Try reinstalling by running the following:

npm install eslint-plugin-eggache@latest --save-dev

The plugin "eslint-plugin-eggache" was referenced from the config file in ".eslintrc » eslint-config-egg/typescript » ./index.js".

If you still can't figure out the problem, please stop by https://eslint.org/chat/help to chat with the team.

core/controller-decorator/src/decorator/mcp/Extra.ts

Oops! Something went wrong! :(

ESLint: 8.57.1

ESLint couldn't find the plugin "eslint-plugin-eggache".

(The package "eslint-plugin-eggache" was not found when loaded as a Node module from the directory "".)

It's likely that the plugin isn't installed correctly. Try reinstalling by running the following:

npm install eslint-plugin-eggache@latest --save-dev

The plugin "eslint-plugin-eggache" was referenced from the config file in ".eslintrc » eslint-config-egg/typescript » ./index.js".

If you still can't figure out the problem, please stop by https://eslint.org/chat/help to chat with the team.

core/controller-decorator/src/decorator/mcp/MCPController.ts

Oops! Something went wrong! :(

ESLint: 8.57.1

ESLint couldn't find the plugin "eslint-plugin-eggache".

(The package "eslint-plugin-eggache" was not found when loaded as a Node module from the directory "".)

It's likely that the plugin isn't installed correctly. Try reinstalling by running the following:

npm install eslint-plugin-eggache@latest --save-dev

The plugin "eslint-plugin-eggache" was referenced from the config file in ".eslintrc » eslint-config-egg/typescript » ./index.js".

If you still can't figure out the problem, please stop by https://eslint.org/chat/help to chat with the team.

  • 32 others
✨ Finishing Touches
  • 📝 Generate Docstrings

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 17

🔭 Outside diff range comments (1)
plugin/mcp-proxy/CHANGELOG.md (1)

1-927: ⚠️ Potential issue

Package name mismatch in CHANGELOG.md

The CHANGELOG.md refers to @eggjs/tegg-aop-plugin throughout the document, but this file is located in the plugin/mcp-proxy directory. This suggests the changelog was either copied from another package or the package name needs to be updated to reflect its actual location.

Update the package name references to match the actual package (@eggjs/mcp-proxy).

-# [3.53.0](https://github.com/eggjs/tegg/compare/v3.52.1...v3.53.0) (2025-04-21)
-
-**Note:** Version bump only for package @eggjs/tegg-aop-plugin
+# [3.53.0](https://github.com/eggjs/tegg/compare/v3.52.1...v3.53.0) (2025-04-21)
+
+**Note:** Version bump only for package @eggjs/mcp-proxy

This change should be applied throughout the entire changelog for consistency.

🧰 Tools
🪛 markdownlint-cli2 (0.17.2)

356-356: Heading levels should only increment by one level at a time
Expected: h2; Actual: h3

(MD001, heading-increment)


653-653: Heading levels should only increment by one level at a time
Expected: h2; Actual: h3

(MD001, heading-increment)


664-664: Heading levels should only increment by one level at a time
Expected: h2; Actual: h3

(MD001, heading-increment)


819-819: Heading levels should only increment by one level at a time
Expected: h2; Actual: h3

(MD001, heading-increment)


830-830: Heading levels should only increment by one level at a time
Expected: h2; Actual: h3

(MD001, heading-increment)


849-849: Heading levels should only increment by one level at a time
Expected: h2; Actual: h3

(MD001, heading-increment)


924-924: Heading levels should only increment by one level at a time
Expected: h2; Actual: h3

(MD001, heading-increment)

🧹 Nitpick comments (42)
.eslintrc (1)

12-12: Disabled unresolved imports check—ensure it's intentional.

Turning off "import/no-unresolved" suppresses unresolved import errors, likely to accommodate new MCP modules. Confirm that this does not mask unintended import issues.

Consider configuring the import/resolver in settings (e.g., eslint-import-resolver-typescript) to handle TypeScript path mappings instead of disabling the rule entirely.

plugin/controller/test/fixtures/apps/mcp-app/config/config.default.js (1)

1-1: Remove redundant 'use strict' directive

JavaScript modules automatically run in strict mode, making this directive unnecessary.

-'use strict';
🧰 Tools
🪛 Biome (1.9.4)

[error] 1-1: Redundant use strict directive.

The entire contents of JavaScript modules are automatically in strict mode, with no statement needed to initiate it.
Safe fix: Remove the redundant use strict directive.

(lint/suspicious/noRedundantUseStrict)

plugin/mcp-proxy/app/extend/agent.ts (1)

9-13: Consider extracting type casting to improve readability

The repeated type casting of this to Application makes the code less readable. Consider extracting this to a local variable.

-      this[MCP_PROXY] = new MCPProxyApiClient({
-        logger: (this as unknown as Application).logger,
-        messenger: (this as unknown as Application).messenger,
-        app: this as unknown as Application,
+      const app = this as unknown as Application;
+      this[MCP_PROXY] = new MCPProxyApiClient({
+        logger: app.logger,
+        messenger: app.messenger,
+        app,
       });
plugin/mcp-proxy/test/fixtures/apps/mcp-proxy/config/config.default.js (1)

1-1: Remove redundant 'use strict' directive

The 'use strict' directive is redundant in ES modules as they are automatically in strict mode.

-'use strict';
🧰 Tools
🪛 Biome (1.9.4)

[error] 1-1: Redundant use strict directive.

The entire contents of JavaScript modules are automatically in strict mode, with no statement needed to initiate it.
Safe fix: Remove the redundant use strict directive.

(lint/suspicious/noRedundantUseStrict)

plugin/mcp-proxy/app/extend/application.ts (1)

8-12: Consider extracting type casting to improve readability

The repeated type casting of this to Application makes the code less readable. Consider extracting this to a local variable.

-      this[MCP_PROXY] = new MCPProxyApiClient({
-        logger: (this as unknown as Application).logger,
-        messenger: (this as unknown as Application).messenger,
-        app: this as unknown as Application,
+      const app = this as unknown as Application;
+      this[MCP_PROXY] = new MCPProxyApiClient({
+        logger: app.logger,
+        messenger: app.messenger,
+        app,
       });
plugin/controller/app/middleware/mcp_body_middleware.ts (1)

7-12: Consider optimizing path matching performance

The current implementation compiles regular expressions for each path on every request. Consider pre-compiling these regular expressions outside the middleware function for better performance.

+const pathMatchers = ['/mcp/init', '/mcp/message', '/mcp/stream'].map(igPath => 
+  pathToRegexp(igPath, [], { end: false })
+);
+
 export default () => {
   return async function mcpBodyMiddleware(ctx: EggContext, next: Next) {
-    const arr = [ '/mcp/init', '/mcp/message', '/mcp/stream' ];
-    const res = arr.some(igPath => {
-      const match = pathToRegexp(igPath, [], {
-        end: false,
-      });
-      return match.test(ctx.path);
-    });
+    const res = pathMatchers.some(matcher => matcher.test(ctx.path));
     if (res) {
       ctx.disableBodyParser = true;
     }
core/controller-decorator/src/decorator/mcp/MCPResource.ts (1)

9-32: The MCPResource decorator looks good, with one suggestion about parameter naming.

The MCPResource decorator properly manages metadata for MCP resource methods, setting the controller type and storing resource parameters. The code is well-structured and follows the decorator pattern correctly.

There's a potential naming conflict in how you're handling the name property:

- mcpName: params.name,
- name: methodName,
+ resourceName: methodName,

While the current implementation works, using name for the method name and mcpName for the original name parameter could be confusing when accessing these values later. Consider using a more descriptive property name like resourceName for the method name to improve code clarity.

plugin/mcp-proxy/test/fixtures/apps/mcp-proxy/app/router.ts (1)

1-7: Router configuration looks good, but could benefit from comments.

The router configuration correctly sets up the necessary routes for the MCP proxy test application. The three endpoints (/stream, /init, /message) with their respective HTTP methods map to the appropriate controller methods.

Consider adding brief comments explaining the purpose of each route to improve maintainability:

 module.exports = (app: Application) => {
+  // Stream endpoint for SSE or streamable HTTP transport
   app.router.all('/stream', app.controller.app.allStream);
+  // Initialize MCP server session
   app.router.get('/init', app.controller.app.init);
+  // Handle MCP message requests
   app.router.post('/message', app.controller.app.message);
 };
core/controller-decorator/src/decorator/mcp/MCPTool.ts (1)

21-29: Consider adding a comment to clarify the name handling

The implementation correctly sets the MCP tool parameters, but the distinction between mcpName (from params?.name) and name (from methodName) might be confusing for future developers. Consider adding a comment explaining why both fields are needed and how they differ.

    MCPInfoUtil.setMCPToolParams(
      {
        ...params,
+       // mcpName is an optional explicit name for the tool in MCP context
+       // while name defaults to the method name
        mcpName: params?.name,
        name: methodName,
      },
      controllerClazz,
      methodName,
    );
core/controller-decorator/src/impl/mcp/MCPControllerResourceMetaBuilder.ts (1)

1-35: Consider refactoring to reduce duplication with MCPControllerToolMetaBuilder

The implementation is correct, but there's significant code duplication with MCPControllerToolMetaBuilder. Both classes share nearly identical logic, differing only in the metadata type they create and the specific parameter getter they use.

Consider creating a base class or utility function to handle the common logic:

// Example base class approach
abstract class MCPControllerBaseMetaBuilder<T> {
  protected readonly clazz: EggProtoImplClass;
  protected readonly methodName: string;

  constructor(clazz: EggProtoImplClass, methodName: string) {
    this.clazz = clazz;
    this.methodName = methodName;
  }

  protected abstract getParams(): any;
  protected abstract createMeta(params: any): T;

  build(): T | undefined {
    MethodValidator.validate(this.clazz, this.methodName);
    const controllerType = MethodInfoUtil.getMethodControllerType(this.clazz, this.methodName);
    if (!controllerType) {
      return undefined;
    }
    const middlewares = MethodInfoUtil.getMethodMiddlewares(this.clazz, this.methodName);
    const needAcl = MethodInfoUtil.hasMethodAcl(this.clazz, this.methodName);
    const aclCode = MethodInfoUtil.getMethodAcl(this.clazz, this.methodName);
    const params = this.getParams();

    return this.createMeta({
      name: this.methodName,
      middlewares,
      needAcl,
      aclCode,
      ...params,
    });
  }
}

// Then implement specific builders
export class MCPControllerResourceMetaBuilder extends MCPControllerBaseMetaBuilder<MCPResourceMeta> {
  protected getParams() {
    return MCPInfoUtil.getMCPResourceParams(this.clazz, this.methodName);
  }
  
  protected createMeta(params: any): MCPResourceMeta {
    return new MCPResourceMeta(params);
  }
}
core/controller-decorator/src/decorator/mcp/MCPPrompt.ts (1)

6-7: Consider simplifying the import paths.

The relative import paths ../../../src/util/MCPInfoUtil and ../../../src/util/MethodInfoUtil seem unnecessarily complex. Consider using more direct import paths if possible, or adjusting the directory structure to reduce path complexity.

core/controller-decorator/src/decorator/mcp/MCPController.ts (3)

17-23: Remove commented-out code paths.

Lines 17-22 contain commented-out file paths that appear to be debugging artifacts or notes. These should be removed for cleaner production code.

-    // './tegg/core/common-util/src/StackUtil.ts',
-    // './tegg/core/core-decorator/src/decorator/Prototype.ts',
-    // './tegg/core/controller-decorator/src/decorator/tr/TRController.ts',
-    // './tegg/core/core-decorator/node_modules/[email protected]@reflect-metadata/Reflect.js',
-    // './tegg/core/core-decorator/node_modules/[email protected]@reflect-metadata/Reflect.js',
-    // './tegg/core/controller-decorator/test/fixtures/TRFooController.ts',

7-7: Avoid shadowing the global "constructor" property.

The parameter name constructor shadows the global JavaScript constructor property, which can lead to confusion. Consider renaming it to something more specific.

-  return function(constructor: EggProtoImplClass) {
+  return function(targetClass: EggProtoImplClass) {
🧰 Tools
🪛 Biome (1.9.4)

[error] 7-7: Do not shadow the global "constructor" property.

Consider renaming this variable. It's easy to confuse the origin of variables when they're named after a known global.

(lint/suspicious/noShadowRestrictedNames)


23-23: Document the magic number in stack retrieval.

Line 23 uses a magic number 5 in the StackUtil.getCalleeFromStack(false, 5) call. It's not clear why that specific stack depth is chosen, which makes the code harder to maintain.

-    PrototypeUtil.setFilePath(constructor, StackUtil.getCalleeFromStack(false, 5));
+    // Skip 5 frames: current function, decorator factory, reflect-metadata internals, and user code
+    const stackDepth = 5;
+    PrototypeUtil.setFilePath(constructor, StackUtil.getCalleeFromStack(false, stackDepth));
core/controller-decorator/src/model/MCPToolMeta.ts (2)

3-4: Remove extra blank lines.

There are two blank lines after the imports (lines 3-4). Consider removing one of them for consistent code style.

import type { MiddlewareFunc } from '@eggjs/tegg-types';
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';

-

12-12: Ensure array immutability.

The middlewares property is marked readonly but this only makes the reference readonly, not the array contents. If full immutability is required, consider making a defensive copy in the constructor.

export class MCPToolMeta {
  // ...
  readonly middlewares: readonly MiddlewareFunc[];
  // ...
  constructor(opt: {
    // ...
  }) {
    // ...
-    this.middlewares = opt.middlewares;
+    // Create a defensive copy to ensure array contents can't be modified
+    this.middlewares = Object.freeze([...opt.middlewares]);
    // ...
  }
}
core/controller-decorator/src/impl/mcp/MCPControllerPromptMetaBuilder.ts (1)

24-25: Consider adding error handling for MCPInfoUtil.getMCPPromptParams

The call to MCPInfoUtil.getMCPPromptParams() assumes the operation will always succeed. Consider adding error handling to gracefully handle any potential errors in retrieving the MCP prompt parameters.

core/controller-decorator/test/MCPMeta.test.ts (2)

11-23: Add descriptive messages to assertions

Consider adding descriptive messages to the assertions to make test failures more informative and easier to debug.

- assert(fooControllerMetaData);
+ assert(fooControllerMetaData, 'Controller metadata should exist');
- assert(fooControllerMetaData.type === ControllerType.MCP);
+ assert(fooControllerMetaData.type === ControllerType.MCP, 'Controller type should be MCP');

Similarly for the other assertions.


7-25: Use a more descriptive test name

The current test name "should work" doesn't clearly describe what functionality is being tested. Consider using a more descriptive name that explains the purpose of the test.

- it.only('should work', () => {
+ it('should correctly build MCP controller metadata', () => {
🧰 Tools
🪛 Biome (1.9.4)

[error] 8-8: Don't focus the test.

The 'only' method is often used for debugging or during implementation. It should be removed before deploying to production.
Consider removing 'only' to ensure all tests are executed.
Unsafe fix: Remove focus from test.

(lint/suspicious/noFocusedTests)

plugin/mcp-proxy/test/fixtures/apps/mcp-proxy/config/plugin.js (2)

1-1: Consider removing redundant 'use strict' directive

JavaScript modules are automatically in strict mode, so the 'use strict' directive is redundant. However, if this follows a consistent pattern in other configuration files in the project, it's acceptable to keep it for consistency.

-'use strict';

// eslint-disable-next-line @typescript-eslint/no-var-requires
🧰 Tools
🪛 Biome (1.9.4)

[error] 1-1: Redundant use strict directive.

The entire contents of JavaScript modules are automatically in strict mode, with no statement needed to initiate it.
Safe fix: Remove the redundant use strict directive.

(lint/suspicious/noRedundantUseStrict)


16-19: Path resolution could be more robust

The current path resolution uses a relative path with multiple parent directory references (../../../../../). This approach can be fragile if the directory structure changes. Consider using a more robust way to locate the plugin.

For test fixtures, you might consider using the actual package name instead of a relative path, or storing the plugin path in a shared test configuration.

plugin/mcp-proxy/lib/MCPProxyDataClient.ts (2)

21-30: Refactor to eliminate code duplication in registerClient method

The registerClient method duplicates the this.clients.set(sessionId, pid) call in both the if and else branches.

async registerClient(sessionId: string, pid: number) {
  if (this.clients.has(sessionId)) {
    const oldPid = this.clients.get(sessionId)!;
    this.logger.info('[MCPClientManager] duplicate register client %s new pid %s old pid', sessionId, pid, oldPid);
-   this.clients.set(sessionId, pid);
  } else {
    this.logger.info('[MCPClientManager] register client %s pid %s', sessionId, pid);
-   this.clients.set(sessionId, pid);
  }
+ this.clients.set(sessionId, pid);
}

18-19: Consider removing empty init method if not needed

The _init method is marked with an ESLint disable comment for an empty function. If this method is not actually needed for overriding a base class method, consider removing it entirely.

-  // eslint-disable-next-line @typescript-eslint/no-empty-function
-  async _init() {}

If it's required by the Base class contract, add a comment explaining why this empty implementation is necessary.

plugin/controller/test/fixtures/apps/mcp-app/config/plugin.js (1)

1-2: Remove redundant 'use strict' directive

The 'use strict' directive is redundant in JavaScript modules as they are automatically in strict mode.

-'use strict';
-
🧰 Tools
🪛 Biome (1.9.4)

[error] 1-1: Redundant use strict directive.

The entire contents of JavaScript modules are automatically in strict mode, with no statement needed to initiate it.
Safe fix: Remove the redundant use strict directive.

(lint/suspicious/noRedundantUseStrict)

plugin/controller/test/fixtures/apps/mcp-app/app/controller/AppController.ts (2)

29-29: Fix spacing in parameter type annotation

There's an extra space before the type annotation colon.

-  async startNotificationStream(args: ToolArgs<typeof NotificationType>, extra :ToolExtra): Promise<MCPToolResponse> {
+  async startNotificationStream(args: ToolArgs<typeof NotificationType>, extra: ToolExtra): Promise<MCPToolResponse> {

35-49: Consider adding a safety timeout for infinite loops

When count === 0, the loop will run indefinitely. Consider adding a maximum runtime limit or a way to terminate the stream to prevent resource exhaustion in production environments.

+    const MAX_RUNTIME_MS = 30 * 60 * 1000; // 30 minutes maximum runtime
+    const startTime = Date.now();

     while (count === 0 || counter < count) {
+      // Safety check for infinite loops
+      if (count === 0 && (Date.now() - startTime) > MAX_RUNTIME_MS) {
+        console.log('Maximum runtime reached, terminating notification stream');
+        break;
+      }
       counter++;
       try {
         await sendNotification({
           method: 'notifications/message',
           params: {
             level: 'info',
             data: `Periodic notification #${counter}`,
           },
         });
       } catch (error) {
         console.error('Error sending notification:', error);
       }
       await sleep(interval);
     }
core/controller-decorator/src/impl/mcp/MCPControllerMetaBuilder.ts (1)

34-45: Duplicated code pattern in metadata builders

The buildPrompt and buildTool methods follow identical patterns. Consider refactoring to a generic method to reduce duplication.

+ private buildMetadata<T>(
+   getMethodNames: (clazz: EggProtoImplClass) => string[],
+   createBuilder: (clazz: EggProtoImplClass, methodName: string) => { build(): T | null }
+ ): T[] {
+   const methodNames = getMethodNames(this.clazz);
+   const methods: T[] = [];
+   for (const methodName of methodNames) {
+     const builder = createBuilder(this.clazz, methodName);
+     const methodMeta = builder.build();
+     if (methodMeta) {
+       methods.push(methodMeta);
+     }
+   }
+   return methods;
+ }
+
+ private buildResource(): MCPResourceMeta[] {
+   return this.buildMetadata<MCPResourceMeta>(
+     MCPInfoUtil.getMCPResource,
+     (clazz, methodName) => new MCPControllerResourceMetaBuilder(clazz, methodName)
+   );
+ }
+
+ private buildPrompt(): MCPPromptMeta[] {
+   return this.buildMetadata<MCPPromptMeta>(
+     MCPInfoUtil.getMCPPrompt,
+     (clazz, methodName) => new MCPControllerPromptMetaBuilder(clazz, methodName)
+   );
+ }
+
+ private buildTool(): MCPToolMeta[] {
+   return this.buildMetadata<MCPToolMeta>(
+     MCPInfoUtil.getMCPTool,
+     (clazz, methodName) => new MCPControllerToolMetaBuilder(clazz, methodName)
+   );
+ }

Also applies to: 47-58

core/types/controller-decorator/MCPController.ts (1)

11-14: Consider improving type safety for the preProxy hook parameters.

The current parameter type for proxyReq and proxyResp uses Parameters<typeof http.createServer> which references the request and response handler parameters. While this works, it might be more explicit and type-safe to directly use http.IncomingMessage and http.ServerResponse types.

- preProxy?: (ctx: Context, proxyReq: Parameters<typeof http.createServer>['0'], proxyResp: Parameters<typeof http.createServer>['1']) => Promise<void>
+ preProxy?: (ctx: Context, proxyReq: http.IncomingMessage, proxyResp: http.ServerResponse) => Promise<void>
plugin/mcp-proxy/test/proxy.test.ts (2)

25-53: Consider error handling for the notification collection.

The function properly collects notifications, but it doesn't handle potential errors in the result content parsing. Consider adding try/catch blocks or validation to ensure robust error handling.

  result.content.forEach(item => {
+   try {
      if (item.type === 'text') {
        notifications.push({
          text: item.text,
        });
      } else {
+       if (!item.data) {
+         throw new Error('Item data is undefined');
+       }
        notifications.push({
          text: item.data!.toString(),
        });
      }
+   } catch (error) {
+     console.error('Error processing notification item:', error);
+   }
  });

93-124: Add timeout mechanism for notification collection.

The test waits 5 seconds using setTimeout regardless of how many notifications were actually received. Consider adding a more robust mechanism to wait until all expected notifications are received or until a timeout occurs.

  await sseClient.connect(sseTransport);
  const tools = await listTools(sseClient);
  const notificationResp = await startNotificationTool(sseClient);
- await new Promise(resolve => setTimeout(resolve, 5000));
+ // Wait for all notifications or timeout after 10 seconds
+ await new Promise((resolve, reject) => {
+   const maxWaitTime = 10000; // 10 seconds
+   const expectedNotifications = 5;
+   
+   const checkInterval = setInterval(() => {
+     if (sseNotifications.length >= expectedNotifications) {
+       clearInterval(checkInterval);
+       clearTimeout(timeout);
+       resolve(undefined);
+     }
+   }, 100);
+   
+   const timeout = setTimeout(() => {
+     clearInterval(checkInterval);
+     if (sseNotifications.length < expectedNotifications) {
+       reject(new Error(`Timed out waiting for notifications. Expected ${expectedNotifications}, received ${sseNotifications.length}`));
+     } else {
+       resolve(undefined);
+     }
+   }, maxWaitTime);
+ });
  await sseTransport.close();
plugin/controller/test/fixtures/apps/mcp-app/app/controller/McpController.ts (1)

60-82: Add validation for resource URI parameters.

The car method accepts a URI but doesn't validate that the required parameters are present. Consider adding validation to ensure the URI contains the expected parameters before processing.

  async car(uri: URL): Promise<MCPResourceResponse> {
+   // Validate URI parameters
+   const params = new URLSearchParams(uri.search);
+   const name = uri.pathname.split('/').pop();
+   if (!name) {
+     throw new Error('Missing required parameter: name');
+   }
+   
    return {
      contents: [{
        uri: uri.toString(),
        text: 'MOCK TEXT',
      }],
    };
  }
core/controller-decorator/test/fixtures/MCPController.ts (2)

5-5: Prefer named import for Zod to avoid relying on esModuleInterop

zod has no default export; importing the default works only when esModuleInterop / allowSyntheticDefaultImports is enabled.
Use a named import to keep the fixture compile-time agnostic of ts-config settings.

-import z from 'zod';
+import { z } from 'zod';

56-57: Inconsistent URI scheme hitu:// – likely a typo

All other fixtures/tests use the mcp:// scheme. A mismatched scheme will make this controller invisible when clients filter by protocol.
Please verify and align.

plugin/controller/test/mcp/mcp.test.ts (1)

129-131: 5 s setTimeout slows test suite – consider event-driven await

Instead of a fixed sleep, await the expected number of notifications via a Promise that resolves when the array length meets the target. This drops test time from 5 s to sub-second and makes failures deterministic.

Also applies to: 220-222

plugin/mcp-proxy/test/fixtures/apps/mcp-proxy/app/controller/app.ts (3)

163-169: Unnecessary const self = this – arrow functions already capture lexical this

Remove the alias and reference this directly to silence the lint warning and improve clarity.

-    const self = this;
     const transport = new SSEServerTransport('/message', this.ctx.res);
     // register handler and client demo
-    this.app.mcpProxy.setProxyHandler('SSE', async (req, res) => {
-      return await self.ssePostHandler(req, res);
-    });
+    this.app.mcpProxy.setProxyHandler('SSE', async (req, res) => {
+      return await this.ssePostHandler(req, res);
+    });
🧰 Tools
🪛 Biome (1.9.4)

[error] 163-163: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


246-254: Same this alias pattern repeated – apply the earlier fix

-        const self = this;
         const transport = new StreamableHTTPServerTransport({
           sessionIdGenerator: () => randomUUID(),
           eventStore,
           onsessioninitialized: async sessionId => {
             transports[sessionId] = transport;
-            this.app.mcpProxy.setProxyHandler('STREAM', async (req, res) => {
-              return await self.streamPostHandler(req, res);
+            this.app.mcpProxy.setProxyHandler('STREAM', async (req, res) => {
+              return await this.streamPostHandler(req, res);
             });
             await this.app.mcpProxy.registerClient(sessionId, process.pid);
           },
         });
🧰 Tools
🪛 Biome (1.9.4)

[error] 246-246: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


58-84: Potential unbounded memory growth in InMemoryEventStore

events is an ever-growing Map without TTL or size cap. Long-lived servers can exhaust memory if clients stream large volumes.
Consider purging old events per stream or moving to a bounded queue.

core/controller-decorator/src/util/MCPInfoUtil.ts (1)

1-9: Utility can be simplified – static-only class is unnecessary

MCPInfoUtil contains only static methods and no private state. Replacing it with a file-level function collection (or a namespace export) removes the noStaticOnlyClass smell, produces leaner JS and avoids accidental instantiation.

plugin/mcp-proxy/index.ts (2)

55-69: Typo in proxy-action constant may bite integrations

'MCP_SEE_PROXY' (double “E”) is used for SSE pathways.
Internally the typo is consistent, but any external code that expects the more obvious 'MCP_SSE_PROXY' will fail to match.

Renaming now (and keeping the misspelt value as a deprecated alias) will avoid future confusion.

-type ProxyAction = 'MCP_STDIO_PROXY' | 'MCP_SEE_PROXY' | 'MCP_STREAM_PROXY';
+type ProxyAction = 'MCP_STDIO_PROXY' | 'MCP_SSE_PROXY' | 'MCP_STREAM_PROXY';
 ...
-        action = 'MCP_SEE_PROXY';
+        action = 'MCP_SSE_PROXY';

127-131: Avoid delete headers['transfer-encoding'] – reassign for better perf

The delete operator de-optimises hidden classes in V8.
Simply overwrite the key instead:

- delete headers['transfer-encoding'];
+ headers['transfer-encoding'] = undefined as unknown as string;

Also applies to: 167-171

🧰 Tools
🪛 Biome (1.9.4)

[error] 130-130: Avoid the delete operator which can impact performance.

Unsafe fix: Use an undefined assignment instead.

(lint/performance/noDelete)

plugin/controller/lib/impl/mcp/MCPControllerRegister.ts (2)

70-73: const self = this is redundant in arrow functions

All nested callbacks here are arrow functions which already capture this.
Removing the extra alias improves readability and keeps “one source of truth”.

-    const allRouterFunc = this.router.all;
-    const self = this;
+    const allRouterFunc = this.router.all;

Apply the same simplification to later occurrences (router.get, router.post, etc.).

🧰 Tools
🪛 Biome (1.9.4)

[error] 72-72: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


108-144: setProxyHandler('STREAM', …) is registered on every session

Each new stream session re-assigns the global handler, but the handler itself already dispatches by sessionId.
Register once during server initialisation to avoid unnecessary overwrites and potential race conditions.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ce2d38c and 443713c.

📒 Files selected for processing (57)
  • .eslintrc (1 hunks)
  • core/controller-decorator/index.ts (2 hunks)
  • core/controller-decorator/package.json (1 hunks)
  • core/controller-decorator/src/decorator/mcp/MCPController.ts (1 hunks)
  • core/controller-decorator/src/decorator/mcp/MCPPrompt.ts (1 hunks)
  • core/controller-decorator/src/decorator/mcp/MCPResource.ts (1 hunks)
  • core/controller-decorator/src/decorator/mcp/MCPTool.ts (1 hunks)
  • core/controller-decorator/src/impl/mcp/MCPControllerMetaBuilder.ts (1 hunks)
  • core/controller-decorator/src/impl/mcp/MCPControllerPromptMetaBuilder.ts (1 hunks)
  • core/controller-decorator/src/impl/mcp/MCPControllerResourceMetaBuilder.ts (1 hunks)
  • core/controller-decorator/src/impl/mcp/MCPControllerToolMetaBuilder.ts (1 hunks)
  • core/controller-decorator/src/model/MCPControllerMeta.ts (1 hunks)
  • core/controller-decorator/src/model/MCPPromptMeta.ts (1 hunks)
  • core/controller-decorator/src/model/MCPResourceMeta.ts (1 hunks)
  • core/controller-decorator/src/model/MCPToolMeta.ts (1 hunks)
  • core/controller-decorator/src/model/index.ts (1 hunks)
  • core/controller-decorator/src/util/MCPInfoUtil.ts (1 hunks)
  • core/controller-decorator/test/MCPMeta.test.ts (1 hunks)
  • core/controller-decorator/test/fixtures/MCPController.ts (1 hunks)
  • core/types/controller-decorator/MCPController.ts (1 hunks)
  • core/types/controller-decorator/MCPPromptParams.ts (1 hunks)
  • core/types/controller-decorator/MCPResourceParams.ts (1 hunks)
  • core/types/controller-decorator/MCPToolParams.ts (1 hunks)
  • core/types/controller-decorator/MetadataKey.ts (1 hunks)
  • core/types/controller-decorator/index.ts (1 hunks)
  • core/types/controller-decorator/model/types.ts (1 hunks)
  • core/types/package.json (1 hunks)
  • plugin/controller/agent.ts (1 hunks)
  • plugin/controller/app.ts (4 hunks)
  • plugin/controller/app/middleware/mcp_body_middleware.ts (1 hunks)
  • plugin/controller/config/config.default.ts (1 hunks)
  • plugin/controller/lib/impl/mcp/MCPControllerRegister.ts (1 hunks)
  • plugin/controller/package.json (1 hunks)
  • plugin/controller/test/fixtures/apps/mcp-app/app/controller/AppController.ts (1 hunks)
  • plugin/controller/test/fixtures/apps/mcp-app/app/controller/McpController.ts (1 hunks)
  • plugin/controller/test/fixtures/apps/mcp-app/config/config.default.js (1 hunks)
  • plugin/controller/test/fixtures/apps/mcp-app/config/plugin.js (1 hunks)
  • plugin/controller/test/fixtures/apps/mcp-app/package.json (1 hunks)
  • plugin/controller/test/mcp/mcp.test.ts (1 hunks)
  • plugin/controller/tsconfig.json (1 hunks)
  • plugin/controller/typings/index.d.ts (1 hunks)
  • plugin/mcp-proxy/CHANGELOG.md (1 hunks)
  • plugin/mcp-proxy/README.md (1 hunks)
  • plugin/mcp-proxy/app/extend/agent.ts (1 hunks)
  • plugin/mcp-proxy/app/extend/application.ts (1 hunks)
  • plugin/mcp-proxy/index.ts (1 hunks)
  • plugin/mcp-proxy/lib/MCPProxyDataClient.ts (1 hunks)
  • plugin/mcp-proxy/package.json (1 hunks)
  • plugin/mcp-proxy/test/fixtures/apps/mcp-proxy/app/controller/app.ts (1 hunks)
  • plugin/mcp-proxy/test/fixtures/apps/mcp-proxy/app/router.ts (1 hunks)
  • plugin/mcp-proxy/test/fixtures/apps/mcp-proxy/config/config.default.js (1 hunks)
  • plugin/mcp-proxy/test/fixtures/apps/mcp-proxy/config/plugin.js (1 hunks)
  • plugin/mcp-proxy/test/fixtures/apps/mcp-proxy/package.json (1 hunks)
  • plugin/mcp-proxy/test/proxy.test.ts (1 hunks)
  • plugin/mcp-proxy/tsconfig.json (1 hunks)
  • plugin/mcp-proxy/tsconfig.pub.json (1 hunks)
  • plugin/mcp-proxy/typings/index.d.ts (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (17)
plugin/mcp-proxy/test/fixtures/apps/mcp-proxy/config/config.default.js (2)
plugin/controller/config/config.default.ts (1)
  • config (3-14)
plugin/controller/test/fixtures/apps/mcp-app/config/config.default.js (1)
  • config (4-11)
plugin/controller/app/middleware/mcp_body_middleware.ts (1)
core/types/controller-decorator/model/types.ts (2)
  • EggContext (3-3)
  • Next (4-4)
plugin/controller/test/fixtures/apps/mcp-app/config/config.default.js (2)
plugin/controller/config/config.default.ts (1)
  • config (3-14)
plugin/mcp-proxy/test/fixtures/apps/mcp-proxy/config/config.default.js (1)
  • config (4-14)
plugin/mcp-proxy/app/extend/application.ts (1)
plugin/mcp-proxy/index.ts (1)
  • MCPProxyApiClient (30-239)
core/controller-decorator/src/decorator/mcp/MCPResource.ts (4)
core/types/controller-decorator/MCPResourceParams.ts (1)
  • MCPResourceParams (19-19)
core/types/core-decorator/model/EggPrototypeInfo.ts (1)
  • EggProtoImplClass (5-5)
core/controller-decorator/src/util/MethodInfoUtil.ts (1)
  • MethodInfoUtil (24-116)
core/controller-decorator/src/util/MCPInfoUtil.ts (1)
  • MCPInfoUtil (9-96)
plugin/controller/app.ts (1)
plugin/controller/lib/impl/mcp/MCPControllerRegister.ts (1)
  • MCPControllerRegister (33-451)
plugin/controller/typings/index.d.ts (1)
plugin/mcp-proxy/typings/index.d.ts (1)
  • MCPProxyApp (6-8)
plugin/mcp-proxy/typings/index.d.ts (2)
plugin/mcp-proxy/index.ts (1)
  • MCPProxyApiClient (30-239)
plugin/controller/typings/index.d.ts (1)
  • Application (15-16)
core/controller-decorator/src/decorator/mcp/MCPPrompt.ts (4)
core/types/controller-decorator/MCPPromptParams.ts (1)
  • MCPPromptParams (10-14)
core/types/core-decorator/model/EggPrototypeInfo.ts (1)
  • EggProtoImplClass (5-5)
core/controller-decorator/src/util/MethodInfoUtil.ts (1)
  • MethodInfoUtil (24-116)
core/controller-decorator/src/util/MCPInfoUtil.ts (1)
  • MCPInfoUtil (9-96)
plugin/controller/config/config.default.ts (2)
plugin/controller/test/fixtures/apps/mcp-app/config/config.default.js (1)
  • config (4-11)
plugin/mcp-proxy/test/fixtures/apps/mcp-proxy/config/config.default.js (1)
  • config (4-14)
core/controller-decorator/test/MCPMeta.test.ts (3)
core/controller-decorator/src/builder/ControllerMetaBuilderFactory.ts (1)
  • ControllerMetaBuilderFactory (11-50)
core/controller-decorator/src/model/MCPControllerMeta.ts (1)
  • MCPControllerMeta (7-70)
core/controller-decorator/test/fixtures/MCPController.ts (2)
  • ToolType (12-16)
  • PromptType (8-10)
core/controller-decorator/src/model/MCPToolMeta.ts (1)
core/types/controller-decorator/model/types.ts (1)
  • MiddlewareFunc (5-5)
core/controller-decorator/src/model/MCPResourceMeta.ts (1)
core/types/controller-decorator/model/types.ts (1)
  • MiddlewareFunc (5-5)
core/controller-decorator/src/model/MCPControllerMeta.ts (5)
core/types/core-decorator/model/EggPrototypeInfo.ts (1)
  • EggPrototypeName (6-6)
core/types/controller-decorator/model/types.ts (1)
  • MiddlewareFunc (5-5)
core/controller-decorator/src/model/MCPToolMeta.ts (1)
  • MCPToolMeta (5-31)
core/controller-decorator/src/model/MCPResourceMeta.ts (1)
  • MCPResourceMeta (5-36)
core/controller-decorator/src/model/MCPPromptMeta.ts (1)
  • MCPPromptMeta (4-30)
core/controller-decorator/src/model/MCPPromptMeta.ts (1)
core/types/controller-decorator/model/types.ts (1)
  • MiddlewareFunc (5-5)
plugin/controller/test/fixtures/apps/mcp-app/app/controller/McpController.ts (9)
core/controller-decorator/test/fixtures/MCPController.ts (3)
  • PromptType (8-10)
  • ToolType (12-16)
  • MCPController (18-71)
core/controller-decorator/src/decorator/mcp/MCPController.ts (1)
  • MCPController (6-32)
plugin/controller/test/fixtures/apps/mcp-app/app/controller/AppController.ts (1)
  • MCPController (21-60)
core/controller-decorator/src/decorator/mcp/MCPPrompt.ts (1)
  • MCPPrompt (9-32)
core/types/controller-decorator/MCPPromptParams.ts (2)
  • PromptArgs (5-5)
  • MCPPromptResponse (8-8)
core/controller-decorator/src/decorator/mcp/MCPTool.ts (1)
  • MCPTool (9-32)
core/types/controller-decorator/MCPToolParams.ts (2)
  • ToolArgs (5-5)
  • MCPToolResponse (8-8)
core/controller-decorator/src/decorator/mcp/MCPResource.ts (1)
  • MCPResource (9-32)
core/types/controller-decorator/MCPResourceParams.ts (1)
  • MCPResourceResponse (21-21)
core/controller-decorator/test/fixtures/MCPController.ts (8)
plugin/controller/test/fixtures/apps/mcp-app/app/controller/McpController.ts (3)
  • PromptType (14-16)
  • ToolType (18-22)
  • MCPController (25-83)
core/controller-decorator/src/decorator/mcp/MCPController.ts (1)
  • MCPController (6-32)
core/controller-decorator/src/decorator/mcp/MCPPrompt.ts (1)
  • MCPPrompt (9-32)
core/types/controller-decorator/MCPPromptParams.ts (2)
  • PromptArgs (5-5)
  • MCPPromptResponse (8-8)
core/controller-decorator/src/decorator/mcp/MCPTool.ts (1)
  • MCPTool (9-32)
core/types/controller-decorator/MCPToolParams.ts (2)
  • ToolArgs (5-5)
  • MCPToolResponse (8-8)
core/controller-decorator/src/decorator/mcp/MCPResource.ts (1)
  • MCPResource (9-32)
core/types/controller-decorator/MCPResourceParams.ts (1)
  • MCPResourceResponse (21-21)
🪛 Biome (1.9.4)
plugin/mcp-proxy/test/fixtures/apps/mcp-proxy/config/config.default.js

[error] 1-1: Redundant use strict directive.

The entire contents of JavaScript modules are automatically in strict mode, with no statement needed to initiate it.
Safe fix: Remove the redundant use strict directive.

(lint/suspicious/noRedundantUseStrict)

plugin/controller/test/fixtures/apps/mcp-app/config/config.default.js

[error] 1-1: Redundant use strict directive.

The entire contents of JavaScript modules are automatically in strict mode, with no statement needed to initiate it.
Safe fix: Remove the redundant use strict directive.

(lint/suspicious/noRedundantUseStrict)

core/controller-decorator/src/decorator/mcp/MCPController.ts

[error] 7-7: Do not shadow the global "constructor" property.

Consider renaming this variable. It's easy to confuse the origin of variables when they're named after a known global.

(lint/suspicious/noShadowRestrictedNames)

plugin/mcp-proxy/test/fixtures/apps/mcp-proxy/config/plugin.js

[error] 1-1: Redundant use strict directive.

The entire contents of JavaScript modules are automatically in strict mode, with no statement needed to initiate it.
Safe fix: Remove the redundant use strict directive.

(lint/suspicious/noRedundantUseStrict)

core/controller-decorator/test/MCPMeta.test.ts

[error] 8-8: Don't focus the test.

The 'only' method is often used for debugging or during implementation. It should be removed before deploying to production.
Consider removing 'only' to ensure all tests are executed.
Unsafe fix: Remove focus from test.

(lint/suspicious/noFocusedTests)

plugin/controller/test/mcp/mcp.test.ts

[error] 88-90: Disallow duplicate setup and teardown hooks.

Disallow after duplicacy inside the describe function.

(lint/suspicious/noDuplicateTestHooks)

plugin/controller/test/fixtures/apps/mcp-app/config/plugin.js

[error] 1-1: Redundant use strict directive.

The entire contents of JavaScript modules are automatically in strict mode, with no statement needed to initiate it.
Safe fix: Remove the redundant use strict directive.

(lint/suspicious/noRedundantUseStrict)

plugin/mcp-proxy/test/fixtures/apps/mcp-proxy/app/controller/app.ts

[error] 163-163: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 246-246: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)

plugin/mcp-proxy/index.ts

[error] 130-130: Avoid the delete operator which can impact performance.

Unsafe fix: Use an undefined assignment instead.

(lint/performance/noDelete)


[error] 170-170: Avoid the delete operator which can impact performance.

Unsafe fix: Use an undefined assignment instead.

(lint/performance/noDelete)

core/controller-decorator/src/util/MCPInfoUtil.ts

[error] 9-96: Avoid classes that contain only static members.

Prefer using simple functions instead of classes with only static members.

(lint/complexity/noStaticOnlyClass)

plugin/mcp-proxy/test/proxy.test.ts

[error] 89-91: Disallow duplicate setup and teardown hooks.

Disallow after duplicacy inside the describe function.

(lint/suspicious/noDuplicateTestHooks)

plugin/controller/lib/impl/mcp/MCPControllerRegister.ts

[error] 72-72: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 204-204: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 278-278: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 333-333: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 368-368: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 391-391: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)

🪛 markdownlint-cli2 (0.17.2)
plugin/mcp-proxy/CHANGELOG.md

356-356: Heading levels should only increment by one level at a time
Expected: h2; Actual: h3

(MD001, heading-increment)


653-653: Heading levels should only increment by one level at a time
Expected: h2; Actual: h3

(MD001, heading-increment)


664-664: Heading levels should only increment by one level at a time
Expected: h2; Actual: h3

(MD001, heading-increment)


819-819: Heading levels should only increment by one level at a time
Expected: h2; Actual: h3

(MD001, heading-increment)


830-830: Heading levels should only increment by one level at a time
Expected: h2; Actual: h3

(MD001, heading-increment)


849-849: Heading levels should only increment by one level at a time
Expected: h2; Actual: h3

(MD001, heading-increment)


924-924: Heading levels should only increment by one level at a time
Expected: h2; Actual: h3

(MD001, heading-increment)

⏰ Context from checks skipped due to timeout of 90000ms (9)
  • GitHub Check: Runner-ubuntu (22)
  • GitHub Check: Runner-macos (20)
  • GitHub Check: Runner-ubuntu (18)
  • GitHub Check: Runner-ubuntu (16)
  • GitHub Check: Runner-ubuntu (20)
  • GitHub Check: Analyze (typescript)
  • GitHub Check: Runner-macos (18)
  • GitHub Check: Runner-macos (16)
  • GitHub Check: Analyze (javascript)
🔇 Additional comments (55)
core/types/package.json (1)

40-40: Approve addition of MCP SDK devDependency
Adding @modelcontextprotocol/sdk@^1.10.0 to devDependencies aligns with the new MCP type definitions.

plugin/mcp-proxy/test/fixtures/apps/mcp-proxy/package.json (1)

1-3: Approve minimal package.json for MCP proxy fixture
The fixture’s package.json correctly defines the minimal manifest for the test app.

core/controller-decorator/package.json (1)

46-46: Approve addition of MCP SDK dependency
Including @modelcontextprotocol/sdk@^1.10.0 under dependencies supports the new MCP decorators and metadata builders.

core/types/controller-decorator/model/types.ts (1)

9-11: Introduce MCP ControllerType enum
The new ControllerType.MCP entry integrates MCP into the controller ecosystem. Ensure any switch statements or mappings consuming ControllerType are updated to handle this new type.

plugin/controller/test/fixtures/apps/mcp-app/package.json (1)

1-3: Approve minimal package.json for MCP controller fixture
Defines the base manifest for the controller-app test fixture appropriately.

plugin/controller/package.json (1)

56-59:

Details

❓ Verification inconclusive

Review new MCP dependencies and version alignment.

Dependencies @eggjs/mcp-proxy@^3.53.0, @modelcontextprotocol/sdk@^1.10.0, content-type@^1.0.5, and raw-body@^2.5.2 have been added. Ensure these versions align with the rest of the ecosystem and verify no critical vulnerabilities are introduced.

Please run:

to confirm there are no high-severity advisories for these new packages.


🏁 Script executed:

npm audit --json | jq '.advisories | keys'

Length of output: 443


🏁 Script executed:

#!/bin/bash
# Generate a lockfile and re-run audit to catch any high/critical advisories
npm i --package-lock-only
npm audit --json | jq '.advisories 
  | to_entries[] 
  | select(.value.severity == "high" or .value.severity == "critical")'

Length of output: 732


Please verify new MCP dependencies and run npm audit

The added packages (@eggjs/mcp-proxy@^3.53.0, @modelcontextprotocol/sdk@^1.10.0, content-type@^1.0.5, raw-body@^2.5.2) should be checked for high- or critical-severity vulnerabilities. Since there’s no lockfile, please:

  • Commit a package-lock.json (e.g. npm i --package-lock-only).
  • Run npm audit --json | jq '.advisories | to_entries[] | select(.value.severity=="high" or .value.severity=="critical")'.
  • Confirm that none of these new dependencies introduce any high- or critical-severity advisories.
plugin/mcp-proxy/tsconfig.pub.json (1)

1-10: TypeScript publish configuration is correct.

Extending the workspace ../../tsconfig.json and excluding node_modules and test is appropriate for the published package.

plugin/mcp-proxy/tsconfig.json (1)

1-10: TypeScript development configuration is valid.

Extending the root tsconfig.json and excluding node_modules and test directories aligns with best practices for plugin development.

plugin/controller/tsconfig.json (1)

6-8: LGTM: Enables compilation of MCP test files

Removing "test" from the exclude array allows TypeScript to compile the newly added MCP test files. This is necessary for the new MCP controller tests to be properly built and executed.

plugin/controller/typings/index.d.ts (2)

3-3: LGTM: Required import for MCP proxy types

Adding the import for '@eggjs/mcp-proxy' makes the MCPProxyApp interface available for use in this file.


9-9: LGTM: Correctly extends TEggControllerApp with MCP proxy capabilities

This change ensures that the TEggControllerApp interface includes the mcpProxy property, allowing type-safe access to MCP proxy functionality in controller applications.

plugin/controller/test/fixtures/apps/mcp-app/config/config.default.js (1)

4-11: LGTM: Appropriate test configuration for MCP testing

The configuration disables CSRF protection, which is a common practice for test fixtures to simplify API testing. This aligns with similar configurations in other MCP-related files.

plugin/controller/agent.ts (1)

3-9: LGTM: Well-structured agent hook class

The AppHook class is properly initialized with the agent instance and follows the standard pattern for Egg.js lifecycle hooks.

plugin/controller/config/config.default.ts (1)

1-14: The CSRF protection configuration looks good

This configuration correctly establishes a regex pattern to bypass CSRF protection for MCP-related routes. This is an important security configuration that allows MCP endpoints to function properly while maintaining CSRF protection for other routes.

plugin/mcp-proxy/app/extend/agent.ts (1)

1-17: Implementation follows Egg.js extension pattern correctly

The implementation of the mcpProxy getter properly follows the Egg.js extension pattern using a Symbol to avoid naming collisions and lazy-loading the client only when needed. The singleton pattern ensures only one instance of the MCPProxyApiClient per agent.

plugin/mcp-proxy/test/fixtures/apps/mcp-proxy/config/config.default.js (1)

3-16: Test configuration is properly structured

The test configuration correctly disables both CSRF protection and the body parser, which is necessary for testing MCP proxy functionality without middleware interference.

plugin/mcp-proxy/app/extend/application.ts (1)

1-16: Implementation correctly extends Application with mcpProxy

This implementation correctly follows the Egg.js pattern for extending the Application object with a new property. The lazy-loading pattern and use of Symbol ensure efficiency and avoid naming collisions.

plugin/controller/app/middleware/mcp_body_middleware.ts (1)

4-18: Middleware implementation is correct

The middleware correctly identifies MCP-specific paths and disables the body parser for those routes, which is necessary for proper handling of MCP requests. The implementation uses path-to-regexp for accurate path matching.

core/controller-decorator/src/model/index.ts (1)

6-9: Exports look good.

The export statements for the new MCP metadata classes follow the existing pattern in the file. This provides a clean public API for the MCP-related metadata models.

plugin/controller/app.ts (3)

14-14: Import of MCPControllerRegister looks good.

The import statement correctly references the MCP controller register implementation.


45-45: MCP controller registration looks good.

The code properly registers the MCPControllerRegister with the controller register factory for the MCP controller type.


66-67: Middleware addition looks good.

Adding the mcpBodyMiddleware at the beginning of the middleware chain ensures that MCP request bodies won't be consumed by other middleware.

core/types/controller-decorator/index.ts (1)

9-12: MCP module exports look good

The new exports for MCP-related modules follow the existing pattern and naming conventions of the file, properly extending the controller decorator types to support the new Model Context Protocol feature.

core/controller-decorator/src/impl/mcp/MCPControllerToolMetaBuilder.ts (1)

1-35: Well-structured metadata builder implementation

The MCPControllerToolMetaBuilder follows a clean builder pattern and properly collects all necessary metadata for MCP tool methods. The implementation is well-typed and handles edge cases appropriately.

core/controller-decorator/src/decorator/mcp/MCPPrompt.ts (2)

9-32: LGTM! The decorator implementation follows best practices.

The decorator handles optional parameters correctly, properly separates concerns between setting controller type and MCP-specific information, and correctly types the function parameters.


9-32:

Details

❌ Incorrect review comment

Check for potential Symbol property keys.

The code casts propertyKey to string on line 15, but if the property key is a Symbol, this might lead to unexpected behavior when using it as a map key.

Consider adding a check for Symbol property keys or ensuring that the expected behavior is documented:

-    const methodName = propertyKey as string;
+    // Handle both string and Symbol property keys
+    const methodName = propertyKey.toString();

Run the following to check if any Symbol property keys are used in the codebase:


🏁 Script executed:

#!/bin/bash
# Search for Symbol property key usage in decorators
rg "Symbol\(" --type=ts

Length of output: 2015


Ignore Symbol key handling suggestion

The propertyKey in decorators is always a string in our codebase—no symbol-named methods are ever decorated with @MCPPrompt. A search for Symbol( only reveals symbol definitions, not symbol-based method names. Casting with as string is sufficient and equivalent to toString() for all current use cases. No changes are needed.

Likely an incorrect or invalid review comment.

plugin/mcp-proxy/typings/index.d.ts (1)

1-12: LGTM! The type declaration follows TypeScript best practices.

The declaration file properly extends the egg module with MCP proxy functionality using TypeScript's declaration merging, making the mcpProxy property type-safe and accessible on the application instance.

core/controller-decorator/src/decorator/mcp/MCPController.ts (1)

6-32: LGTM! The decorator implementation is well-structured.

Overall, the decorator handles optional parameters gracefully, properly composes multiple decorations, and extracts metadata automatically to simplify usage.

🧰 Tools
🪛 Biome (1.9.4)

[error] 7-7: Do not shadow the global "constructor" property.

Consider renaming this variable. It's easy to confuse the origin of variables when they're named after a known global.

(lint/suspicious/noShadowRestrictedNames)

core/controller-decorator/src/model/MCPToolMeta.ts (1)

5-31: LGTM! The metadata class is well-structured.

The class uses proper TypeScript typing, readonly properties for immutability, and a structured options object for initialization. The implementation correctly handles optional parameters and provides a clean interface for tool metadata.

plugin/mcp-proxy/package.json (1)

1-69: Overall package structure looks good

The package definition is well-structured with appropriate dependencies for an MCP proxy implementation. The inclusion of cluster-client, content-type handling, and raw-body parsing aligns with the needs of a proxy service handling Model Context Protocol.

core/controller-decorator/src/impl/mcp/MCPControllerPromptMetaBuilder.ts (1)

1-35: Overall implementation is clean and follows the builder pattern well

The builder implementation follows the established patterns in the codebase, with a clear separation of concerns and proper metadata collection. The code is well-structured and maintainable.

core/controller-decorator/test/MCPMeta.test.ts (1)

1-25: The test coverage is comprehensive

The test covers all essential aspects of the MCP metadata building process, including the controller properties, tools, resources, and prompts. It verifies that the metadata is correctly built and that the properties have the expected types and values.

🧰 Tools
🪛 Biome (1.9.4)

[error] 8-8: Don't focus the test.

The 'only' method is often used for debugging or during implementation. It should be removed before deploying to production.
Consider removing 'only' to ensure all tests are executed.
Unsafe fix: Remove focus from test.

(lint/suspicious/noFocusedTests)

plugin/mcp-proxy/test/fixtures/apps/mcp-proxy/config/plugin.js (1)

1-22: Configuration looks correct for test purposes

The plugin configuration properly enables the required plugins for testing the MCP proxy functionality. Disabling the watcher is also appropriate for test environments.

🧰 Tools
🪛 Biome (1.9.4)

[error] 1-1: Redundant use strict directive.

The entire contents of JavaScript modules are automatically in strict mode, with no statement needed to initiate it.
Safe fix: Remove the redundant use strict directive.

(lint/suspicious/noRedundantUseStrict)

core/controller-decorator/src/model/MCPResourceMeta.ts (1)

1-36: Well-structured implementation for MCP resource metadata handling.

The MCPResourceMeta class provides a clean encapsulation of resource metadata for the Model Context Protocol system. It properly handles various types of metadata, including ACL flags, URI paths, templates, and middlewares.

A few implementation details to note:

  • The use of readonly for class properties enforces immutability
  • Boolean normalization using !! on line 26 ensures proper type conversion
  • Conditional template instantiation provides flexibility in usage
plugin/controller/test/fixtures/apps/mcp-app/app/controller/AppController.ts (2)

10-19: Zod schema definition looks good!

The Zod schema for NotificationType is well-defined with appropriate descriptions and default values.


51-58: Good response format

The response format follows the MCP protocol requirements with the proper content structure.

core/controller-decorator/index.ts (3)

2-2: Good addition of MCPControllerMetaBuilder import

This import correctly brings in the MCP controller metadata builder implementation.


13-17: Well-structured exports for MCP decorators

The MCP decorator exports are logically organized alongside the existing HTTP decorator exports.


19-19: Proper export of MCPInfoUtil utility

The MCPInfoUtil is correctly exported as a default import, consistent with other utility exports.

core/controller-decorator/src/model/MCPPromptMeta.ts (3)

1-2: Good type imports

The imports for MiddlewareFunc and McpServer types are appropriate and will ensure proper type safety.


4-12: Well-designed metadata class properties

The MCPPromptMeta class has a good set of properties for MCP prompts with appropriate readonly modifiers. The use of Parameters<McpServer['prompt']>['2'] for schema type is a clever way to maintain type compatibility with the MCP server API.


13-29: Constructor implementation looks good

The constructor properly initializes all properties and handles optional fields. The conversion of needAcl to boolean using !! ensures type consistency.

core/controller-decorator/src/impl/mcp/MCPControllerMetaBuilder.ts (3)

21-32: Good implementation of resource metadata collection

The buildResource method correctly retrieves resource method names and builds metadata for each one.


60-80: Thorough metadata collection in build method

The build method properly validates the controller type, collects all required metadata, and constructs the final MCPControllerMeta. The use of assert for type validation is a good practice.


82-87: Factory method and registration

The static create method and registration with ControllerMetaBuilderFactory follow the established pattern in the codebase.

core/types/controller-decorator/MCPPromptParams.ts (1)

1-14: Well-structured type definitions

This file provides a solid foundation for type-safe handling of MCP prompts. The types are clearly defined and properly extract parameter types from the MCP server's prompt method.

core/types/controller-decorator/MCPToolParams.ts (1)

1-15: Type definitions follow consistent pattern

The tool parameter types are well-defined and follow the same pattern as the prompt parameters, which maintains consistency across the MCP type system.

core/types/controller-decorator/MCPResourceParams.ts (1)

1-21: Appropriate dual-approach to resource identification

The type definitions effectively handle both URI-based and template-based resource identification patterns, providing flexibility in how resources can be defined and accessed.

core/controller-decorator/src/model/MCPControllerMeta.ts (1)

7-70: Overall class structure is well-designed

The MCPControllerMeta class properly implements the ControllerMetadata interface and provides good utility methods for handling middleware and ACL requirements. The class effectively organizes metadata for MCP tools, resources, and prompts.

core/types/controller-decorator/MetadataKey.ts (1)

27-34: Well-structured MCP metadata keys.

The implementation of MCP-related metadata keys follows the established pattern in the codebase. The naming convention is consistent with existing metadata keys, which makes the code more maintainable and readable.

core/types/controller-decorator/MCPController.ts (1)

4-9: LGTM: Clear interface definition for MCP controller parameters.

The MCPControllerParams interface provides a well-defined structure for configuring MCP controllers with optional parameters.

plugin/mcp-proxy/test/proxy.test.ts (1)

9-24: LGTM: Well-implemented helper functions for MCP testing.

These helper functions encapsulate common operations nicely, improving code reusability and readability in the tests.

plugin/controller/test/fixtures/apps/mcp-app/app/controller/McpController.ts (3)

14-16: LGTM: Well-defined schema types using Zod.

The schema definitions using Zod are clear and include appropriate descriptions for better documentation.

Also applies to: 18-22


25-43: LGTM: Well-implemented MCPPrompt method.

The foo method is properly decorated with @MCPPrompt and uses the correct schema and return type. The implementation is straightforward and follows the same pattern as in the test fixtures.


45-57: LGTM: Well-implemented MCPTool method.

The bar method is properly decorated with @MCPTool and uses the correct schema and return type. The implementation returns an appropriate response structure.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
plugin/controller/test/fixtures/apps/mcp-app/config/plugin.js (1)

1-1: Remove redundant 'use strict' directive
Node.js modules run in strict mode by default—this directive can be omitted to reduce noise.

🧰 Tools
🪛 Biome (1.9.4)

[error] 1-1: Redundant use strict directive.

The entire contents of JavaScript modules are automatically in strict mode, with no statement needed to initiate it.
Safe fix: Remove the redundant use strict directive.

(lint/suspicious/noRedundantUseStrict)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 443713c and fb0f5c8.

📒 Files selected for processing (4)
  • plugin/controller/agent.ts (1 hunks)
  • plugin/controller/app.ts (4 hunks)
  • plugin/controller/package.json (2 hunks)
  • plugin/controller/test/fixtures/apps/mcp-app/config/plugin.js (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • plugin/controller/agent.ts
  • plugin/controller/package.json
  • plugin/controller/app.ts
🧰 Additional context used
🪛 Biome (1.9.4)
plugin/controller/test/fixtures/apps/mcp-app/config/plugin.js

[error] 1-1: Redundant use strict directive.

The entire contents of JavaScript modules are automatically in strict mode, with no statement needed to initiate it.
Safe fix: Remove the redundant use strict directive.

(lint/suspicious/noRedundantUseStrict)

⏰ Context from checks skipped due to timeout of 90000ms (9)
  • GitHub Check: Analyze (typescript)
  • GitHub Check: Analyze (javascript)
  • GitHub Check: Runner-ubuntu (22)
  • GitHub Check: Runner-ubuntu (16)
  • GitHub Check: Runner-macos (20)
  • GitHub Check: Runner-macos (18)
  • GitHub Check: Runner-ubuntu (18)
  • GitHub Check: Runner-macos (16)
  • GitHub Check: Runner-ubuntu (20)
🔇 Additional comments (5)
plugin/controller/test/fixtures/apps/mcp-app/config/plugin.js (5)

3-6: Verify tracer plugin configuration
The egg-tracer export is correctly defined and enabled. Ensure the version of egg-tracer in package.json aligns with your test fixture’s requirements.


8-11: Verify tegg plugin configuration
The @eggjs/tegg-plugin export follows conventions and is enabled. Confirm it's listed as a dependency and matches your test app’s version constraints.


13-16: Verify tegg-config plugin configuration
The @eggjs/tegg-config plugin is properly exported and enabled. Double-check that any required config files are available in your fixture.


18-21: Verify aopModule plugin configuration
The @eggjs/tegg-aop-plugin is correctly exported here. There’s no duplicate export in this file, so the previous duplication warning does not apply.


23-26: Verify mcpProxy plugin configuration
The new @eggjs/mcp-proxy plugin is enabled as intended for MCP tests. Ensure this matches the MCP proxy package version your pull request introduces.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (3)
plugin/mcp-proxy/test/proxy.test.ts (2)

95-160: DRY: Extract common test logic to a shared function.

The "streamable should work" test duplicates much of the logic from the "sse should work" test. Consider refactoring to extract the common test logic into a shared function to improve maintainability.

+ async function testTransport(transportType, createTransport, getBaseUrl) {
+   const client = new Client({
+     name: `${transportType}-demo-client`,
+     version: '1.0.0',
+   });
+   const baseUrl = await getBaseUrl();
+   const transport = createTransport(new URL(baseUrl));
+   const notifications = [];
+   client.setNotificationHandler(LoggingMessageNotificationSchema, notification => {
+     notifications.push({ level: notification.params.level, data: notification.params.data as string });
+   });
+   await client.connect(transport);
+   const tools = await listTools(client);
+   const notificationResp = await startNotificationTool(client);
+   await new Promise(resolve => setTimeout(resolve, 5000));
+   
+   if (transportType === 'streamable') {
+     await transport.terminateSession();
+     await client.close();
+   } else {
+     await transport.close();
+   }
+   
+   assert.deepEqual(tools, [
+     {
+       name: 'start-notification-stream',
+       description: 'Starts sending periodic notifications for testing resumability',
+     },
+   ]);
+   assert.deepEqual(notificationResp, [{ text: 'Started sending periodic notifications every 1000ms' }]);
+   assert.deepEqual(notifications, [
+     { level: 'info', data: 'Periodic notification #1' },
+     { level: 'info', data: 'Periodic notification #2' },
+     { level: 'info', data: 'Periodic notification #3' },
+     { level: 'info', data: 'Periodic notification #4' },
+     { level: 'info', data: 'Periodic notification #5' },
+   ]);
+ }

  it('sse should work', async () => {
-   const sseClient = new Client({
-     name: 'sse-demo-client',
-     version: '1.0.0',
-   });
-   const baseUrl = await app.httpRequest()
-     .get('/init').url;
-   const sseTransport = new SSEClientTransport(new URL(baseUrl));
-   const sseNotifications: { level: string, data: string }[] = [];
-   sseClient.setNotificationHandler(LoggingMessageNotificationSchema, notification => {
-     sseNotifications.push({ level: notification.params.level, data: notification.params.data as string });
-   });
-   await sseClient.connect(sseTransport);
-   const tools = await listTools(sseClient);
-   const notificationResp = await startNotificationTool(sseClient);
-   await new Promise(resolve => setTimeout(resolve, 5000));
-   await sseTransport.close();
-   assert.deepEqual(tools, [
-     {
-       name: 'start-notification-stream',
-       description: 'Starts sending periodic notifications for testing resumability',
-     },
-   ]);
-   assert.deepEqual(notificationResp, [{ text: 'Started sending periodic notifications every 1000ms' }]);
-   assert.deepEqual(sseNotifications, [
-     { level: 'info', data: 'Periodic notification #1' },
-     { level: 'info', data: 'Periodic notification #2' },
-     { level: 'info', data: 'Periodic notification #3' },
-     { level: 'info', data: 'Periodic notification #4' },
-     { level: 'info', data: 'Periodic notification #5' },
-   ]);
+   await testTransport(
+     'sse',
+     url => new SSEClientTransport(url),
+     async () => await app.httpRequest().get('/init').url
+   );
  });

  it('streamable should work', async () => {
-   const streamableClient = new Client({
-     name: 'streamable-demo-client',
-     version: '1.0.0',
-   });
-   const baseUrl = await app.httpRequest()
-     .post('/stream').url;
-   const streamableTransport = new StreamableHTTPClientTransport(new URL(baseUrl));
-   const streamableNotifications: { level: string, data: string }[] = [];
-   streamableClient.setNotificationHandler(LoggingMessageNotificationSchema, notification => {
-     streamableNotifications.push({ level: notification.params.level, data: notification.params.data as string });
-   });
-   await streamableClient.connect(streamableTransport);
-   const tools = await listTools(streamableClient);
-   const notificationResp = await startNotificationTool(streamableClient);
-   await new Promise(resolve => setTimeout(resolve, 5000));
-   await streamableTransport.terminateSession();
-   await streamableClient.close();
-   assert.deepEqual(tools, [
-     {
-       name: 'start-notification-stream',
-       description: 'Starts sending periodic notifications for testing resumability',
-     },
-   ]);
-   assert.deepEqual(notificationResp, [{ text: 'Started sending periodic notifications every 1000ms' }]);
-   assert.deepEqual(streamableNotifications, [
-     { level: 'info', data: 'Periodic notification #1' },
-     { level: 'info', data: 'Periodic notification #2' },
-     { level: 'info', data: 'Periodic notification #3' },
-     { level: 'info', data: 'Periodic notification #4' },
-     { level: 'info', data: 'Periodic notification #5' },
-   ]);
+   await testTransport(
+     'streamable',
+     url => new StreamableHTTPClientTransport(url),
+     async () => await app.httpRequest().post('/stream').url
+   );
  });

91-93: ⚠️ Potential issue

Remove duplicate after hook.

There are two identical after hooks that call app.close(). This was flagged by the static analysis tool as a duplicate hook, which can lead to confusion and potential issues.

  after(async () => {
    await app.close();
  });

  afterEach(() => {
    // mm.restore();
  });

  before(async () => {
    mm(process.env, 'EGG_TYPESCRIPT', true);
    mm(process, 'cwd', () => {
      return path.join(__dirname, '..');
    });
    app = mm.cluster({
      baseDir: path.join(__dirname, 'fixtures/apps/mcp-proxy'),
      framework: path.dirname(require.resolve('egg')),
      // eslint-disable-next-line @typescript-eslint/ban-ts-comment
      // @ts-ignore
      workers: 4,
      sticky: false,
      opt: {
        env: {
          ...process.env,
          NODE_OPTIONS: '--require ts-node/register tsconfig-paths/register',
        },
      },
    });
    await app.ready();
  });

- after(() => {
-   return app.close();
- });
🧰 Tools
🪛 Biome (1.9.4)

[error] 91-93: Disallow duplicate setup and teardown hooks.

Disallow after duplicacy inside the describe function.

(lint/suspicious/noDuplicateTestHooks)

plugin/controller/test/mcp/mcp.test.ts (1)

91-93: ⚠️ Potential issue

Duplicate after() hooks – causes Biome lint error

Two identical after hooks exist inside the same describe, triggering noDuplicateTestHooks and risking double shutdown.

Keep one and remove the other:

-  after(() => {
-    return app.close();
-  });
🧰 Tools
🪛 Biome (1.9.4)

[error] 91-93: Disallow duplicate setup and teardown hooks.

Disallow after duplicacy inside the describe function.

(lint/suspicious/noDuplicateTestHooks)

🧹 Nitpick comments (6)
plugin/mcp-proxy/test/proxy.test.ts (2)

66-66: Clean up commented code.

The commented mm.restore() should either be removed entirely or uncommented if needed. Commented-out code can lead to confusion about its purpose and necessity.

  afterEach(() => {
-   // mm.restore();
  });

110-110: Extract hardcoded timeout to a constant.

The hardcoded timeout value of 5000ms appears in multiple places. Extract it to a named constant at the top of the file to improve maintainability and ensure consistency.

+ const NOTIFICATION_WAIT_TIMEOUT_MS = 5000;
...
- await new Promise(resolve => setTimeout(resolve, 5000));
+ await new Promise(resolve => setTimeout(resolve, NOTIFICATION_WAIT_TIMEOUT_MS));

Also applies to: 143-143

plugin/controller/test/mcp/mcp.test.ts (4)

66-66: Clean up commented code.

The commented mm.restore() should either be removed entirely or uncommented if needed. Commented-out code can lead to confusion about its purpose and necessity.

  afterEach(() => {
-   // mm.restore();
  });

132-132: Extract hardcoded timeout to a constant.

The hardcoded timeout value of 5000ms appears in multiple places. Extract it to a named constant at the top of the file to improve maintainability and ensure consistency.

+ const NOTIFICATION_WAIT_TIMEOUT_MS = 5000;
...
- await new Promise(resolve => setTimeout(resolve, 5000));
+ await new Promise(resolve => setTimeout(resolve, NOTIFICATION_WAIT_TIMEOUT_MS));

Also applies to: 223-223


1-54: Extract common helper functions to a shared utility file.

The helper functions listTools and startNotificationTool are duplicated between this file and plugin/mcp-proxy/test/proxy.test.ts. Consider extracting these to a shared test utility file to improve maintainability and reduce duplication.

// Create a new file: test/utils/mcp-test-utils.ts
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
+ import { CallToolRequest, CallToolResultSchema, ListToolsRequest, ListToolsResultSchema } from '@modelcontextprotocol/sdk/types.js';
+ 
+ export async function listTools(client: Client) {
+   const toolsRequest: ListToolsRequest = {
+     method: 'tools/list',
+     params: {},
+   };
+   const toolsResult = await client.request(toolsRequest, ListToolsResultSchema);
+ 
+   const tools: { name: string; description?: string; }[] = [];
+   for (const tool of toolsResult.tools) {
+     tools.push({
+       name: tool.name,
+       description: tool.description,
+     });
+   }
+   return tools;
+ }
+ 
+ export async function startNotificationTool(client: Client) {
+   // Call the notification tool using reasonable defaults
+   const request: CallToolRequest = {
+     method: 'tools/call',
+     params: {
+       name: 'start-notification-stream',
+       arguments: {
+         interval: 1000, // 1 second between notifications
+         count: 5, // Send 5 notifications
+       },
+     },
+   };
+   const result = await client.request(request, CallToolResultSchema);
+ 
+   const notifications: { text: string }[] = [];
+ 
+   result.content.forEach(item => {
+     if (item.type === 'text') {
+       notifications.push({
+         text: item.text,
+       });
+     } else {
+       notifications.push({
+         text: item.data ? item.data.toString() : '',
+       });
+     }
+   });
+   return notifications;
+ }

Then update both test files to import from this shared utility:

- async function listTools(client: Client) { ... }
- async function startNotificationTool(client: Client) { ... }
+ import { listTools, startNotificationTool } from '../../utils/mcp-test-utils';

58-58: Add a comment explaining the Node.js version check.

The test skipping based on Node.js version lacks a comment explaining why version 17 is the threshold. Add a comment to clarify why these tests don't run on Node.js versions 17 and below.

- if (parseInt(process.version.slice(1, 3)) > 17) {
+ // Skip tests on Node.js <= 17 as MCP features require Node.js >= 18 for native fetch API and other modern features
+ if (parseInt(process.version.slice(1, 3)) > 17) {
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 152ccf1 and df70781.

📒 Files selected for processing (3)
  • plugin/controller/test/mcp/mcp.test.ts (1 hunks)
  • plugin/mcp-proxy/package.json (1 hunks)
  • plugin/mcp-proxy/test/proxy.test.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • plugin/mcp-proxy/package.json
🧰 Additional context used
🪛 Biome (1.9.4)
plugin/controller/test/mcp/mcp.test.ts

[error] 91-93: Disallow duplicate setup and teardown hooks.

Disallow after duplicacy inside the describe function.

(lint/suspicious/noDuplicateTestHooks)

plugin/mcp-proxy/test/proxy.test.ts

[error] 91-93: Disallow duplicate setup and teardown hooks.

Disallow after duplicacy inside the describe function.

(lint/suspicious/noDuplicateTestHooks)

⏰ Context from checks skipped due to timeout of 90000ms (7)
  • GitHub Check: Runner-ubuntu (22)
  • GitHub Check: Runner-ubuntu (16)
  • GitHub Check: Runner-macos (20)
  • GitHub Check: Runner-macos (18)
  • GitHub Check: Runner-ubuntu (18)
  • GitHub Check: Runner-macos (16)
  • GitHub Check: Runner-ubuntu (20)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (2)
core/controller-decorator/src/util/MCPInfoUtil.ts (2)

86-94: ⚠️ Potential issue

Critical bug: Prompt params are stored in resource params metadata

The setMCPPromptParams and getMCPPromptParams methods currently use CONTROLLER_MCP_RESOURCE_PARAMS_MAP instead of a dedicated prompt params map. This will cause prompt metadata to overwrite resource metadata or vice versa.

  static setMCPPromptParams(params: MCPPromptParams & { mcpName?: string }, clazz: EggProtoImplClass, resourceName: string) {
-    const methodMap: MCPPromptMap = MetadataUtil.initOwnMapMetaData(CONTROLLER_MCP_RESOURCE_PARAMS_MAP, clazz, new Map());
+    const methodMap: MCPPromptMap = MetadataUtil.initOwnMapMetaData(CONTROLLER_MCP_PROMPT_PARAMS_MAP, clazz, new Map());
     methodMap.set(resourceName, params);
   }

   static getMCPPromptParams(clazz: EggProtoImplClass, resourceName: string): MCPPromptParams & { mcpName?: string } | undefined {
-    const methodMap: MCPPromptMap | undefined = MetadataUtil.getMetaData(CONTROLLER_MCP_RESOURCE_PARAMS_MAP, clazz);
+    const methodMap: MCPPromptMap | undefined = MetadataUtil.getMetaData(CONTROLLER_MCP_PROMPT_PARAMS_MAP, clazz);
     return methodMap?.get(resourceName);
   }

If CONTROLLER_MCP_PROMPT_PARAMS_MAP doesn't exist yet, it needs to be added to @eggjs/tegg-types.


1-1: 🛠️ Refactor suggestion

Consider importing the CONTROLLER_MCP_PROMPT_PARAMS_MAP constant

The imported constants list is missing CONTROLLER_MCP_PROMPT_PARAMS_MAP which would be needed for the fix to the prompt params methods.

-import { CONTROLLER_MCP_NAME, CONTROLLER_MCP_PROMPT_MAP, CONTROLLER_MCP_RESOURCE_MAP, CONTROLLER_MCP_RESOURCE_PARAMS_MAP, CONTROLLER_MCP_TOOL_MAP, CONTROLLER_MCP_TOOL_PARAMS_MAP, CONTROLLER_MCP_VERSION, MCPPromptParams, MCPResourceParams, MCPToolParams, EggProtoImplClass } from '@eggjs/tegg-types';
+import { CONTROLLER_MCP_NAME, CONTROLLER_MCP_PROMPT_MAP, CONTROLLER_MCP_PROMPT_PARAMS_MAP, CONTROLLER_MCP_RESOURCE_MAP, CONTROLLER_MCP_RESOURCE_PARAMS_MAP, CONTROLLER_MCP_TOOL_MAP, CONTROLLER_MCP_TOOL_PARAMS_MAP, CONTROLLER_MCP_VERSION, MCPPromptParams, MCPResourceParams, MCPToolParams, EggProtoImplClass } from '@eggjs/tegg-types';
🧹 Nitpick comments (9)
core/controller-decorator/src/decorator/mcp/MCPController.ts (4)

8-8: Consider renaming the "constructor" parameter to avoid shadowing the global property

The parameter name constructor shadows the global constructor property. While this is a common pattern in TypeScript decorators, consider renaming it to something like target or clazz for better code clarity.

-  return function(constructor: EggProtoImplClass) {
+  return function(clazz: EggProtoImplClass) {
🧰 Tools
🪛 Biome (1.9.4)

[error] 8-8: Do not shadow the global "constructor" property.

Consider renaming this variable. It's easy to confuse the origin of variables when they're named after a known global.

(lint/suspicious/noShadowRestrictedNames)


18-24: Remove commented file paths that appear to be debugging information

These commented-out file paths don't provide value to the codebase and should be removed.

-    // './tegg/core/common-util/src/StackUtil.ts',
-    // './tegg/core/core-decorator/src/decorator/Prototype.ts',
-    // './tegg/core/controller-decorator/src/decorator/tr/TRController.ts',
-    // './tegg/core/core-decorator/node_modules/[email protected]@reflect-metadata/Reflect.js',
-    // './tegg/core/core-decorator/node_modules/[email protected]@reflect-metadata/Reflect.js',
-    // './tegg/core/controller-decorator/test/fixtures/TRFooController.ts',
     PrototypeUtil.setFilePath(constructor, StackUtil.getCalleeFromStack(false, 5));

24-24: Document or refactor the hardcoded stack frame index

The hardcoded value 5 in StackUtil.getCalleeFromStack(false, 5) makes the code brittle if the call stack structure changes. Consider adding a comment explaining why this specific value is used or refactor to calculate it dynamically.

-    PrototypeUtil.setFilePath(constructor, StackUtil.getCalleeFromStack(false, 5));
+    // Skip 5 frames: decorator function, decorator application, internal reflections, etc.
+    PrototypeUtil.setFilePath(constructor, StackUtil.getCalleeFromStack(false, 5));

5-5: Use consistent import path style

The import path ../../../src/util/MCPInfoUtil is unusual and might cause issues. Consider using a more direct import path.

-import MCPInfoUtil from '../../../src/util/MCPInfoUtil';
+import MCPInfoUtil from '../../util/MCPInfoUtil';
core/controller-decorator/src/util/MCPInfoUtil.ts (5)

63-66: Use consistent parameter naming

The parameter resourceName is used here but this method deals with tools, not resources. Consider renaming for consistency.

-  static getMCPToolParams(clazz: EggProtoImplClass, resourceName: string): MCPToolParams & { mcpName?: string } | undefined {
+  static getMCPToolParams(clazz: EggProtoImplClass, toolName: string): MCPToolParams & { mcpName?: string } | undefined {
     const methodMap: MCPToolMap | undefined = MetadataUtil.getMetaData(CONTROLLER_MCP_TOOL_PARAMS_MAP, clazz);
-    return methodMap?.get(resourceName);
+    return methodMap?.get(toolName);
   }

68-71: Use consistent parameter naming

Similar to the previous comment, resourceName should be renamed to toolName for clarity and consistency.

-  static setMCPToolParams(params: MCPToolParams & { mcpName?: string }, clazz: EggProtoImplClass, resourceName: string) {
+  static setMCPToolParams(params: MCPToolParams & { mcpName?: string }, clazz: EggProtoImplClass, toolName: string) {
     const methodMap: MCPToolMap = MetadataUtil.initOwnMapMetaData(CONTROLLER_MCP_TOOL_PARAMS_MAP, clazz, new Map());
-    methodMap.set(resourceName, params);
+    methodMap.set(toolName, params);
   }

91-94: Use consistent parameter naming

The parameter resourceName is used but this method deals with prompts, not resources. Consider renaming for consistency.

-  static getMCPPromptParams(clazz: EggProtoImplClass, resourceName: string): MCPPromptParams & { mcpName?: string } | undefined {
+  static getMCPPromptParams(clazz: EggProtoImplClass, promptName: string): MCPPromptParams & { mcpName?: string } | undefined {
     const methodMap: MCPPromptMap | undefined = MetadataUtil.getMetaData(CONTROLLER_MCP_RESOURCE_PARAMS_MAP, clazz);
-    return methodMap?.get(resourceName);
+    return methodMap?.get(promptName);
   }

86-89: Use consistent parameter naming

Similar to the previous comment, resourceName should be renamed to promptName for clarity and consistency.

-  static setMCPPromptParams(params: MCPPromptParams & { mcpName?: string }, clazz: EggProtoImplClass, resourceName: string) {
+  static setMCPPromptParams(params: MCPPromptParams & { mcpName?: string }, clazz: EggProtoImplClass, promptName: string) {
     const methodMap: MCPPromptMap = MetadataUtil.initOwnMapMetaData(CONTROLLER_MCP_RESOURCE_PARAMS_MAP, clazz, new Map());
-    methodMap.set(resourceName, params);
+    methodMap.set(promptName, params);
   }

9-96: Consider refactoring to use namespace or individual functions

The static analysis hints that this class only contains static methods. Consider refactoring to use a namespace or individual functions for better maintainability.

Instead of a class with static methods, you could use a namespace:

export namespace MCPInfoUtil {
  export function setMCPName(name: string, clazz: EggProtoImplClass) {
    // implementation
  }
  
  // Other methods...
}

Or individual exported functions:

export function setMCPName(name: string, clazz: EggProtoImplClass) {
  // implementation
}

export function getMCPName(clazz: EggProtoImplClass): string | undefined {
  // implementation
}

// Other functions...
🧰 Tools
🪛 Biome (1.9.4)

[error] 9-96: Avoid classes that contain only static members.

Prefer using simple functions instead of classes with only static members.

(lint/complexity/noStaticOnlyClass)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between df70781 and d31eba6.

📒 Files selected for processing (4)
  • core/controller-decorator/src/decorator/mcp/MCPController.ts (1 hunks)
  • core/controller-decorator/src/impl/mcp/MCPControllerMetaBuilder.ts (1 hunks)
  • core/controller-decorator/src/model/MCPResourceMeta.ts (1 hunks)
  • core/controller-decorator/src/util/MCPInfoUtil.ts (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • core/controller-decorator/src/model/MCPResourceMeta.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • core/controller-decorator/src/impl/mcp/MCPControllerMetaBuilder.ts
🧰 Additional context used
🪛 Biome (1.9.4)
core/controller-decorator/src/decorator/mcp/MCPController.ts

[error] 8-8: Do not shadow the global "constructor" property.

Consider renaming this variable. It's easy to confuse the origin of variables when they're named after a known global.

(lint/suspicious/noShadowRestrictedNames)

core/controller-decorator/src/util/MCPInfoUtil.ts

[error] 9-96: Avoid classes that contain only static members.

Prefer using simple functions instead of classes with only static members.

(lint/complexity/noStaticOnlyClass)

⏰ Context from checks skipped due to timeout of 90000ms (7)
  • GitHub Check: Runner-ubuntu (20)
  • GitHub Check: Runner-ubuntu (16)
  • GitHub Check: Runner-ubuntu (22)
  • GitHub Check: Runner-macos (18)
  • GitHub Check: Runner-ubuntu (18)
  • GitHub Check: Runner-macos (20)
  • GitHub Check: Runner-macos (16)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

♻️ Duplicate comments (2)
plugin/mcp-proxy/index.ts (1)

112-141: SSE response is buffered – streaming lost
See earlier review; the body is consumed with resp.text() which disables true Server-Sent-Events streaming.

Refactor to pipe the readable body to ctx.res (or reuse handleSseStream).

🧰 Tools
🪛 Biome (1.9.4)

[error] 134-134: Avoid the delete operator which can impact performance.

Unsafe fix: Use an undefined assignment instead.

(lint/performance/noDelete)

plugin/mcp-proxy/test/fixtures/apps/mcp-proxy/app/controller/app.ts (1)

164-170: Redundant const self = this alias

Same issue as in the register class; can be simplified.

🧰 Tools
🪛 Biome (1.9.4)

[error] 164-164: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)

🧹 Nitpick comments (5)
plugin/mcp-proxy/index.ts (2)

59-73: Type safety gap in setProxyHandler

proxyHandlerMap is declared as handleRequest only, but for SSE you store a handlePostMessage callback which has a different signature.
Either widen the map type or overload the method so TS will catch accidental mis-wiring.

-  private proxyHandlerMap: { [P in ProxyAction]?: StreamableHTTPServerTransport['handleRequest']; } = {};
+  private proxyHandlerMap: {
+    MCP_STDIO_PROXY?: StreamableHTTPServerTransport['handleRequest'];
+    MCP_SSE_PROXY?: SSEServerTransport['handlePostMessage'];
+    MCP_STREAM_PROXY?: StreamableHTTPServerTransport['handleRequest'];
+  } = {};

134-135: delete on hot path – minor perf nit

delete headers['transfer-encoding'] creates hidden-class churn.
Assigning undefined or filtering keys into a new object avoids that.
Not critical, just a micro-optimisation.

Also applies to: 174-175

🧰 Tools
🪛 Biome (1.9.4)

[error] 134-134: Avoid the delete operator which can impact performance.

Unsafe fix: Use an undefined assignment instead.

(lint/performance/noDelete)

plugin/controller/lib/impl/mcp/MCPControllerRegister.ts (1)

71-92: Unnecessary self = this aliasing – clutters closures

Arrow functions already capture lexical this; the extra alias adds noise and risks accidental divergence.

Replace const self = this with direct this references throughout mcpStreamServerInit (and the other init helpers).

🧰 Tools
🪛 Biome (1.9.4)

[error] 73-73: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)

plugin/mcp-proxy/test/fixtures/apps/mcp-proxy/app/controller/app.ts (2)

129-154: Potential protocol confusion between SSE and STREAM

transports is a single shared record that stores both SSE and Streamable transports.
Down-casting with instanceof works, but the same sessionId could theoretically collide across protocols, leading to class-mismatch 400 errors.

Add a namespaced key, e.g. ${protocol}:${sessionId}, or maintain two separate maps.


232-305: Large JSON body parsing without timeout/size guard

getRawBody is called with 4 MB limit but no request-timeout.
A slow-loris client could keep the connection open and tie up the worker.

Set an absolute timeout:

-        limit: '4mb',
+        limit: '4mb',
+        timeout: 10_000, // 10 s safeguard
🧰 Tools
🪛 Biome (1.9.4)

[error] 250-250: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 57f7830 and 773cca3.

📒 Files selected for processing (5)
  • plugin/controller/lib/impl/mcp/MCPControllerRegister.ts (1 hunks)
  • plugin/mcp-proxy/app/extend/agent.ts (1 hunks)
  • plugin/mcp-proxy/app/extend/application.ts (1 hunks)
  • plugin/mcp-proxy/index.ts (1 hunks)
  • plugin/mcp-proxy/test/fixtures/apps/mcp-proxy/app/controller/app.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • plugin/mcp-proxy/app/extend/agent.ts
  • plugin/mcp-proxy/app/extend/application.ts
🧰 Additional context used
🪛 Biome (1.9.4)
plugin/mcp-proxy/test/fixtures/apps/mcp-proxy/app/controller/app.ts

[error] 164-164: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 250-250: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)

plugin/controller/lib/impl/mcp/MCPControllerRegister.ts

[error] 73-73: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 208-208: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 285-285: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 340-340: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 375-375: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 398-398: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)

plugin/mcp-proxy/index.ts

[error] 134-134: Avoid the delete operator which can impact performance.

Unsafe fix: Use an undefined assignment instead.

(lint/performance/noDelete)


[error] 174-174: Avoid the delete operator which can impact performance.

Unsafe fix: Use an undefined assignment instead.

(lint/performance/noDelete)

🪛 GitHub Check: CodeQL
plugin/mcp-proxy/index.ts

[failure] 53-53: Unvalidated dynamic method call
Invocation of method with user-controlled name may dispatch to unexpected target and cause an exception.

⏰ Context from checks skipped due to timeout of 90000ms (7)
  • GitHub Check: Runner-ubuntu (16)
  • GitHub Check: Runner-ubuntu (18)
  • GitHub Check: Runner-ubuntu (22)
  • GitHub Check: Runner-macos (20)
  • GitHub Check: Runner-ubuntu (20)
  • GitHub Check: Runner-macos (18)
  • GitHub Check: Runner-macos (16)
🔇 Additional comments (1)
plugin/controller/lib/impl/mcp/MCPControllerRegister.ts (1)

291-305: transports[sessionId] may be undefined – null-check before use

If the session was never initialised (or was already closed) handlePostMessage will throw.

-        await self.transports[sessionId].handlePostMessage(ctx.req, ctx.res);
+        const t = self.transports[sessionId];
+        if (!t) {
+          ctx.status = 400;
+          ctx.body = { error: 'Unknown sessionId' };
+          return;
+        }
+        await t.handlePostMessage(ctx.req, ctx.res);

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (15)
plugin/mcp-proxy/test/proxy.test.ts (3)

61-63: ⚠️ Potential issue

Remove duplicate after hook.

There are two identical after hooks that close the application. This duplicates cleanup logic and triggers the noDuplicateTestHooks linting error.

  after(async () => {
    await app.close();
  });

  afterEach(() => {
    // mm.restore();
  });

  before(async () => {
    mm(process.env, 'EGG_TYPESCRIPT', true);
    mm(process, 'cwd', () => {
      return path.join(__dirname, '..');
    });
    app = mm.cluster({
      baseDir: path.join(__dirname, 'fixtures/apps/mcp-proxy'),
      framework: path.dirname(require.resolve('egg')),
      // eslint-disable-next-line @typescript-eslint/ban-ts-comment
      // @ts-ignore
      workers: 3,
      sticky: false,
      opt: {
        env: {
          ...process.env,
          NODE_OPTIONS: '--require ts-node/register tsconfig-paths/register',
        },
      },
    });
    await app.ready();
  });

- after(() => {
-   return app.close();
- });

Also applies to: 91-93


48-48: 🛠️ Refactor suggestion

Add proper null checking instead of using non-null assertion.

Using the non-null assertion operator (!) can lead to runtime errors if item.data is actually null.

- text: item.data!.toString(),
+ text: item.data ? item.data.toString() : '',

95-160: 🛠️ Refactor suggestion

DRY: Extract common test logic between SSE and streamable tests.

Both "sse should work" and "streamable should work" tests contain nearly identical code with minor differences in transport types. Extract this common logic to improve maintainability.

+ async function testTransport(transportType, createTransport, getBaseUrl) {
+   const client = new Client({
+     name: `${transportType}-demo-client`,
+     version: '1.0.0',
+   });
+   const baseUrl = await getBaseUrl();
+   const transport = createTransport(new URL(baseUrl));
+   const notifications = [];
+   client.setNotificationHandler(LoggingMessageNotificationSchema, notification => {
+     notifications.push({ level: notification.params.level, data: notification.params.data as string });
+   });
+   await client.connect(transport);
+   const tools = await listTools(client);
+   const notificationResp = await startNotificationTool(client);
+   await new Promise(resolve => setTimeout(resolve, 5000));
+   
+   if (transportType === 'streamable') {
+     await transport.terminateSession();
+     await client.close();
+   } else {
+     await transport.close();
+   }
+   
+   assert.deepEqual(tools, [
+     {
+       name: 'start-notification-stream',
+       description: 'Starts sending periodic notifications for testing resumability',
+     },
+   ]);
+   assert.deepEqual(notificationResp, [{ text: 'Started sending periodic notifications every 1000ms' }]);
+   assert.deepEqual(notifications, [
+     { level: 'info', data: 'Periodic notification #1' },
+     { level: 'info', data: 'Periodic notification #2' },
+     { level: 'info', data: 'Periodic notification #3' },
+     { level: 'info', data: 'Periodic notification #4' },
+     { level: 'info', data: 'Periodic notification #5' },
+   ]);
+ }

  it('sse should work', async () => {
-   const sseClient = new Client({
-     name: 'sse-demo-client',
-     version: '1.0.0',
-   });
-   const baseUrl = await app.httpRequest()
-     .get('/init').url;
-   const sseTransport = new SSEClientTransport(new URL(baseUrl));
-   const sseNotifications: { level: string, data: string }[] = [];
-   sseClient.setNotificationHandler(LoggingMessageNotificationSchema, notification => {
-     sseNotifications.push({ level: notification.params.level, data: notification.params.data as string });
-   });
-   await sseClient.connect(sseTransport);
-   const tools = await listTools(sseClient);
-   const notificationResp = await startNotificationTool(sseClient);
-   await new Promise(resolve => setTimeout(resolve, 5000));
-   await sseTransport.close();
-   assert.deepEqual(tools, [
-     {
-       name: 'start-notification-stream',
-       description: 'Starts sending periodic notifications for testing resumability',
-     },
-   ]);
-   assert.deepEqual(notificationResp, [{ text: 'Started sending periodic notifications every 1000ms' }]);
-   assert.deepEqual(sseNotifications, [
-     { level: 'info', data: 'Periodic notification #1' },
-     { level: 'info', data: 'Periodic notification #2' },
-     { level: 'info', data: 'Periodic notification #3' },
-     { level: 'info', data: 'Periodic notification #4' },
-     { level: 'info', data: 'Periodic notification #5' },
-   ]);
+   await testTransport(
+     'sse', 
+     url => new SSEClientTransport(url),
+     async () => await app.httpRequest().get('/init').url
+   );
  });
plugin/controller/test/mcp/mcp.test.ts (3)

61-63: ⚠️ Potential issue

Remove duplicate after hook.

There are two identical after hooks that close the application. This duplicates cleanup logic and triggers the noDuplicateTestHooks linting error.

  after(async () => {
    await app.close();
  });

  afterEach(() => {
    // mm.restore();
  });

  before(async () => {
    mm(process.env, 'EGG_TYPESCRIPT', true);
    mm(process, 'cwd', () => {
      return path.join(__dirname, '../..');
    });
    app = mm.cluster({
      baseDir: path.join(__dirname, '../fixtures/apps/mcp-app'),
      framework: path.dirname(require.resolve('egg')),
      // eslint-disable-next-line @typescript-eslint/ban-ts-comment
      // @ts-ignore
      workers: 3,
      sticky: false,
      opt: {
        env: {
          ...process.env,
          NODE_OPTIONS: '--require ts-node/register tsconfig-paths/register',
        },
      },
    });
    await app.ready();
  });

- after(() => {
-   return app.close();
- });

Also applies to: 91-93


48-48: 🛠️ Refactor suggestion

Add proper null checking instead of using non-null assertion.

Using the non-null assertion operator (!) can lead to runtime errors if item.data is actually null.

- text: item.data!.toString(),
+ text: item.data ? item.data.toString() : '',

95-277: 🛠️ Refactor suggestion

DRY: Extract common test logic between SSE and streamable tests.

Both test cases contain duplicate code with only minor differences. Creating a shared test function would improve maintainability and make the tests more concise.

+ async function testMCPTransport(
+   transportType: 'sse' | 'streamable',
+   createTransport: (url: URL) => SSEClientTransport | StreamableHTTPClientTransport,
+   getEndpointUrl: () => Promise<string>
+ ) {
+   const client = new Client({
+     name: `${transportType}-demo-client`,
+     version: '1.0.0',
+   });
+   const baseUrl = await getEndpointUrl();
+   const transport = createTransport(new URL(baseUrl));
+   const notifications: { level: string, data: string }[] = [];
+   client.setNotificationHandler(LoggingMessageNotificationSchema, notification => {
+     notifications.push({ level: notification.params.level, data: notification.params.data as string });
+   });
+   await client.connect(transport);
+   
+   // Test tools
+   const tools = await listTools(client);
+   assert.deepEqual(tools, [
+     {
+       name: 'start-notification-stream',
+       description: 'Starts sending periodic notifications for testing resumability',
+     },
+     {
+       description: undefined,
+       name: 'bar',
+     },
+   ]);
+
+   const toolRes = await client.callTool({
+     name: 'bar',
+     arguments: {
+       name: 'aaa',
+     },
+   });
+   assert.deepEqual(toolRes, {
+     content: [{ type: 'text', text: 'npm package: aaa not found' }],
+   });
+   
+   // Test notifications
+   const notificationResp = await startNotificationTool(client);
+   await new Promise(resolve => setTimeout(resolve, 5000));
+   assert.deepEqual(notificationResp, [{ text: 'Started sending periodic notifications every 1000ms' }]);
+   assert.deepEqual(notifications, [
+     { level: 'info', data: 'Periodic notification #1' },
+     { level: 'info', data: 'Periodic notification #2' },
+     { level: 'info', data: 'Periodic notification #3' },
+     { level: 'info', data: 'Periodic notification #4' },
+     { level: 'info', data: 'Periodic notification #5' },
+   ]);
+
+   // Test resources
+   const resources = await client.listResources();
+   assert.deepEqual(resources, {
+     resources: [
+       { uri: 'mcp://npm/egg?version=4.10.0', name: 'egg' },
+       { uri: 'mcp://npm/mcp?version=0.10.0', name: 'mcp' },
+     ],
+   });
+
+   const resourceRes = await client.readResource({
+     uri: 'mcp://npm/egg?version=4.10.0',
+   });
+   assert.deepEqual(resourceRes, {
+     contents: [{ uri: 'mcp://npm/egg?version=4.10.0', text: 'MOCK TEXT' }],
+   });
+
+   // Test prompts
+   const prompts = await client.listPrompts();
+   assert.deepEqual(prompts, {
+     prompts: [
+       { name: 'foo', arguments: [{ name: 'name', required: true }] },
+     ],
+   });
+
+   const promptRes = await client.getPrompt({
+     name: 'foo',
+     arguments: {
+       name: 'bbb',
+     },
+   });
+   assert.deepEqual(promptRes, {
+     messages: [
+       {
+         role: 'user',
+         content: {
+           type: 'text',
+           text: 'Generate a concise but descriptive commit message for these changes:\n\nbbb',
+         },
+       },
+     ],
+   });
+
+   // Close transport appropriately based on type
+   if (transportType === 'sse') {
+     await (transport as SSEClientTransport).close();
+   } else {
+     await (transport as StreamableHTTPClientTransport).terminateSession();
+     await client.close();
+   }
+ }

  it('sse should work', async () => {
-   const sseClient = new Client({
-     name: 'sse-demo-client',
-     version: '1.0.0',
-   });
-   const baseUrl = await app.httpRequest()
-     .get('/mcp/init').url;
-   const sseTransport = new SSEClientTransport(new URL(baseUrl));
-   const sseNotifications: { level: string, data: string }[] = [];
-   sseClient.setNotificationHandler(LoggingMessageNotificationSchema, notification => {
-     sseNotifications.push({ level: notification.params.level, data: notification.params.data as string });
-   });
-   await sseClient.connect(sseTransport);
-   // tool
-   const tools = await listTools(sseClient);
-   assert.deepEqual(tools, [
-     {
-       name: 'start-notification-stream',
-       description: 'Starts sending periodic notifications for testing resumability',
-     },
-     {
-       description: undefined,
-       name: 'bar',
-     },
-   ]);
-
-   const toolRes = await sseClient.callTool({
-     name: 'bar',
-     arguments: {
-       name: 'aaa',
-     },
-   });
-   assert.deepEqual(toolRes, {
-     content: [{ type: 'text', text: 'npm package: aaa not found' }],
-   });
-   // notification
-   const notificationResp = await startNotificationTool(sseClient);
-   await new Promise(resolve => setTimeout(resolve, 5000));
-   assert.deepEqual(notificationResp, [{ text: 'Started sending periodic notifications every 1000ms' }]);
-   assert.deepEqual(sseNotifications, [
-     { level: 'info', data: 'Periodic notification #1' },
-     { level: 'info', data: 'Periodic notification #2' },
-     { level: 'info', data: 'Periodic notification #3' },
-     { level: 'info', data: 'Periodic notification #4' },
-     { level: 'info', data: 'Periodic notification #5' },
-   ]);
-
-   // resources
-   const resources = await sseClient.listResources();
-   assert.deepEqual(resources, {
-     resources: [
-       { uri: 'mcp://npm/egg?version=4.10.0', name: 'egg' },
-       { uri: 'mcp://npm/mcp?version=0.10.0', name: 'mcp' },
-     ],
-   });
-
-   const resourceRes = await sseClient.readResource({
-     uri: 'mcp://npm/egg?version=4.10.0',
-   });
-   assert.deepEqual(resourceRes, {
-     contents: [{ uri: 'mcp://npm/egg?version=4.10.0', text: 'MOCK TEXT' }],
-   });
-
-   // prompts
-   const prompts = await sseClient.listPrompts();
-   assert.deepEqual(prompts, {
-     prompts: [
-       { name: 'foo', arguments: [{ name: 'name', required: true }] },
-     ],
-   });
-
-   const promptRes = await sseClient.getPrompt({
-     name: 'foo',
-     arguments: {
-       name: 'bbb',
-     },
-   });
-   assert.deepEqual(promptRes, {
-     messages: [
-       {
-         role: 'user',
-         content: {
-           type: 'text',
-           text: 'Generate a concise but descriptive commit message for these changes:\n\nbbb',
-         },
-       },
-     ],
-   });
-   await sseTransport.close();
+   await testMCPTransport(
+     'sse',
+     url => new SSEClientTransport(url),
+     async () => await app.httpRequest().get('/mcp/init').url
+   );
  });

  it('streamable should work', async () => {
-   const streamableClient = new Client({
-     name: 'streamable-demo-client',
-     version: '1.0.0',
-   });
-   const baseUrl = await app.httpRequest()
-     .post('/mcp/stream').url;
-   const streamableTransport = new StreamableHTTPClientTransport(new URL(baseUrl));
-   const streamableNotifications: { level: string, data: string }[] = [];
-   streamableClient.setNotificationHandler(LoggingMessageNotificationSchema, notification => {
-     streamableNotifications.push({ level: notification.params.level, data: notification.params.data as string });
-   });
-   await streamableClient.connect(streamableTransport);
-   // tool
-   const tools = await listTools(streamableClient);
-   assert.deepEqual(tools, [
-     {
-       name: 'start-notification-stream',
-       description: 'Starts sending periodic notifications for testing resumability',
-     },
-     {
-       description: undefined,
-       name: 'bar',
-     },
-   ]);
-
-   const toolRes = await streamableClient.callTool({
-     name: 'bar',
-     arguments: {
-       name: 'aaa',
-     },
-   });
-   assert.deepEqual(toolRes, {
-     content: [{ type: 'text', text: 'npm package: aaa not found' }],
-   });
-   // notification
-   const notificationResp = await startNotificationTool(streamableClient);
-   await new Promise(resolve => setTimeout(resolve, 5000));
-   assert.deepEqual(notificationResp, [{ text: 'Started sending periodic notifications every 1000ms' }]);
-   assert.deepEqual(streamableNotifications, [
-     { level: 'info', data: 'Periodic notification #1' },
-     { level: 'info', data: 'Periodic notification #2' },
-     { level: 'info', data: 'Periodic notification #3' },
-     { level: 'info', data: 'Periodic notification #4' },
-     { level: 'info', data: 'Periodic notification #5' },
-   ]);
-
-   // resources
-   const resources = await streamableClient.listResources();
-   assert.deepEqual(resources, {
-     resources: [
-       { uri: 'mcp://npm/egg?version=4.10.0', name: 'egg' },
-       { uri: 'mcp://npm/mcp?version=0.10.0', name: 'mcp' },
-     ],
-   });
-
-   const resourceRes = await streamableClient.readResource({
-     uri: 'mcp://npm/egg?version=4.10.0',
-   });
-   assert.deepEqual(resourceRes, {
-     contents: [{ uri: 'mcp://npm/egg?version=4.10.0', text: 'MOCK TEXT' }],
-   });
-
-   // prompts
-   const prompts = await streamableClient.listPrompts();
-   assert.deepEqual(prompts, {
-     prompts: [
-       { name: 'foo', arguments: [{ name: 'name', required: true }] },
-     ],
-   });
-
-   const promptRes = await streamableClient.getPrompt({
-     name: 'foo',
-     arguments: {
-       name: 'bbb',
-     },
-   });
-   assert.deepEqual(promptRes, {
-     messages: [
-       {
-         role: 'user',
-         content: {
-           type: 'text',
-           text: 'Generate a concise but descriptive commit message for these changes:\n\nbbb',
-         },
-       },
-     ],
-   });
-
-   await streamableTransport.terminateSession();
-   await streamableClient.close();
+   await testMCPTransport(
+     'streamable',
+     url => new StreamableHTTPClientTransport(url),
+     async () => await app.httpRequest().post('/mcp/stream').url
+   );
  });
plugin/mcp-proxy/test/fixtures/apps/mcp-proxy/app/controller/app.ts (1)

107-145: ⚠️ Potential issue

Fix proxy handler overwriting issue in onsessioninitialized.

The setProxyHandler('STREAM', ...) call in the onsessioninitialized callback will overwrite any existing handler for each new session. This breaks earlier sessions in multi-tenant scenarios.

- this.app.mcpProxy.setProxyHandler('STREAM', async (req, res) => {
-   return await self.streamPostHandler(req, res);
- });

Consider setting up the proxy handler once outside the session initialization, or use a more sophisticated approach that maps handlers to specific session IDs.

plugin/controller/lib/impl/mcp/MCPControllerRegister.ts (3)

291-306: ⚠️ Potential issue

Validate sessionId before using it.

The sessionId obtained from ctx.query.sessionId is not validated before use, potentially causing runtime errors if it's undefined.

-  const sessionId = ctx.query.sessionId;
+  const sessionId = String(ctx.query.sessionId ?? '');
+  if (!sessionId) {
+    ctx.status = 400;
+    ctx.body = { error: 'sessionId required' };
+    return;
+  }
   const detail = await self.app.mcpProxy.getClient(sessionId);

429-437: ⚠️ Potential issue

Fix initialization order to prevent race conditions.

The mcpStreamServerInit, mcpServerInit, and mcpServerRegister methods are called immediately after creating the server but before completing async operations. This creates a potential race condition if register() is called concurrently.

Consider either:

  1. Making the initialization methods return promises and awaiting them sequentially, or
  2. Implementing a mutex to ensure atomic initialization

107-144: ⚠️ Potential issue

Fix proxy handler overwriting issue in onsessioninitialized.

Each new session registers a new handler for the 'STREAM' protocol, overwriting any previous handlers. This will break earlier sessions in multi-tenant scenarios.

Consider either:

  1. Moving the setProxyHandler call outside the per-session callback, or
  2. Storing handlers in a map keyed by both protocol and sessionId
plugin/mcp-proxy/index.ts (5)

21-21: ⚠️ Potential issue

Typo in ProxyAction type definition

The type definition includes 'MCP_SEE_PROXY' which should be 'MCP_SSE_PROXY' (for Server-Sent Events).

-type ProxyAction = 'MCP_STDIO_PROXY' | 'MCP_SEE_PROXY' | 'MCP_STREAM_PROXY';
+type ProxyAction = 'MCP_STDIO_PROXY' | 'MCP_SSE_PROXY' | 'MCP_STREAM_PROXY';

37-37: ⚠️ Potential issue

_client property never initialized – all delegated calls will throw

The subclass redeclares private _client: any but never assigns it, causing all delegate methods to fail.

-  private _client: any;

54-55: ⚠️ Potential issue

Port listener initialization needs error handling

Variable name is incorrect and the server.listen call lacks error handling.

-    const post = 17031 + (cluster.worker?.id ?? 0);
-    await new Promise(resolve => server.listen(post, () => { resolve(null); }));
+    const port = 17031 + (cluster.worker?.id ?? 0);
+    await new Promise((resolve, reject) => {
+      server.once('error', reject);
+      server.listen(port, () => resolve(null));
+    });

62-62: ⚠️ Potential issue

Typo in SSE proxy action name

The same typo as in the type definition appears here.

-        action = 'MCP_SEE_PROXY';
+        action = 'MCP_SSE_PROXY';

115-140: ⚠️ Potential issue

SSE response is read fully – streaming is lost

The SSE implementation reads the entire response body as text and sends it back as one chunk, defeating the purpose of SSE where the client expects a stream of events.

        case 'SSE': {
-          action = 'MCP_SEE_PROXY';
+          action = 'MCP_SSE_PROXY';
          ctx.req.headers['mcp-proxy-type'] = action;
          ctx.req.headers['mcp-proxy-sessionid'] = sessionId;
          const resp = await fetch(`http://localhost:${17031 + detail.workerId}/mcp/message?sessionId=${sessionId}`, {
            headers: ctx.req.headers as unknown as Record<string, string>,
            body: body as string,
            method: ctx.req.method,
          });
          const headers: Record<string, string> = {};
          for (const [ key, value ] of resp.headers.entries()) {
            headers[key] = value;
          }
          delete headers['transfer-encoding'];
          ctx.set(headers);
          ctx.res.statusCode = resp.status;
          ctx.res.statusMessage = resp.statusText;
-          const resData = await resp.text();
-          ctx.body = resData;
+          ctx.respond = false;
+          Readable.fromWeb(resp.body!).pipe(ctx.res);
          break;
        }
🧰 Tools
🪛 Biome (1.9.4)

[error] 133-133: Avoid the delete operator which can impact performance.

Unsafe fix: Use an undefined assignment instead.

(lint/performance/noDelete)

🧹 Nitpick comments (9)
plugin/mcp-proxy/test/fixtures/apps/mcp-proxy/app/controller/app.ts (1)

164-164: Remove unnecessary this aliasing.

Arrow functions inherit this from their enclosing scope. The self variable is redundant.

-    const self = this;
     const transport = new SSEServerTransport('/message', this.ctx.res);
-    const self = this;
     const transport = new StreamableHTTPServerTransport({

Also applies to: 250-250

🧰 Tools
🪛 Biome (1.9.4)

[error] 164-164: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)

plugin/controller/lib/impl/mcp/MCPControllerRegister.ts (1)

73-73: Remove unnecessary this aliasing throughout the file.

Arrow functions inherit this from their enclosing scope, making the self variable redundant. This appears multiple times in the file.

- const self = this;

Also applies to: 208-208, 285-285, 340-340, 375-375, 398-398

🧰 Tools
🪛 Biome (1.9.4)

[error] 73-73: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)

plugin/mcp-proxy/index.ts (7)

112-112: Remove or uncomment socket path configuration

The commented code suggests an alternative implementation using Unix sockets that was replaced with TCP ports.

Either remove this commented code or add a comment explaining why it's being kept for reference.


133-133: Use undefined assignment instead of delete operator

The delete operator can impact performance.

-          delete headers['transfer-encoding'];
+          headers['transfer-encoding'] = undefined;
🧰 Tools
🪛 Biome (1.9.4)

[error] 133-133: Avoid the delete operator which can impact performance.

Unsafe fix: Use an undefined assignment instead.

(lint/performance/noDelete)


173-173: Use undefined assignment instead of delete operator

The delete operator can impact performance.

-          delete headers['transfer-encoding'];
+          headers['transfer-encoding'] = undefined;
🧰 Tools
🪛 Biome (1.9.4)

[error] 173-173: Avoid the delete operator which can impact performance.

Unsafe fix: Use an undefined assignment instead.

(lint/performance/noDelete)


191-197: Clean up TypeScript ignores with proper types

Multiple @ts-ignore comments indicate TypeScript compatibility issues that should be properly addressed.

Consider creating proper type definitions for these interfaces or using the correct types from the library:

-        const reader = stream
-          // eslint-disable-next-line @typescript-eslint/ban-ts-comment
-          // @ts-ignore
-          .pipeThrough(new TextDecoderStream())
-          // eslint-disable-next-line @typescript-eslint/ban-ts-comment
-          // @ts-ignore
-          .pipeThrough(new EventSourceParserStream())
-          .getReader();
+        // Cast to proper web streams API types
+        const reader = (stream as ReadableStream<Uint8Array>)
+          .pipeThrough(new TextDecoderStream())
+          .pipeThrough(new EventSourceParserStream())
+          .getReader();

218-218: Correct error message typo

The error message contains a typo.

-        ctx.res.write(`see stream error ${error}`);
+        ctx.res.write(`SSE stream error: ${error}`);

82-84: Add error handling to getClient method

The method calls this._client.getClient(sessionId) which could be undefined. Add proper error handling.

  async getClient(sessionId: string): Promise<ClientDetail | undefined> {
-    return this._client.getClient(sessionId);
+    try {
+      return this._client.getClient(sessionId);
+    } catch (error) {
+      this.logger.error(`Failed to get client for session ${sessionId}:`, error);
+      return undefined;
+    }
  }

182-182: Uncomment or replace error response code

There's a commented-out error response that should be uncommented or replaced with an appropriate error handling.

-      // ctx.res.writeHead(400).end(`Invalid message: ${body}`);
+      ctx.res.writeHead(500).end(`Proxy error: ${error.message}`);
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 773cca3 and 7f0cf9c.

📒 Files selected for processing (7)
  • plugin/controller/lib/impl/mcp/MCPControllerRegister.ts (1 hunks)
  • plugin/controller/test/mcp/mcp.test.ts (1 hunks)
  • plugin/mcp-proxy/app/extend/agent.ts (1 hunks)
  • plugin/mcp-proxy/app/extend/application.ts (1 hunks)
  • plugin/mcp-proxy/index.ts (1 hunks)
  • plugin/mcp-proxy/test/fixtures/apps/mcp-proxy/app/controller/app.ts (1 hunks)
  • plugin/mcp-proxy/test/proxy.test.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • plugin/mcp-proxy/app/extend/agent.ts
  • plugin/mcp-proxy/app/extend/application.ts
🧰 Additional context used
🪛 Biome (1.9.4)
plugin/controller/lib/impl/mcp/MCPControllerRegister.ts

[error] 73-73: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 208-208: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 285-285: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 340-340: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 375-375: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 398-398: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)

plugin/controller/test/mcp/mcp.test.ts

[error] 91-93: Disallow duplicate setup and teardown hooks.

Disallow after duplicacy inside the describe function.

(lint/suspicious/noDuplicateTestHooks)

plugin/mcp-proxy/index.ts

[error] 133-133: Avoid the delete operator which can impact performance.

Unsafe fix: Use an undefined assignment instead.

(lint/performance/noDelete)


[error] 173-173: Avoid the delete operator which can impact performance.

Unsafe fix: Use an undefined assignment instead.

(lint/performance/noDelete)

plugin/mcp-proxy/test/fixtures/apps/mcp-proxy/app/controller/app.ts

[error] 164-164: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 250-250: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)

plugin/mcp-proxy/test/proxy.test.ts

[error] 91-93: Disallow duplicate setup and teardown hooks.

Disallow after duplicacy inside the describe function.

(lint/suspicious/noDuplicateTestHooks)

🪛 GitHub Check: CodeQL
plugin/mcp-proxy/index.ts

[failure] 53-53: Unvalidated dynamic method call
Invocation of method with user-controlled name may dispatch to unexpected target and cause an exception.

⏰ Context from checks skipped due to timeout of 90000ms (7)
  • GitHub Check: Runner-ubuntu (22)
  • GitHub Check: Runner-macos (18)
  • GitHub Check: Runner-ubuntu (16)
  • GitHub Check: Runner-ubuntu (20)
  • GitHub Check: Runner-macos (16)
  • GitHub Check: Runner-macos (20)
  • GitHub Check: Runner-ubuntu (18)

@fengmk2
Copy link
Member

fengmk2 commented Apr 26, 2025

README 先写一下用户视角的 usage?

@fengmk2
Copy link
Member

fengmk2 commented Apr 26, 2025

@coderabbitai 提一个 issue 跟进此 pr 合并后会被 pick 到 next 分支

@fengmk2 fengmk2 added the enhancement New feature or request label Apr 26, 2025
@coderabbitai
Copy link

coderabbitai bot commented Apr 26, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

@fengmk2 I've created an issue to track picking PR #307 to the next branch after it's merged. The issue has been assigned to you and includes the necessary context for follow-up action.


🏁 Script executed:

#!/bin/bash
# Create a GitHub issue to track picking PR #307 to the next branch

gh issue create \
  --title "Pick PR #307 (feat: add mcp) to next branch after merging" \
  --body "## Follow-up Task

This issue is to track that PR #307 (feat: add mcp) needs to be picked to the \`next\` branch after merging.

## Context

This was requested by @fengmk2 in PR #307: https://github.com/eggjs/tegg/pull/307#issuecomment-1971851033

## Action Items

- [ ] After PR #307 is merged to master, cherry-pick the changes to the \`next\` branch
- [ ] Close this issue once completed

/cc @fengmk2" \
  --assignee "fengmk2"

Length of output: 555


@fengmk2 Great! The issue has been successfully created at #308. The follow-up task to pick PR #307 to the next branch after merging is now tracked and assigned to you.


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

@fengmk2 fengmk2 requested a review from Copilot April 26, 2025 01:12
Copy link

Copilot AI left a comment

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 introduces comprehensive MCP controller support by adding new decorators, metadata builders, utility functions, and tests to enable MCP operations with Egg.js. Key changes include:

  • New MCP decorators (MCPController, MCPPrompt, MCPTool, MCPResource) to mark controller methods.
  • Utility and meta builder updates for aggregating MCP metadata.
  • Extensive tests that assert proper MCP metadata generation and integration.

Reviewed Changes

Copilot reviewed 54 out of 56 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
core/types/controller-decorator/MCPController.ts New type definitions for MCP controllers and hooks.
core/controller-decorator/test/fixtures/MCPController.ts Example MCP controller with prompt, tool, and resource implementations.
core/controller-decorator/test/MCPMeta.test.ts Test suite ensuring that MCP metadata is correctly generated.
core/controller-decorator/src/util/MCPInfoUtil.ts Utility methods for setting/getting MCP metadata.
core/controller-decorator/src/model/* New MCP metadata models for controllers, prompts, tools, and resources.
core/controller-decorator/src/impl/mcp/* Meta builder implementations for assembling MCP metadata.
core/controller-decorator/src/decorator/mcp/* New MCP decorators for controllers and related methods.
core/controller-decorator/index.ts Updated exports to include new MCP components.
Files not reviewed (2)
  • .eslintrc: Language not supported
  • core/controller-decorator/package.json: Language not supported

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
core/controller-decorator/src/util/MCPInfoUtil.ts (1)

86-94: ⚠️ Potential issue

Prompt params are still stored under the resource key – metadata collision

Previous reviews already flagged this:
setMCPPromptParams / getMCPPromptParams persist prompt metadata in CONTROLLER_MCP_RESOURCE_PARAMS_MAP, causing prompts & resources to overwrite each other.

Please switch to a dedicated metadata key (e.g. CONTROLLER_MCP_PROMPT_PARAMS_MAP) just like tools have their own.

-import { ..., CONTROLLER_MCP_RESOURCE_PARAMS_MAP, ... }
+import { ..., CONTROLLER_MCP_PROMPT_PARAMS_MAP, CONTROLLER_MCP_RESOURCE_PARAMS_MAP, ... }
@@
-    const methodMap: MCPPromptMap = MetadataUtil.initOwnMapMetaData(CONTROLLER_MCP_RESOURCE_PARAMS_MAP, clazz, new Map());
+    const methodMap: MCPPromptMap = MetadataUtil.initOwnMapMetaData(CONTROLLER_MCP_PROMPT_PARAMS_MAP, clazz, new Map());
@@
-    const methodMap: MCPPromptMap | undefined = MetadataUtil.getMetaData(CONTROLLER_MCP_RESOURCE_PARAMS_MAP, clazz);
+    const methodMap: MCPPromptMap | undefined = MetadataUtil.getMetaData(CONTROLLER_MCP_PROMPT_PARAMS_MAP, clazz);
🧹 Nitpick comments (4)
core/controller-decorator/src/decorator/mcp/MCPController.ts (2)

18-23: Remove stray stack-trace comments

Lines 18-23 contain what look like pasted stack-trace snippets.
They add no value, clutter the source, and will confuse future readers.

-    // './tegg/core/common-util/src/StackUtil.ts',
-    // './tegg/core/core-decorator/src/decorator/Prototype.ts',
-    // './tegg/core/controller-decorator/src/decorator/tr/TRController.ts',
-    // './tegg/core/core-decorator/node_modules/[email protected]@reflect-metadata/Reflect.js',
-    // './tegg/core/core-decorator/node_modules/[email protected]@reflect-metadata/Reflect.js',
-    // './tegg/core/controller-decorator/test/fixtures/TRFooController.ts',

24-24: Magic number for stack depth obscures intent

StackUtil.getCalleeFromStack(false, 5) hard-codes the depth.
Consider extracting 5 into a named constant (e.g. CALLER_STACK_DEPTH) or adding a short explanatory comment so future refactors don’t break file-path detection.

core/types/controller-decorator/MCPController.ts (1)

17-24: Hook naming is inconsistent and may confuse users

preHandleInitHandle (line 19) is the only hook whose name ends with “InitHandle”.
All others follow a simpler preSomething / onSomething pattern.
Consider renaming to either preHandleInit or clarifying its purpose in JSDoc so implementers don’t wonder which phase it runs in.

core/controller-decorator/src/util/MCPInfoUtil.ts (1)

9-10: Static-only utility class – consider converting to a module of free functions

MCPInfoUtil only hosts static methods, triggering the noStaticOnlyClass lint rule.
While not critical, replacing it with plain named exports can reduce TS output size and avoid accidental instantiation.

No change required if you prefer the current style; just be aware of the rule.

🧰 Tools
🪛 Biome (1.9.4)

[error] 9-96: Avoid classes that contain only static members.

Prefer using simple functions instead of classes with only static members.

(lint/complexity/noStaticOnlyClass)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7f0cf9c and 2bbb93e.

📒 Files selected for processing (13)
  • core/controller-decorator/index.ts (2 hunks)
  • core/controller-decorator/src/decorator/mcp/MCPController.ts (1 hunks)
  • core/controller-decorator/src/decorator/mcp/MCPPrompt.ts (1 hunks)
  • core/controller-decorator/src/decorator/mcp/MCPResource.ts (1 hunks)
  • core/controller-decorator/src/decorator/mcp/MCPTool.ts (1 hunks)
  • core/controller-decorator/src/impl/mcp/MCPControllerMetaBuilder.ts (1 hunks)
  • core/controller-decorator/src/impl/mcp/MCPControllerPromptMetaBuilder.ts (1 hunks)
  • core/controller-decorator/src/impl/mcp/MCPControllerResourceMetaBuilder.ts (1 hunks)
  • core/controller-decorator/src/impl/mcp/MCPControllerToolMetaBuilder.ts (1 hunks)
  • core/controller-decorator/src/util/MCPInfoUtil.ts (1 hunks)
  • core/controller-decorator/test/MCPMeta.test.ts (1 hunks)
  • core/types/controller-decorator/MCPController.ts (1 hunks)
  • core/types/package.json (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (10)
  • core/types/package.json
  • core/controller-decorator/src/decorator/mcp/MCPResource.ts
  • core/controller-decorator/src/impl/mcp/MCPControllerResourceMetaBuilder.ts
  • core/controller-decorator/src/decorator/mcp/MCPPrompt.ts
  • core/controller-decorator/src/impl/mcp/MCPControllerToolMetaBuilder.ts
  • core/controller-decorator/src/decorator/mcp/MCPTool.ts
  • core/controller-decorator/src/impl/mcp/MCPControllerPromptMetaBuilder.ts
  • core/controller-decorator/index.ts
  • core/controller-decorator/test/MCPMeta.test.ts
  • core/controller-decorator/src/impl/mcp/MCPControllerMetaBuilder.ts
🧰 Additional context used
🧬 Code Graph Analysis (1)
core/types/controller-decorator/MCPController.ts (4)
plugin/controller/lib/impl/mcp/MCPControllerRegister.ts (2)
  • register (409-457)
  • MCPControllerRegister (34-458)
core/controller-decorator/src/model/MCPControllerMeta.ts (1)
  • MCPControllerMeta (7-70)
core/controller-decorator/src/model/MCPPromptMeta.ts (1)
  • MCPPromptMeta (4-30)
core/controller-decorator/src/model/MCPToolMeta.ts (1)
  • MCPToolMeta (5-31)
🪛 Biome (1.9.4)
core/controller-decorator/src/decorator/mcp/MCPController.ts

[error] 8-8: Do not shadow the global "constructor" property.

Consider renaming this variable. It's easy to confuse the origin of variables when they're named after a known global.

(lint/suspicious/noShadowRestrictedNames)

core/controller-decorator/src/util/MCPInfoUtil.ts

[error] 9-96: Avoid classes that contain only static members.

Prefer using simple functions instead of classes with only static members.

(lint/complexity/noStaticOnlyClass)

⏰ Context from checks skipped due to timeout of 90000ms (10)
  • GitHub Check: Analyze (typescript)
  • GitHub Check: Analyze (javascript)
  • GitHub Check: Runner-ubuntu (18)
  • GitHub Check: Runner-ubuntu (20)
  • GitHub Check: Runner-ubuntu (22)
  • GitHub Check: Runner-ubuntu (16)
  • GitHub Check: Runner-macos (20)
  • GitHub Check: Runner-macos (18)
  • GitHub Check: Runner-macos (16)
  • GitHub Check: Socket Security: Pull Request Alerts

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (7)
plugin/mcp-proxy/test/fixtures/apps/mcp-proxy/app/controller/app.ts (3)

185-188: Add proper type checking for detail before using non-null assertion.

Using non-null assertion (!) on detail is risky if detail is null or undefined. Adding proper null checking is safer.

  const detail = await this.app.mcpProxy.getClient(this.ctx.request.query.sessionId);
  if (detail?.pid !== process.pid) {
    await this.app.mcpProxy.proxyMessage(this.ctx, {
-     detail: detail!,
+     detail,
      sessionId: this.ctx.request.query.sessionId,
      type: MCPProtocols.SSE,
    });
  } else {
    await this.ssePostHandler(this.ctx.req, this.ctx.res);
  }

260-265: Add proper null checking for sid before using non-null assertion.

Using non-null assertion (!) on sid is risky if sid is null or undefined. Add proper type checking instead.

  const sid = transport.sessionId;
  if (sid && transports[sid]) {
    delete transports[sid];
+   await this.app.mcpProxy.unregisterClient(sid);
+ } else if (sid) {
+   await this.app.mcpProxy.unregisterClient(sid);
  }
- await this.app.mcpProxy.unregisterClient(sid!);

283-288: 🛠️ Refactor suggestion

Add proper type checking for detail before using non-null assertion.

Using non-null assertion (!) on detail in line 285 is risky if detail is null or undefined. Adding proper null checking is safer.

  const detail = await this.app.mcpProxy.getClient(sessionId);
  if (detail?.pid !== process.pid) {
    await this.app.mcpProxy.proxyMessage(this.ctx, {
-      detail: detail!,
+      detail,
      sessionId,
      type: MCPProtocols.STREAM,
    });
  } else {
    this.ctx.respond = false;
plugin/controller/lib/impl/mcp/MCPControllerRegister.ts (2)

349-357: ⚠️ Potential issue

Initialisation order – router hooks may run before mcpServer ready.

mcpStreamServerInit, mcpServerInit, and mcpServerRegister rely on this.mcpServer being non-null, yet they are called immediately after creation and before awaiting any async work. If later code calls register() concurrently, there's a race.

  if (!this.mcpServer) {
    this.mcpServer = new McpServer({
      name: this.controllerMeta.name ?? `chair-mcp-${this.app.name}-server`,
      version: this.controllerMeta.version ?? '1.0.0',
    }, { capabilities: { logging: {} } });
-   this.mcpStreamServerInit();
-   this.mcpServerInit();
-   this.mcpServerRegister();
+   // Initialize mcpServer fully before registering routes
+   await this.mcpStreamServerInit();
+   this.mcpServerInit();
+   this.mcpServerRegister();
  }

222-239: ⚠️ Potential issue

Validate sessionId to prevent crashes when accessing undefined.

sessionId is used directly from ctx.query without validation. This can lead to errors when trying to access a transport with an undefined key or when passing undefined to getClient().

- const sessionId = ctx.query.sessionId;
+ const sessionId = ctx.query.sessionId as string;
+ if (!sessionId) {
+   ctx.status = 400;
+   ctx.body = {
+     jsonrpc: '2.0',
+     error: {
+       code: -32000,
+       message: 'Bad Request: Missing sessionId parameter',
+     },
+     id: null,
+   };
+   return;
+ }

  if (MCPControllerRegister.hooks.length > 0) {
plugin/mcp-proxy/index.ts (2)

189-192: ⚠️ Potential issue

Enforce validation of mcp-proxy-type before dispatching proxy handlers.

The current code casts the untrusted header directly to ProxyAction and invokes the handler without checking whether it's one of the expected values. This can lead to silent failures or runtime errors.

  const server = http.createServer(async (req, res) => {
-   const type = req.headers['mcp-proxy-type'] as ProxyAction;
-   await this.proxyHandlerMap[type]?.(req, res);
+   const typeHeader = req.headers['mcp-proxy-type'] as string;
+   // Validate that the header matches one of the allowed ProxyAction keys
+   if (!typeHeader || !this.proxyHandlerMap[typeHeader as ProxyAction]) {
+     res.writeHead(400).end('Invalid or unsupported MCP proxy type');
+     return;
+   }
+   const action = typeHeader as ProxyAction;
+   await this.proxyHandlerMap[action]!(req, res);
  });

194-199: 🛠️ Refactor suggestion

Add proper error handling for server.listen.

The current implementation doesn't handle errors when starting the HTTP server. If the port is already in use, this will silently fail.

  this.port = (this.app.config.mcp?.proxyPort ?? 17031) + (cluster.worker?.id ?? 0);
  this.addProxyHook();
- await new Promise(resolve => server.listen(this.port, () => {
+ await new Promise((resolve, reject) => {
+   server.once('error', err => {
+     this.logger.error('[MCPProxyApiClient] Failed to start HTTP server:', err);
+     reject(err);
+   });
+   server.listen(this.port, () => {
    // const address = server.address()! as AddressInfo;
    // this.port = address.port;
    resolve(null);
-  }));
+  });
+ });
🧹 Nitpick comments (5)
plugin/mcp-proxy/app.ts (1)

4-8: Improve type safety in Application reference.

The current implementation uses Application from egg but refers to it as agent in the property and constructor. This creates confusion as agent and application are distinct concepts in Egg.js.

- private readonly agent: Application;
+ private readonly agent: Application;

  constructor(agent: Application) {
    this.agent = agent;
  }
plugin/mcp-proxy/test/fixtures/apps/mcp-proxy/app/controller/app.ts (2)

164-165: Unnecessary this aliasing.

Arrow functions inherit this from their enclosing scope, making the self variable unnecessary.

-    const self = this;
     const transport = new SSEServerTransport('/message', this.ctx.res);
🧰 Tools
🪛 Biome (1.9.4)

[error] 164-164: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


247-248: Unnecessary this aliasing.

Arrow functions inherit this from their enclosing scope, making the self variable unnecessary.

-        const self = this;
         const transport = new StreamableHTTPServerTransport({
🧰 Tools
🪛 Biome (1.9.4)

[error] 247-247: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)

plugin/controller/lib/impl/mcp/MCPControllerRegister.ts (2)

69-70: Remove unnecessary this aliasing in arrow functions.

Arrow functions inherit this from their enclosing scope, making the self variable unnecessary.

- const self = this;

Apply this change to all similar instances throughout the file (lines 69, 114, 180, 217, etc.)

Also applies to: 114-115, 180-181, 217-218

🧰 Tools
🪛 Biome (1.9.4)

[error] 69-69: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


101-105: Fix grammar in error message.

The error message "body should is json" has incorrect grammar.

  ctx.body = {
    jsonrpc: '2.0',
    error: {
      code: -32000,
-     message: 'Bad Request: body should is json',
+     message: 'Bad Request: body should be JSON',
    },
    id: null,
  };
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2bbb93e and e50199c.

📒 Files selected for processing (12)
  • core/types/controller-decorator/MCPController.ts (1 hunks)
  • core/types/package.json (1 hunks)
  • plugin/controller/app.ts (2 hunks)
  • plugin/controller/lib/impl/mcp/MCPControllerRegister.ts (1 hunks)
  • plugin/mcp-proxy/agent.ts (1 hunks)
  • plugin/mcp-proxy/app.ts (1 hunks)
  • plugin/mcp-proxy/app/extend/agent.ts (1 hunks)
  • plugin/mcp-proxy/app/extend/application.ts (1 hunks)
  • plugin/mcp-proxy/index.ts (1 hunks)
  • plugin/mcp-proxy/package.json (1 hunks)
  • plugin/mcp-proxy/test/fixtures/apps/mcp-proxy/app/controller/app.ts (1 hunks)
  • plugin/mcp-proxy/typings/index.d.ts (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • plugin/mcp-proxy/agent.ts
🚧 Files skipped from review as they are similar to previous changes (7)
  • core/types/package.json
  • plugin/mcp-proxy/typings/index.d.ts
  • plugin/mcp-proxy/app/extend/application.ts
  • plugin/controller/app.ts
  • plugin/mcp-proxy/app/extend/agent.ts
  • plugin/mcp-proxy/package.json
  • core/types/controller-decorator/MCPController.ts
🧰 Additional context used
🧬 Code Graph Analysis (1)
plugin/mcp-proxy/index.ts (3)
core/types/controller-decorator/MCPController.ts (1)
  • MCPControllerHook (17-30)
plugin/controller/lib/impl/mcp/MCPControllerRegister.ts (1)
  • MCPControllerRegister (32-368)
plugin/mcp-proxy/lib/MCPProxyDataClient.ts (1)
  • MCPProxyDataClient (4-39)
🪛 Biome (1.9.4)
plugin/controller/lib/impl/mcp/MCPControllerRegister.ts

[error] 69-69: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 180-180: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 217-217: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 267-267: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 295-295: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 318-318: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)

plugin/mcp-proxy/test/fixtures/apps/mcp-proxy/app/controller/app.ts

[error] 164-164: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 247-247: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)

⏰ Context from checks skipped due to timeout of 90000ms (9)
  • GitHub Check: Runner-macos (18)
  • GitHub Check: Analyze (typescript)
  • GitHub Check: Analyze (javascript)
  • GitHub Check: Runner-ubuntu (20)
  • GitHub Check: Runner-ubuntu (18)
  • GitHub Check: Runner-macos (16)
  • GitHub Check: Runner-ubuntu (16)
  • GitHub Check: Runner-macos (20)
  • GitHub Check: Runner-ubuntu (22)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (3)
plugin/controller/lib/impl/mcp/MCPControllerRegister.ts (3)

87-87: Consider if cache-control is necessary here

The cache-control header might not be relevant for MCP connections. Verify if it's necessary or if it can be removed to avoid confusion.


236-253: ⚠️ Potential issue

sessionId is not validated – may crash on undefined

No validation exists for sessionId which can lead to runtime errors if undefined or invalid.

-      const sessionId = ctx.query.sessionId;
+      const sessionId = String(ctx.query.sessionId ?? '');
+      if (!sessionId) {
+        ctx.status = 400;
+        ctx.body = {
+          jsonrpc: '2.0',
+          error: {
+            code: -32000,
+            message: 'Bad Request: sessionId required',
+          },
+          id: null,
+        };
+        return;
+      }
+      
+      if (!self.transports[sessionId]) {
+        ctx.status = 404;
+        ctx.body = {
+          jsonrpc: '2.0',
+          error: {
+            code: -32001,
+            message: 'Session not found',
+          },
+          id: null,
+        };
+        return;
+      }

363-370: ⚠️ Potential issue

Initialisation order – router hooks may run before mcpServer ready

The initialization order can create race conditions as the router hooks may execute before mcpServer is ready.

Consider sequentially awaiting the initialization methods:

if (!this.mcpServer) {
  this.mcpServer = new McpServer({
    name: this.controllerMeta.name ?? `chair-mcp-${this.app.name}-server`,
    version: this.controllerMeta.version ?? '1.0.0',
  }, { capabilities: { logging: {} } });
-  this.mcpStreamServerInit();
-  this.mcpServerInit();
-  this.mcpServerRegister();
+  await this.mcpStreamServerInit();
+  this.mcpServerInit();
+  this.mcpServerRegister();
}
🧹 Nitpick comments (5)
plugin/controller/lib/impl/mcp/MCPControllerRegister.ts (5)

114-114: Grammar error in error message

The error message contains a grammatical error.

-              message: 'Bad Request: body should is json',
+              message: 'Bad Request: body should be JSON',

126-130: Allow custom eventStore and sessionIdGenerator implementations

The code could be simplified while still allowing for custom implementations.

-          const eventStore = this.app.config.mcp?.eventStore ?? new InMemoryEventStore();
-          const self = this;
-          const transport = new StreamableHTTPServerTransport({
-            sessionIdGenerator: () => this.app.config.mcp?.sessionIdGenerator ?? randomUUID(),
-            eventStore,
+          const transport = new StreamableHTTPServerTransport({
+            sessionIdGenerator: this.app.config.mcp?.sessionIdGenerator || (() => randomUUID()),
+            eventStore: this.app.config.mcp?.eventStore || new InMemoryEventStore(),

82-82: Remove unnecessary self variables

Arrow functions inherit this from their enclosing scope, making these aliases unnecessary.

- const self = this;
  const handler = async (...args) => {
-   const eggObj = await self.eggContainerFactory.getOrCreateEggObject(...);
+   const eggObj = await this.eggContainerFactory.getOrCreateEggObject(...);

Also applies to: 127-127, 193-193, 230-230, 280-280, 308-308, 331-331

🧰 Tools
🪛 Biome (1.9.4)

[error] 82-82: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


215-215: Make heartbeat interval configurable

The default heartbeat interval is hardcoded to 25000ms. Consider extracting this to a constant or default config value for better maintainability.

-      }, self.app.config.mcp?.sseHeartTime ?? 25000);
+      }, self.app.config.mcp?.sseHeartTime ?? self.app.config.mcp?.defaultSseHeartbeatTime ?? 25000);

371-379: Use object destructuring when iterating through Map entries

The code can be simplified by using destructuring in the map iteration.

-    for (const [ prompt, controllerProto ] of promptMap.entries()) {
-      await this.mcpPromptRegister(controllerProto, prompt);
+    for (const [prompt, controllerProto] of promptMap) {
+      await this.mcpPromptRegister(controllerProto, prompt);
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e50199c and 80d29e2.

📒 Files selected for processing (3)
  • core/types/controller-decorator/MCPController.ts (1 hunks)
  • plugin/controller/lib/impl/mcp/MCPControllerRegister.ts (1 hunks)
  • plugin/mcp-proxy/index.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • core/types/controller-decorator/MCPController.ts
  • plugin/mcp-proxy/index.ts
🧰 Additional context used
🪛 Biome (1.9.4)
plugin/controller/lib/impl/mcp/MCPControllerRegister.ts

[error] 82-82: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 193-193: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 230-230: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 280-280: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 308-308: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 331-331: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)

⏰ Context from checks skipped due to timeout of 90000ms (9)
  • GitHub Check: Analyze (javascript)
  • GitHub Check: Analyze (typescript)
  • GitHub Check: Runner-ubuntu (22)
  • GitHub Check: Runner-ubuntu (20)
  • GitHub Check: Runner-ubuntu (18)
  • GitHub Check: Runner-ubuntu (16)
  • GitHub Check: Runner-macos (20)
  • GitHub Check: Runner-macos (18)
  • GitHub Check: Runner-macos (16)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (4)
plugin/controller/lib/impl/mcp/MCPControllerRegister.ts (4)

95-154: ⚠️ Potential issue

Fix missing return after JSON parse error.

The code continues execution after catching a JSON parse error, leading to potential undefined behavior with uninitialized body variable. This issue was previously identified in a review but hasn't been addressed.

try {
  body = JSON.parse(rawBody);
} catch (e) {
  ctx.status = 400;
  ctx.body = {
    jsonrpc: '2.0',
    error: {
      code: -32000,
      message: 'Bad Request: body should is json',
    },
    id: null,
  };
+  return;
}

235-237: ⚠️ Potential issue

Missing sessionId validation could cause application crash.

The sessionId from query parameters is used without validation, which could lead to undefined behavior or crashes when accessing self.transports[sessionId] if sessionId is undefined. This was previously flagged in a review but hasn't been addressed.

-      const sessionId = ctx.query.sessionId;
+      const sessionId = String(ctx.query.sessionId ?? '');
+      if (!sessionId) {
+        ctx.status = 400;
+        ctx.body = {
+          jsonrpc: '2.0',
+          error: {
+            code: -32000,
+            message: 'Bad Request: sessionId required',
+          },
+          id: null,
+        };
+        return;
+      }

281-288: 🛠️ Refactor suggestion

Use type-safe approach for registering handlers.

The code uses unsafe type casting with as unknown as [any, any, any, any] when calling MCP server methods, which bypasses type checking. This was previously identified in a review.

Consider a more type-safe approach:

// Define typed wrapper for prompt registration
function registerPrompt(
  name: string, 
  description: string | undefined, 
  schema: any | undefined,
  handler: (...args: any[]) => Promise<any>
) {
  if (description && schema) {
    this.mcpServer.prompt(name, description, schema, handler);
  } else if (description) {
    this.mcpServer.prompt(name, description, handler);
  } else {
    this.mcpServer.prompt(name, handler);
  }
}

362-370: ⚠️ Potential issue

Potential race condition in server initialization.

mcpStreamServerInit, mcpServerInit, and mcpServerRegister rely on this.mcpServer being available, yet they are called immediately after creation and before awaiting any async work. If later code calls register() concurrently, there's a potential race condition.

Consider awaiting the initialization sequence:

-      this.mcpStreamServerInit();
-      this.mcpServerInit();
-      this.mcpServerRegister();
+      await this.mcpStreamServerInit();
+      await this.mcpServerInit();
+      await this.mcpServerRegister();
🧹 Nitpick comments (4)
plugin/controller/lib/impl/mcp/MCPControllerRegister.ts (4)

87-87: Remove unrelated 'cache-control' header.

The cache-control: no-cache header is not directly related to MCP and its SSE implementation. This header seems to be included from typical SSE implementations but not required for MCP functionality.

- 'cache-control': 'no-cache',

193-193: Unnecessary 'this' aliasing.

Using const self = this is unnecessary in arrow functions as they inherit the this context from their enclosing scope.

- const self = this;

Replace all instances of self with this in the handler functions that follow.

Also applies to: 230-230, 280-280, 308-308, 331-331

🧰 Tools
🪛 Biome (1.9.4)

[error] 193-193: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


208-215: Configurable heartbeat interval not documented.

The SSE heartbeat interval is configurable via app.config.mcp?.sseHeartTime but there's no documentation about this configuration or its default value (25 seconds).

-      }, self.app.config.mcp?.sseHeartTime ?? 25000);
+      // Send SSE keep-alive comments to prevent connection timeout
+      // Default: 25 seconds; configurable via app.config.mcp.sseHeartTime
+      }, self.app.config.mcp?.sseHeartTime ?? 25000);

371-379: Improve entry iteration for Maps.

The code retrieves controller prototype from the map twice - once during iteration and again when calling the register method.

- for (const [ prompt, controllerProto ] of promptMap.entries()) {
-   await this.mcpPromptRegister(controllerProto, prompt);
- }
+ for (const [ prompt, controllerProto ] of promptMap) {
+   await this.mcpPromptRegister(controllerProto, prompt);
+ }

Apply similar changes to the other Map iterations.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 80d29e2 and 681273b.

📒 Files selected for processing (2)
  • plugin/controller/app/middleware/mcp_body_middleware.ts (1 hunks)
  • plugin/controller/lib/impl/mcp/MCPControllerRegister.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • plugin/controller/app/middleware/mcp_body_middleware.ts
🧰 Additional context used
🧬 Code Graph Analysis (1)
plugin/controller/lib/impl/mcp/MCPControllerRegister.ts (11)
core/controller-decorator/src/model/MCPControllerMeta.ts (2)
  • MCPControllerMeta (7-70)
  • id (22-24)
core/controller-decorator/src/model/MCPPromptMeta.ts (1)
  • MCPPromptMeta (4-30)
core/controller-decorator/src/model/MCPToolMeta.ts (1)
  • MCPToolMeta (5-31)
plugin/mcp-proxy/typings/index.d.ts (2)
  • MCPProtocols (5-5)
  • Application (12-13)
plugin/controller/lib/ControllerRegister.ts (1)
  • ControllerRegister (4-6)
core/runtime/src/factory/EggContainerFactory.ts (1)
  • EggContainerFactory (18-88)
core/types/metadata/model/EggPrototype.ts (1)
  • EggPrototype (117-154)
benchmark/http/app/router.js (1)
  • app (4-4)
plugin/mcp-proxy/test/fixtures/apps/mcp-proxy/app/controller/app.ts (1)
  • InMemoryEventStore (59-127)
core/types/controller-decorator/MetadataKey.ts (1)
  • CONTROLLER_META_DATA (9-9)
core/controller-decorator/src/model/MCPResourceMeta.ts (1)
  • MCPResourceMeta (5-36)
🪛 Biome (1.9.4)
plugin/controller/lib/impl/mcp/MCPControllerRegister.ts

[error] 82-82: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 193-193: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 230-230: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 280-280: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 308-308: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 331-331: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)

⏰ Context from checks skipped due to timeout of 90000ms (9)
  • GitHub Check: Runner-ubuntu (22)
  • GitHub Check: Analyze (typescript)
  • GitHub Check: Runner-ubuntu (20)
  • GitHub Check: Runner-ubuntu (16)
  • GitHub Check: Runner-ubuntu (18)
  • GitHub Check: Analyze (javascript)
  • GitHub Check: Runner-macos (18)
  • GitHub Check: Runner-macos (20)
  • GitHub Check: Runner-macos (16)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (5)
plugin/controller/lib/impl/mcp/MCPControllerRegister.ts (5)

250-250: ⚠️ Potential issue

Missing error handling for transport.handlePostMessage.

There's no try/catch block around the transport method call, which could lead to unhandled exceptions if the transport fails.

-      await self.transports[sessionId].handlePostMessage(ctx.req, ctx.res);
+      try {
+        await self.transports[sessionId].handlePostMessage(ctx.req, ctx.res);
+      } catch (error) {
+        self.app.logger.error('Error handling MCP message', error);
+        if (!ctx.res.headersSent) {
+          ctx.status = 500;
+          ctx.body = {
+            jsonrpc: '2.0',
+            error: {
+              code: -32603,
+              message: `Internal error: ${error.message}`,
+            },
+            id: null,
+          };
+        }
+      }

279-286: 🛠️ Refactor suggestion

Use type-safe approach for registering handlers.

The code uses unsafe type casting with as unknown as [any, any, any, any] when calling MCP server methods, which bypasses type checking.

Consider creating properly typed wrappers for the MCP server methods or defining proper interfaces for the arguments to maintain type safety:

- this.mcpServer.prompt(...(args as unknown as [any, any, any, any]));
+ // Type-safe wrapper for mcpServer.prompt
+ if (args.length === 4) {
+   this.mcpServer.prompt(args[0], args[1], args[2], args[3]);
+ } else if (args.length === 3) {
+   this.mcpServer.prompt(args[0], args[1], args[2]);
+ } else if (args.length === 2) {
+   this.mcpServer.prompt(args[0], args[1]);
+ } else {
+   this.mcpServer.prompt(args[0]);
+ }

Same applies to the calls in lines 314 and 337.


105-117: ⚠️ Potential issue

Missing return after JSON parse error.

The code continues execution after catching a JSON parse error which could lead to undefined behavior. Add a return statement to exit the handler after responding with an error.

try {
  body = JSON.parse(rawBody);
} catch (e) {
  ctx.status = 400;
  ctx.body = {
    jsonrpc: '2.0',
    error: {
      code: -32000,
      message: 'Bad Request: body should is json',
    },
    id: null,
  };
+  return;
}

234-234: ⚠️ Potential issue

sessionId is not validated – may crash on undefined.

ctx.query.sessionId can be undefined, leading to:

  • Using sessionId as an object key in self.transports[sessionId]
  • Could throw "can't convert undefined to string" or access invalid properties
-      const sessionId = ctx.query.sessionId;
+      const sessionId = String(ctx.query.sessionId ?? '');
+      if (!sessionId) {
+        ctx.status = 400;
+        ctx.body = {
+          jsonrpc: '2.0',
+          error: {
+            code: -32000,
+            message: 'Bad Request: sessionId is required',
+          },
+          id: null,
+        };
+        return;
+      }

361-368: ⚠️ Potential issue

Initialisation order – router hooks may run before mcpServer ready.

mcpStreamServerInit, mcpServerInit, and mcpServerRegister rely on this.mcpServer being non-null, yet they are called immediately after creation and before awaiting any async work. If later code calls register() concurrently, there's a race.

Wrap the first-time initialisation in a mutex or await the three server-init methods sequentially inside the constructor:

    if (!this.mcpServer) {
      this.mcpServer = new McpServer({
        name: this.controllerMeta.name ?? `chair-mcp-${this.app.name}-server`,
        version: this.controllerMeta.version ?? '1.0.0',
      }, { capabilities: { logging: {} } });
-      this.mcpStreamServerInit();
-      this.mcpServerInit();
-      this.mcpServerRegister();
+      // Initialize in sequence to avoid race conditions
+      await this.mcpStreamServerInit();
+      await this.mcpServerInit();
+      await this.mcpServerRegister();
    }
🧹 Nitpick comments (8)
plugin/controller/lib/impl/mcp/MCPControllerRegister.ts (8)

29-42: Improve documentation for hook interface methods.

The MCPControllerHook interface defines several methods but lacks documentation explaining when each method is called and what it should do.

Add JSDoc comments to document each hook method:

export interface MCPControllerHook {
+  /**
+   * Called before initializing an SSE transport.
+   * Can be used to customize the transport or perform authentication.
+   */
  // SSE
  preSSEInitHandle?: (ctx: Context, transport: SSEServerTransport, register: MCPControllerRegister) => Promise<void>
+  /**
+   * Called before handling an SSE message request.
+   * Can be used for authentication or request validation.
+   */
  preHandleInitHandle?: (ctx: Context) => Promise<void>

  // STREAM
+  /**
+   * Called before handling a stream request.
+   * Can be used for request validation or preprocessing.
+   */
  preHandle?: (ctx: Context) => Promise<void>
+  /**
+   * Called when a stream session is initialized.
+   * Can be used to configure the transport or register session data.
+   */
  onStreamSessionInitialized?: (ctx: Context, transport: StreamableHTTPServerTransport, register: MCPControllerRegister) => Promise<void>

  // COMMON
+  /**
+   * Called before proxying a request to the MCP server.
+   * Can be used to modify request headers or perform authentication.
+   */
  preProxy?: (ctx: Context, proxyReq: Parameters<typeof http.createServer>['0'], proxyResp: Parameters<typeof http.createServer>['1']) => Promise<void>
+  /**
+   * Called to load schemas for tools and prompts.
+   * Can be used to load schemas from external sources.
+   */
  schemaLoader?: (controllerMeta: MCPControllerMeta, meta: MCPPromptMeta | MCPToolMeta) => Promise<Parameters<McpServer['tool']>['2'] | Parameters<McpServer['prompt']>['2']>
+  /**
+   * Called to check if a request should be proxied.
+   * Return true to indicate the proxy has handled the request.
+   */
  checkAndRunProxy?: (ctx: Context, type: MCPProtocols, sessionId: string) => Promise<boolean>;
}

75-78: Document intended usage of hooks API.

The static addHook method allows adding hooks to customize the MCP controller behavior, but there's no documentation on when and how to use it.

Add documentation comments to explain the intended usage:

+  /**
+   * Add a hook to customize the behavior of MCP controllers.
+   * Hooks can modify request/response handling, load schemas, or intercept proxy requests.
+   * 
+   * @param hook MCPControllerHook implementation with optional lifecycle methods
+   * @example
+   * // Register a hook that loads schemas from a JSON file
+   * MCPControllerRegister.addHook({
+   *   schemaLoader: async (controllerMeta, meta) => {
+   *     const schema = await loadSchema(`${meta.name}.json`);
+   *     return schema;
+   *   }
+   * });
+   */
  static addHook(hook: MCPControllerHook) {
    MCPControllerRegister.hooks.push(hook);
  }

82-82: Remove unnecessary self variable.

The self variable is used to reference this inside arrow functions, but arrow functions already inherit this from their enclosing scope.

- const self = this;

The same issue exists in lines 191, 228, 278, 306, 329.

🧰 Tools
🪛 Biome (1.9.4)

[error] 82-82: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


125-129: Allow configurable event store implementation.

The code provides fallback to InMemoryEventStore but does not document the configuration option. The event store might need to be persistent in production environments.

- const eventStore = this.app.config.mcp?.eventStore ?? new InMemoryEventStore();
+ // Use configured event store or fall back to in-memory implementation
+ // For production, consider configuring a persistent event store implementation
+ const eventStore = this.app.config.mcp?.eventStore ?? new InMemoryEventStore();

128-128: Make session ID generator configurable.

The session ID generator is hardcoded to use either a provided function or randomUUID(), but there's no documentation about this configuration option.

- sessionIdGenerator: () => this.app.config.mcp?.sessionIdGenerator ?? randomUUID(),
+ // Allow custom session ID generation strategy
+ sessionIdGenerator: () => {
+   const generator = this.app.config.mcp?.sessionIdGenerator;
+   return generator ? generator() : randomUUID();
+ },

213-213: Make heartbeat interval configurable.

The heartbeat interval is hardcoded to 25 seconds if not configured. This should be documented and possibly have a more descriptive property name.

-      }, self.app.config.mcp?.sseHeartTime ?? 25000);
+      // Send keepalive messages to prevent connection timeout
+      // Default: 25 seconds (most proxies timeout after 30 seconds of inactivity)
+      }, self.app.config.mcp?.sseHeartbeatInterval ?? 25000);

349-357: Use Map entries to avoid multiple lookups.

The code first puts items in maps and then immediately iterates to call registration methods. This could be more efficient.

-    for (const prompt of metadata.prompts) {
-      promptMap.set(prompt, proto);
-    }
-    for (const resource of metadata.resources) {
-      resourceMap.set(resource, proto);
-    }
-    for (const tool of metadata.tools) {
-      toolMap.set(tool, proto);
-    }

+    // Add all metadata items to their respective maps
+    metadata.prompts.forEach(prompt => promptMap.set(prompt, proto));
+    metadata.resources.forEach(resource => resourceMap.set(resource, proto));
+    metadata.tools.forEach(tool => toolMap.set(tool, proto));

152-153: Simplify conditional logic in stream handler.

The code first checks for the absence of sessionId and then has a redundant check for its presence.

-      } else if (sessionId) {
+      } else {
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 681273b and eb050f5.

📒 Files selected for processing (1)
  • plugin/controller/lib/impl/mcp/MCPControllerRegister.ts (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
plugin/controller/lib/impl/mcp/MCPControllerRegister.ts (9)
core/controller-decorator/src/model/MCPControllerMeta.ts (2)
  • MCPControllerMeta (7-70)
  • id (22-24)
core/controller-decorator/src/model/MCPPromptMeta.ts (1)
  • MCPPromptMeta (4-30)
core/controller-decorator/src/model/MCPToolMeta.ts (1)
  • MCPToolMeta (5-31)
plugin/controller/lib/ControllerRegister.ts (1)
  • ControllerRegister (4-6)
core/runtime/src/factory/EggContainerFactory.ts (1)
  • EggContainerFactory (18-88)
core/types/metadata/model/EggPrototype.ts (1)
  • EggPrototype (117-154)
plugin/mcp-proxy/test/fixtures/apps/mcp-proxy/app/controller/app.ts (1)
  • InMemoryEventStore (59-127)
core/types/controller-decorator/MetadataKey.ts (1)
  • CONTROLLER_META_DATA (9-9)
core/controller-decorator/src/model/MCPResourceMeta.ts (1)
  • MCPResourceMeta (5-36)
🪛 Biome (1.9.4)
plugin/controller/lib/impl/mcp/MCPControllerRegister.ts

[error] 82-82: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 191-191: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 228-228: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 278-278: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 306-306: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 329-329: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)

⏰ Context from checks skipped due to timeout of 90000ms (7)
  • GitHub Check: Analyze (typescript)
  • GitHub Check: Runner-macos (18)
  • GitHub Check: Runner-ubuntu (22)
  • GitHub Check: Runner-macos (16)
  • GitHub Check: Runner-ubuntu (18)
  • GitHub Check: Runner-ubuntu (20)
  • GitHub Check: Runner-ubuntu (16)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (15)
plugin/mcp-proxy/test/fixtures/apps/mcp-proxy/app/controller/app.ts (2)

192-195: ⚠️ Potential issue

Add proper null checking for detail before using non-null assertion

Using non-null assertion (!) on detail in line 192 is risky if detail is null or undefined. Adding proper null checking is safer.

  const detail = await this.app.mcpProxy.getClient(this.ctx.request.query.sessionId);
-  if (detail?.pid !== process.pid) {
+  if (!detail) {
+    this.ctx.status = 404;
+    this.ctx.body = {
+      jsonrpc: '2.0',
+      error: {
+        code: -32001,
+        message: 'Session not found',
+      },
+      id: null,
+    };
+    return;
+  } else if (detail.pid !== process.pid) {
    await this.app.mcpProxy.proxyMessage(this.ctx, {
-     detail: detail!,
+     detail,
      sessionId: this.ctx.request.query.sessionId,
      type: MCPProtocols.SSE,
    });

274-279: ⚠️ Potential issue

Add proper null checking for sid before using non-null assertion

Using non-null assertion (!) on sid is risky if sid is null or undefined. Add proper type checking instead.

  const sid = transport.sessionId;
  if (sid && transports[sid]) {
    delete transports[sid];
+   await this.app.mcpProxy.unregisterClient(sid);
+ } else if (sid) {
+   await this.app.mcpProxy.unregisterClient(sid);
  }
- await this.app.mcpProxy.unregisterClient(sid!);
core/controller-decorator/src/util/MCPInfoUtil.ts (1)

116-124: ⚠️ Potential issue

Fix potential metadata collision between prompts and resources

setMCPPromptParams and getMCPPromptParams are using the resource params map key instead of a dedicated key for prompt parameters. This could lead to metadata collisions where prompt metadata overwrites resource metadata or vice versa.

  static setMCPPromptParams(params: MCPPromptParams & { mcpName?: string }, clazz: EggProtoImplClass, resourceName: string) {
-   const methodMap: MCPPromptMap = MetadataUtil.initOwnMapMetaData(CONTROLLER_MCP_RESOURCE_PARAMS_MAP, clazz, new Map());
+   const methodMap: MCPPromptMap = MetadataUtil.initOwnMapMetaData(CONTROLLER_MCP_PROMPT_PARAMS_MAP, clazz, new Map());
    methodMap.set(resourceName, params);
  }

  static getMCPPromptParams(clazz: EggProtoImplClass, resourceName: string): MCPPromptParams & { mcpName?: string } | undefined {
-   const methodMap: MCPPromptMap | undefined = MetadataUtil.getMetaData(CONTROLLER_MCP_RESOURCE_PARAMS_MAP, clazz);
+   const methodMap: MCPPromptMap | undefined = MetadataUtil.getMetaData(CONTROLLER_MCP_PROMPT_PARAMS_MAP, clazz);
    return methodMap?.get(resourceName);
  }

You'll need to define CONTROLLER_MCP_PROMPT_PARAMS_MAP in your types file if it doesn't exist yet.

plugin/controller/lib/impl/mcp/MCPControllerRegister.ts (5)

28-41: Add documentation for hook interface methods

The MCPControllerHook interface defines several methods but lacks documentation explaining when each method is called and what it should do.

 export interface MCPControllerHook {
   // SSE
+  /**
+   * Called before initializing an SSE transport.
+   * Can be used to customize the transport or perform authentication.
+   */
   preSSEInitHandle?: (ctx: Context, transport: SSEServerTransport, register: MCPControllerRegister) => Promise<void>
+  /**
+   * Called before handling an SSE message request.
+   * Can be used for authentication or request validation.
+   */
   preHandleInitHandle?: (ctx: Context) => Promise<void>

   // STREAM
+  /**
+   * Called before handling a stream request.
+   * Can be used for authentication or request validation.
+   */
   preHandle?: (ctx: Context) => Promise<void>
+  /**
+   * Called when a stream session is initialized.
+   * Can be used to customize session behavior or register additional handlers.
+   */
   onStreamSessionInitialized?: (ctx: Context, transport: StreamableHTTPServerTransport, register: MCPControllerRegister) => Promise<void>

   // COMMON
+  /**
+   * Called before proxying a request to another process.
+   * Can be used to modify the request or response before proxying.
+   */
   preProxy?: (ctx: Context, proxyReq: Parameters<typeof http.createServer>['0'], proxyResp: Parameters<typeof http.createServer>['1']) => Promise<void>
+  /**
+   * Called to load schemas for MCP prompts or tools.
+   * Can be used to customize schema loading from various sources.
+   */
   schemaLoader?: (controllerMeta: MCPControllerMeta, meta: MCPPromptMeta | MCPToolMeta) => Promise<Parameters<McpServer['tool']>['2'] | Parameters<McpServer['prompt']>['2']>
+  /**
+   * Called to check if a request should be proxied to another process.
+   * Return true if the request has been handled by this hook, false otherwise.
+   */
   checkAndRunProxy?: (ctx: Context, type: MCPProtocols, sessionId: string) => Promise<boolean>;
 }

74-77: Document intended usage of hooks API

The static addHook method allows adding hooks to customize the MCP controller behavior, but there's no documentation on when and how to use it.

+  /**
+   * Add a hook to customize the behavior of MCP controllers.
+   * Hooks can modify request/response handling, load schemas, or intercept proxy requests.
+   * 
+   * @param hook MCPControllerHook implementation with optional lifecycle methods
+   */
  static addHook(hook: MCPControllerHook) {
    MCPControllerRegister.hooks.push(hook);
  }

323-326: Use proper type-safe approach for registering handlers

The code uses unsafe type casting with as unknown as [any, any, any, any] when calling MCP server methods, which bypasses type checking.

- this.mcpServer.prompt(...(args as unknown as [any, any, any, any]));
+ // Create properly typed function signature
+ type PromptParams = Parameters<typeof this.mcpServer.prompt>;
+ this.mcpServer.prompt(...(args as PromptParams));

Apply similar changes for the other MCP method calls at lines 369 and 392.


415-423: 🛠️ Refactor suggestion

Improve initialization order to prevent race conditions

mcpStreamServerInit, mcpServerInit, and mcpServerRegister rely on this.mcpServer being non-null, yet they are called immediately after creation and before awaiting any async work. If later code calls register() concurrently, there's a race condition.

 if (!this.mcpServer) {
   this.mcpServer = new McpServer({
     name: this.controllerMeta.name ?? `chair-mcp-${this.app.name}-server`,
     version: this.controllerMeta.version ?? '1.0.0',
   }, { capabilities: { logging: {} } });
-  this.mcpStreamServerInit();
-  this.mcpServerInit();
-  this.mcpServerRegister();
+  // Ensure sequential initialization to prevent race conditions
+  await Promise.all([
+    this.mcpStreamServerInit(),
+    this.mcpServerInit(),
+    this.mcpServerRegister()
+  ]);
 }

254-256: ⚠️ Potential issue

Add proper sessionId validation in message handler

The message handler does not validate that sessionId is defined before using it, which could lead to errors.

- const sessionId = ctx.query.sessionId;
+ const sessionId = String(ctx.query.sessionId ?? '');
+ if (!sessionId) {
+   ctx.status = 400;
+   ctx.body = {
+     jsonrpc: '2.0',
+     error: {
+       code: -32602,
+       message: 'Bad Request: No sessionId provided',
+     },
+     id: null,
+   };
+   return;
+ }
 
 if (self.transports[sessionId]) {
plugin/mcp-proxy/index.ts (7)

25-25: ⚠️ Potential issue

Fix typo in ProxyAction type and usage

The constant "MCP_SEE_PROXY" should be "MCP_SSE_PROXY" to match the SSE protocol type. This typo appears in both the type definition and usage locations.

-type ProxyAction = 'MCP_STDIO_PROXY' | 'MCP_SEE_PROXY' | 'MCP_STREAM_PROXY';
+type ProxyAction = 'MCP_STDIO_PROXY' | 'MCP_SSE_PROXY' | 'MCP_STREAM_PROXY';

// At line 229:
case MCPProtocols.SSE:
-  action = 'MCP_SEE_PROXY';
+  action = 'MCP_SSE_PROXY';

// At line 293:
case 'SSE': {
-  action = 'MCP_SEE_PROXY';
+  action = 'MCP_SSE_PROXY';

Also applies to: 229-229, 293-293


191-192: ⚠️ Potential issue

Add proper null checking for sid before using non-null assertion

Using non-null assertion (!) on sid is risky if sid is null or undefined. Add proper type checking instead.

  const sid = transport.sessionId;
  if (sid && self.streamTransports[sid]) {
    delete self.streamTransports[sid];
  }
- await apiClient.unregisterClient(sid!);
+ if (sid) {
+   await apiClient.unregisterClient(sid);
+ }

196-199: ⚠️ Potential issue

Add proper null checking for detail before using non-null assertion

Using non-null assertion (!) on detail is risky if detail is null or undefined. Add proper type checking before usage.

  const detail = await apiClient.getClient(sessionId);
- if (detail?.pid !== process.pid) {
+ if (!detail) {
+   ctx.status = 404;
+   ctx.body = {
+     jsonrpc: '2.0',
+     error: {
+       code: -32001,
+       message: 'Session not found',
+     },
+     id: null,
+   };
+   return true;
+ } else if (detail.pid !== process.pid) {
    await apiClient.proxyMessage(ctx, {
-     detail: detail!,
+     detail,
      sessionId,
      type,
    });

216-222: 🛠️ Refactor suggestion

Add error handling for server initialization

The server initialization lacks error handling. If the port is already in use, the promise may never resolve, causing the application to hang.

  this.port = this.app.config.mcp?.proxyPort + (cluster.worker?.id ?? 0);
  this.addProxyHook();
- await new Promise(resolve => server.listen(this.port, () => {
-   // const address = server.address()! as AddressInfo;
-   // this.port = address.port;
-   resolve(null);
- }));
+ await new Promise((resolve, reject) => {
+   server.once('error', err => {
+     this.logger.error('Failed to start MCP proxy server:', err);
+     reject(err);
+   });
+   server.listen(this.port, () => {
+     this.logger.info(`MCP proxy server listening on port ${this.port}`);
+     resolve(null);
+   });
+ });

55-55: ⚠️ Potential issue

Initialize _client field or remove redundant declaration

The subclass redeclares private _client: any; but never assigns it. The base class APIClientBase normally sets this._client internally, but the redeclaration hides that property.

-  private _client: any;

Remove the redundant field declaration to rely on the base-class implementation, or initialize it properly in the _init method.


212-215: 💡 Verification agent

🧩 Analysis chain

Enforce validation of mcp-proxy-type before dispatching proxy handlers

The current code casts the untrusted header directly to ProxyAction and invokes the handler without checking whether it's one of the expected values. This can lead to security vulnerabilities.

  const server = http.createServer(async (req, res) => {
-   const type = req.headers['mcp-proxy-type'] as ProxyAction;
-   await this.proxyHandlerMap[type]?.(req, res);
+   const typeHeader = req.headers['mcp-proxy-type'] as string;
+   // Validate that the header matches one of the allowed ProxyAction keys
+   if (!typeHeader || !this.proxyHandlerMap[typeHeader as ProxyAction]) {
+     res.writeHead(400).end('Invalid or unsupported MCP proxy type');
+     return;
+   }
+   const action = typeHeader as ProxyAction;
+   await this.proxyHandlerMap[action]!(req, res);
  });

Let's verify if the mcp-proxy-type header can be controlled by untrusted sources:


🏁 Script executed:

#!/bin/bash
# Find where 'mcp-proxy-type' headers are set
rg "mcp-proxy-type" --no-heading -n

Length of output: 382


Enforce validation of the mcp-proxy-type header before dispatching proxy handlers

The request handler currently casts the raw header straight to ProxyAction and invokes the handler, allowing any client-supplied value—even unsupported ones—to be invoked. We should validate that the header matches one of our known ProxyAction keys and return a 400 if not.

• File: plugin/mcp-proxy/index.ts
• Around lines 212–215, replace:

- const type = req.headers['mcp-proxy-type'] as ProxyAction;
- await this.proxyHandlerMap[type]?.(req, res);

with:

+ const typeHeader = req.headers['mcp-proxy-type'] as string;
+ // Reject missing or unsupported proxy actions
+ if (!typeHeader || !this.proxyHandlerMap[typeHeader as ProxyAction]) {
+   res.writeHead(400).end('Invalid or unsupported MCP proxy type');
+   return;
+ }
+ const action = typeHeader as ProxyAction;
+ await this.proxyHandlerMap[action]!(req, res);

This ensures only valid, whitelisted proxy actions are executed.

🧰 Tools
🪛 GitHub Check: CodeQL

[failure] 214-214: Unvalidated dynamic method call
Invocation of method with user-controlled name may dispatch to unexpected target and cause an exception.


294-318: 🛠️ Refactor suggestion

Refactor SSE response handling to properly support streaming

For the SSE protocol, the proxy fetches the entire response at once with resp.text() and sends it back in one chunk. This defeats the purpose of SSE which requires streaming events incrementally.

      ctx.set(headers);
      ctx.res.statusCode = resp.status;
      ctx.res.statusMessage = resp.statusText;
-     const resData = await resp.text();
-     ctx.body = resData;
+     // Stream the response to preserve SSE functionality
+     ctx.respond = false;
+     Readable.fromWeb(resp.body!).pipe(ctx.res);

This ensures that SSE events are streamed to the client as they arrive, rather than waiting for all events before sending.

🧹 Nitpick comments (9)
core/controller-decorator/src/model/MCPToolMeta.ts (1)

1-34: Consider adding input validation for critical fields

The class structure looks good with readonly properties and proper typing. The constructor properly initializes all fields and coerces needAcl to a boolean.

Consider adding validation for critical fields like name to prevent potential runtime issues:

  constructor(opt: {
    name: string;
    middlewares: MiddlewareFunc[];
    needAcl?: boolean;
    aclCode?: string,
    description?: string;
    mcpName?: string;
    detail?: ToolArgsSchemaDetail;
    extra?: number;
  }) {
+   if (!opt.name || opt.name.trim() === '') {
+     throw new Error('MCPToolMeta name cannot be empty');
+   }
+   if (!Array.isArray(opt.middlewares)) {
+     throw new Error('MCPToolMeta middlewares must be an array');
+   }
    this.name = opt.name;
    this.needAcl = !!opt.needAcl;
    this.description = opt.description;
    this.mcpName = opt.mcpName;
    this.middlewares = opt.middlewares;
    this.aclCode = opt.aclCode;
    this.detail = opt.detail;
    this.extra = opt.extra;
  }
core/controller-decorator/src/decorator/mcp/MCPPrompt.ts (2)

10-33: Consider explicit error handling for decorator application

The MCPPrompt decorator correctly configures the method for MCP use, but lacks error handling if applied incorrectly.

Consider adding error handling to detect incorrect usage and provide better diagnostic messages:

export function MCPPrompt(params?: MCPPromptParams) {
  return function(
    target: any,
    propertyKey: PropertyKey,
  ) {
+   if (typeof propertyKey !== 'string') {
+     throw new TypeError('MCPPrompt decorator can only be applied to methods with string names');
+   }
    const controllerClazz = target.constructor as EggProtoImplClass;
    const methodName = propertyKey as string;
    MethodInfoUtil.setMethodControllerType(
      controllerClazz,
      methodName,
      ControllerType.MCP,
    );
    MCPInfoUtil.setMCPPromptParams(
      {
        ...params,
        mcpName: params?.name,
        name: methodName,
      },
      controllerClazz,
      methodName,
    );
    MCPInfoUtil.setMCPPrompt(controllerClazz, methodName);
  };
}

35-52: Consider validation for argsSchema parameter

The PromptArgsSchema decorator correctly records schema information but doesn't validate the schema parameter.

Add basic validation to ensure the schema parameter is correctly provided:

export function PromptArgsSchema(argsSchema: Parameters<McpServer['prompt']>['2']) {
  return function(
    target: any,
    propertyKey: PropertyKey,
    parameterIndex: number,
  ) {
+   if (!argsSchema) {
+     throw new TypeError('PromptArgsSchema requires a valid schema parameter');
+   }
    const controllerClazz = target.constructor as EggProtoImplClass;
    const methodName = propertyKey as string;
    MCPInfoUtil.setMCPPromptArgsInArgs(
      {
        argsSchema,
        index: parameterIndex,
      },
      controllerClazz,
      methodName,
    );
  };
}
plugin/mcp-proxy/test/fixtures/apps/mcp-proxy/app/controller/app.ts (4)

171-172: Remove unnecessary this aliasing

Arrow functions inherit this from their enclosing scope, making this aliasing unnecessary.

-    const self = this;
-    const transport = new SSEServerTransport('/message', this.ctx.res);
+    const transport = new SSEServerTransport('/message', this.ctx.res);
🧰 Tools
🪛 Biome (1.9.4)

[error] 171-171: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


174-176: Simplify and improve the proxy handler registration

The handler registration uses self unnecessarily and could be simplified.

-    this.app.mcpProxy.setProxyHandler(MCPProtocols.SSE, async (req, res) => {
-      return await self.ssePostHandler(req, res);
-    });
+    this.app.mcpProxy.setProxyHandler(MCPProtocols.SSE, (req, res) => 
+      this.ssePostHandler(req, res)
+    );

261-262: Remove unnecessary this aliasing

Arrow functions inherit this from their enclosing scope, making this aliasing unnecessary.

-        const eventStore = new InMemoryEventStore();
-        const self = this;
+        const eventStore = new InMemoryEventStore();
🧰 Tools
🪛 Biome (1.9.4)

[error] 261-261: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


267-270: Use a consistent approach for proxy handler registration

The handler registration could be simplified for better readability.

-            this.app.mcpProxy.setProxyHandler(MCPProtocols.STREAM, async (req, res) => {
-              return await self.streamPostHandler(req, res);
-            });
+            this.app.mcpProxy.setProxyHandler(MCPProtocols.STREAM, (req, res) => 
+              this.streamPostHandler(req, res)
+            );
core/controller-decorator/src/util/MCPInfoUtil.ts (1)

39-155: Consider using namespace or functions instead of a static-only class

This class contains only static methods and no instance properties. Consider using a namespace or individual functions to improve maintainability.

-export class MCPInfoUtil {
+export namespace MCPInfoUtil {
   // All methods stay the same, just remove the 'static' keyword

Alternatively, you could export individual functions directly:

export function setMCPName(name: string, clazz: EggProtoImplClass) {
  MetadataUtil.defineMetaData(CONTROLLER_MCP_NAME, name, clazz);
}

export function getMCPName(clazz: EggProtoImplClass): string | undefined {
  return MetadataUtil.getMetaData(CONTROLLER_MCP_NAME, clazz);
}

// ... other functions
🧰 Tools
🪛 Biome (1.9.4)

[error] 39-155: Avoid classes that contain only static members.

Prefer using simple functions instead of classes with only static members.

(lint/complexity/noStaticOnlyClass)

plugin/controller/lib/impl/mcp/MCPControllerRegister.ts (1)

81-81: Remove unnecessary this aliasing

Arrow functions inherit this from their enclosing scope, making this aliasing unnecessary in multiple places.

- const self = this;

Make this change at lines 81, 117, 195, 249, 303, 347, and 384.

Also applies to: 117-117, 195-195, 249-249, 303-303, 347-347, 384-384

🧰 Tools
🪛 Biome (1.9.4)

[error] 81-81: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between eb050f5 and 3273176.

📒 Files selected for processing (26)
  • core/controller-decorator/index.ts (2 hunks)
  • core/controller-decorator/src/decorator/mcp/Extra.ts (1 hunks)
  • core/controller-decorator/src/decorator/mcp/MCPPrompt.ts (1 hunks)
  • core/controller-decorator/src/decorator/mcp/MCPTool.ts (1 hunks)
  • core/controller-decorator/src/impl/mcp/MCPControllerPromptMetaBuilder.ts (1 hunks)
  • core/controller-decorator/src/impl/mcp/MCPControllerResourceMetaBuilder.ts (1 hunks)
  • core/controller-decorator/src/impl/mcp/MCPControllerToolMetaBuilder.ts (1 hunks)
  • core/controller-decorator/src/model/MCPPromptMeta.ts (1 hunks)
  • core/controller-decorator/src/model/MCPResourceMeta.ts (1 hunks)
  • core/controller-decorator/src/model/MCPToolMeta.ts (1 hunks)
  • core/controller-decorator/src/util/MCPInfoUtil.ts (1 hunks)
  • core/controller-decorator/test/MCPMeta.test.ts (1 hunks)
  • core/controller-decorator/test/fixtures/MCPController.ts (1 hunks)
  • core/types/controller-decorator/MCPPromptParams.ts (1 hunks)
  • core/types/controller-decorator/MCPToolParams.ts (1 hunks)
  • core/types/controller-decorator/MetadataKey.ts (1 hunks)
  • plugin/controller/app.ts (2 hunks)
  • plugin/controller/app/middleware/mcp_body_middleware.ts (1 hunks)
  • plugin/controller/config/config.default.ts (1 hunks)
  • plugin/controller/lib/impl/mcp/MCPControllerRegister.ts (1 hunks)
  • plugin/controller/test/fixtures/apps/mcp-app/app/controller/AppController.ts (1 hunks)
  • plugin/controller/test/fixtures/apps/mcp-app/app/controller/McpController.ts (1 hunks)
  • plugin/mcp-proxy/config/config.default.ts (1 hunks)
  • plugin/mcp-proxy/index.ts (1 hunks)
  • plugin/mcp-proxy/package.json (1 hunks)
  • plugin/mcp-proxy/test/fixtures/apps/mcp-proxy/app/controller/app.ts (1 hunks)
✅ Files skipped from review due to trivial changes (2)
  • plugin/mcp-proxy/config/config.default.ts
  • core/controller-decorator/src/decorator/mcp/Extra.ts
🚧 Files skipped from review as they are similar to previous changes (16)
  • plugin/controller/app/middleware/mcp_body_middleware.ts
  • plugin/controller/app.ts
  • core/controller-decorator/src/impl/mcp/MCPControllerPromptMetaBuilder.ts
  • core/controller-decorator/index.ts
  • core/controller-decorator/test/MCPMeta.test.ts
  • core/controller-decorator/src/impl/mcp/MCPControllerResourceMetaBuilder.ts
  • core/controller-decorator/src/model/MCPResourceMeta.ts
  • core/controller-decorator/src/impl/mcp/MCPControllerToolMetaBuilder.ts
  • core/controller-decorator/src/model/MCPPromptMeta.ts
  • core/controller-decorator/src/decorator/mcp/MCPTool.ts
  • plugin/controller/test/fixtures/apps/mcp-app/app/controller/AppController.ts
  • plugin/mcp-proxy/package.json
  • core/types/controller-decorator/MCPToolParams.ts
  • plugin/controller/test/fixtures/apps/mcp-app/app/controller/McpController.ts
  • core/types/controller-decorator/MCPPromptParams.ts
  • core/controller-decorator/test/fixtures/MCPController.ts
🧰 Additional context used
🧬 Code Graph Analysis (3)
plugin/controller/config/config.default.ts (3)
plugin/mcp-proxy/config/config.default.ts (1)
  • config (1-10)
plugin/controller/test/fixtures/apps/mcp-app/config/config.default.js (1)
  • config (4-11)
plugin/mcp-proxy/test/fixtures/apps/mcp-proxy/config/config.default.js (1)
  • config (4-14)
core/controller-decorator/src/model/MCPToolMeta.ts (2)
core/controller-decorator/src/util/MCPInfoUtil.ts (1)
  • ToolArgsSchemaDetail (25-28)
core/types/controller-decorator/model/types.ts (1)
  • MiddlewareFunc (5-5)
plugin/mcp-proxy/index.ts (2)
plugin/controller/lib/impl/mcp/MCPControllerRegister.ts (2)
  • MCPControllerHook (28-41)
  • MCPControllerRegister (44-434)
plugin/mcp-proxy/lib/MCPProxyDataClient.ts (1)
  • MCPProxyDataClient (4-39)
🪛 Biome (1.9.4)
core/controller-decorator/src/util/MCPInfoUtil.ts

[error] 39-155: Avoid classes that contain only static members.

Prefer using simple functions instead of classes with only static members.

(lint/complexity/noStaticOnlyClass)

plugin/controller/lib/impl/mcp/MCPControllerRegister.ts

[error] 81-81: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 195-195: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 249-249: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 303-303: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 349-349: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 386-386: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)

plugin/mcp-proxy/test/fixtures/apps/mcp-proxy/app/controller/app.ts

[error] 171-171: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 261-261: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)

🪛 GitHub Check: CodeQL
plugin/mcp-proxy/index.ts

[failure] 214-214: Unvalidated dynamic method call
Invocation of method with user-controlled name may dispatch to unexpected target and cause an exception.

⏰ Context from checks skipped due to timeout of 90000ms (7)
  • GitHub Check: Runner-macos (16)
  • GitHub Check: Runner-macos (20)
  • GitHub Check: Runner-macos (18)
  • GitHub Check: Runner-ubuntu (20)
  • GitHub Check: Runner-ubuntu (16)
  • GitHub Check: Runner-ubuntu (22)
  • GitHub Check: Runner-ubuntu (18)
🔇 Additional comments (2)
plugin/controller/config/config.default.ts (1)

1-15:

Details

✅ Verification successful

Ensure configuration respects user overrides and existing CSRF ignore settings

This new default configuration for MCP paths looks good, but based on previous reviews, you need to:

  1. Make sure these paths respect user configuration rather than being hard-coded
  2. Ensure compatibility with existing CSRF ignore settings when these paths are added to the ignore list

Consider updating the implementation to follow the pattern used in other Egg.js configurations where user settings take precedence and are properly merged with defaults.


🏁 Script executed:

#!/bin/bash
# Check how other configs handle merging with user settings
grep -r "security.csrf.ignore" --include="*.ts" .

Length of output: 635


🏁 Script executed:

#!/bin/bash
# Show how CSRF ignore paths are merged in plugin/controller/app.ts
grep -R -n -A5 -B5 "security.csrf.ignore" plugin/controller/app.ts

Length of output: 1115


No changes needed—CSRF ignore merging already respects user overrides
The plugin/controller/app.ts logic prepends the MCP paths to app.config.security.csrf.ignore when it exists (and initializes it otherwise), so any user-defined ignore entries are preserved. The default MCP paths in config.default.ts can still be overridden via the standard Egg.js config merge.

core/types/controller-decorator/MetadataKey.ts (1)

27-37: Clear and consistent Symbol naming for MCP metadata keys

The new metadata symbols for MCP follow the established pattern and naming conventions of the codebase, providing clear intent for each key.

The naming is consistent and clearly describes the purpose of each metadata key, making them easy to understand and use throughout the MCP implementation.

Copy link
Contributor

@killagu killagu left a comment

Choose a reason for hiding this comment

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

LGTM

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (2)
plugin/mcp-proxy/index.ts (2)

55-70: ⚠️ Potential issue

_client shadowing breaks every proxy API call.

APIClientBase already declares a protected _client; redeclaring it here leaves the new field undefined, so
this._client.registerClient(...) crashes at runtime.

-  private _client: any;

Delete the field and rely on the base-class implementation.


321-333: 🛠️ Refactor suggestion

SSE proxy response is buffered, destroying stream semantics.

await resp.text() loads the entire stream into memory and prevents incremental delivery.
Pipe the body instead:

-const resData = await resp.text();
-ctx.body = resData;
+ctx.respond = false;
+Readable.fromWeb(resp.body!).pipe(ctx.res);

This matches SSE’s event streaming model and avoids unbounded memory usage.

🧹 Nitpick comments (3)
core/controller-decorator/src/util/MCPInfoUtil.ts (2)

94-102: Parameter name is misleading (resourceName used for tools).

setMCPToolParams / getMCPToolParams accept a resourceName argument, although these APIs clearly deal with tools.
This confuses readers and increases the chance of copy-paste errors.

-static getMCPToolParams(clazz: EggProtoImplClass, resourceName: string): ...
+static getMCPToolParams(clazz: EggProtoImplClass, toolName: string): ...
 ...
-static setMCPToolParams(params, clazz, resourceName: string) {
-  const methodMap = ...
-  methodMap.set(resourceName, params);
+static setMCPToolParams(params, clazz, toolName: string) {
+  const methodMap = ...
+  methodMap.set(toolName, params);
 }

40-57: Class contains only statics – consider a namespace or plain functions.

Biome warns about “noStaticOnlyClass”.
Using a namespace / module with exported helper functions avoids the unnecessary class wrapper and the need for the export class MCPInfoUtil construct, resulting in tree-shakable code with zero runtime cost.

Not blocking, but worth considering for a follow-up refactor.

🧰 Tools
🪛 Biome (1.9.4)

[error] 40-156: Avoid classes that contain only static members.

Prefer using simple functions instead of classes with only static members.

(lint/complexity/noStaticOnlyClass)

plugin/controller/lib/impl/mcp/MCPControllerRegister.ts (1)

80-88: Unnecessary self = this aliasing clutters code.

Inside arrow functions this is lexically bound; storing it in self is redundant and violates the project’s clean-code guidelines surfaced by Biome.

Search & drop the pattern across the file:

-const self = this;
-const initHandler = async (ctx) => {
-  ...
-  if (MCPControllerRegister.hooks.length > 0) {
-    for (const hook of MCPControllerRegister.hooks) {
-      await hook.preHandle?.(self.app.currentContext);
+const initHandler = async (ctx) => {
+  ...
+  for (const hook of MCPControllerRegister.hooks) {
+    await hook.preHandle?.(this.app.currentContext);
   }
🧰 Tools
🪛 Biome (1.9.4)

[error] 81-81: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3273176 and 4f4e838.

📒 Files selected for processing (4)
  • core/controller-decorator/src/util/MCPInfoUtil.ts (1 hunks)
  • plugin/controller/lib/impl/mcp/MCPControllerRegister.ts (1 hunks)
  • plugin/mcp-proxy/index.ts (1 hunks)
  • plugin/mcp-proxy/package.json (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • plugin/mcp-proxy/package.json
🧰 Additional context used
🪛 Biome (1.9.4)
plugin/controller/lib/impl/mcp/MCPControllerRegister.ts

[error] 81-81: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 196-196: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 250-250: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 319-319: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 365-365: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 402-402: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)

core/controller-decorator/src/util/MCPInfoUtil.ts

[error] 40-156: Avoid classes that contain only static members.

Prefer using simple functions instead of classes with only static members.

(lint/complexity/noStaticOnlyClass)

⏰ Context from checks skipped due to timeout of 90000ms (7)
  • GitHub Check: Runner-ubuntu (18)
  • GitHub Check: Runner-ubuntu (22)
  • GitHub Check: Runner-ubuntu (16)
  • GitHub Check: Runner-ubuntu (20)
  • GitHub Check: Runner-macos (16)
  • GitHub Check: Runner-macos (18)
  • GitHub Check: Runner-macos (20)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (7)
plugin/controller/lib/impl/mcp/MCPControllerRegister.ts (7)

28-41: Improve documentation for hook interface methods

The MCPControllerHook interface defines several important methods but lacks documentation explaining when each method is called and what it should do.

Add JSDoc comments to document each hook method:

export interface MCPControllerHook {
+  /**
+   * Called before initializing an SSE transport.
+   * Can be used to customize the transport or perform authentication.
+   */
  preSSEInitHandle?: (ctx: Context, transport: SSEServerTransport, register: MCPControllerRegister) => Promise<void>
+  /**
+   * Called before handling an SSE message request.
+   * Can be used for authentication or request validation.
+   */
  preHandleInitHandle?: (ctx: Context) => Promise<void>

  // SSE
+  /**
+   * Called before handling a stream request.
+   * Can be used for authentication or request validation.
+   */
  preHandle?: (ctx: Context) => Promise<void>
+  /**
+   * Called when a stream session is initialized.
+   * Can be used to set up session-specific resources or handlers.
+   */
  onStreamSessionInitialized?: (ctx: Context, transport: StreamableHTTPServerTransport, register: MCPControllerRegister) => Promise<void>

74-77: Document intended usage of hooks API

The static addHook method allows adding hooks to customize the MCP controller behavior, but there's no documentation on when and how to use it.

Add documentation comments to explain the intended usage:

+  /**
+   * Add a hook to customize the behavior of MCP controllers.
+   * Hooks can modify request/response handling, load schemas, or intercept proxy requests.
+   * 
+   * @param hook MCPControllerHook implementation with optional lifecycle methods
+   */
  static addHook(hook: MCPControllerHook) {
    MCPControllerRegister.hooks.push(hook);
  }

255-282: Validate sessionId before using it

The messageHandler doesn't validate sessionId before using it to access the transports object, which could lead to errors if sessionId is undefined.

Add explicit validation before proceeding:

- const sessionId = ctx.query.sessionId;
+ const sessionId = String(ctx.query.sessionId ?? '');
+ if (!sessionId) {
+   ctx.status = 400;
+   ctx.body = { 
+     jsonrpc: '2.0',
+     error: {
+       code: -32000,
+       message: 'Bad Request: sessionId required',
+     },
+     id: null,
+   };
+   return;
+ }

117-117: Document event store configuration option

The code provides a fallback to InMemoryEventStore but does not document the configuration option.

Add a comment documenting the configuration option:

- const eventStore = this.app.config.mcp?.eventStore ?? new InMemoryEventStore();
+ // Use configured event store or fall back to in-memory implementation
+ // For production, consider configuring a persistent event store implementation
+ const eventStore = this.app.config.mcp?.eventStore ?? new InMemoryEventStore();

432-439: 🛠️ Refactor suggestion

Potential race condition in server initialization

The initialization of mcpServer and subsequent calls happen without any synchronization mechanism, which could lead to race conditions if register() is called concurrently.

Wrap the first-time initialization in a way that prevents race conditions:

- if (!this.mcpServer) {
-   this.mcpServer = new McpServer({
-     name: this.controllerMeta.name ?? `chair-mcp-${this.app.name}-server`,
-     version: this.controllerMeta.version ?? '1.0.0',
-   }, { capabilities: { logging: {} } });
-   this.mcpStreamServerInit();
-   this.mcpServerInit();
-   this.mcpServerRegister();
- }
+ if (!this.mcpServer) {
+   // Create the server only once
+   this.mcpServer = new McpServer({
+     name: this.controllerMeta.name ?? `chair-mcp-${this.app.name}-server`, 
+     version: this.controllerMeta.version ?? '1.0.0',
+   }, { capabilities: { logging: {} } });
+   
+   // Initialize in sequence to avoid race conditions
+   await Promise.resolve()
+     .then(() => this.mcpStreamServerInit())
+     .then(() => this.mcpServerInit())
+     .then(() => this.mcpServerRegister());
+ }

199-199: ⚠️ Potential issue

Add optional chaining for config value access

The code accesses self.app.config.mcp.sseMessagePath without optional chaining, which could lead to runtime errors if the configuration is missing.

Add optional chaining and provide a fallback:

- const transport = new SSEServerTransport(self.app.config.mcp.sseMessagePath, ctx.res);
+ const transport = new SSEServerTransport(
+   self.app.config.mcp?.sseMessagePath ?? '/api/mcp/message', 
+   ctx.res
+ );

300-342: ⚠️ Potential issue

Fix bug in mcpPromptRegister arguments handling

There's a critical bug in the mcpPromptRegister method - newArgs is correctly built but the original args are passed to the method instead.

The method builds newArgs to align arguments with the controller method's signature but then passes the original args to Reflect.apply, discarding the transformation. This will break prompts that use @MCPPromptArgs or @MCPExtra decorators.

  let newArgs: any[] = [];
  if (schema && promptMeta.detail) {
    // 如果有 schema 则证明入参第一个就是 schema
    newArgs[promptMeta.detail.index] = args[0];

    // 如果有 schema 则证明入参第二个就是 extra
    if (promptMeta.extra) {
      newArgs[promptMeta.extra] = args[1];
    }
  } else if (promptMeta.extra) {
    // 无 schema, 那么入参第一个就是 extra
    newArgs[promptMeta.extra] = args[0];
  }
- newArgs = [ ...newArgs, ...args ];
+ // Fill any gaps in newArgs with undefined
+ const maxIndex = Math.max(
+   promptMeta.detail?.index ?? -1,
+   promptMeta.extra ?? -1
+ );
+ for (let i = 0; i <= maxIndex; i++) {
+   if (newArgs[i] === undefined) {
+     newArgs[i] = undefined;
+   }
+ }
- return Reflect.apply(realMethod, realObj, newArgs);
+ return Reflect.apply(realMethod, realObj, newArgs);

Also, translate the Chinese comments for better maintainability:

- // 如果有 schema 则证明入参第一个就是 schema
+ // If there's a schema, the first argument is the schema input
- // 如果有 schema 则证明入参第二个就是 extra
+ // If there's a schema, the second argument is the extra
- // 无 schema, 那么入参第一个就是 extra
+ // Without schema, the first argument is the extra
🧰 Tools
🪛 Biome (1.9.4)

[error] 319-319: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)

🧹 Nitpick comments (3)
plugin/controller/lib/impl/mcp/MCPControllerRegister.ts (3)

341-341: Replace unsafe type casting with proper type definition

The code uses unsafe type casting with as unknown as [any, any, any, any] when calling MCP server methods, which bypasses type checking.

Consider using more type-safe approaches:

- this.mcpServer.prompt(...(args as unknown as [any, any, any, any]));
+ type PromptArgs = Parameters<typeof this.mcpServer.prompt>;
+ this.mcpServer.prompt(...(args as unknown as PromptArgs));

Or define a properly typed wrapper method:

private typedPrompt(name: string, description?: string, schema?: any, handler?: any): void {
  this.mcpServer.prompt(name, description, schema, handler);
}

Then use this method instead of direct calls with type casting.


81-81: Remove unnecessary this aliasing in arrow functions

Several places in the code use unnecessary this aliasing with const self = this. Arrow functions inherit this from their enclosing scope, making this alias redundant.

Remove the unnecessary alias:

- const self = this;

And replace all occurrences of self with this in each of the arrow functions where this pattern is used.

Also applies to: 196-196, 250-250, 319-319, 365-365, 402-402

🧰 Tools
🪛 Biome (1.9.4)

[error] 81-81: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


307-318: Add error handling for schema loading hooks

The code runs schema loader hooks without try/catch blocks, which could lead to unhandled promise rejections if a hook throws an error.

Add error handling for schema loading:

  if (promptMeta.detail?.argsSchema) {
    schema = promptMeta.detail?.argsSchema;
    args.push(schema);
  } else if (MCPControllerRegister.hooks.length > 0) {
    for (const hook of MCPControllerRegister.hooks) {
-     schema = await hook.schemaLoader?.(controllerMeta, promptMeta);
+     try {
+       schema = await hook.schemaLoader?.(controllerMeta, promptMeta);
+     } catch (error) {
+       this.app.logger.error('Error loading schema for prompt %s: %o', promptMeta.name, error);
+       // Continue with other hooks but log the error
+       continue;
+     }
      if (schema) {
        args.push(schema);
        break;
      }
    }
  }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4f4e838 and 4a6d17d.

📒 Files selected for processing (1)
  • plugin/controller/lib/impl/mcp/MCPControllerRegister.ts (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
plugin/controller/lib/impl/mcp/MCPControllerRegister.ts (8)
core/controller-decorator/src/model/MCPControllerMeta.ts (2)
  • MCPControllerMeta (7-70)
  • id (22-24)
core/controller-decorator/src/model/MCPPromptMeta.ts (1)
  • MCPPromptMeta (4-33)
core/controller-decorator/src/model/MCPToolMeta.ts (1)
  • MCPToolMeta (5-34)
plugin/controller/lib/ControllerRegister.ts (1)
  • ControllerRegister (4-6)
core/runtime/src/factory/EggContainerFactory.ts (1)
  • EggContainerFactory (18-88)
plugin/mcp-proxy/test/fixtures/apps/mcp-proxy/app/controller/app.ts (1)
  • InMemoryEventStore (59-127)
core/types/controller-decorator/MetadataKey.ts (1)
  • CONTROLLER_META_DATA (9-9)
core/controller-decorator/src/model/MCPResourceMeta.ts (1)
  • MCPResourceMeta (5-39)
🪛 Biome (1.9.4)
plugin/controller/lib/impl/mcp/MCPControllerRegister.ts

[error] 81-81: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 196-196: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 250-250: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 319-319: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 365-365: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 402-402: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)

⏰ Context from checks skipped due to timeout of 90000ms (7)
  • GitHub Check: Runner-ubuntu (18)
  • GitHub Check: Runner-macos (20)
  • GitHub Check: Runner-ubuntu (20)
  • GitHub Check: Runner-ubuntu (22)
  • GitHub Check: Runner-ubuntu (16)
  • GitHub Check: Runner-macos (18)
  • GitHub Check: Runner-macos (16)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

♻️ Duplicate comments (14)
plugin/controller/lib/impl/mcp/MCPControllerRegister.ts (5)

337-339: ⚠️ Potential issue

Fix critical bug in mcpPromptRegister arguments handling.

The function builds newArgs with correct ordering based on metadata but then uses the original args in the Reflect.apply call, making the reordering ineffective.

- newArgs = [ ...newArgs, ...args ];
- return Reflect.apply(realMethod, realObj, args);
+ // Fill any gaps in newArgs with undefined
+ const maxIndex = Math.max(
+   promptMeta.detail?.index ?? -1,
+   promptMeta.extra ?? -1
+ );
+ for (let i = 0; i <= maxIndex; i++) {
+   if (newArgs[i] === undefined) {
+     newArgs[i] = undefined;
+   }
+ }
+ return Reflect.apply(realMethod, realObj, newArgs);

255-258: ⚠️ Potential issue

Validate sessionId from query parameters.

sessionId from query parameters is used without validation, which could lead to errors when accessing self.transports[sessionId].

- const sessionId = ctx.query.sessionId;
+ const sessionId = ctx.query.sessionId as string | undefined;
+ 
+ if (!sessionId) {
+   ctx.status = 400;
+   ctx.body = {
+     jsonrpc: '2.0',
+     error: {
+       code: -32000,
+       message: 'Bad Request: sessionId is required',
+     },
+     id: null,
+   };
+   return;
+ }

  if (self.transports[sessionId]) {
    // ...

432-439: 🛠️ Refactor suggestion

Add sequential initialization to prevent race conditions.

The mcpStreamServerInit(), mcpServerInit(), and mcpServerRegister() methods are called immediately after creating the server but before awaiting any async work. This could lead to race conditions if register() is called concurrently.

  if (!this.mcpServer) {
    this.mcpServer = new McpServer({
      name: this.controllerMeta.name ?? `chair-mcp-${this.app.name}-server`,
      version: this.controllerMeta.version ?? '1.0.0',
    }, { capabilities: { logging: {} } });
-   this.mcpStreamServerInit();
-   this.mcpServerInit();
-   this.mcpServerRegister();
+   // Initialize servers sequentially to prevent race conditions
+   await this.mcpStreamServerInit();
+   await this.mcpServerInit();
+   await this.mcpServerRegister();
  }

369-383: ⚠️ Potential issue

Fix the same argument handling bug in mcpToolRegister.

The same bug exists in the mcpToolRegister method - newArgs is created but then not used in the final Reflect.apply call.

  let newArgs: any[] = [];
  if (schema && toolMeta.detail) {
    // If there's a schema, the first parameter is the schema
    newArgs[toolMeta.detail.index] = args[0];

    // If there's a schema, the second parameter is extra
    if (toolMeta.extra) {
      newArgs[toolMeta.extra] = args[1];
    }
  } else if (toolMeta.extra) {
    // Without schema, the first parameter is extra
    newArgs[toolMeta.extra] = args[0];
  }
- newArgs = [ ...newArgs, ...args ];
+ // Fill any gaps in newArgs with undefined
+ const maxIndex = Math.max(
+   toolMeta.detail?.index ?? -1,
+   toolMeta.extra ?? -1
+ );
+ for (let i = 0; i <= maxIndex; i++) {
+   if (newArgs[i] === undefined) {
+     newArgs[i] = undefined;
+   }
+ }
- return Reflect.apply(realMethod, realObj, args);
+ return Reflect.apply(realMethod, realObj, newArgs);

83-180: 🛠️ Refactor suggestion

Fix the session validation and response flow in mcpStreamServerInit.

Setting ctx.respond = false too early prevents proper error responses. The method also lacks proper error handling when a session ID is provided but not found.

- ctx.respond = false;
  if (MCPControllerRegister.hooks.length > 0) {
    // Hooks execution...
  }
  const sessionId = ctx.req.headers['mcp-session-id'] as string | undefined;
  // ...existing code for when sessionId is not provided...
  } else if (sessionId) {
    const transport = self.streamTransports[sessionId];
    if (transport) {
      // ...existing code...
+     ctx.respond = false;
      // ...rest of the code...
    }
    if (MCPControllerRegister.hooks.length > 0) {
      // ...hooks execution...
    }
+   // If we get here, no transport was found and no hook handled it
+   ctx.status = 404;
+   ctx.body = {
+     jsonrpc: '2.0',
+     error: {
+       code: -32001,
+       message: 'Session not found',
+     },
+     id: null,
+   };
+   return;
  }
- return;
plugin/mcp-proxy/index.ts (9)

25-25: ⚠️ Potential issue

Fix typo in ProxyAction type and usage ("SEE" should be "SSE").

The constant "MCP_SEE_PROXY" should be "MCP_SSE_PROXY" to match the SSE protocol type. This typo appears in the type definition and usage locations.

-type ProxyAction = 'MCP_STDIO_PROXY' | 'MCP_SEE_PROXY' | 'MCP_STREAM_PROXY';
+type ProxyAction = 'MCP_STDIO_PROXY' | 'MCP_SSE_PROXY' | 'MCP_STREAM_PROXY';

Also apply to lines 247 and 311 where this type is used.


54-56: ⚠️ Potential issue

Remove redundant _client field declaration.

The class extends APIClientBase which already has a _client property. Declaring it again hides the property from the base class.

export class MCPProxyApiClient extends APIClientBase {
-  private _client: any;
  private logger: EggLogger;
  private proxyHandlerMap: { [P in ProxyAction]?: StreamableHTTPServerTransport['handleRequest']; } = {};

209-210: 🛠️ Refactor suggestion

Add proper null checking for sid before using non-null assertion.

Using non-null assertion (!) on sid is risky if sid is null or undefined. Add proper type checking instead.

  const sid = transport.sessionId;
  if (sid && self.streamTransports[sid]) {
    delete self.streamTransports[sid];
  }
- await apiClient.unregisterClient(sid!);
+ if (sid) {
+   await apiClient.unregisterClient(sid);
+ }

246-254: ⚠️ Potential issue

Fix the SSE protocol action mapping.

The SSE protocol is mapped to 'MCP_SEE_PROXY' which appears to be a typo and should be 'MCP_SSE_PROXY'.

  switch (type) {
    case MCPProtocols.SSE:
-     action = 'MCP_SEE_PROXY';
+     action = 'MCP_SSE_PROXY';
      break;
    case MCPProtocols.STDIO:
      action = 'MCP_STDIO_PROXY';
      break;
    default:
      action = 'MCP_STREAM_PROXY';
      break;
  }

94-103: ⚠️ Potential issue

Fix ServerResponse method usage.

Using res.status(400).json() on a Node.js ServerResponse object will throw at runtime because these methods don't exist on that object.

- res.status(400).json({
+ res.writeHead(400, { 'content-type': 'application/json' });
+ res.end(JSON.stringify({
  jsonrpc: '2.0',
  error: {
    code: -32000,
    message: 'Bad Request: Session exists but uses a different transport protocol',
  },
  id: null,
-});
+}));

Apply this pattern to all similar response patterns in the file.


310-311: ⚠️ Potential issue

Fix the SSE protocol action mapping in the proxy message handler.

The same typo appears here, mapping SSE to 'MCP_SEE_PROXY'.

  switch (type) {
    case 'SSE': {
-     action = 'MCP_SEE_PROXY';
+     action = 'MCP_SSE_PROXY';
      ctx.req.headers['mcp-proxy-type'] = action;

122-130: ⚠️ Potential issue

Fix ServerResponse method usage here too.

Same issue with using Express-style methods on a Node.js ServerResponse.

- res.status(404).json({
+ res.writeHead(404, { 'content-type': 'application/json' });
+ res.end(JSON.stringify({
  jsonrpc: '2.0',
  error: {
    code: -32602,
    message: 'Bad Request: No transport found for sessionId',
  },
  id: null,
-});
+}));

230-233: ⚠️ Potential issue

Validate the mcp-proxy-type header before using it.

The code uses the header value directly to index into a handler map without validation, which is a security risk.

- const type = req.headers['mcp-proxy-type'] as ProxyAction;
- await this.proxyHandlerMap[type]?.(req, res);
+ const typeHeader = req.headers['mcp-proxy-type'] as string;
+ // Validate that the header matches one of the allowed ProxyAction keys
+ if (!typeHeader || !this.proxyHandlerMap[typeHeader as ProxyAction]) {
+   res.writeHead(400, { 'content-type': 'application/json' }).end(JSON.stringify({
+     jsonrpc: '2.0',
+     error: {
+       code: -32000,
+       message: 'Invalid or unsupported MCP proxy type',
+     },
+     id: null,
+   }));
+   return;
+ }
+ const action = typeHeader as ProxyAction;
+ await this.proxyHandlerMap[action]!(req, res);

214-220: 🛠️ Refactor suggestion

Add proper null checking for detail before using non-null assertion.

Using non-null assertion (!) on detail is risky if detail is null or undefined. Add proper type checking before usage.

  const detail = await apiClient.getClient(sessionId);
- if (detail?.pid !== process.pid) {
+ if (!detail) {
+   ctx.status = 404;
+   ctx.body = {
+     jsonrpc: '2.0',
+     error: {
+       code: -32001,
+       message: 'Session not found',
+     },
+     id: null,
+   };
+   return true;
+ } else if (detail.pid !== process.pid) {
   await apiClient.proxyMessage(ctx, {
-    detail: detail!,
+    detail,
     sessionId,
     type,
   });
🧹 Nitpick comments (7)
plugin/controller/lib/impl/mcp/MCPControllerRegister.ts (5)

28-41: Document the hook interface methods.

The MCPControllerHook interface defines several important lifecycle hooks without any documentation. Add JSDoc comments to explain when each hook is called and what it should do.

export interface MCPControllerHook {
+  /**
+   * Called before initializing an SSE transport. Useful for authentication,
+   * authorization, or custom transport configuration.
+   */
  preSSEInitHandle?: (ctx: Context, transport: SSEServerTransport, register: MCPControllerRegister) => Promise<void>
+  /**
+   * Called before handling an SSE message request.
+   */
  preHandleInitHandle?: (ctx: Context) => Promise<void>

  // STREAM
+  /**
+   * Called before handling a STREAM request.
+   */
  preHandle?: (ctx: Context) => Promise<void>
+  /**
+   * Called when a STREAM transport session is initialized.
+   */
  onStreamSessionInitialized?: (ctx: Context, transport: StreamableHTTPServerTransport, register: MCPControllerRegister) => Promise<void>

  // COMMON
+  /**
+   * Called before proxying a request to another process or server.
+   */
  preProxy?: (ctx: Context, proxyReq: http.IncomingMessage, proxyResp: http.ServerResponse) => Promise<void>
+  /**
+   * Called to load JSON schema for MCP prompt or tool.
+   */
  schemaLoader?: (controllerMeta: MCPControllerMeta, meta: MCPPromptMeta | MCPToolMeta) => Promise<Parameters<McpServer['tool']>['2'] | Parameters<McpServer['prompt']>['2']>
+  /**
+   * Called to check if the request should be proxied to another process.
+   * Return true if the proxy was handled, false otherwise.
+   */
  checkAndRunProxy?: (ctx: Context, type: MCPProtocols, sessionId: string) => Promise<boolean>;
}

74-77: Document intended usage of the hooks API.

The static addHook method lacks documentation explaining how and when to use it, which is crucial for developers integrating with this API.

+  /**
+   * Add a hook to customize the behavior of MCP controllers.
+   * Hooks can intercept and modify requests, responses, and other lifecycle events.
+   * Multiple hooks can be registered and will be executed in the order they were added.
+   * 
+   * @param hook The hook implementation with optional lifecycle methods
+   */
  static addHook(hook: MCPControllerHook) {
    MCPControllerRegister.hooks.push(hook);
  }

81-81: Remove unnecessary this aliases.

Arrow functions inherit this from their enclosing scope, making these aliases unnecessary and potentially confusing.

- const self = this;

This applies to lines 81, 196, 250, 319, 364, and 401.

Also applies to: 196-196, 250-250, 319-319, 364-365, 401-402

🧰 Tools
🪛 Biome (1.9.4)

[error] 81-81: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


326-338: Translate the Chinese comments for better maintainability.

The file has Chinese comments which can be difficult for international contributors to understand.

- // 如果有 schema 则证明入参第一个就是 schema
+ // If there's a schema, the first parameter is the schema
  newArgs[promptMeta.detail.index] = args[0];

- // 如果有 schema 则证明入参第二个就是 extra
+ // If there's a schema, the second parameter is extra
  if (promptMeta.extra) {
    newArgs[promptMeta.extra] = args[1];
  }
} else if (promptMeta.extra) {
- // 无 schema, 那么入参第一个就是 extra
+ // Without schema, the first parameter is extra
  newArgs[promptMeta.extra] = args[0];
}

341-342: Use type-safe approach for registering handlers.

The code uses unsafe type casting with as unknown as [any, any, any, any] when calling MCP server methods, which bypasses type checking.

- this.mcpServer.prompt(...(args as unknown as [any, any, any, any]));
+ // Define specific generic types for each handler to improve type safety
+ type PromptHandler = Parameters<typeof this.mcpServer.prompt>;
+ this.mcpServer.prompt(...(args as unknown as PromptHandler));

Apply similar changes to the other MCP server method calls.

Also applies to: 385-386, 408-409

plugin/mcp-proxy/index.ts (2)

73-74: Remove unnecessary this alias with ESLint disable comment.

The ESLint disable comment is used but you're still using a this alias, which is unnecessary in arrow functions.

- // eslint-disable-next-line @typescript-eslint/no-this-alias
- const apiClient = this;

Update the code to use this directly instead of apiClient throughout the hooks.


274-304: Enhance error handling for getRawBody failures.

The error handling for request body parsing has room for improvement. The current code catches errors but doesn't specify what types are expected.

  try {
    let encoding = 'utf-8';
    if (ctx.req.headers['content-type']) {
      const ct = contentType.parse(ctx.req.headers['content-type'] ?? '');
      if (ct.type !== 'application/json') {
        throw new Error(`Unsupported content-type: ${ct}`);
      }
      encoding = ct.parameters.charset;
    }

    // ctx.respond = false;

    body = await getRawBody(ctx.req, {
      limit: MAXIMUM_MESSAGE_SIZE,
      encoding,
    });
-  } catch (error) {
+  } catch (error: unknown) {
    this.logger.error(error);
+   const errorMessage = error instanceof Error ? error.message : String(error);
    ctx.res.writeHead(400).end(JSON.stringify({
      jsonrpc: '2.0',
      error: {
        code: -32602,
-       message: `Bad Request: ${String(error)}`,
+       message: `Bad Request: ${errorMessage}`,
      },
      id: null,
    }));
    return;
  }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4a6d17d and 33941b3.

📒 Files selected for processing (2)
  • plugin/controller/lib/impl/mcp/MCPControllerRegister.ts (1 hunks)
  • plugin/mcp-proxy/index.ts (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
plugin/mcp-proxy/index.ts (2)
plugin/controller/lib/impl/mcp/MCPControllerRegister.ts (2)
  • MCPControllerHook (28-41)
  • MCPControllerRegister (44-450)
plugin/mcp-proxy/lib/MCPProxyDataClient.ts (1)
  • MCPProxyDataClient (4-39)
🪛 Biome (1.9.4)
plugin/controller/lib/impl/mcp/MCPControllerRegister.ts

[error] 81-81: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 196-196: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 250-250: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 319-319: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 365-365: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 402-402: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)

⏰ Context from checks skipped due to timeout of 90000ms (7)
  • GitHub Check: Runner-macos (20)
  • GitHub Check: Runner-ubuntu (22)
  • GitHub Check: Runner-ubuntu (18)
  • GitHub Check: Runner-macos (18)
  • GitHub Check: Runner-ubuntu (20)
  • GitHub Check: Runner-ubuntu (16)
  • GitHub Check: Runner-macos (16)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (22)
core/controller-decorator/src/decorator/mcp/MCPController.ts (4)

8-8: Rename shadowed parameter to avoid noShadowRestrictedNames lint error

Using constructor as the parameter name shadows the global constructor property and fails @biome / ESLint restricted-names rules. Renaming also makes the intent (the decorated class) clearer.

-  return function(constructor: EggProtoImplClass) {
+  return function(clazz: EggProtoImplClass) {
🧰 Tools
🪛 Biome (1.9.4)

[error] 8-8: Do not shadow the global "constructor" property.

Consider renaming this variable. It's easy to confuse the origin of variables when they're named after a known global.

(lint/suspicious/noShadowRestrictedNames)


26-31: ⚠️ Potential issue

Update the remaining instances of the renamed parameter

Complete the renaming in the MCP metadata setting.

    if (param?.name) {
-      MCPInfoUtil.setMCPName(param.name, constructor);
+      MCPInfoUtil.setMCPName(param.name, clazz);
    }
    if (param?.version) {
-      MCPInfoUtil.setMCPVersion(param.version, constructor);
+      MCPInfoUtil.setMCPVersion(param.version, clazz);
    }

24-24: ⚠️ Potential issue

Use the renamed parameter in the file path registration

Apply the parameter rename consistently in the setFilePath call as well.

-    PrototypeUtil.setFilePath(constructor, StackUtil.getCalleeFromStack(false, 5));
+    PrototypeUtil.setFilePath(clazz, StackUtil.getCalleeFromStack(false, 5));

13-17: ⚠️ Potential issue

Update all occurrences of the renamed parameter

After renaming the parameter in the function signature, make sure to update all references to it within the function body.

-    func(constructor);
-    ControllerInfoUtil.setControllerType(constructor, ControllerType.MCP);
-    if (param?.controllerName) {
-      ControllerInfoUtil.setControllerName(constructor, param?.controllerName);
-    }
+    func(clazz);
+    ControllerInfoUtil.setControllerType(clazz, ControllerType.MCP);
+    if (param?.controllerName) {
+      ControllerInfoUtil.setControllerName(clazz, param?.controllerName);
+    }
plugin/mcp-proxy/test/proxy.test.ts (3)

48-48: Add proper null checking instead of using non-null assertion

Using the non-null assertion operator (!) can lead to runtime errors if item.data is actually null. Consider using proper null checking or providing a default value.

- text: item.data!.toString(),
+ text: item.data ? item.data.toString() : '',

91-93: Remove duplicate after hook

There are two identical after hooks that call app.close(). This causes a linting error (noDuplicateTestHooks) and could lead to confusion or potential issues.

- after(() => {
-   return app.close();
- });
🧰 Tools
🪛 Biome (1.9.4)

[error] 91-93: Disallow duplicate setup and teardown hooks.

Disallow after duplicacy inside the describe function.

(lint/suspicious/noDuplicateTestHooks)


95-160: DRY: Extract common test logic to a shared function

The "sse should work" and "streamable should work" tests contain nearly identical logic with minor differences in transport types. Consider refactoring to extract the common test logic into a shared function.

+ async function testTransport(
+   transportType: 'sse' | 'streamable',
+   createTransport: (url: URL) => SSEClientTransport | StreamableHTTPClientTransport,
+   getBaseUrl: () => Promise<string>
+ ) {
+   const client = new Client({
+     name: `${transportType}-demo-client`,
+     version: '1.0.0',
+   });
+   const baseUrl = await getBaseUrl();
+   const transport = createTransport(new URL(baseUrl));
+   const notifications: { level: string, data: string }[] = [];
+   client.setNotificationHandler(LoggingMessageNotificationSchema, notification => {
+     notifications.push({ level: notification.params.level, data: notification.params.data as string });
+   });
+   await client.connect(transport);
+   const tools = await listTools(client);
+   const notificationResp = await startNotificationTool(client);
+   await new Promise(resolve => setTimeout(resolve, 5000));
+   
+   if (transportType === 'streamable') {
+     await transport.terminateSession();
+     await client.close();
+   } else {
+     await transport.close();
+   }
+   
+   assert.deepEqual(tools, [
+     {
+       name: 'start-notification-stream',
+       description: 'Starts sending periodic notifications for testing resumability',
+     },
+   ]);
+   assert.deepEqual(notificationResp, [{ text: 'Started sending periodic notifications every 1000ms' }]);
+   assert.deepEqual(notifications, [
+     { level: 'info', data: 'Periodic notification #1' },
+     { level: 'info', data: 'Periodic notification #2' },
+     { level: 'info', data: 'Periodic notification #3' },
+     { level: 'info', data: 'Periodic notification #4' },
+     { level: 'info', data: 'Periodic notification #5' },
+   ]);
+ }

  it('sse should work', async () => {
-   const sseClient = new Client({
-     name: 'sse-demo-client',
-     version: '1.0.0',
-   });
-   const baseUrl = await app.httpRequest()
-     .get('/init').url;
-   const sseTransport = new SSEClientTransport(new URL(baseUrl));
-   const sseNotifications: { level: string, data: string }[] = [];
-   sseClient.setNotificationHandler(LoggingMessageNotificationSchema, notification => {
-     sseNotifications.push({ level: notification.params.level, data: notification.params.data as string });
-   });
-   await sseClient.connect(sseTransport);
-   const tools = await listTools(sseClient);
-   const notificationResp = await startNotificationTool(sseClient);
-   await new Promise(resolve => setTimeout(resolve, 5000));
-   await sseTransport.close();
-   assert.deepEqual(tools, [
-     {
-       name: 'start-notification-stream',
-       description: 'Starts sending periodic notifications for testing resumability',
-     },
-   ]);
-   assert.deepEqual(notificationResp, [{ text: 'Started sending periodic notifications every 1000ms' }]);
-   assert.deepEqual(sseNotifications, [
-     { level: 'info', data: 'Periodic notification #1' },
-     { level: 'info', data: 'Periodic notification #2' },
-     { level: 'info', data: 'Periodic notification #3' },
-     { level: 'info', data: 'Periodic notification #4' },
-     { level: 'info', data: 'Periodic notification #5' },
-   ]);
+   await testTransport(
+     'sse',
+     url => new SSEClientTransport(url),
+     async () => await app.httpRequest().get('/init').url
+   );
  });

  it('streamable should work', async () => {
-   const streamableClient = new Client({
-     name: 'streamable-demo-client',
-     version: '1.0.0',
-   });
-   const baseUrl = await app.httpRequest()
-     .post('/stream').url;
-   const streamableTransport = new StreamableHTTPClientTransport(new URL(baseUrl));
-   const streamableNotifications: { level: string, data: string }[] = [];
-   streamableClient.setNotificationHandler(LoggingMessageNotificationSchema, notification => {
-     streamableNotifications.push({ level: notification.params.level, data: notification.params.data as string });
-   });
-   await streamableClient.connect(streamableTransport);
-   const tools = await listTools(streamableClient);
-   const notificationResp = await startNotificationTool(streamableClient);
-   await new Promise(resolve => setTimeout(resolve, 5000));
-   await streamableTransport.terminateSession();
-   await streamableClient.close();
-   assert.deepEqual(tools, [
-     {
-       name: 'start-notification-stream',
-       description: 'Starts sending periodic notifications for testing resumability',
-     },
-   ]);
-   assert.deepEqual(notificationResp, [{ text: 'Started sending periodic notifications every 1000ms' }]);
-   assert.deepEqual(streamableNotifications, [
-     { level: 'info', data: 'Periodic notification #1' },
-     { level: 'info', data: 'Periodic notification #2' },
-     { level: 'info', data: 'Periodic notification #3' },
-     { level: 'info', data: 'Periodic notification #4' },
-     { level: 'info', data: 'Periodic notification #5' },
-   ]);
+   await testTransport(
+     'streamable',
+     url => new StreamableHTTPClientTransport(url),
+     async () => await app.httpRequest().post('/stream').url
+   );
  });
plugin/controller/test/mcp/mcp.test.ts (3)

48-48: Add proper null checking instead of using non-null assertion

Using the non-null assertion operator (!) can lead to runtime errors if item.data is actually null. Consider using proper null checking or providing a default value.

- text: item.data!.toString(),
+ text: item.data ? item.data.toString() : '',

91-93: Remove duplicate after hook

There are two identical after hooks that call app.close() (lines 61-63 and 91-93). This causes a linting error and could lead to confusion or potential issues.

- after(() => {
-   return app.close();
- });
🧰 Tools
🪛 Biome (1.9.4)

[error] 91-93: Disallow duplicate setup and teardown hooks.

Disallow after duplicacy inside the describe function.

(lint/suspicious/noDuplicateTestHooks)


95-277: DRY: Extract common test logic between SSE and streamable tests

Both "sse should work" and "streamable should work" tests contain nearly identical logic with minor differences in transport types. Consider refactoring to create a common test function that takes the transport type and specific functions as parameters.

+ async function testMCPTransport(
+   transportType: 'sse' | 'streamable',
+   createTransport: (url: URL) => SSEClientTransport | StreamableHTTPClientTransport,
+   getEndpointUrl: () => Promise<string>
+ ) {
+   const client = new Client({
+     name: `${transportType}-demo-client`,
+     version: '1.0.0',
+   });
+   const baseUrl = await getEndpointUrl();
+   const transport = createTransport(new URL(baseUrl));
+   const notifications: { level: string, data: string }[] = [];
+   client.setNotificationHandler(LoggingMessageNotificationSchema, notification => {
+     notifications.push({ level: notification.params.level, data: notification.params.data as string });
+   });
+   await client.connect(transport);
+   
+   // Test tools
+   const tools = await listTools(client);
+   assert.deepEqual(tools, [
+     {
+       name: 'start-notification-stream',
+       description: 'Starts sending periodic notifications for testing resumability',
+     },
+     {
+       description: undefined,
+       name: 'bar',
+     },
+   ]);
+
+   const toolRes = await client.callTool({
+     name: 'bar',
+     arguments: {
+       name: 'aaa',
+     },
+   });
+   assert.deepEqual(toolRes, {
+     content: [{ type: 'text', text: 'npm package: aaa not found' }],
+   });
+   
+   // Test notifications
+   const notificationResp = await startNotificationTool(client);
+   await new Promise(resolve => setTimeout(resolve, 5000));
+   assert.deepEqual(notificationResp, [{ text: 'Started sending periodic notifications every 1000ms' }]);
+   assert.deepEqual(notifications, [
+     { level: 'info', data: 'Periodic notification #1' },
+     { level: 'info', data: 'Periodic notification #2' },
+     { level: 'info', data: 'Periodic notification #3' },
+     { level: 'info', data: 'Periodic notification #4' },
+     { level: 'info', data: 'Periodic notification #5' },
+   ]);
+
+   // Test resources
+   const resources = await client.listResources();
+   assert.deepEqual(resources, {
+     resources: [
+       { uri: 'mcp://npm/egg?version=4.10.0', name: 'egg' },
+       { uri: 'mcp://npm/mcp?version=0.10.0', name: 'mcp' },
+     ],
+   });
+
+   const resourceRes = await client.readResource({
+     uri: 'mcp://npm/egg?version=4.10.0',
+   });
+   assert.deepEqual(resourceRes, {
+     contents: [{ uri: 'mcp://npm/egg?version=4.10.0', text: 'MOCK TEXT' }],
+   });
+
+   // Test prompts
+   const prompts = await client.listPrompts();
+   assert.deepEqual(prompts, {
+     prompts: [
+       { name: 'foo', arguments: [{ name: 'name', required: true }] },
+     ],
+   });
+
+   const promptRes = await client.getPrompt({
+     name: 'foo',
+     arguments: {
+       name: 'bbb',
+     },
+   });
+   assert.deepEqual(promptRes, {
+     messages: [
+       {
+         role: 'user',
+         content: {
+           type: 'text',
+           text: 'Generate a concise but descriptive commit message for these changes:\n\nbbb',
+         },
+       },
+     ],
+   });
+
+   // Close transport appropriately based on type
+   if (transportType === 'sse') {
+     await (transport as SSEClientTransport).close();
+   } else {
+     await (transport as StreamableHTTPClientTransport).terminateSession();
+     await client.close();
+   }
+ }

  it('sse should work', async () => {
-   // [... existing implementation ...]
+   await testMCPTransport(
+     'sse',
+     url => new SSEClientTransport(url),
+     async () => await app.httpRequest().get('/mcp/init').url
+   );
  });

  it('streamable should work', async () => {
-   // [... existing implementation ...]
+   await testMCPTransport(
+     'streamable',
+     url => new StreamableHTTPClientTransport(url),
+     async () => await app.httpRequest().post('/mcp/stream').url
+   );
  });
plugin/mcp-proxy/test/fixtures/apps/mcp-proxy/app/controller/app.ts (3)

192-193: 🛠️ Refactor suggestion

Add proper type checking for detail before using non-null assertion.

Using non-null assertion (!) on detail is risky if detail is null or undefined. Add proper null checking before usage.

    if (detail?.pid !== process.pid) {
      await this.app.mcpProxy.proxyMessage(this.ctx, {
-        detail: detail!,
+        detail,
        sessionId: this.ctx.request.query.sessionId,
        type: MCPProtocols.SSE,
      });

275-279: 🛠️ Refactor suggestion

Add proper null checking for sid before using non-null assertion.

Using non-null assertion (!) on sid is risky if sid is null or undefined. Add proper type checking instead.

  const sid = transport.sessionId;
  if (sid && transports[sid]) {
    delete transports[sid];
+   await this.app.mcpProxy.unregisterClient(sid);
+ } else if (sid) {
+   await this.app.mcpProxy.unregisterClient(sid);
  }
- await this.app.mcpProxy.unregisterClient(sid!);

296-302: ⚠️ Potential issue

Add proper null checking for detail before using non-null assertion.

Using non-null assertion (!) on detail in line 299 is risky if detail is null or undefined. Add proper null checking before usage.

  const detail = await this.app.mcpProxy.getClient(sessionId);
- if (detail?.pid !== process.pid) {
+ if (!detail) {
+   this.ctx.status = 404;
+   this.ctx.body = {
+     jsonrpc: '2.0',
+     error: {
+       code: -32001,
+       message: 'Session not found',
+     },
+     id: null,
+   };
+   return;
+ } else if (detail.pid !== process.pid) {
    await this.app.mcpProxy.proxyMessage(this.ctx, {
-     detail: detail!,
+     detail,
      sessionId,
      type: MCPProtocols.STREAM,
    });
plugin/controller/lib/impl/mcp/MCPControllerRegister.ts (9)

74-77: Document intended usage of the addHook method.

The static addHook method allows adding hooks to customize the MCP controller behavior, but there's no documentation on when and how to use it.

+  /**
+   * Add a hook to customize the behavior of MCP controllers.
+   * Hooks can modify request/response handling, load schemas, or intercept proxy requests.
+   * 
+   * @param hook MCPControllerHook implementation with optional lifecycle methods
+   */
  static addHook(hook: MCPControllerHook) {
    MCPControllerRegister.hooks.push(hook);
  }

341-342: Use type-safe approach for registering handlers.

The code uses unsafe type casting with as unknown as [any, any, any, any] when calling MCP server methods, which bypasses type checking. Consider creating properly typed wrappers for the MCP server methods.

A safer approach would be to use spread operators with explicit typing:

-    this.mcpServer.prompt(...(args as unknown as [any, any, any, any]));
+    // Define the proper argument types
+    type PromptArgs = Parameters<typeof this.mcpServer.prompt>;
+    // Apply spread operator with explicit typing
+    this.mcpServer.prompt(...(args as PromptArgs));

Apply similar changes to the other server method calls.

Also applies to: 385-386, 408-409


82-87: 🛠️ Refactor suggestion

Add error handling for hook execution.

Hook executions aren't wrapped in try/catch blocks, which could lead to unhandled promise rejections if a hook throws an exception.

  if (MCPControllerRegister.hooks.length > 0) {
    for (const hook of MCPControllerRegister.hooks) {
-     await hook.preHandle?.(self.app.currentContext);
+     try {
+       await hook.preHandle?.(this.app.currentContext);
+     } catch (error) {
+       this.app.logger.error('Hook execution error:', error);
+       ctx.status = 500;
+       ctx.body = {
+         jsonrpc: '2.0',
+         error: {
+           code: -32603,
+           message: `Internal server error: ${error.message}`,
+         },
+         id: null,
+       };
+       return;
+     }
    }
  }

283-291: ⚠️ Potential issue

Add error response for unknown session in message handler.

If a session ID is provided but not found in self.transports, and no hook handles it, the function returns without providing an error response to the client.

  }
  if (MCPControllerRegister.hooks.length > 0) {
    for (const hook of MCPControllerRegister.hooks) {
      const checked = await hook.checkAndRunProxy?.(self.app.currentContext, MCPProtocols.SSE, sessionId);
      if (checked) {
        return;
      }
    }
  }
+ // If we reach here, the session was not found and no hook handled it
+ ctx.status = 404;
+ ctx.body = {
+   jsonrpc: '2.0',
+   error: {
+     code: -32001,
+     message: 'Session not found',
+   },
+   id: null,
+ };

381-383: ⚠️ Potential issue

Fix the same argument handling bug in mcpToolRegister.

The same bug exists in the mcpToolRegister method - newArgs is created but then not used in the final Reflect.apply call.

-      newArgs = [ ...newArgs, ...args ];
-      return Reflect.apply(realMethod, realObj, newArgs);
+      // Fill any gaps in newArgs with undefined
+      const maxIndex = Math.max(
+        toolMeta.detail?.index ?? -1,
+        toolMeta.extra ?? -1
+      );
+      for (let i = 0; i <= maxIndex; i++) {
+        if (newArgs[i] === undefined) {
+          newArgs[i] = undefined;
+        }
+      }
+      return Reflect.apply(realMethod, realObj, newArgs);

432-439: 🛠️ Refactor suggestion

Fix initialization order to prevent potential race conditions.

mcpStreamServerInit, mcpServerInit, and mcpServerRegister rely on this.mcpServer being non-null, yet they are called immediately after creation and before awaiting any async work. If later code calls register() concurrently, there's a race.

    if (!this.mcpServer) {
      this.mcpServer = new McpServer({
        name: this.controllerMeta.name ?? `chair-mcp-${this.app.name}-server`,
        version: this.controllerMeta.version ?? '1.0.0',
      }, { capabilities: { logging: {} } });
-      this.mcpStreamServerInit();
-      this.mcpServerInit();
-      this.mcpServerRegister();
+      await Promise.all([
+        this.mcpStreamServerInit(),
+        this.mcpServerInit(),
+        this.mcpServerRegister()
+      ]);
    }

179-181: ⚠️ Potential issue

Add error response for unknown session.

If a session ID is provided but not found, and no hook handles it, the function returns without providing an error response to the client.

        }
      }
+     // If we reach here, the session ID was provided but not found and no hook handled it
+     ctx.status = 404;
+     ctx.body = {
+       jsonrpc: '2.0',
+       error: {
+         code: -32001,
+         message: 'Session not found',
+       },
+       id: null,
+     };
    }
    return;

255-257: ⚠️ Potential issue

Add validation for session ID.

The session ID is used without validation. If it's undefined, this can lead to errors when using it as an object key.

-      const sessionId = ctx.query.sessionId;
+      const sessionId = ctx.query.sessionId as string | undefined;
+      if (!sessionId) {
+        ctx.status = 400;
+        ctx.body = {
+          jsonrpc: '2.0',
+          error: {
+            code: -32000,
+            message: 'Bad Request: sessionId is required',
+          },
+          id: null,
+        };
+        return;
+      }

28-41: 🛠️ Refactor suggestion

Add JSDoc comments to document the MCPControllerHook interface.

The MCPControllerHook interface defines several important lifecycle methods but lacks documentation explaining when each method is called and what it should do.

 export interface MCPControllerHook {
+  /**
+   * Called before initializing an SSE transport.
+   * Can be used to customize the transport or perform authentication.
+   * @param ctx Current request context
+   * @param transport The SSE transport being initialized
+   * @param register Reference to the MCPControllerRegister instance
+   */
   // SSE
   preSSEInitHandle?: (ctx: Context, transport: SSEServerTransport, register: MCPControllerRegister) => Promise<void>
+  /**
+   * Called before handling an SSE message request.
+   * Can be used for authentication or request validation.
+   * @param ctx Current request context
+   */
   preHandleInitHandle?: (ctx: Context) => Promise<void>

+  /**
+   * Called before handling a stream request.
+   * Can be used for authentication or request validation.
+   * @param ctx Current request context
+   */
   // STREAM
   preHandle?: (ctx: Context) => Promise<void>
+  /**
+   * Called after a stream session is initialized.
+   * Can be used for session setup or custom logic.
+   * @param ctx Current request context
+   * @param transport The stream transport that was initialized
+   * @param register Reference to the MCPControllerRegister instance
+   */
   onStreamSessionInitialized?: (ctx: Context, transport: StreamableHTTPServerTransport, register: MCPControllerRegister) => Promise<void>

+  /**
+   * Called before proxying a request.
+   * Can be used to modify the proxy request or response.
+   * @param ctx Current request context
+   * @param proxyReq The proxy request
+   * @param proxyResp The proxy response
+   */
   // COMMON
   preProxy?: (ctx: Context, proxyReq: http.IncomingMessage, proxyResp: http.ServerResponse) => Promise<void>
+  /**
+   * Called to load schemas for prompts or tools.
+   * Can be used to dynamically load schemas from external sources.
+   * @param controllerMeta Controller metadata
+   * @param meta Prompt or tool metadata
+   * @returns Schema for the prompt or tool
+   */
   schemaLoader?: (controllerMeta: MCPControllerMeta, meta: MCPPromptMeta | MCPToolMeta) => Promise<Parameters<McpServer['tool']>['2'] | Parameters<McpServer['prompt']>['2']>
+  /**
+   * Called to check if a proxy can handle a request.
+   * Can be used to delegate request handling to a proxy.
+   * @param ctx Current request context
+   * @param type Protocol type (SSE or STREAM)
+   * @param sessionId Session ID
+   * @returns True if the proxy handled the request, false otherwise
+   */
   checkAndRunProxy?: (ctx: Context, type: MCPProtocols, sessionId: string) => Promise<boolean>;
 }
🧹 Nitpick comments (9)
core/controller-decorator/src/decorator/mcp/MCPController.ts (1)

18-24: Remove redundant comments showing import paths

These comments appear to be debug information about import paths and don't add value to the code. Consider removing them for a cleaner implementation.

-    // './tegg/core/common-util/src/StackUtil.ts',
-    // './tegg/core/core-decorator/src/decorator/Prototype.ts',
-    // './tegg/core/controller-decorator/src/decorator/tr/TRController.ts',
-    // './tegg/core/core-decorator/node_modules/[email protected]@reflect-metadata/Reflect.js',
-    // './tegg/core/core-decorator/node_modules/[email protected]@reflect-metadata/Reflect.js',
-    // './tegg/core/controller-decorator/test/fixtures/TRFooController.ts',
     PrototypeUtil.setFilePath(clazz, StackUtil.getCalleeFromStack(false, 5));
plugin/controller/test/mcp/mcp.test.ts (1)

58-58: Add Node.js version check explanation

The test is conditionally run only on Node.js versions greater than 17. Consider adding a comment explaining why this version restriction exists to help future maintainers understand the constraints.

-  if (parseInt(process.version.slice(1, 3)) > 17) {
+  // MCP functionality requires Node.js version > 17 due to dependencies on newer features
+  if (parseInt(process.version.slice(1, 3)) > 17) {
plugin/mcp-proxy/test/fixtures/apps/mcp-proxy/app/controller/app.ts (5)

171-172: Remove unnecessary this aliasing.

The const self = this declaration is unnecessary in this context since arrow functions inherit this from their enclosing scope.

-    const self = this;
-    const transport = new SSEServerTransport('/message', this.ctx.res);
+    const transport = new SSEServerTransport('/message', this.ctx.res);
🧰 Tools
🪛 Biome (1.9.4)

[error] 171-171: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


174-176: Improve type safety in proxy handler function.

The transport variable is accessible directly from the outer scope, so there's no need to use self here.

    this.app.mcpProxy.setProxyHandler(MCPProtocols.SSE, async (req, res) => {
-      return await self.ssePostHandler(req, res);
+      return await this.ssePostHandler(req, res);
    });

208-212: Improve error message for missing session ID.

The error message "session id not have and run in proxy" is not grammatically correct and could be clearer.

    if (!sessionId) {
      res.status(500).json({
        jsonrpc: '2.0',
        error: {
          code: -32603,
-          message: 'session id not have and run in proxy',
+          message: 'Session ID is required but not provided',
        },
        id: null,
      });

260-262: Remove unnecessary this aliasing.

The const self = this declaration is unnecessary in this context since arrow functions inherit this from their enclosing scope.

-        const self = this;
        const transport = new StreamableHTTPServerTransport({
          sessionIdGenerator: () => randomUUID(),
          eventStore,
          onsessioninitialized: async sessionId => {
🧰 Tools
🪛 Biome (1.9.4)

[error] 261-261: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


267-269: Improve type safety in proxy handler function.

Similar to the SSE handler, there's no need to use self here.

            this.app.mcpProxy.setProxyHandler(MCPProtocols.STREAM, async (req, res) => {
-              return await self.streamPostHandler(req, res);
+              return await this.streamPostHandler(req, res);
            });
core/controller-decorator/src/util/MCPInfoUtil.ts (1)

40-156: Consider using namespaced functions instead of a static-only class.

This class contains only static members, which is generally an anti-pattern. Consider using simple exported functions or a TypeScript namespace instead of a class with only static methods.

For example, you could refactor this to:

export namespace MCPInfoUtil {
  export function setMCPName(name: string, clazz: EggProtoImplClass) {
    MetadataUtil.defineMetaData(CONTROLLER_MCP_NAME, name, clazz);
  }

  export function getMCPName(clazz: EggProtoImplClass): string | undefined {
    return MetadataUtil.getMetaData(CONTROLLER_MCP_NAME, clazz);
  }
  
  // ... other functions
}

Or use individual exported functions:

export function setMCPName(name: string, clazz: EggProtoImplClass) {
  MetadataUtil.defineMetaData(CONTROLLER_MCP_NAME, name, clazz);
}

export function getMCPName(clazz: EggProtoImplClass): string | undefined {
  return MetadataUtil.getMetaData(CONTROLLER_MCP_NAME, clazz);
}

// ... other functions

This would align better with modern JavaScript/TypeScript practices and avoid the class-that-is-not-a-class pattern.

🧰 Tools
🪛 Biome (1.9.4)

[error] 40-156: Avoid classes that contain only static members.

Prefer using simple functions instead of classes with only static members.

(lint/complexity/noStaticOnlyClass)

plugin/controller/lib/impl/mcp/MCPControllerRegister.ts (1)

81-81: Remove unnecessary this aliasing.

Arrow functions inherit this from their enclosing scope, making the alias unnecessary.

-    const self = this;

The code would need to be updated throughout to replace self with this.

🧰 Tools
🪛 Biome (1.9.4)

[error] 81-81: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 33941b3 and 164f5fb.

📒 Files selected for processing (37)
  • core/controller-decorator/index.ts (2 hunks)
  • core/controller-decorator/src/decorator/mcp/Extra.ts (1 hunks)
  • core/controller-decorator/src/decorator/mcp/MCPController.ts (1 hunks)
  • core/controller-decorator/src/decorator/mcp/MCPPrompt.ts (1 hunks)
  • core/controller-decorator/src/decorator/mcp/MCPResource.ts (1 hunks)
  • core/controller-decorator/src/decorator/mcp/MCPTool.ts (1 hunks)
  • core/controller-decorator/src/impl/mcp/MCPControllerMetaBuilder.ts (1 hunks)
  • core/controller-decorator/src/impl/mcp/MCPControllerPromptMetaBuilder.ts (1 hunks)
  • core/controller-decorator/src/impl/mcp/MCPControllerResourceMetaBuilder.ts (1 hunks)
  • core/controller-decorator/src/impl/mcp/MCPControllerToolMetaBuilder.ts (1 hunks)
  • core/controller-decorator/src/model/MCPPromptMeta.ts (1 hunks)
  • core/controller-decorator/src/model/MCPResourceMeta.ts (1 hunks)
  • core/controller-decorator/src/model/MCPToolMeta.ts (1 hunks)
  • core/controller-decorator/src/util/MCPInfoUtil.ts (1 hunks)
  • core/controller-decorator/test/MCPMeta.test.ts (1 hunks)
  • core/controller-decorator/test/fixtures/MCPController.ts (1 hunks)
  • core/types/controller-decorator/MCPController.ts (1 hunks)
  • core/types/controller-decorator/MCPPromptParams.ts (1 hunks)
  • core/types/controller-decorator/MCPToolParams.ts (1 hunks)
  • core/types/controller-decorator/MetadataKey.ts (1 hunks)
  • core/types/package.json (1 hunks)
  • plugin/controller/app.ts (2 hunks)
  • plugin/controller/app/middleware/mcp_body_middleware.ts (1 hunks)
  • plugin/controller/config/config.default.ts (1 hunks)
  • plugin/controller/lib/impl/mcp/MCPControllerRegister.ts (1 hunks)
  • plugin/controller/test/fixtures/apps/mcp-app/app/controller/AppController.ts (1 hunks)
  • plugin/controller/test/fixtures/apps/mcp-app/app/controller/McpController.ts (1 hunks)
  • plugin/controller/test/fixtures/apps/mcp-app/config/plugin.js (1 hunks)
  • plugin/controller/test/mcp/mcp.test.ts (1 hunks)
  • plugin/mcp-proxy/agent.ts (1 hunks)
  • plugin/mcp-proxy/app.ts (1 hunks)
  • plugin/mcp-proxy/config/config.default.ts (1 hunks)
  • plugin/mcp-proxy/index.ts (1 hunks)
  • plugin/mcp-proxy/package.json (1 hunks)
  • plugin/mcp-proxy/test/fixtures/apps/mcp-proxy/app/controller/app.ts (1 hunks)
  • plugin/mcp-proxy/test/proxy.test.ts (1 hunks)
  • plugin/mcp-proxy/typings/index.d.ts (1 hunks)
✅ Files skipped from review due to trivial changes (4)
  • core/controller-decorator/src/decorator/mcp/Extra.ts
  • core/controller-decorator/src/model/MCPResourceMeta.ts
  • core/types/controller-decorator/MCPPromptParams.ts
  • plugin/controller/test/fixtures/apps/mcp-app/app/controller/McpController.ts
🚧 Files skipped from review as they are similar to previous changes (26)
  • plugin/mcp-proxy/config/config.default.ts
  • core/types/package.json
  • plugin/controller/app/middleware/mcp_body_middleware.ts
  • core/controller-decorator/src/decorator/mcp/MCPResource.ts
  • plugin/mcp-proxy/agent.ts
  • plugin/mcp-proxy/app.ts
  • core/types/controller-decorator/MCPController.ts
  • core/controller-decorator/src/impl/mcp/MCPControllerToolMetaBuilder.ts
  • plugin/controller/app.ts
  • plugin/controller/config/config.default.ts
  • core/controller-decorator/src/model/MCPToolMeta.ts
  • core/controller-decorator/src/impl/mcp/MCPControllerPromptMetaBuilder.ts
  • plugin/controller/test/fixtures/apps/mcp-app/app/controller/AppController.ts
  • plugin/mcp-proxy/typings/index.d.ts
  • core/controller-decorator/src/model/MCPPromptMeta.ts
  • core/controller-decorator/src/impl/mcp/MCPControllerResourceMetaBuilder.ts
  • plugin/mcp-proxy/package.json
  • core/controller-decorator/test/MCPMeta.test.ts
  • core/types/controller-decorator/MCPToolParams.ts
  • core/controller-decorator/src/impl/mcp/MCPControllerMetaBuilder.ts
  • core/controller-decorator/src/decorator/mcp/MCPPrompt.ts
  • core/controller-decorator/src/decorator/mcp/MCPTool.ts
  • core/controller-decorator/index.ts
  • core/controller-decorator/test/fixtures/MCPController.ts
  • core/types/controller-decorator/MetadataKey.ts
  • plugin/mcp-proxy/index.ts
🧰 Additional context used
🪛 Biome (1.9.4)
core/controller-decorator/src/decorator/mcp/MCPController.ts

[error] 8-8: Do not shadow the global "constructor" property.

Consider renaming this variable. It's easy to confuse the origin of variables when they're named after a known global.

(lint/suspicious/noShadowRestrictedNames)

core/controller-decorator/src/util/MCPInfoUtil.ts

[error] 40-156: Avoid classes that contain only static members.

Prefer using simple functions instead of classes with only static members.

(lint/complexity/noStaticOnlyClass)

plugin/controller/lib/impl/mcp/MCPControllerRegister.ts

[error] 81-81: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 196-196: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 250-250: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 319-319: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 365-365: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 402-402: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)

plugin/controller/test/fixtures/apps/mcp-app/config/plugin.js

[error] 1-1: Redundant use strict directive.

The entire contents of JavaScript modules are automatically in strict mode, with no statement needed to initiate it.
Safe fix: Remove the redundant use strict directive.

(lint/suspicious/noRedundantUseStrict)

plugin/controller/test/mcp/mcp.test.ts

[error] 91-93: Disallow duplicate setup and teardown hooks.

Disallow after duplicacy inside the describe function.

(lint/suspicious/noDuplicateTestHooks)

plugin/mcp-proxy/test/fixtures/apps/mcp-proxy/app/controller/app.ts

[error] 171-171: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)


[error] 261-261: This aliasing of this is unnecessary.

Arrow functions inherits this from their enclosing scope.
Safe fix: Use this instead of an alias.

(lint/complexity/noUselessThisAlias)

plugin/mcp-proxy/test/proxy.test.ts

[error] 91-93: Disallow duplicate setup and teardown hooks.

Disallow after duplicacy inside the describe function.

(lint/suspicious/noDuplicateTestHooks)

⏰ Context from checks skipped due to timeout of 90000ms (7)
  • GitHub Check: Runner-ubuntu (22)
  • GitHub Check: Runner-macos (20)
  • GitHub Check: Runner-ubuntu (20)
  • GitHub Check: Runner-macos (16)
  • GitHub Check: Runner-macos (18)
  • GitHub Check: Runner-ubuntu (18)
  • GitHub Check: Runner-ubuntu (16)
🔇 Additional comments (2)
plugin/controller/test/fixtures/apps/mcp-app/config/plugin.js (1)

1-27: The plugin configuration looks good!

All plugin configurations are properly defined with unique export names and have consistent structure. The test application is configured to use the necessary plugins for testing MCP functionality.

🧰 Tools
🪛 Biome (1.9.4)

[error] 1-1: Redundant use strict directive.

The entire contents of JavaScript modules are automatically in strict mode, with no statement needed to initiate it.
Safe fix: Remove the redundant use strict directive.

(lint/suspicious/noRedundantUseStrict)

core/controller-decorator/src/util/MCPInfoUtil.ts (1)

117-124: The method now correctly uses CONTROLLER_MCP_PROMPT_PARAMS_MAP.

The implementation correctly uses the separate CONTROLLER_MCP_PROMPT_PARAMS_MAP constant to store prompt parameters, which prevents collision with resource parameters. This is a good fix from previous iterations.

@akitaSummer akitaSummer merged commit a9a57b4 into eggjs:master Apr 28, 2025
13 checks passed
@coderabbitai coderabbitai bot mentioned this pull request Apr 29, 2025
4 tasks
@coderabbitai coderabbitai bot mentioned this pull request Jul 1, 2025
4 tasks
@coderabbitai coderabbitai bot mentioned this pull request Jul 26, 2025
4 tasks
@coderabbitai coderabbitai bot mentioned this pull request Nov 5, 2025
4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants