Skip to content

Commit db58bd2

Browse files
committed
Add support for MCP in export mode
1 parent 3809375 commit db58bd2

File tree

8 files changed

+173
-48
lines changed

8 files changed

+173
-48
lines changed

app/components/mcp-market.tsx

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,12 @@ import {
2323
} from "../mcp/actions";
2424
import {
2525
ListToolsResponse,
26+
ToolSchema,
2627
McpConfigData,
2728
PresetServer,
2829
ServerConfig,
2930
ServerStatusResponse,
31+
isServerStdioConfig,
3032
} from "../mcp/types";
3133
import clsx from "clsx";
3234
import PlayIcon from "../icons/play.svg";
@@ -46,7 +48,7 @@ export function McpMarketPage() {
4648
const [searchText, setSearchText] = useState("");
4749
const [userConfig, setUserConfig] = useState<Record<string, any>>({});
4850
const [editingServerId, setEditingServerId] = useState<string | undefined>();
49-
const [tools, setTools] = useState<ListToolsResponse["tools"] | null>(null);
51+
const [tools, setTools] = useState<ListToolsResponse | null>(null);
5052
const [viewingServerId, setViewingServerId] = useState<string | undefined>();
5153
const [isLoading, setIsLoading] = useState(false);
5254
const [config, setConfig] = useState<McpConfigData>();
@@ -136,7 +138,7 @@ export function McpMarketPage() {
136138
useEffect(() => {
137139
if (!editingServerId || !config) return;
138140
const currentConfig = config.mcpServers[editingServerId];
139-
if (currentConfig) {
141+
if (isServerStdioConfig(currentConfig)) {
140142
// 从当前配置中提取用户配置
141143
const preset = presetServers.find((s) => s.id === editingServerId);
142144
if (preset?.configSchema) {
@@ -732,16 +734,14 @@ export function McpMarketPage() {
732734
{isLoading ? (
733735
<div>Loading...</div>
734736
) : tools?.tools ? (
735-
tools.tools.map(
736-
(tool: ListToolsResponse["tools"], index: number) => (
737-
<div key={index} className={styles["tool-item"]}>
738-
<div className={styles["tool-name"]}>{tool.name}</div>
739-
<div className={styles["tool-description"]}>
740-
{tool.description}
741-
</div>
737+
tools.tools.map((tool: ToolSchema, index: number) => (
738+
<div key={index} className={styles["tool-item"]}>
739+
<div className={styles["tool-name"]}>{tool.name}</div>
740+
<div className={styles["tool-description"]}>
741+
{tool.description}
742742
</div>
743-
),
744-
)
743+
</div>
744+
))
745745
) : (
746746
<div>No tools available</div>
747747
)}

app/config/build.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,17 @@ export const getBuildConfig = () => {
4040
buildMode,
4141
isApp,
4242
template: process.env.DEFAULT_INPUT_TEMPLATE ?? DEFAULT_INPUT_TEMPLATE,
43+
44+
needCode: !!process.env.CODE,
45+
hideUserApiKey: !!process.env.HIDE_USER_API_KEY,
46+
baseUrl: process.env.BASE_URL,
47+
openaiUrl: process.env.OPENAI_BASE_URL ?? process.env.BASE_URL,
48+
disableGPT4: !!process.env.DISABLE_GPT4,
49+
useCustomConfig: !!process.env.USE_CUSTOM_CONFIG,
50+
hideBalanceQuery: !process.env.ENABLE_BALANCE_QUERY,
51+
disableFastLink: !!process.env.DISABLE_FAST_LINK,
52+
defaultModel: process.env.DEFAULT_MODEL ?? "",
53+
enableMcp: process.env.ENABLE_MCP === "true",
4354
};
4455
};
4556

app/mcp/actions.ts

Lines changed: 74 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
"use server";
1+
if (!EXPORT_MODE) {
2+
("use server");
3+
}
24
import {
35
createClient,
46
executeRequest,
@@ -14,12 +16,17 @@ import {
1416
ServerConfig,
1517
ServerStatusResponse,
1618
} from "./types";
17-
import fs from "fs/promises";
18-
import path from "path";
19-
import { getServerSideConfig } from "../config/server";
2019

2120
const logger = new MCPClientLogger("MCP Actions");
22-
const CONFIG_PATH = path.join(process.cwd(), "app/mcp/mcp_config.json");
21+
22+
const getConfigPath = async () => {
23+
if (EXPORT_MODE) {
24+
return "/mcp/config.json";
25+
} else {
26+
const path = await import("path");
27+
return path.join(process.cwd(), "app/mcp/mcp_config.json");
28+
}
29+
};
2330

2431
const clientsMap = new Map<string, McpClientData>();
2532

@@ -339,7 +346,18 @@ export async function executeMcpAction(
339346
request: McpRequestMessage,
340347
) {
341348
try {
342-
const client = clientsMap.get(clientId);
349+
let client = clientsMap.get(clientId);
350+
if (
351+
!client &&
352+
request.params?.name &&
353+
typeof request.params.name === "string"
354+
) {
355+
// Use a tool-to-client mapping that's maintained when tools are initialized
356+
const toolName = request.params.name;
357+
client = [...clientsMap.values()].find(
358+
(c) => c.tools?.tools && c.tools.tools.some((t) => t.name == toolName),
359+
);
360+
}
343361
if (!client?.client) {
344362
throw new Error(`Client ${clientId} not found`);
345363
}
@@ -354,8 +372,30 @@ export async function executeMcpAction(
354372
// 获取 MCP 配置文件
355373
export async function getMcpConfigFromFile(): Promise<McpConfigData> {
356374
try {
357-
const configStr = await fs.readFile(CONFIG_PATH, "utf-8");
358-
return JSON.parse(configStr);
375+
if (EXPORT_MODE) {
376+
const res = await fetch(await getConfigPath());
377+
const config: McpConfigData = await res.json();
378+
const storage = localStorage;
379+
const storedConfig_str = storage.getItem("McpConfig");
380+
if (storedConfig_str) {
381+
const storedConfig: McpConfigData = JSON.parse(storedConfig_str);
382+
// Create a merged configuration that combines both sources
383+
const merged = { ...config.mcpServers };
384+
if (storedConfig.mcpServers) {
385+
// Ensure we process all servers from stored config
386+
for (const id in storedConfig.mcpServers) {
387+
merged[id] = { ...merged[id], ...storedConfig.mcpServers[id] };
388+
}
389+
}
390+
391+
config.mcpServers = merged;
392+
}
393+
return config;
394+
} else {
395+
const fs = await import("fs/promises");
396+
const configStr = await fs.readFile(await getConfigPath(), "utf-8");
397+
return JSON.parse(configStr);
398+
}
359399
} catch (error) {
360400
logger.error(`Failed to load MCP config, using default config: ${error}`);
361401
return DEFAULT_MCP_CONFIG;
@@ -364,20 +404,39 @@ export async function getMcpConfigFromFile(): Promise<McpConfigData> {
364404

365405
// 更新 MCP 配置文件
366406
async function updateMcpConfig(config: McpConfigData): Promise<void> {
367-
try {
407+
if (EXPORT_MODE) {
408+
try {
409+
const storage = localStorage;
410+
storage.setItem("McpConfig", JSON.stringify(config));
411+
} catch (storageError) {
412+
logger.warn(`Failed to save MCP config to localStorage: ${storageError}`);
413+
// Continue execution without storage
414+
}
415+
} else {
416+
const fs = await import("fs/promises");
417+
const path = await import("path");
368418
// 确保目录存在
369-
await fs.mkdir(path.dirname(CONFIG_PATH), { recursive: true });
370-
await fs.writeFile(CONFIG_PATH, JSON.stringify(config, null, 2));
371-
} catch (error) {
372-
throw error;
419+
await fs.mkdir(path.dirname(await getConfigPath()), { recursive: true });
420+
await fs.writeFile(await getConfigPath(), JSON.stringify(config, null, 2));
373421
}
374422
}
375423

376424
// 检查 MCP 是否启用
377425
export async function isMcpEnabled() {
378426
try {
379-
const serverConfig = getServerSideConfig();
380-
return serverConfig.enableMcp;
427+
const config = await getMcpConfigFromFile();
428+
if (typeof config.enableMcp === "boolean") {
429+
return config.enableMcp;
430+
}
431+
if (EXPORT_MODE) {
432+
const { getClientConfig } = await import("../config/client");
433+
const clientConfig = getClientConfig();
434+
return clientConfig?.enableMcp === true;
435+
} else {
436+
const { getServerSideConfig } = await import("../config/server");
437+
const serverConfig = getServerSideConfig();
438+
return serverConfig.enableMcp;
439+
}
381440
} catch (error) {
382441
logger.error(`Failed to check MCP status: ${error}`);
383442
return false;

app/mcp/client.ts

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2-
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
32
import { MCPClientLogger } from "./logger";
4-
import { ListToolsResponse, McpRequestMessage, ServerConfig } from "./types";
3+
import {
4+
ListToolsResponse,
5+
McpRequestMessage,
6+
ServerConfig,
7+
isServerSseConfig,
8+
} from "./types";
59
import { z } from "zod";
610

711
const logger = new MCPClientLogger();
@@ -12,18 +16,34 @@ export async function createClient(
1216
): Promise<Client> {
1317
logger.info(`Creating client for ${id}...`);
1418

15-
const transport = new StdioClientTransport({
16-
command: config.command,
17-
args: config.args,
18-
env: {
19-
...Object.fromEntries(
20-
Object.entries(process.env)
21-
.filter(([_, v]) => v !== undefined)
22-
.map(([k, v]) => [k, v as string]),
23-
),
24-
...(config.env || {}),
25-
},
26-
});
19+
let transport;
20+
21+
if (isServerSseConfig(config)) {
22+
const { SSEClientTransport } = await import(
23+
"@modelcontextprotocol/sdk/client/sse.js"
24+
);
25+
transport = new SSEClientTransport(new URL(config.url));
26+
} else {
27+
if (EXPORT_MODE) {
28+
throw new Error("Cannot use stdio transport in export mode");
29+
} else {
30+
const { StdioClientTransport } = await import(
31+
"@modelcontextprotocol/sdk/client/stdio.js"
32+
);
33+
transport = new StdioClientTransport({
34+
command: config.command,
35+
args: config.args,
36+
env: {
37+
...Object.fromEntries(
38+
Object.entries(process.env)
39+
.filter(([_, v]) => v !== undefined)
40+
.map(([k, v]) => [k, v as string]),
41+
),
42+
...(config.env || {}),
43+
},
44+
});
45+
}
46+
}
2747

2848
const client = new Client(
2949
{

app/mcp/types.ts

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export interface McpRequestMessage {
88
id?: string | number;
99
method: "tools/call" | string;
1010
params?: {
11+
name?: string;
1112
[key: string]: unknown;
1213
};
1314
}
@@ -65,12 +66,14 @@ export const McpNotificationsSchema: z.ZodType<McpNotifications> = z.object({
6566
// Next Chat
6667
////////////
6768
export interface ListToolsResponse {
68-
tools: {
69-
name?: string;
70-
description?: string;
71-
inputSchema?: object;
72-
[key: string]: any;
73-
};
69+
tools: ToolSchema[];
70+
}
71+
72+
export interface ToolSchema {
73+
name?: string;
74+
description?: string;
75+
inputSchema?: object;
76+
[key: string]: any;
7477
}
7578

7679
export type McpClientData =
@@ -110,14 +113,31 @@ export interface ServerStatusResponse {
110113
}
111114

112115
// MCP 服务器配置相关类型
113-
export interface ServerConfig {
116+
117+
export const isServerSseConfig = (c?: ServerConfig): c is ServerSseConfig =>
118+
c !== null && typeof c === "object" && c.type === "sse";
119+
export const isServerStdioConfig = (c?: ServerConfig): c is ServerStdioConfig =>
120+
c !== null && typeof c === "object" && (!c.type || c.type === "stdio");
121+
122+
export type ServerConfig = ServerStdioConfig | ServerSseConfig;
123+
124+
export interface ServerStdioConfig {
125+
type?: "stdio";
114126
command: string;
115127
args: string[];
116128
env?: Record<string, string>;
117129
status?: "active" | "paused" | "error";
118130
}
119131

132+
export interface ServerSseConfig {
133+
type: "sse";
134+
url: string;
135+
headers?: Record<string, string>;
136+
status?: "active" | "paused" | "error";
137+
}
138+
120139
export interface McpConfigData {
140+
enableMcp?: boolean;
121141
// MCP Server 的配置
122142
mcpServers: Record<string, ServerConfig>;
123143
}

app/store/access.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,12 @@ export const useAccessStore = createPersistStore(
243243
);
244244
},
245245
fetch() {
246-
if (fetchState > 0 || getClientConfig()?.buildMode === "export") return;
246+
const clientConfig = getClientConfig();
247+
if (!(fetchState > 0) && clientConfig?.buildMode === "export") {
248+
set(clientConfig);
249+
fetchState = 2;
250+
}
251+
if (fetchState > 0 || clientConfig?.buildMode === "export") return;
247252
fetchState = 1;
248253
fetch("/api/config", {
249254
method: "post",

app/typing.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
declare global {
2+
const EXPORT_MODE: boolean;
3+
}
4+
15
export type Updater<T> = (updater: (value: T) => void) => void;
26

37
export const ROLES = ["system", "user", "assistant"] as const;

next.config.mjs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,15 @@ console.log("[Next] build mode", mode);
66
const disableChunk = !!process.env.DISABLE_CHUNK || mode === "export";
77
console.log("[Next] build with chunk: ", !disableChunk);
88

9+
const EXPORT_MODE = mode === "export";
10+
11+
912
/** @type {import('next').NextConfig} */
1013
const nextConfig = {
1114
webpack(config) {
15+
config.plugins.push(new webpack.DefinePlugin({
16+
EXPORT_MODE: EXPORT_MODE
17+
}));
1218
config.module.rules.push({
1319
test: /\.svg$/,
1420
use: ["@svgr/webpack"],

0 commit comments

Comments
 (0)