Skip to content

Add support for MCP(sse) in export mode #6481

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

mokeyish
Copy link

@mokeyish mokeyish commented May 8, 2025

closes #6473#6448, #6437,#6323,#6154

💻 变更类型 | Change Type

  • feat
  • fix
  • refactor
  • perf
  • style
  • test
  • docs
  • ci
  • chore
  • build

🔀 变更说明 | Description of Change

📝 补充信息 | Additional Information

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features

    • Added support for both server and client modes, allowing configuration and tool data to be managed via local storage or file system based on build mode.
    • Introduced new build configuration options, including toggles for features like GPT-4, custom config, balance query, and more, all controllable via environment variables.
    • Added global compile-time constant to indicate export mode, improving environment-specific behavior.
    • Implemented a comprehensive MCP client and server management system with lifecycle controls, tool mapping, and configuration persistence.
    • Enabled dynamic delegation of MCP actions to client or server modules based on build mode.
  • Improvements

    • Enhanced type safety and clarity in handling server configurations and tool lists.
    • Improved client lookup logic for tool execution, providing better fallback mechanisms.
    • Optimized initialization for access store in export mode to avoid unnecessary network requests.
    • Updated client creation to dynamically select transport type based on server config and build mode.
    • Simplified tools state management and rendering with clearer typing and type guards.
  • Bug Fixes

    • Refined checks for server configuration types to ensure correct extraction and usage of user configurations.

Copy link

vercel bot commented May 8, 2025

@mokeyish is attempting to deploy a commit to the NextChat Team on Vercel.

A member of the Team first needs to authorize it.

Copy link
Contributor

coderabbitai bot commented May 8, 2025

"""

Walkthrough

This update introduces dual client/server mode support for MCP-related logic, governed by a new global EXPORT_MODE constant. It refactors server configuration types, enhances type safety for tool schemas, expands build configuration options, and adapts client/server behaviors to use localStorage or filesystem as appropriate. Type guards and conditional logic are added throughout.

Changes

File(s) Change Summary
app/components/mcp-market.tsx Refined type handling for the tools state, updated modal rendering logic, and improved server config checks using new type guards for better type safety and clarity. Added imports for ToolSchema and isServerStdioConfig.
app/config/build.ts Extended getBuildConfig to include new properties (feature toggles, URLs, flags) sourced from environment variables, enriching the build configuration.
app/mcp/actions.ts Replaced entire implementation with a dynamic import that conditionally loads either client or server-specific MCP actions module based on EXPORT_MODE. Removed all previous functions and internal logic.
app/mcp/actions.base.ts Added comprehensive MCP client/server management module with functions for initializing, adding, pausing, resuming, removing, restarting clients, managing tools, and executing MCP actions. Supports config loading/saving via file or localStorage depending on mode, with logging and error handling.
app/mcp/actions.client.ts New module re-exporting all exports from actions.base for client-side usage.
app/mcp/actions.server.ts New server-side module re-exporting all exports from actions.base with "use server" directive for server context.
app/mcp/client.ts Modified createClient to dynamically select between SSE and stdio transports using type guards and dynamic imports. Throws error if stdio transport is requested in export mode.
app/mcp/types.ts Refactored server config to discriminated union of ServerStdioConfig and ServerSseConfig with type guards. Added ToolSchema interface and changed tools array type accordingly. Extended McpRequestMessage and McpConfigData interfaces with new properties.
app/store/access.ts Modified fetch logic in useAccessStore to initialize state from client config in export mode before returning early, avoiding unnecessary network requests.
app/typing.ts Declared global constant EXPORT_MODE: boolean for compile-time usage throughout the app.
next.config.mjs Injected EXPORT_MODE as a compile-time constant into webpack build via DefinePlugin for conditional bundling logic.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant MCPMarketPage
    participant MCPClient
    participant ConfigStore
    participant LocalStorage
    participant FileSystem

    User->>MCPMarketPage: Open MCP Market
    MCPMarketPage->>ConfigStore: Load MCP Config
    alt EXPORT_MODE enabled
        ConfigStore->>LocalStorage: Read MCP Config
        LocalStorage-->>ConfigStore: Config Data (possibly merged)
    else Not EXPORT_MODE
        ConfigStore->>FileSystem: Read MCP Config
        FileSystem-->>ConfigStore: Config Data
    end
    MCPMarketPage->>MCPClient: List Tools (with config)
    MCPClient->>MCPMarketPage: Tools List
    MCPMarketPage-->>User: Render Tools Modal
Loading

Assessment against linked issues

Objective Addressed Explanation
Support MCP SSE transport (#6473) Introduced ServerSseConfig type, isServerSseConfig guard, and SSE transport in client creation to support SSE.

Suggested labels

enhancement, planned

Suggested reviewers

  • Dogtiti

Poem

A toggle for export, a config anew,
With type guards and unions, the logic grew.
Now tools are more clear,
And configs appear
From disk or from storage, depending on view!

🐇✨
"""

✨ 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.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

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
Contributor

@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

🔭 Outside diff range comments (1)
app/components/mcp-market.tsx (1)

231-244: 🛠️ Refactor suggestion

Loading indicator never toggles during loadTools

loadTools fetches data asynchronously but never flips isLoading, so the modal may show “No tools available” before the request completes. A dedicated loading flag (or re-use isLoading) fixes the UX:

 const loadTools = async (id: string) => {
-  try {
+  try {
+    setIsLoading(true);
     const result = await getClientTools(id);
     if (result) {
-      setTools(result);
+      setToolsResponse(result);
     } else {
       throw new Error("Failed to load tools");
     }
   } catch (error) {
@@
-    setTools(null);
+    setToolsResponse(null);
   } finally {
+    setIsLoading(false);
   }
 };
🧹 Nitpick comments (5)
app/mcp/client.ts (1)

27-29: Consider adding more context in the error message.

The error message when attempting to use stdio transport in export mode could be more helpful by explaining alternatives.

-      throw new Error("Cannot use stdio transport in export mode");
+      throw new Error("Cannot use stdio transport in export mode. Please use SSE transport configuration instead.");
app/components/mcp-market.tsx (2)

49-53: tools state now holds the whole response – consider renaming for clarity

tools is no longer an array but a ListToolsResponse object. The plural name can mis-lead future readers into thinking it is an array, and makes the later tools.tools access look odd.

-const [tools, setTools] = useState<ListToolsResponse | null>(null);
+const [toolsResponse, setToolsResponse] = useState<ListToolsResponse | null>(null);

A small rename avoids the double-tools accessor and improves readability.


735-744: Two minor list-rendering issues

  1. index is used as key, risking re-ordering bugs; prefer a stable identifier (e.g. tool.name ?? index).
  2. tool.name and tool.description are optional, so rendering may output empty <div>s. Guard or fall back to placeholders.
-  tools.tools.map((tool: ToolSchema, index: number) => (
-    <div key={index} className={styles["tool-item"]}>
+  toolsResponse.tools.map((tool: ToolSchema, index: number) => (
+    <div
+      key={tool.name ?? index}
+      className={styles["tool-item"]}
+    >
       <div className={styles["tool-name"]}>{tool.name ?? "Unnamed Tool"}</div>
       <div className={styles["tool-description"]}>
         {tool.description ?? "No description"}
       </div>
     </div>
   ))
app/mcp/types.ts (1)

71-76: inputSchema?: object is too loose

Using object loses type information and prevents validation. Prefer a more precise type:

inputSchema?: Record<string, unknown>; // or a Zod schema if available

This will still accept arbitrary JSON shapes while keeping unknown safety.

app/mcp/actions.ts (1)

24-29: Top-level await inside constant initialisation can break older Node builds

await at module scope requires ES 2022 modules; some bundlers/serverless targets still transpile to CommonJS. Compute CONFIG_PATH lazily inside an async helper to stay compatible.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 3809375 and 529f6b5.

📒 Files selected for processing (8)
  • app/components/mcp-market.tsx (4 hunks)
  • app/config/build.ts (1 hunks)
  • app/mcp/actions.ts (6 hunks)
  • app/mcp/client.ts (2 hunks)
  • app/mcp/types.ts (2 hunks)
  • app/store/access.ts (1 hunks)
  • app/typing.ts (1 hunks)
  • next.config.mjs (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (2)
app/components/mcp-market.tsx (1)
app/mcp/types.ts (3)
  • ListToolsResponse (67-69)
  • isServerStdioConfig (118-119)
  • ToolSchema (71-76)
app/mcp/client.ts (1)
app/mcp/types.ts (1)
  • isServerSseConfig (116-117)
🪛 Biome (1.9.4)
app/mcp/actions.ts

[error] 437-437: The catch clause that only rethrows the original error is useless.

An unnecessary catch clause can be confusing.
Unsafe fix: Remove the try/catch clause.

(lint/complexity/noUselessCatch)

🔇 Additional comments (7)
app/typing.ts (1)

1-3: Looks good: Global constant declaration for export mode.

The global declaration of EXPORT_MODE as a boolean constant is a clean approach for making this build-mode flag available throughout the application. This will help conditionally switch between client-side and server-side logic.

next.config.mjs (2)

9-10: Good addition: Build mode constant definition.

Creating a dedicated constant for export mode makes the code more readable and maintainable than checking mode === "export" throughout the configuration.


15-17: Proper implementation: Injecting EXPORT_MODE through webpack.

The webpack DefinePlugin is correctly used to make the EXPORT_MODE constant available to the application at compile time. This approach ensures the value is available as a global constant during runtime.

app/store/access.ts (1)

246-251: Export mode state initialization is now properly handled.

This change correctly handles state initialization in export mode by:

  1. Using client config directly instead of making a network request
  2. Setting fetchState to 2 (done) to avoid unnecessary fetch attempts
  3. Short-circuiting the fetch process when in export mode

This is consistent with the broader pattern of adapting behavior based on build mode.

app/mcp/client.ts (2)

3-8: Good update: Enhanced type imports.

The expanded import statement properly includes all necessary types and the type guard function needed for the transport selection logic.


19-46: Well-implemented transport selection based on server config type.

The refactored transport creation logic:

  1. Supports SSE transport for SSE server configurations
  2. Prevents stdio transport usage in export mode
  3. Uses dynamic imports to load only the necessary transport implementation
  4. Properly handles environment variables for stdio transport

This implementation elegantly handles different server configurations while respecting the export mode constraints.

app/config/build.ts (1)

46-48: openaiUrl silently falls back to BASE_URL

When OPENAI_BASE_URL is unset but BASE_URL is, it might accidentally point OpenAI traffic to an unrelated backend. Consider explicit fallback order with a comment or validate that the chosen URL looks like an OpenAI endpoint.

Copy link
Contributor

@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 (3)
app/mcp/actions.ts (3)

1-3: "use server" directive won't work conditionally.

React/NextJS recognizes the directive only when it is the very first statement. Wrapping it in an if block makes it ineffective.

Consider using separate entry files or the webpack alias approach, or split into actions.client.ts & actions.server.ts with conditional imports.


347-354: Fallback client search uses O(n²) scan and fragile name matching.

  1. Scanning all clients on every request is O(#clients × #tools).
  2. Comparing t.name == request.params?.name assumes params.name exists and is string-typed.

Maintain a Map<toolName, clientId> during client initialization for O(1) lookup and add proper type guards.


369-393: 🛠️ Refactor suggestion

Incorrect merge logic between bundled config and localStorage override.

The loop iterates over config.mcpServers, so any additional servers present only in storedConfig are ignored.

Apply this fix:

-for (const mcpId in config.mcpServers) {
+for (const mcpId in storedConfig.mcpServers) {
  if (mcpId in mcpServers) {
    mcpServers[mcpId] = {
      ...mcpServers[mcpId],
      ...storedConfig.mcpServers[mcpId],
    };
  } else {
    mcpServers[mcpId] = storedConfig.mcpServers[mcpId];
  }
}
🧹 Nitpick comments (1)
app/mcp/actions.ts (1)

417-419: Useless catch clause that only rethrows the error.

The catch block on line 417-419 simply rethrows the original error, which is redundant.

-  try {
    // 确保目录存在
    if (EXPORT_MODE) {
      const storage = localStorage;
      storage.setItem("McpConfig", JSON.stringify(config));
    } else {
      const fs = await import("fs/promises");
      const path = await import("path");
      await fs.mkdir(path.dirname(CONFIG_PATH), { recursive: true });
      await fs.writeFile(CONFIG_PATH, JSON.stringify(config, null, 2));
    }
-  } catch (error) {
-    throw error;
-  }
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 529f6b5 and 61e98a2.

📒 Files selected for processing (8)
  • app/components/mcp-market.tsx (4 hunks)
  • app/config/build.ts (1 hunks)
  • app/mcp/actions.ts (6 hunks)
  • app/mcp/client.ts (2 hunks)
  • app/mcp/types.ts (2 hunks)
  • app/store/access.ts (1 hunks)
  • app/typing.ts (1 hunks)
  • next.config.mjs (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
  • app/typing.ts
  • next.config.mjs
  • app/store/access.ts
  • app/config/build.ts
  • app/components/mcp-market.tsx
  • app/mcp/types.ts
🧰 Additional context used
🧬 Code Graph Analysis (1)
app/mcp/client.ts (1)
app/mcp/types.ts (1)
  • isServerSseConfig (116-117)
🪛 Biome (1.9.4)
app/mcp/actions.ts

[error] 435-435: The catch clause that only rethrows the original error is useless.

An unnecessary catch clause can be confusing.
Unsafe fix: Remove the try/catch clause.

(lint/complexity/noUselessCatch)

🔇 Additional comments (5)
app/mcp/client.ts (2)

3-8: LGTM! Good import organization.

Your approach to expanding the imports to include the type guard and necessary types is clean and follows good practices.


19-46:

❌ Incorrect review comment

Well-implemented transport selection based on server config type.

The dynamic transport selection logic elegantly handles different server configurations and correctly uses the type guard for type safety. Dynamic imports are a good practice for code splitting.

I do have one concern:

The EXPORT_MODE constant is used but not imported. This appears to be a global constant injected at build time. Let's verify its definition:


🏁 Script executed:

#!/bin/bash
# Check where EXPORT_MODE is defined in the codebase
rg -A 1 "EXPORT_MODE" --type ts

Length of output: 736


No import needed for EXPORT_MODE
EXPORT_MODE is declared as a global constant in app/typing.ts (inside a declare global block), so it’s available at build time without an import.

Likely an incorrect or invalid review comment.

app/mcp/actions.ts (3)

22-27: Good dynamic path handling based on environment.

The conditional path configuration logic handles both client and server environments well by using dynamic imports for Node.js modules only when needed in server mode.


407-417: Clean environment-specific config persistence.

The implementation properly handles configuration persistence on both client (localStorage) and server (filesystem) environments.


424-438: Good prioritization of config sources.

The function properly prioritizes the enableMcp flag from the loaded config before falling back to environment-specific configurations.

🧰 Tools
🪛 Biome (1.9.4)

[error] 435-435: The catch clause that only rethrows the original error is useless.

An unnecessary catch clause can be confusing.
Unsafe fix: Remove the try/catch clause.

(lint/complexity/noUselessCatch)

Copy link
Contributor

@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)
app/mcp/actions.ts (3)

1-3: ⚠️ Potential issue

"use server" directive will not be effective inside the conditional block

React/NextJS directive "use server" must be the very first statement in a file to work properly. Wrapping it in an if block turns it into a regular string expression that won't be recognized by the compiler.

Consider one of these alternatives:

  1. Use separate files (actions.client.ts and actions.server.ts) with conditional imports
  2. Use build-time code transformation in your Webpack/Next.js configuration to inject the directive correctly

347-354: 🛠️ Refactor suggestion

Fallback client search uses O(n²) scan and fragile name matching

  1. Scanning all clients on every request is O(#clients × #tools).
  2. Comparing t.name == request.params?.name uses loose equality and assumes params.name exists.

Consider these improvements:

  1. Maintain a Map<toolName, clientId> when clients are initialized for O(1) lookup.
  2. Use strict equality (===) and proper type guards.
let client = clientsMap.get(clientId);
if (!client) {
-  client = [...clientsMap.values()].find(
-    (c) =>
-      c.tools?.tools &&
-      c.tools.tools.find((t) => t.name == request.params?.name),
-  );
+  // Use type guard to ensure params.name exists and is a string
+  const toolName = typeof request.params?.name === "string" ? request.params.name : null;
+  if (toolName) {
+    client = [...clientsMap.values()].find(
+      (c) =>
+        c.tools?.tools &&
+        c.tools.tools.some((t) => t.name === toolName),
+    );
+  }
}

369-393: ⚠️ Potential issue

Incorrect merge logic between bundled config and localStorage override

The loop iterates over config.mcpServers, so any additional servers present only in storedConfig are ignored.

Apply this fix to properly merge the configurations:

if (storedConfig_str) {
  const storedConfig: McpConfigData = JSON.parse(storedConfig_str);
-  const mcpServers = config.mcpServers;
-  if (storedConfig.mcpServers) {
-    for (const mcpId in config.mcpServers) {
-      if (mcpId in mcpServers) {
-        mcpServers[mcpId] = {
-          ...mcpServers[mcpId],
-          ...storedConfig.mcpServers[mcpId],
-        };
-      } else {
-        mcpServers[mcpId] = storedConfig.mcpServers[mcpId];
-      }
-    }
-  }
-
-  config.mcpServers = mcpServers;
+  if (storedConfig.mcpServers) {
+    const merged = { ...config.mcpServers };
+    for (const mcpId in storedConfig.mcpServers) {
+      merged[mcpId] = { 
+        ...merged[mcpId], 
+        ...storedConfig.mcpServers[mcpId] 
+      };
+    }
+    config.mcpServers = merged;
+  }
}
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 61e98a2 and 37e2fc5.

📒 Files selected for processing (8)
  • app/components/mcp-market.tsx (4 hunks)
  • app/config/build.ts (1 hunks)
  • app/mcp/actions.ts (6 hunks)
  • app/mcp/client.ts (2 hunks)
  • app/mcp/types.ts (2 hunks)
  • app/store/access.ts (1 hunks)
  • app/typing.ts (1 hunks)
  • next.config.mjs (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (7)
  • app/typing.ts
  • next.config.mjs
  • app/store/access.ts
  • app/config/build.ts
  • app/components/mcp-market.tsx
  • app/mcp/types.ts
  • app/mcp/client.ts
🧰 Additional context used
🪛 Biome (1.9.4)
app/mcp/actions.ts

[error] 435-435: The catch clause that only rethrows the original error is useless.

An unnecessary catch clause can be confusing.
Unsafe fix: Remove the try/catch clause.

(lint/complexity/noUselessCatch)

🔇 Additional comments (1)
app/mcp/actions.ts (1)

425-437: LGTM: Enhanced configuration access with appropriate fallbacks

The implementation correctly adds fallback behavior by checking config sources in a prioritized manner. It first checks the loaded config, then falls back to client or server config based on the environment.

🧰 Tools
🪛 Biome (1.9.4)

[error] 435-435: The catch clause that only rethrows the original error is useless.

An unnecessary catch clause can be confusing.
Unsafe fix: Remove the try/catch clause.

(lint/complexity/noUselessCatch)

Copy link
Contributor

@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)
app/mcp/actions.ts (2)

1-3: ⚠️ Potential issue

The "use server" directive inside a conditional won't have the intended effect

This conditional directive won't be recognized by Next.js, as directives must be the first statement of a file. The compiler cannot evaluate the EXPORT_MODE condition at compilation time.

Consider using separate files (e.g., actions.server.ts and actions.client.ts) with appropriate directives at the top of each file, and re-export from a common entry point based on the build config.


349-356: ⚠️ Potential issue

Fallback client search uses inefficient lookup and lacks type safety

While adding a fallback mechanism is good, the implementation has two issues:

  1. The lookup is inefficient, scanning all clients and tools on each request
  2. The comparison t.name == request.params?.name assumes params.name exists and is string-typed

Consider implementing a tool-to-client mapping:

-let client = clientsMap.get(clientId);
-if (!client) {
-  client = [...clientsMap.values()].find(
-    (c) =>
-      c.tools?.tools &&
-      c.tools.tools.find((t) => t.name == request.params?.name),
-  );
-}
+let client = clientsMap.get(clientId);
+if (!client && request.params?.name && typeof request.params.name === 'string') {
+  // Use a tool-to-client mapping that's maintained when tools are initialized
+  const toolName = request.params.name;
+  client = [...clientsMap.values()].find(
+    (c) => c.tools?.tools && c.tools.tools.some(t => t.name === toolName)
+  );
+}
🧹 Nitpick comments (2)
app/mcp/actions.ts (2)

371-392: Config merging logic is improved but could be refined

The logic for merging configurations has been improved compared to previous implementations. However, consider a more thorough approach to ensure no entries are missed.

-const merged = { ...config.mcpServers };
-if (storedConfig.mcpServers) {
-  for (const id in storedConfig.mcpServers) {
-    merged[id] = { ...merged[id], ...storedConfig.mcpServers[id] };
-  }
-}
+// Create a merged configuration that combines both sources
+const merged = { ...config.mcpServers };
+if (storedConfig.mcpServers) {
+  // Ensure we process all servers from stored config
+  for (const id in storedConfig.mcpServers) {
+    merged[id] = { ...merged[id], ...storedConfig.mcpServers[id] };
+  }
+}

422-424: Unnecessary catch clause

This catch clause simply rethrows the original error without any additional processing.

-try {
  // ... existing code ...
-} catch (error) {
-  throw error;
-}
+// ... existing code ...
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 37e2fc5 and 18bebf5.

📒 Files selected for processing (8)
  • app/components/mcp-market.tsx (4 hunks)
  • app/config/build.ts (1 hunks)
  • app/mcp/actions.ts (6 hunks)
  • app/mcp/client.ts (2 hunks)
  • app/mcp/types.ts (3 hunks)
  • app/store/access.ts (1 hunks)
  • app/typing.ts (1 hunks)
  • next.config.mjs (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (7)
  • app/typing.ts
  • next.config.mjs
  • app/mcp/client.ts
  • app/components/mcp-market.tsx
  • app/store/access.ts
  • app/config/build.ts
  • app/mcp/types.ts
🧰 Additional context used
🪛 Biome (1.9.4)
app/mcp/actions.ts

[error] 440-440: The catch clause that only rethrows the original error is useless.

An unnecessary catch clause can be confusing.
Unsafe fix: Remove the try/catch clause.

(lint/complexity/noUselessCatch)

🔇 Additional comments (3)
app/mcp/actions.ts (3)

22-29: Good refactoring of path handling with async function

The async function pattern improves on the previous top-level await approach, making initialization more predictable and maintainable.


403-421: Good addition of error handling for localStorage operations

The try-catch block for localStorage operations is a good improvement that addresses previous review comments.


430-442: Improved MCP enabled check with better layering

Good enhancement of the isMcpEnabled function to first check the config before falling back to client/server config based on mode.

🧰 Tools
🪛 Biome (1.9.4)

[error] 440-440: The catch clause that only rethrows the original error is useless.

An unnecessary catch clause can be confusing.
Unsafe fix: Remove the try/catch clause.

(lint/complexity/noUselessCatch)

@mokeyish mokeyish changed the title Add support for MCP in export mode Add support for MCP(sse) in export mode May 8, 2025
Copy link
Contributor

@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)
app/mcp/actions.ts (2)

1-3: "use server" directive inside a conditional will not work as expected

The "use server" directive must be the very first statement in a file to be recognized by the NextJS compiler. Wrapping it in a conditional turns it into a regular string expression without any special meaning.

Consider splitting this into separate files:

  • actions.server.ts with "use server" at the top
  • actions.client.ts without the directive
  • A common entry point that conditionally imports based on EXPORT_MODE

349-360: 🛠️ Refactor suggestion

Optimize client search with a tool-to-client mapping

The current implementation performs an O(n²) operation by iterating through all clients and their tools for each request that doesn't specify a clientId. This can become inefficient as the number of clients and tools grows.

Consider maintaining a Map<toolName, clientId> when tools are initialized:

// Add at file level
+ const toolToClientMap = new Map<string, string>();

// Update in places where tools are initialized (e.g., line 136)
  clientsMap.set(clientId, { client, tools, errorMsg: null });
+ if (tools?.tools) {
+   for (const tool of tools.tools) {
+     if (tool.name) {
+       toolToClientMap.set(tool.name, clientId);
+     }
+   }
+ }

// Then in executeMcpAction:
  if (
    !client &&
    request.params?.name &&
    typeof request.params.name === "string"
  ) {
    // Use a direct lookup instead of scanning all clients
+   const toolClientId = toolToClientMap.get(request.params.name);
+   if (toolClientId) {
+     client = clientsMap.get(toolClientId);
+   }
-   const toolName = request.params.name;
-   client = [...clientsMap.values()].find(
-     (c) => c.tools?.tools && c.tools.tools.some((t) => t.name == toolName),
-   );
  }

Don't forget to update the map when clients/tools are removed or changed.

🧹 Nitpick comments (2)
app/mcp/actions.ts (2)

424-426: Small suggestion: Consider using a constant for the formatting configuration

The indentation value '2' is hardcoded in the JSON.stringify call. For consistency, consider extracting this to a constant.

+ const JSON_INDENT = 2;

// Then use it in both places where JSON is formatted
  await fs.writeFile(
    await getConfigPath(),
-   JSON.stringify(config, null, 2),
+   JSON.stringify(config, null, JSON_INDENT),
  );

428-430: Useless catch clause that only rethrows the error

The catch clause simply rethrows the original error without any additional processing, making it redundant.

  try {
    // ...code...
- } catch (error) {
-   throw error;
  }
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 18bebf5 and 7cc6475.

📒 Files selected for processing (8)
  • app/components/mcp-market.tsx (4 hunks)
  • app/config/build.ts (1 hunks)
  • app/mcp/actions.ts (6 hunks)
  • app/mcp/client.ts (2 hunks)
  • app/mcp/types.ts (3 hunks)
  • app/store/access.ts (1 hunks)
  • app/typing.ts (1 hunks)
  • next.config.mjs (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (7)
  • next.config.mjs
  • app/typing.ts
  • app/mcp/client.ts
  • app/store/access.ts
  • app/components/mcp-market.tsx
  • app/config/build.ts
  • app/mcp/types.ts
🧰 Additional context used
🪛 Biome (1.9.4)
app/mcp/actions.ts

[error] 446-446: The catch clause that only rethrows the original error is useless.

An unnecessary catch clause can be confusing.
Unsafe fix: Remove the try/catch clause.

(lint/complexity/noUselessCatch)

🔇 Additional comments (3)
app/mcp/actions.ts (3)

436-448: Good implementation of conditional config loading

The function properly handles both export mode and server mode with appropriate dynamic imports. Good job implementing the fallbacks and type checks.

🧰 Tools
🪛 Biome (1.9.4)

[error] 446-446: The catch clause that only rethrows the original error is useless.

An unnecessary catch clause can be confusing.
Unsafe fix: Remove the try/catch clause.

(lint/complexity/noUselessCatch)


375-398: Good improvement to the config merge logic

The merge logic now correctly processes all servers from stored config, addressing previous issues. The conditional handling of EXPORT_MODE is well implemented.


409-427: Well-implemented error handling for localStorage operations

The try-catch block for localStorage operations is a good safeguard against environments where storage might be unavailable. The warning logged without stopping execution is the right approach.

Copy link
Contributor

@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 (1)
app/mcp/actions.ts (1)

1-3: ⚠️ Potential issue

The "use server" directive will be ineffective due to conditional wrapping

Next.js requires the "use server" directive to be the very first statement in a file to be recognized. Placing it inside a conditional statement effectively turns it into a no-op string expression.

Consider separating the client and server code into different files, such as actions.client.ts and actions.server.ts, and use dynamic imports to load the appropriate file based on EXPORT_MODE.

🧹 Nitpick comments (1)
app/mcp/actions.ts (1)

263-269: Consider extracting duplicate mapping logic to a function

This code block duplicates the logic from lines 139-145 for populating the tool-to-client map.

+// Helper function to populate tool to client map
+function populateToolToClientMap(tools: any, clientId: string) {
+  if (tools?.tools) {
+    for (const tool of tools.tools) {
+      if (tool.name) {
+        toolToClientMap.set(tool.name, clientId);
+      }
+    }
+  }
+}

// In initializeSingleClient
-if (tools?.tools) {
-  for (const tool of tools.tools) {
-    if (tool.name) {
-      toolToClientMap.set(tool.name, clientId);
-    }
-  }
-}
+populateToolToClientMap(tools, clientId);

// In resumeMcpServer
-if (tools?.tools) {
-  for (const tool of tools.tools) {
-    if (tool.name) {
-      toolToClientMap.set(tool.name, clientId);
-    }
-  }
-}
+populateToolToClientMap(tools, clientId);
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between db58bd2 and d1baaba.

📒 Files selected for processing (8)
  • app/components/mcp-market.tsx (5 hunks)
  • app/config/build.ts (1 hunks)
  • app/mcp/actions.ts (7 hunks)
  • app/mcp/client.ts (2 hunks)
  • app/mcp/types.ts (3 hunks)
  • app/store/access.ts (1 hunks)
  • app/typing.ts (1 hunks)
  • next.config.mjs (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (7)
  • next.config.mjs
  • app/typing.ts
  • app/store/access.ts
  • app/mcp/client.ts
  • app/components/mcp-market.tsx
  • app/mcp/types.ts
  • app/config/build.ts
🔇 Additional comments (6)
app/mcp/actions.ts (6)

24-31: Good refactoring of config path handling

The asynchronous getConfigPath function is a much better approach than the previous top-level await in an IIFE pattern. This implementation provides cleaner separation between client and server modes.


34-34: Great addition of tool-to-client mapping

Adding a toolToClientMap addresses the previous O(n²) scan issue, providing O(1) lookup time when searching for a client by tool name.


139-145: Good implementation of tool-to-client mapping population

The code properly checks for the existence of tool fields before attempting to access them and correctly populates the mapping.


366-378: Excellent enhancement to client lookup logic

This implementation correctly addresses the previous review comment about O(n²) scan and fragile name matching. The code properly handles type checking and provides a fallback mechanism when a client isn't found by ID.


425-433: Good error handling for localStorage operations

The try-catch block for localStorage operations is a good improvement over the previous implementation, as it allows the application to continue even if localStorage is not available.


448-460: Well-structured conditional configuration loading

The implementation correctly checks the loaded config first before falling back to conditionally imported configurations, providing a clean separation between client and server modes.

Comment on lines 393 to 412
if (EXPORT_MODE) {
const res = await fetch(await getConfigPath());
const config: McpConfigData = await res.json();
const storage = localStorage;
const storedConfig_str = storage.getItem("McpConfig");
if (storedConfig_str) {
const storedConfig: McpConfigData = JSON.parse(storedConfig_str);
// Create a merged configuration that combines both sources
const merged = { ...config.mcpServers };
if (storedConfig.mcpServers) {
// Ensure we process all servers from stored config
for (const id in storedConfig.mcpServers) {
merged[id] = { ...merged[id], ...storedConfig.mcpServers[id] };
}
}

config.mcpServers = merged;
}
return config;
} else {
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add error handling for localStorage and JSON parsing operations

While fetching and merging configs looks good, there's no error handling for localStorage access or JSON parsing, which could fail in various scenarios.

if (EXPORT_MODE) {
  const res = await fetch(await getConfigPath());
+  if (!res.ok) {
+    throw new Error(`Failed to fetch config: ${res.status} ${res.statusText}`);
+  }
  const config: McpConfigData = await res.json();
-  const storage = localStorage;
-  const storedConfig_str = storage.getItem("McpConfig");
-  if (storedConfig_str) {
-    const storedConfig: McpConfigData = JSON.parse(storedConfig_str);
+  try {
+    const storage = localStorage;
+    const storedConfigStr = storage.getItem("McpConfig");
+    if (storedConfigStr) {
+      try {
+        const storedConfig: McpConfigData = JSON.parse(storedConfigStr);
+        // Create a merged configuration that combines both sources
+        const merged = { ...config.mcpServers };
+        if (storedConfig.mcpServers) {
+          // Ensure we process all servers from stored config
+          for (const id in storedConfig.mcpServers) {
+            merged[id] = { ...merged[id], ...storedConfig.mcpServers[id] };
+          }
+        }
+        config.mcpServers = merged;
+      } catch (parseError) {
+        logger.warn(`Failed to parse stored MCP config: ${parseError}`);
+        // Continue with default config
+      }
+    }
+  } catch (storageError) {
+    logger.warn(`Failed to access localStorage: ${storageError}`);
+    // Continue with default config
+  }
  return config;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (EXPORT_MODE) {
const res = await fetch(await getConfigPath());
const config: McpConfigData = await res.json();
const storage = localStorage;
const storedConfig_str = storage.getItem("McpConfig");
if (storedConfig_str) {
const storedConfig: McpConfigData = JSON.parse(storedConfig_str);
// Create a merged configuration that combines both sources
const merged = { ...config.mcpServers };
if (storedConfig.mcpServers) {
// Ensure we process all servers from stored config
for (const id in storedConfig.mcpServers) {
merged[id] = { ...merged[id], ...storedConfig.mcpServers[id] };
}
}
config.mcpServers = merged;
}
return config;
} else {
if (EXPORT_MODE) {
const res = await fetch(await getConfigPath());
if (!res.ok) {
throw new Error(`Failed to fetch config: ${res.status} ${res.statusText}`);
}
const config: McpConfigData = await res.json();
try {
const storage = localStorage;
const storedConfigStr = storage.getItem("McpConfig");
if (storedConfigStr) {
try {
const storedConfig: McpConfigData = JSON.parse(storedConfigStr);
// Create a merged configuration that combines both sources
const merged = { ...config.mcpServers };
if (storedConfig.mcpServers) {
// Ensure we process all servers from stored config
for (const id in storedConfig.mcpServers) {
merged[id] = { ...merged[id], ...storedConfig.mcpServers[id] };
}
}
config.mcpServers = merged;
} catch (parseError) {
logger.warn(`Failed to parse stored MCP config: ${parseError}`);
// Continue with default config
}
}
} catch (storageError) {
logger.warn(`Failed to access localStorage: ${storageError}`);
// Continue with default config
}
return config;
} else {

Copy link
Contributor

@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

🧹 Nitpick comments (1)
app/mcp/actions.base.ts (1)

109-153: initializeSingleClient resolves before the client is ready

The function is declared async yet it never awaits the createClient chain—it sets up a .then/.catch handler and returns immediately.
Callers like initializeMcpSystem therefore “await” a promise that is already resolved, giving a false sense of completion while clients are still starting in the background.

If the intent is fire-and-forget, mark the function void and drop async to avoid confusion.
Otherwise, return the inner promise to let callers truly wait:

-  createClient(clientId, serverConfig)
+  return createClient(clientId, serverConfig)
     .then(async (client) => { /* … */ })
     .catch((error) => { /* … */ });
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between d1baaba and f707eff.

📒 Files selected for processing (4)
  • app/mcp/actions.base.ts (1 hunks)
  • app/mcp/actions.client.ts (1 hunks)
  • app/mcp/actions.server.ts (1 hunks)
  • app/mcp/actions.ts (1 hunks)
✅ Files skipped from review due to trivial changes (2)
  • app/mcp/actions.client.ts
  • app/mcp/actions.server.ts

Comment on lines +388 to +436
export async function getMcpConfigFromFile(): Promise<McpConfigData> {
try {
if (EXPORT_MODE) {
const res = await fetch(await getConfigPath());
const config: McpConfigData = await res.json();
const storage = localStorage;
const storedConfig_str = storage.getItem("McpConfig");
if (storedConfig_str) {
const storedConfig: McpConfigData = JSON.parse(storedConfig_str);
// Create a merged configuration that combines both sources
const merged = { ...config.mcpServers };
if (storedConfig.mcpServers) {
// Ensure we process all servers from stored config
for (const id in storedConfig.mcpServers) {
merged[id] = { ...merged[id], ...storedConfig.mcpServers[id] };
}
}

config.mcpServers = merged;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Local-storage failure currently discards a successfully fetched config

If localStorage is unavailable (e.g. browser privacy mode) or the stored JSON is malformed, the catch at 388–418 falls back to DEFAULT_MCP_CONFIG, losing the remote /mcp/config.json that was already fetched successfully.

A more resilient flow:

      const config: McpConfigData = await res.json();
-      const storage = localStorage;
-      const storedConfig_str = storage.getItem("McpConfig");
-      if (storedConfig_str) {
-        const storedConfig: McpConfigData = JSON.parse(storedConfig_str);
+      try {
+        const storedConfigStr = localStorage.getItem("McpConfig");
+        if (storedConfigStr) {
+          const storedConfig: McpConfigData = JSON.parse(storedConfigStr);
           // merge logic …
-      }
-      return config;
+        }
+      } catch (lsError) {
+        logger.warn(`Ignoring invalid localStorage MCP config: ${lsError}`);
+      }
+      return config;   // always return the remote (possibly merged) config

This way a transient storage issue does not wipe out the authoritative server configuration.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function getMcpConfigFromFile(): Promise<McpConfigData> {
try {
if (EXPORT_MODE) {
const res = await fetch(await getConfigPath());
const config: McpConfigData = await res.json();
const storage = localStorage;
const storedConfig_str = storage.getItem("McpConfig");
if (storedConfig_str) {
const storedConfig: McpConfigData = JSON.parse(storedConfig_str);
// Create a merged configuration that combines both sources
const merged = { ...config.mcpServers };
if (storedConfig.mcpServers) {
// Ensure we process all servers from stored config
for (const id in storedConfig.mcpServers) {
merged[id] = { ...merged[id], ...storedConfig.mcpServers[id] };
}
}
config.mcpServers = merged;
}
export async function getMcpConfigFromFile(): Promise<McpConfigData> {
try {
if (EXPORT_MODE) {
const res = await fetch(await getConfigPath());
const config: McpConfigData = await res.json();
// Only the local-storage merge is now wrapped in its own try/catch
try {
const storedConfigStr = localStorage.getItem("McpConfig");
if (storedConfigStr) {
const storedConfig: McpConfigData = JSON.parse(storedConfigStr);
// Create a merged configuration that combines both sources
const merged = { ...config.mcpServers };
if (storedConfig.mcpServers) {
for (const id in storedConfig.mcpServers) {
merged[id] = { ...merged[id], ...storedConfig.mcpServers[id] };
}
}
config.mcpServers = merged;
}
} catch (lsError) {
logger.warn(`Ignoring invalid localStorage MCP config: ${lsError}`);
}
// Always return at least what we fetched from the server
return config;
}
} catch (error) {
// Fallback only if the fetch/json itself failed
return DEFAULT_MCP_CONFIG;
}
}

@mokeyish mokeyish force-pushed the export-mcp branch 2 times, most recently from cd5824e to cf050d5 Compare May 8, 2025 12:37
Copy link
Contributor

@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)
app/mcp/actions.base.ts (5)

31-32: ⚠️ Potential issue

The toolToClientMap lacks cleanup when clients are removed or restarted.

When a client is removed, paused, or all clients are restarted, the entries in toolToClientMap are not cleaned up. This can lead to stale mappings that cause requests to be sent to non-existent clients.

Implement a helper function to purge tool mappings:

const toolToClientMap = new Map<string, string>();

+// Helper function to purge tool mappings for a specific client
+function purgeToolMappings(clientId: string) {
+  for (const [tool, mappedId] of toolToClientMap.entries()) {
+    if (mappedId === clientId) {
+      toolToClientMap.delete(tool);
+    }
+  }
+}

Then call this function in pauseMcpServer, removeMcpServer, and use toolToClientMap.clear() in restartAllClients.


388-418: 🛠️ Refactor suggestion

Improve localStorage error handling with nested try-catch blocks.

The current implementation falls back to the default config if either fetch fails or localStorage operations fail. It should preserve the successfully fetched config even if localStorage operations fail.

if (EXPORT_MODE) {
  const res = await fetch(await getConfigPath());
+  if (!res.ok) {
+    throw new Error(`Failed to fetch config: ${res.status} ${res.statusText}`);
+  }
  const config: McpConfigData = await res.json();
-  const storage = localStorage;
-  const storedConfig_str = storage.getItem("McpConfig");
-  if (storedConfig_str) {
-    const storedConfig: McpConfigData = JSON.parse(storedConfig_str);
+  try {
+    const storedConfigStr = localStorage.getItem("McpConfig");
+    if (storedConfigStr) {
+      try {
+        const storedConfig: McpConfigData = JSON.parse(storedConfigStr);
        // Create a merged configuration that combines both sources
        const merged = { ...config.mcpServers };
        if (storedConfig.mcpServers) {
          // Ensure we process all servers from stored config
          for (const id in storedConfig.mcpServers) {
            merged[id] = { ...merged[id], ...storedConfig.mcpServers[id] };
          }
        }
        config.mcpServers = merged;
+      } catch (parseError) {
+        logger.warn(`Failed to parse stored MCP config: ${parseError}`);
+        // Continue with default config
+      }
    }
+  } catch (storageError) {
+    logger.warn(`Failed to access localStorage: ${storageError}`);
+    // Continue with default config
+  }
  return config;

318-323: ⚠️ Potential issue

Add tool mapping cleanup when removing a server.

When a server is removed, the tool mappings for that client should be removed to prevent routing requests to a non-existent client.

clientsMap.delete(clientId);
+// Purge tool mappings for this client
+for (const [tool, mappedId] of toolToClientMap.entries()) {
+  if (mappedId === clientId) {
+    toolToClientMap.delete(tool);
+  }
+}

343-344: ⚠️ Potential issue

Clear tool-to-client mappings when restarting all clients.

When all clients are restarted, the tool-to-client mappings should be cleared to prevent routing requests to non-existent clients.

// 清空状态
clientsMap.clear();
+toolToClientMap.clear();

232-237: ⚠️ Potential issue

Add tool mapping cleanup when pausing a server.

When a server is paused, the tool mappings for that client should be removed to prevent routing requests to a non-existent client.

clientsMap.delete(clientId);
+// Purge tool mappings for this client
+for (const [tool, mappedId] of toolToClientMap.entries()) {
+  if (mappedId === clientId) {
+    toolToClientMap.delete(tool);
+  }
+}
🧹 Nitpick comments (2)
app/mcp/actions.ts (2)

3-3: Consider adding type guard for consistent error handling.

The variable actionsHost is initialized with a type but no value. When accessing this variable before initialization, TypeScript's type checking won't catch potential issues.

-let actionsHost: typeof import("./actions.base");
+let actionsHost: typeof import("./actions.base") | undefined;

48-53: Add error handling for the dynamic import chain.

The current implementation doesn't handle potential failures in the dynamic import chain. Consider adding error handling to provide better diagnostics.

export const executeMcpAction = async (
  clientId: string,
  request: McpRequestMessage,
) => {
-  return (await actions()).executeMcpAction(clientId, request);
+  try {
+    return (await actions()).executeMcpAction(clientId, request);
+  } catch (error) {
+    console.error(`Failed to execute MCP action: ${error}`);
+    throw error;
+  }
};
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between f707eff and cf050d5.

📒 Files selected for processing (4)
  • app/mcp/actions.base.ts (1 hunks)
  • app/mcp/actions.client.ts (1 hunks)
  • app/mcp/actions.server.ts (1 hunks)
  • app/mcp/actions.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/mcp/actions.client.ts
  • app/mcp/actions.server.ts
🧰 Additional context used
🧬 Code Graph Analysis (1)
app/mcp/actions.ts (2)
app/mcp/actions.base.ts (12)
  • getAvailableClientsCount (90-94)
  • isMcpEnabled (443-462)
  • initializeMcpSystem (156-175)
  • addMcpServer (178-207)
  • getClientsStatus (34-82)
  • getClientTools (85-87)
  • getMcpConfigFromFile (388-418)
  • pauseMcpServer (210-243)
  • restartAllClients (332-355)
  • resumeMcpServer (246-304)
  • executeMcpAction (358-385)
  • getAllTools (97-106)
app/mcp/types.ts (2)
  • ServerConfig (122-122)
  • McpRequestMessage (6-14)
🔇 Additional comments (7)
app/mcp/actions.ts (3)

1-15: Good implementation of dynamic module loading based on EXPORT_MODE.

The code properly implements a lazy-loading pattern that defers the import of environment-specific modules until needed. This approach is efficient and maintainable.


17-22: Well-implemented function wrapper pattern.

The pattern of exporting thin wrapper functions that delegate to the dynamically imported module is clean and maintainable. This preserves the original API surface while allowing for environment-specific implementations.


7-7:

❌ Incorrect review comment

Missing EXPORT_MODE import or declaration.

The global constant EXPORT_MODE is used without being imported or declared in this file.


🏁 Script executed:

#!/bin/bash
# Check for EXPORT_MODE definition and usage
echo "Checking for EXPORT_MODE declaration or import:"
rg -A 2 "EXPORT_MODE" --type ts 

Length of output: 1264


🏁 Script executed:

#!/bin/bash
# Display context around EXPORT_MODE declaration in app/typing.ts
rg -C 3 "EXPORT_MODE" app/typing.ts

Length of output: 149


No import needed for EXPORT_MODE

The global constant EXPORT_MODE is declared via an ambient declaration in app/typing.ts, so it’s available throughout the project without an explicit import. You can ignore the “Missing EXPORT_MODE import or declaration” comment.

Likely an incorrect or invalid review comment.

app/mcp/actions.base.ts (4)

109-153: Good implementation of client initialization with robust error handling.

The client initialization process is well-implemented with proper error handling and logging. It correctly manages client state transitions and updates the tool-to-client mapping when tools are discovered.


358-385: Effective tool-based client resolution with proper error handling.

The implementation correctly resolves the appropriate client based on either client ID or tool name, with proper type checking for the tool name parameter. The error handling is robust with appropriate logging.


421-440: Good implementation of environment-aware configuration updates.

The implementation correctly handles configuration updates in both export and non-export modes, with proper error handling for localStorage operations and filesystem operations.


443-462: Comprehensive MCP enablement check with fallbacks.

The function properly checks multiple sources to determine if MCP is enabled, with appropriate fallbacks and error handling. It first checks the config file, then falls back to client or server config depending on the environment.

Copy link
Contributor

@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 (1)
app/mcp/actions.base.ts (1)

417-447: 🛠️ Refactor suggestion

Fragile localStorage error handling.

This implementation has the same issue that was flagged in a previous review. If localStorage is unavailable or the stored JSON is malformed, you'll lose the remote config data.

Apply the solution from the previous review to ensure resilient config handling:

      const res = await fetch(await getConfigPath());
      const config: McpConfigData = await res.json();
-      const storage = localStorage;
-      const storedConfig_str = storage.getItem("McpConfig");
-      if (storedConfig_str) {
-        const storedConfig: McpConfigData = JSON.parse(storedConfig_str);
+      try {
+        const storedConfigStr = localStorage.getItem("McpConfig");
+        if (storedConfigStr) {
+          const storedConfig: McpConfigData = JSON.parse(storedConfigStr);
           // merge logic …
-      }
-      return config;
+        }
+      } catch (lsError) {
+        logger.warn(`Ignoring invalid localStorage MCP config: ${lsError}`);
+      }
+      return config;   // always return the remote (possibly merged) config
🧹 Nitpick comments (1)
app/mcp/actions.base.ts (1)

218-248: Potential issue with config status handling.

In the addMcpServer function, there's a check if (isNewServer && !config.status) that only sets default status for new servers. This might cause issues if an existing server's config is provided without a status.

Consider adding a default status for all servers without status, not just new ones:

-    // 如果是新服务器,设置默认状态为 active
-    if (isNewServer && !config.status) {
+    // 设置默认状态为 active(如果未指定)
+    if (!config.status) {
       config.status = "active";
     }
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between cf050d5 and 62d32f3.

📒 Files selected for processing (4)
  • app/mcp/actions.base.ts (1 hunks)
  • app/mcp/actions.client.ts (1 hunks)
  • app/mcp/actions.server.ts (1 hunks)
  • app/mcp/actions.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • app/mcp/actions.server.ts
  • app/mcp/actions.client.ts
  • app/mcp/actions.ts
🧰 Additional context used
🧬 Code Graph Analysis (1)
app/mcp/actions.base.ts (4)
app/mcp/logger.ts (2)
  • MCPClientLogger (12-65)
  • error (32-34)
app/mcp/types.ts (6)
  • McpClientData (79-82)
  • ServerStatusResponse (110-113)
  • ServerConfig (122-122)
  • McpConfigData (139-143)
  • McpRequestMessage (6-14)
  • DEFAULT_MCP_CONFIG (145-147)
app/mcp/client.ts (4)
  • createClient (13-61)
  • listTools (68-70)
  • removeClient (63-66)
  • executeRequest (72-77)
app/config/server.ts (1)
  • getServerSideConfig (128-269)
🔇 Additional comments (12)
app/mcp/actions.base.ts (12)

22-28: Good implementation of dual-mode path handling.

The getConfigPath function correctly handles different environments by returning the appropriate path based on the EXPORT_MODE global constant. This allows for seamless operation in both client-side (using relative URL) and server-side (using filesystem path) contexts.


30-77: Good implementation addressing the previously identified stale mapping issue.

The ClientsMap class properly implements the suggested purgeToolMappings functionality from the previous review and ensures it's called in all appropriate places: the set method (when tools are null), the delete method, and the clear method. This prevents the leaked stale entries issue that was identified in the previous review.


81-130: LGTM: Thorough status checking implementation.

The getClientsStatus function thoroughly checks all possible states of a client (undefined, paused, initializing, error, active) and returns appropriate status responses. It provides a comprehensive view of the system state.


156-194: Robust client initialization with proper error handling.

The initializeSingleClient function initializes clients asynchronously with good error handling. It properly sets initial state, skips paused clients, and updates client state with tools or error information appropriately.


197-216: Good system initialization with existing client detection.

The initializeMcpSystem function correctly checks for existing clients before initialization and properly initializes all clients from configuration.


251-284: Server pause implementation follows best practices.

The pauseMcpServer function correctly updates the configuration first, then handles the cleanup of the client, following the pattern established in the past review comments that suggested purging tool mappings.


340-363: LGTM: Server removal with proper cleanup.

The removeMcpServer function correctly removes the server from configuration and cleans up the client and its tool mappings.


365-389: LGTM: Comprehensive client restart implementation.

The restartAllClients function properly shuts down all clients, clears the clients map (including tool mappings), and reinitializes all clients from the latest configuration.


391-414: LGTM: Action execution with tool-based client resolution.

The executeMcpAction function correctly resolves the client either by client ID or by tool name, which enables flexible invocation patterns.


449-469: Efficient configuration update handling.

The updateMcpConfig function appropriately handles both export mode (using localStorage) and server mode (using filesystem), with proper directory creation and error handling.


471-491: Comprehensive MCP enablement check.

The isMcpEnabled function correctly prioritizes the explicit config setting, then falls back to environment-specific configuration checks, making it robust across different deployment scenarios.


63-68:

✅ Verification successful

Verify error handling in the getByToolName method.

The method returns undefined implicitly when a tool isn't found, but doesn't validate if the client exists in the map after finding a tool mapping. Consider adding a check to ensure the client exists.


🏁 Script executed:

#!/bin/bash
# Check for places where getByToolName is used without explicit null/undefined checks

rg -A 3 "getByToolName" --type ts

Length of output: 548


No action needed: getByToolName result is already validated
The lookup is followed by a null‐safe check (if (!client?.client) throw …), which covers both missing mappings and missing client entries.

Comment on lines +287 to +338
export async function resumeMcpServer(clientId: string): Promise<void> {
try {
const currentConfig = await getMcpConfigFromFile();
const serverConfig = currentConfig.mcpServers[clientId];
if (!serverConfig) {
throw new Error(`Server ${clientId} not found`);
}

// 先尝试初始化客户端
logger.info(`Trying to initialize client [${clientId}]...`);
try {
const client = await createClient(clientId, serverConfig);
const tools = await listTools(client);
clientsMap.set(clientId, { client, tools, errorMsg: null });
logger.success(`Client [${clientId}] initialized successfully`);

// 初始化成功后更新配置
const newConfig: McpConfigData = {
...currentConfig,
mcpServers: {
...currentConfig.mcpServers,
[clientId]: {
...serverConfig,
status: "active" as const,
},
},
};
await updateMcpConfig(newConfig);
} catch (error) {
const currentConfig = await getMcpConfigFromFile();
const serverConfig = currentConfig.mcpServers[clientId];

// 如果配置中存在该服务器,则更新其状态为 error
if (serverConfig) {
serverConfig.status = "error";
await updateMcpConfig(currentConfig);
}

// 初始化失败
clientsMap.set(clientId, {
client: null,
tools: null,
errorMsg: error instanceof Error ? error.message : String(error),
});
logger.error(`Failed to initialize client [${clientId}]: ${error}`);
throw error;
}
} catch (error) {
logger.error(`Failed to resume server [${clientId}]: ${error}`);
throw error;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Potential race condition in resumeMcpServer.

The function updates the server status to "active" only after successful initialization, which is good. However, if initialization fails, it updates the status to "error" by modifying the original config object directly, which might cause issues if the config is used elsewhere.

-      // 如果配置中存在该服务器,则更新其状态为 error
-      if (serverConfig) {
-        serverConfig.status = "error";
-        await updateMcpConfig(currentConfig);
-      }
+      // 如果配置中存在该服务器,则更新其状态为 error
+      if (serverConfig) {
+        const updatedConfig = {
+          ...currentConfig,
+          mcpServers: {
+            ...currentConfig.mcpServers,
+            [clientId]: {
+              ...serverConfig,
+              status: "error" as const,
+            },
+          },
+        };
+        await updateMcpConfig(updatedConfig);
+      }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function resumeMcpServer(clientId: string): Promise<void> {
try {
const currentConfig = await getMcpConfigFromFile();
const serverConfig = currentConfig.mcpServers[clientId];
if (!serverConfig) {
throw new Error(`Server ${clientId} not found`);
}
// 先尝试初始化客户端
logger.info(`Trying to initialize client [${clientId}]...`);
try {
const client = await createClient(clientId, serverConfig);
const tools = await listTools(client);
clientsMap.set(clientId, { client, tools, errorMsg: null });
logger.success(`Client [${clientId}] initialized successfully`);
// 初始化成功后更新配置
const newConfig: McpConfigData = {
...currentConfig,
mcpServers: {
...currentConfig.mcpServers,
[clientId]: {
...serverConfig,
status: "active" as const,
},
},
};
await updateMcpConfig(newConfig);
} catch (error) {
const currentConfig = await getMcpConfigFromFile();
const serverConfig = currentConfig.mcpServers[clientId];
// 如果配置中存在该服务器,则更新其状态为 error
if (serverConfig) {
serverConfig.status = "error";
await updateMcpConfig(currentConfig);
}
// 初始化失败
clientsMap.set(clientId, {
client: null,
tools: null,
errorMsg: error instanceof Error ? error.message : String(error),
});
logger.error(`Failed to initialize client [${clientId}]: ${error}`);
throw error;
}
} catch (error) {
logger.error(`Failed to resume server [${clientId}]: ${error}`);
throw error;
}
}
} catch (error) {
const currentConfig = await getMcpConfigFromFile();
const serverConfig = currentConfig.mcpServers[clientId];
// 如果配置中存在该服务器,则更新其状态为 error
if (serverConfig) {
const updatedConfig = {
...currentConfig,
mcpServers: {
...currentConfig.mcpServers,
[clientId]: {
...serverConfig,
status: "error" as const,
},
},
};
await updateMcpConfig(updatedConfig);
}
// 初始化失败
clientsMap.set(clientId, {
client: null,
tools: null,
errorMsg: error instanceof Error ? error.message : String(error),
});
logger.error(`Failed to initialize client [${clientId}]: ${error}`);
throw error;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

support mcp sse
1 participant