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

Merged
merged 31 commits into from
Jan 19, 2025
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
e1c7c54
chore: change md
Leizhenpeng Dec 23, 2024
c3108ad
feat: simple MCP example
Kadxy Dec 28, 2024
664879b
feat: Create all MCP Servers at startup
Kadxy Dec 28, 2024
e1ba8f1
feat: Send MCP response as a user
Kadxy Dec 29, 2024
fe67f79
feat: MCP message type
Kadxy Dec 29, 2024
77be190
feat: carry mcp primitives content as a system prompt
Kadxy Jan 9, 2025
f2a2b40
feat: carry mcp primitives content as a system prompt
Kadxy Jan 9, 2025
0c14ce6
fix: MCP execution content matching failed.
Kadxy Jan 9, 2025
7d51bfd
feat: MCP market
Kadxy Jan 9, 2025
b410ec3
feat: auto scroll to bottom when MCP response
Kadxy Jan 9, 2025
125a71f
fix: unnecessary initialization
Kadxy Jan 9, 2025
e95c94d
fix: inaccurate content
Kadxy Jan 9, 2025
a3af563
feat: Reset mcp_config.json to empty
Kadxy Jan 9, 2025
ce13cf6
feat: ignore mcp_config.json
Kadxy Jan 9, 2025
8aa9a50
feat: Optimize MCP configuration logic
Kadxy Jan 15, 2025
a70e9a3
chore:update mcp icon
Leizhenpeng Jan 15, 2025
be59de5
feat: Display the number of clients instead of the number of availabl…
Kadxy Jan 15, 2025
ac3d940
Merge branch 'feat-mcp' of https://github.com/ChatGPTNextWeb/ChatGPT-…
Leizhenpeng Jan 15, 2025
c89e488
chore: update icon
Leizhenpeng Jan 15, 2025
e440ff5
fix: env not work
Kadxy Jan 15, 2025
07c6349
feat: support stop/start MCP servers
Kadxy Jan 16, 2025
4d63d73
feat: load MCP preset data from server
Kadxy Jan 16, 2025
d4f499e
feat: adjust form style
Kadxy Jan 16, 2025
588d81e
feat: remove unused files
Kadxy Jan 16, 2025
4d535b1
chore: enhance mcp prompt
Leizhenpeng Jan 16, 2025
65810d9
feat: improve async operations and UI feedback
Kadxy Jan 16, 2025
0112b54
fix: missing en translation
Kadxy Jan 16, 2025
bc71ae2
feat: add ENABLE_MCP env var to toggle MCP feature globally and in Do…
Kadxy Jan 18, 2025
bfeea4e
fix: prevent MCP operations from blocking chat interface
Kadxy Jan 18, 2025
611e97e
docs: update README.md
Kadxy Jan 19, 2025
a3d3ce3
Merge branch 'main' into feat-mcp
Kadxy Jan 19, 2025
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