Skip to content

Commit d1baaba

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

File tree

8 files changed

+198
-51
lines changed

8 files changed

+198
-51
lines changed

app/components/mcp-market.tsx

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,12 @@ import {
2222
resumeMcpServer,
2323
} from "../mcp/actions";
2424
import {
25-
ListToolsResponse,
25+
ToolSchema,
2626
McpConfigData,
2727
PresetServer,
2828
ServerConfig,
2929
ServerStatusResponse,
30+
isServerStdioConfig,
3031
} from "../mcp/types";
3132
import clsx from "clsx";
3233
import PlayIcon from "../icons/play.svg";
@@ -46,7 +47,7 @@ export function McpMarketPage() {
4647
const [searchText, setSearchText] = useState("");
4748
const [userConfig, setUserConfig] = useState<Record<string, any>>({});
4849
const [editingServerId, setEditingServerId] = useState<string | undefined>();
49-
const [tools, setTools] = useState<ListToolsResponse["tools"] | null>(null);
50+
const [tools, setTools] = useState<ToolSchema[] | null>(null);
5051
const [viewingServerId, setViewingServerId] = useState<string | undefined>();
5152
const [isLoading, setIsLoading] = useState(false);
5253
const [config, setConfig] = useState<McpConfigData>();
@@ -136,7 +137,7 @@ export function McpMarketPage() {
136137
useEffect(() => {
137138
if (!editingServerId || !config) return;
138139
const currentConfig = config.mcpServers[editingServerId];
139-
if (currentConfig) {
140+
if (isServerStdioConfig(currentConfig)) {
140141
// 从当前配置中提取用户配置
141142
const preset = presetServers.find((s) => s.id === editingServerId);
142143
if (preset?.configSchema) {
@@ -230,7 +231,7 @@ export function McpMarketPage() {
230231
try {
231232
const result = await getClientTools(id);
232233
if (result) {
233-
setTools(result);
234+
setTools(result?.tools);
234235
} else {
235236
throw new Error("Failed to load tools");
236237
}
@@ -731,17 +732,15 @@ export function McpMarketPage() {
731732
<div className={styles["tools-list"]}>
732733
{isLoading ? (
733734
<div>Loading...</div>
734-
) : 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>
735+
) : tools ? (
736+
tools.map((tool: ToolSchema, 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}
742741
</div>
743-
),
744-
)
742+
</div>
743+
))
745744
) : (
746745
<div>No tools available</div>
747746
)}

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: 95 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,14 +16,22 @@ 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";
19+
20+
const JSON_INDENT = 2;
2021

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

2433
const clientsMap = new Map<string, McpClientData>();
34+
const toolToClientMap = new Map<string, string>();
2535

2636
// 获取客户端状态
2737
export async function getClientsStatus(): Promise<
@@ -126,6 +136,13 @@ async function initializeSingleClient(
126136
`Supported tools for [${clientId}]: ${JSON.stringify(tools, null, 2)}`,
127137
);
128138
clientsMap.set(clientId, { client, tools, errorMsg: null });
139+
if (tools?.tools) {
140+
for (const tool of tools.tools) {
141+
if (tool.name) {
142+
toolToClientMap.set(tool.name, clientId);
143+
}
144+
}
145+
}
129146
logger.success(`Client [${clientId}] initialized successfully`);
130147
})
131148
.catch((error) => {
@@ -243,6 +260,13 @@ export async function resumeMcpServer(clientId: string): Promise<void> {
243260
const client = await createClient(clientId, serverConfig);
244261
const tools = await listTools(client);
245262
clientsMap.set(clientId, { client, tools, errorMsg: null });
263+
if (tools?.tools) {
264+
for (const tool of tools.tools) {
265+
if (tool.name) {
266+
toolToClientMap.set(tool.name, clientId);
267+
}
268+
}
269+
}
246270
logger.success(`Client [${clientId}] initialized successfully`);
247271

248272
// 初始化成功后更新配置
@@ -339,7 +363,19 @@ export async function executeMcpAction(
339363
request: McpRequestMessage,
340364
) {
341365
try {
342-
const client = clientsMap.get(clientId);
366+
let client = clientsMap.get(clientId);
367+
if (
368+
!client &&
369+
request.params?.name &&
370+
typeof request.params.name === "string"
371+
) {
372+
// Use a tool-to-client mapping that's maintained when tools are initialized
373+
const toolName = request.params.name;
374+
const toolClientId = toolToClientMap.get(toolName);
375+
if (toolClientId) {
376+
client = clientsMap.get(toolClientId);
377+
}
378+
}
343379
if (!client?.client) {
344380
throw new Error(`Client ${clientId} not found`);
345381
}
@@ -354,8 +390,30 @@ export async function executeMcpAction(
354390
// 获取 MCP 配置文件
355391
export async function getMcpConfigFromFile(): Promise<McpConfigData> {
356392
try {
357-
const configStr = await fs.readFile(CONFIG_PATH, "utf-8");
358-
return JSON.parse(configStr);
393+
if (EXPORT_MODE) {
394+
const res = await fetch(await getConfigPath());
395+
const config: McpConfigData = await res.json();
396+
const storage = localStorage;
397+
const storedConfig_str = storage.getItem("McpConfig");
398+
if (storedConfig_str) {
399+
const storedConfig: McpConfigData = JSON.parse(storedConfig_str);
400+
// Create a merged configuration that combines both sources
401+
const merged = { ...config.mcpServers };
402+
if (storedConfig.mcpServers) {
403+
// Ensure we process all servers from stored config
404+
for (const id in storedConfig.mcpServers) {
405+
merged[id] = { ...merged[id], ...storedConfig.mcpServers[id] };
406+
}
407+
}
408+
409+
config.mcpServers = merged;
410+
}
411+
return config;
412+
} else {
413+
const fs = await import("fs/promises");
414+
const configStr = await fs.readFile(await getConfigPath(), "utf-8");
415+
return JSON.parse(configStr);
416+
}
359417
} catch (error) {
360418
logger.error(`Failed to load MCP config, using default config: ${error}`);
361419
return DEFAULT_MCP_CONFIG;
@@ -364,20 +422,42 @@ export async function getMcpConfigFromFile(): Promise<McpConfigData> {
364422

365423
// 更新 MCP 配置文件
366424
async function updateMcpConfig(config: McpConfigData): Promise<void> {
367-
try {
425+
if (EXPORT_MODE) {
426+
try {
427+
const storage = localStorage;
428+
storage.setItem("McpConfig", JSON.stringify(config));
429+
} catch (storageError) {
430+
logger.warn(`Failed to save MCP config to localStorage: ${storageError}`);
431+
// Continue execution without storage
432+
}
433+
} else {
434+
const fs = await import("fs/promises");
435+
const path = await import("path");
368436
// 确保目录存在
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;
437+
await fs.mkdir(path.dirname(await getConfigPath()), { recursive: true });
438+
await fs.writeFile(
439+
await getConfigPath(),
440+
JSON.stringify(config, null, JSON_INDENT),
441+
);
373442
}
374443
}
375444

376445
// 检查 MCP 是否启用
377446
export async function isMcpEnabled() {
378447
try {
379-
const serverConfig = getServerSideConfig();
380-
return serverConfig.enableMcp;
448+
const config = await getMcpConfigFromFile();
449+
if (typeof config.enableMcp === "boolean") {
450+
return config.enableMcp;
451+
}
452+
if (EXPORT_MODE) {
453+
const { getClientConfig } = await import("../config/client");
454+
const clientConfig = getClientConfig();
455+
return clientConfig?.enableMcp === true;
456+
} else {
457+
const { getServerSideConfig } = await import("../config/server");
458+
const serverConfig = getServerSideConfig();
459+
return serverConfig.enableMcp;
460+
}
381461
} catch (error) {
382462
logger.error(`Failed to check MCP status: ${error}`);
383463
return false;

app/mcp/client.ts

Lines changed: 36 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,36 @@ 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(
29+
"Cannot use stdio transport in export mode. Please use SSE transport configuration instead.",
30+
);
31+
} else {
32+
const { StdioClientTransport } = await import(
33+
"@modelcontextprotocol/sdk/client/stdio.js"
34+
);
35+
transport = new StdioClientTransport({
36+
command: config.command,
37+
args: config.args,
38+
env: {
39+
...Object.fromEntries(
40+
Object.entries(process.env)
41+
.filter(([_, v]) => v !== undefined)
42+
.map(([k, v]) => [k, v as string]),
43+
),
44+
...(config.env || {}),
45+
},
46+
});
47+
}
48+
}
2749

2850
const client = new Client(
2951
{

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;

0 commit comments

Comments
 (0)