Skip to content

Commit 61e98a2

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

File tree

8 files changed

+167
-45
lines changed

8 files changed

+167
-45
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: false,
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: 69 additions & 12 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,15 @@ 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 CONFIG_PATH = EXPORT_MODE
23+
? "/mcp/config.json"
24+
: await (async () => {
25+
const path = await import("path");
26+
return path.join(process.cwd(), "app/mcp/mcp_config.json");
27+
})();
2328

2429
const clientsMap = new Map<string, McpClientData>();
2530

@@ -339,7 +344,14 @@ export async function executeMcpAction(
339344
request: McpRequestMessage,
340345
) {
341346
try {
342-
const client = clientsMap.get(clientId);
347+
let client = clientsMap.get(clientId);
348+
if (!client) {
349+
client = [...clientsMap.values()].find(
350+
(c) =>
351+
c.tools?.tools &&
352+
c.tools.tools.find((t) => t.name == request.params?.name),
353+
);
354+
}
343355
if (!client?.client) {
344356
throw new Error(`Client ${clientId} not found`);
345357
}
@@ -354,8 +366,35 @@ export async function executeMcpAction(
354366
// 获取 MCP 配置文件
355367
export async function getMcpConfigFromFile(): Promise<McpConfigData> {
356368
try {
357-
const configStr = await fs.readFile(CONFIG_PATH, "utf-8");
358-
return JSON.parse(configStr);
369+
if (EXPORT_MODE) {
370+
const res = await fetch(CONFIG_PATH);
371+
const config: McpConfigData = await res.json();
372+
const storage = localStorage;
373+
const storedConfig_str = storage.getItem("McpConfig");
374+
if (storedConfig_str) {
375+
const storedConfig: McpConfigData = JSON.parse(storedConfig_str);
376+
const mcpServers = config.mcpServers;
377+
if (storedConfig.mcpServers) {
378+
for (const mcpId in config.mcpServers) {
379+
if (mcpId in mcpServers) {
380+
mcpServers[mcpId] = {
381+
...mcpServers[mcpId],
382+
...storedConfig.mcpServers[mcpId],
383+
};
384+
} else {
385+
mcpServers[mcpId] = storedConfig.mcpServers[mcpId];
386+
}
387+
}
388+
}
389+
390+
config.mcpServers = mcpServers;
391+
}
392+
return config;
393+
} else {
394+
const fs = await import("fs/promises");
395+
const configStr = await fs.readFile(CONFIG_PATH, "utf-8");
396+
return JSON.parse(configStr);
397+
}
359398
} catch (error) {
360399
logger.error(`Failed to load MCP config, using default config: ${error}`);
361400
return DEFAULT_MCP_CONFIG;
@@ -366,8 +405,15 @@ export async function getMcpConfigFromFile(): Promise<McpConfigData> {
366405
async function updateMcpConfig(config: McpConfigData): Promise<void> {
367406
try {
368407
// 确保目录存在
369-
await fs.mkdir(path.dirname(CONFIG_PATH), { recursive: true });
370-
await fs.writeFile(CONFIG_PATH, JSON.stringify(config, null, 2));
408+
if (EXPORT_MODE) {
409+
const storage = localStorage;
410+
storage.setItem("McpConfig", JSON.stringify(config));
411+
} else {
412+
const fs = await import("fs/promises");
413+
const path = await import("path");
414+
await fs.mkdir(path.dirname(CONFIG_PATH), { recursive: true });
415+
await fs.writeFile(CONFIG_PATH, JSON.stringify(config, null, 2));
416+
}
371417
} catch (error) {
372418
throw error;
373419
}
@@ -376,8 +422,19 @@ async function updateMcpConfig(config: McpConfigData): Promise<void> {
376422
// 检查 MCP 是否启用
377423
export async function isMcpEnabled() {
378424
try {
379-
const serverConfig = getServerSideConfig();
380-
return serverConfig.enableMcp;
425+
const config = await getMcpConfigFromFile();
426+
if (typeof config.enableMcp === "boolean") {
427+
return config.enableMcp;
428+
}
429+
if (EXPORT_MODE) {
430+
const { getClientConfig } = await import("../config/client");
431+
const clientConfig = getClientConfig();
432+
return clientConfig?.enableMcp === true;
433+
} else {
434+
const { getServerSideConfig } = await import("../config/server");
435+
const serverConfig = getServerSideConfig();
436+
return serverConfig.enableMcp;
437+
}
381438
} catch (error) {
382439
logger.error(`Failed to check MCP status: ${error}`);
383440
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: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,14 @@ export const McpNotificationsSchema: z.ZodType<McpNotifications> = z.object({
6565
// Next Chat
6666
////////////
6767
export interface ListToolsResponse {
68-
tools: {
69-
name?: string;
70-
description?: string;
71-
inputSchema?: object;
72-
[key: string]: any;
73-
};
68+
tools: ToolSchema[];
69+
}
70+
71+
export interface ToolSchema {
72+
name?: string;
73+
description?: string;
74+
inputSchema?: object;
75+
[key: string]: any;
7476
}
7577

7678
export type McpClientData =
@@ -110,14 +112,31 @@ export interface ServerStatusResponse {
110112
}
111113

112114
// MCP 服务器配置相关类型
113-
export interface ServerConfig {
115+
116+
export const isServerSseConfig = (c?: ServerConfig): c is ServerSseConfig =>
117+
typeof c === "object" && c.type === "sse";
118+
export const isServerStdioConfig = (c?: ServerConfig): c is ServerStdioConfig =>
119+
typeof c === "object" && (!c.type || c.type === "stdio");
120+
121+
export type ServerConfig = ServerStdioConfig | ServerSseConfig;
122+
123+
export interface ServerStdioConfig {
124+
type?: "stdio";
114125
command: string;
115126
args: string[];
116127
env?: Record<string, string>;
117128
status?: "active" | "paused" | "error";
118129
}
119130

131+
export interface ServerSseConfig {
132+
type: "sse";
133+
url: string;
134+
headers?: Record<string, string>;
135+
status?: "active" | "paused" | "error";
136+
}
137+
120138
export interface McpConfigData {
139+
enableMcp?: boolean;
121140
// MCP Server 的配置
122141
mcpServers: Record<string, ServerConfig>;
123142
}

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)