Skip to content
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

Support MCP( WIP) #5974

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<h1 align="center">NextChat</h1>

一键免费部署你的私人 ChatGPT 网页应用,支持 GPT3, GPT4 & Gemini Pro 模型。
一键免费部署你的私人 ChatGPT 网页应用,支持 Claude, GPT4 & Gemini Pro 模型。

[NextChatAI](https://nextchat.dev/chat?utm_source=readme) / [企业版](#%E4%BC%81%E4%B8%9A%E7%89%88) / [演示 Demo](https://chat-gpt-next-web.vercel.app/) / [反馈 Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [加入 Discord](https://discord.gg/zrhvHCr79N)

Expand Down
33 changes: 33 additions & 0 deletions app/mcp/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"use server";

import { createClient, executeRequest } from "./client";
import { MCPClientLogger } from "./logger";
import { MCP_CONF } from "@/app/mcp/mcp_config";

const logger = new MCPClientLogger("MCP Server");

let fsClient: any = null;

async function initFileSystemClient() {
if (!fsClient) {
fsClient = await createClient(MCP_CONF.filesystem, "fs");
logger.success("FileSystem client initialized");
}
return fsClient;
}

export async function executeMcpAction(request: any) {
"use server";

try {
if (!fsClient) {
await initFileSystemClient();
}

logger.info("Executing MCP request for fs");
return await executeRequest(fsClient, request);
} catch (error) {
logger.error(`MCP execution error: ${error}`);
throw error;
}
}
87 changes: 87 additions & 0 deletions app/mcp/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { MCPClientLogger } from "./logger";
import { z } from "zod";

export interface ServerConfig {
command: string;
args?: string[];
env?: Record<string, string>;
}

const logger = new MCPClientLogger();

export async function createClient(
serverConfig: ServerConfig,
name: string,
): Promise<Client> {
logger.info(`Creating client for server ${name}`);

const transport = new StdioClientTransport({
command: serverConfig.command,
args: serverConfig.args,
env: serverConfig.env,
});
const client = new Client(
{
name: `nextchat-mcp-client-${name}`,
version: "1.0.0",
},
{
capabilities: {
roots: {
// listChanged indicates whether the client will emit notifications when the list of roots changes.
// listChanged 指示客户端在根列表更改时是否发出通知。
listChanged: true,
},
},
},
);
await client.connect(transport);
return client;
}
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

Consider robust error handling for client creation.

When creating the client and connecting to the transport (line 40), include error handling for potential failures (e.g., invalid command, environment issues). This ensures unexpected errors are caught gracefully.

  ...
  await client.connect(transport);
+ // TODO: Consider wrapping this in try/catch:
+ // try {
+ //   await client.connect(transport);
+ // } catch (err) {
+ //   logger.error('Failed to connect to transport', err);
+ //   throw err;
+ // }
  return client;
}

Committable suggestion skipped: line range outside the PR's diff.


interface Primitive {
type: "resource" | "tool" | "prompt";
value: any;
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

Replace 'any' type with a more specific type.

Using 'any' type reduces type safety. Consider defining specific types for resource, tool, and prompt values.

}

/** List all resources, tools, and prompts */
export async function listPrimitives(client: Client) {
const capabilities = client.getServerCapabilities();
const primitives: Primitive[] = [];
const promises = [];
if (capabilities?.resources) {
promises.push(
client.listResources().then(({ resources }) => {
resources.forEach((item) =>
primitives.push({ type: "resource", value: item }),
);
}),
);
}
if (capabilities?.tools) {
promises.push(
client.listTools().then(({ tools }) => {
tools.forEach((item) => primitives.push({ type: "tool", value: item }));
}),
);
}
if (capabilities?.prompts) {
promises.push(
client.listPrompts().then(({ prompts }) => {
prompts.forEach((item) =>
primitives.push({ type: "prompt", value: item }),
);
}),
);
}
await Promise.all(promises);
return primitives;
}

export async function executeRequest(client: Client, request: any) {
const r = client.request(request, z.any());
console.log(r);
return r;
}
92 changes: 92 additions & 0 deletions app/mcp/example.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { createClient, listPrimitives } from "@/app/mcp/client";
import { MCPClientLogger } from "@/app/mcp/logger";
import { z } from "zod";
import { MCP_CONF } from "@/app/mcp/mcp_config";

const logger = new MCPClientLogger("MCP FS Example", true);

const ListAllowedDirectoriesResultSchema = z.object({
content: z.array(
z.object({
type: z.string(),
text: z.string(),
}),
),
});

const ReadFileResultSchema = z.object({
content: z.array(
z.object({
type: z.string(),
text: z.string(),
}),
),
});

async function main() {
logger.info("Connecting to server...");

const client = await createClient(MCP_CONF.filesystem, "fs");
const primitives = await listPrimitives(client);

logger.success(`Connected to server fs`);

logger.info(
`server capabilities: ${Object.keys(
client.getServerCapabilities() ?? [],
).join(", ")}`,
);

logger.debug("Server supports the following primitives:");

primitives.forEach((primitive) => {
logger.debug("\n" + JSON.stringify(primitive, null, 2));
});

const listAllowedDirectories = async () => {
const result = await client.request(
{
method: "tools/call",
params: {
name: "list_allowed_directories",
arguments: {},
},
},
ListAllowedDirectoriesResultSchema,
);
logger.success(`Allowed directories: ${result.content[0].text}`);
return result;
};

const readFile = async (path: string) => {
const result = await client.request(
{
method: "tools/call",
params: {
name: "read_file",
arguments: {
path: path,
},
},
},
ReadFileResultSchema,
);
logger.success(`File contents for ${path}:\n${result.content[0].text}`);
return result;
};

try {
logger.info("Example 1: List allowed directories\n");
await listAllowedDirectories();

logger.info("\nExample 2: Read a file\n");
await readFile("/users/kadxy/desktop/test.txt");
} catch (error) {
logger.error(`Error executing examples: ${error}`);
}
}

main().catch((error) => {
logger.error(error);
process.exit(1);
});
60 changes: 60 additions & 0 deletions app/mcp/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
const colors = {
reset: "\x1b[0m",
bright: "\x1b[1m",
dim: "\x1b[2m",
green: "\x1b[32m",
yellow: "\x1b[33m",
red: "\x1b[31m",
blue: "\x1b[34m",
};

export class MCPClientLogger {
private readonly prefix: string;
private readonly debugMode: boolean;

constructor(
prefix: string = "NextChat MCP Client",
debugMode: boolean = false,
) {
this.prefix = prefix;
this.debugMode = debugMode;
}

info(message: any) {
this.log(colors.blue, message);
}

success(message: any) {
this.log(colors.green, message);
}

error(message: any) {
const formattedMessage = this.formatMessage(message);
console.error(
`${colors.red}${colors.bright}[${this.prefix}]${colors.reset} ${formattedMessage}`,
);
}

warn(message: any) {
this.log(colors.yellow, message);
}

debug(message: any) {
if (this.debugMode) {
this.log(colors.dim, message);
}
}

private formatMessage(message: any): string {
return typeof message === "object"
? JSON.stringify(message, null, 2)
: message;
}

private log(color: string, message: any) {
const formattedMessage = this.formatMessage(message);
console.log(
`${color}${colors.bright}[${this.prefix}]${colors.reset} ${formattedMessage}`,
);
}
}
40 changes: 40 additions & 0 deletions app/mcp/mcp_config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
export const MCP_CONF = {
"brave-search": {
command: "npx",
args: ["-y", "@modelcontextprotocol/server-brave-search"],
env: {
BRAVE_API_KEY: "<YOUR_API_KEY>",
},
},
filesystem: {
command: "npx",
args: [
"-y",
"@modelcontextprotocol/server-filesystem",
"/Users/kadxy/Desktop",
],
},
github: {
command: "npx",
args: ["-y", "@modelcontextprotocol/server-github"],
env: {
GITHUB_PERSONAL_ACCESS_TOKEN: "<YOUR_TOKEN>",
},
},
"google-maps": {
command: "npx",
args: ["-y", "@modelcontextprotocol/server-google-maps"],
env: {
GOOGLE_MAPS_API_KEY: "<YOUR_API_KEY>",
},
},
"aws-kb-retrieval": {
command: "npx",
args: ["-y", "@modelcontextprotocol/server-aws-kb-retrieval"],
env: {
AWS_ACCESS_KEY_ID: "<YOUR_ACCESS_KEY_HERE>",
AWS_SECRET_ACCESS_KEY: "<YOUR_SECRET_ACCESS_KEY_HERE>",
AWS_REGION: "<YOUR_AWS_REGION_HERE>",
},
},
};
19 changes: 18 additions & 1 deletion app/store/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { ModelConfig, ModelType, useAppConfig } from "./config";
import { useAccessStore } from "./access";
import { collectModelsWithDefaultModel } from "../utils/model";
import { createEmptyMask, Mask } from "./mask";
import { executeMcpAction } from "../mcp/actions";

const localStorage = safeLocalStorage();

Expand Down Expand Up @@ -425,9 +426,25 @@ export const useChatStore = createPersistStore(
session.messages = session.messages.concat();
});
},
onFinish(message) {
async onFinish(message) {
botMessage.streaming = false;
if (message) {
// console.log("[Bot Response] ", message);
const mcpMatch = message.match(/```json:mcp([\s\S]*?)```/);
if (mcpMatch) {
try {
const mcp = JSON.parse(mcpMatch[1]);
console.log("[MCP Request]", mcp);

// 直接调用服务器端 action
const result = await executeMcpAction(mcp);
console.log("[MCP Response]", result);
} catch (error) {
console.error("[MCP Error]", error);
}
} else {
console.log("[MCP] No MCP found in response");
}
botMessage.content = message;
botMessage.date = new Date().toLocaleString();
get().onNewMessage(botMessage, session);
Expand Down
9 changes: 6 additions & 3 deletions next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const nextConfig = {
},
experimental: {
forceSwcTransforms: true,
serverActions: true,
},
};

Expand Down Expand Up @@ -71,8 +72,10 @@ if (mode !== "export") {
// },
{
// https://{resource_name}.openai.azure.com/openai/deployments/{deploy_name}/chat/completions
source: "/api/proxy/azure/:resource_name/deployments/:deploy_name/:path*",
destination: "https://:resource_name.openai.azure.com/openai/deployments/:deploy_name/:path*",
source:
"/api/proxy/azure/:resource_name/deployments/:deploy_name/:path*",
destination:
"https://:resource_name.openai.azure.com/openai/deployments/:deploy_name/:path*",
},
{
source: "/api/proxy/google/:path*",
Expand All @@ -99,7 +102,7 @@ if (mode !== "export") {
destination: "https://dashscope.aliyuncs.com/api/:path*",
},
];

return {
beforeFiles: ret,
};
Expand Down
Loading