Skip to content

Commit 529f6b5

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

File tree

8 files changed

+169
-45
lines changed

8 files changed

+169
-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: 71 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
"use server";
1+
if (EXPORT_MODE) {
2+
("use client");
3+
} else {
4+
("use server");
5+
}
26
import {
37
createClient,
48
executeRequest,
@@ -14,12 +18,15 @@ import {
1418
ServerConfig,
1519
ServerStatusResponse,
1620
} from "./types";
17-
import fs from "fs/promises";
18-
import path from "path";
19-
import { getServerSideConfig } from "../config/server";
2021

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

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

@@ -339,7 +346,14 @@ export async function executeMcpAction(
339346
request: McpRequestMessage,
340347
) {
341348
try {
342-
const client = clientsMap.get(clientId);
349+
let client = clientsMap.get(clientId);
350+
if (!client) {
351+
client = [...clientsMap.values()].find(
352+
(c) =>
353+
c.tools?.tools &&
354+
c.tools.tools.find((t) => t.name == request.params?.name),
355+
);
356+
}
343357
if (!client?.client) {
344358
throw new Error(`Client ${clientId} not found`);
345359
}
@@ -354,8 +368,35 @@ export async function executeMcpAction(
354368
// 获取 MCP 配置文件
355369
export async function getMcpConfigFromFile(): Promise<McpConfigData> {
356370
try {
357-
const configStr = await fs.readFile(CONFIG_PATH, "utf-8");
358-
return JSON.parse(configStr);
371+
if (EXPORT_MODE) {
372+
const res = await fetch(CONFIG_PATH);
373+
const config: McpConfigData = await res.json();
374+
const storage = localStorage;
375+
const storedConfig_str = storage.getItem("McpConfig");
376+
if (storedConfig_str) {
377+
const storedConfig: McpConfigData = JSON.parse(storedConfig_str);
378+
const mcpServers = config.mcpServers;
379+
if (storedConfig.mcpServers) {
380+
for (const mcpId in config.mcpServers) {
381+
if (mcpId in mcpServers) {
382+
mcpServers[mcpId] = {
383+
...mcpServers[mcpId],
384+
...storedConfig.mcpServers[mcpId],
385+
};
386+
} else {
387+
mcpServers[mcpId] = storedConfig.mcpServers[mcpId];
388+
}
389+
}
390+
}
391+
392+
config.mcpServers = mcpServers;
393+
}
394+
return config;
395+
} else {
396+
const fs = await import("fs/promises");
397+
const configStr = await fs.readFile(CONFIG_PATH, "utf-8");
398+
return JSON.parse(configStr);
399+
}
359400
} catch (error) {
360401
logger.error(`Failed to load MCP config, using default config: ${error}`);
361402
return DEFAULT_MCP_CONFIG;
@@ -366,8 +407,15 @@ export async function getMcpConfigFromFile(): Promise<McpConfigData> {
366407
async function updateMcpConfig(config: McpConfigData): Promise<void> {
367408
try {
368409
// 确保目录存在
369-
await fs.mkdir(path.dirname(CONFIG_PATH), { recursive: true });
370-
await fs.writeFile(CONFIG_PATH, JSON.stringify(config, null, 2));
410+
if (EXPORT_MODE) {
411+
const storage = localStorage;
412+
storage.setItem("McpConfig", JSON.stringify(config));
413+
} else {
414+
const fs = await import("fs/promises");
415+
const path = await import("path");
416+
await fs.mkdir(path.dirname(CONFIG_PATH), { recursive: true });
417+
await fs.writeFile(CONFIG_PATH, JSON.stringify(config, null, 2));
418+
}
371419
} catch (error) {
372420
throw error;
373421
}
@@ -376,8 +424,19 @@ async function updateMcpConfig(config: McpConfigData): Promise<void> {
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: 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)