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 5 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
3 changes: 2 additions & 1 deletion .eslintignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
public/serviceWorker.js
public/serviceWorker.js
app/mcp/mcp_config.json
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
82 changes: 82 additions & 0 deletions app/mcp/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"use server";

import { createClient, executeRequest } from "./client";
import { MCPClientLogger } from "./logger";
import conf from "./mcp_config.json";
import { McpRequestMessage } from "./types";

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

// Use Map to store all clients
const clientsMap = new Map<string, any>();

// Whether initialized
let initialized = false;

// Store failed clients
let errorClients: string[] = [];

// Initialize all configured clients
export async function initializeMcpClients() {
// If already initialized, return
if (initialized) {
return;
}

logger.info("Starting to initialize MCP clients...");

// Initialize all clients, key is clientId, value is client config
for (const [clientId, config] of Object.entries(conf.mcpServers)) {
try {
logger.info(`Initializing MCP client: ${clientId}`);
const client = await createClient(config, clientId);
clientsMap.set(clientId, client);
logger.success(`Client ${clientId} initialized`);
} catch (error) {
errorClients.push(clientId);
logger.error(`Failed to initialize client ${clientId}: ${error}`);
}
}

initialized = true;

if (errorClients.length > 0) {
logger.warn(`Failed to initialize clients: ${errorClients.join(", ")}`);
} else {
logger.success("All MCP clients initialized");
}

const availableClients = await getAvailableClients();

logger.info(`Available clients: ${availableClients.join(",")}`);
}

// Execute MCP request
export async function executeMcpAction(
clientId: string,
request: McpRequestMessage,
) {
try {
// Find the corresponding client
const client = clientsMap.get(clientId);
if (!client) {
logger.error(`Client ${clientId} not found`);
return;
}

logger.info(`Executing MCP request for ${clientId}`);

// Execute request and return result
return await executeRequest(client, request);
} catch (error) {
logger.error(`MCP execution error: ${error}`);
throw error;
}
}

// Get all available client IDs
export async function getAvailableClients() {
return Array.from(clientsMap.keys()).filter(
(clientId) => !errorClients.includes(clientId),
);
}
88 changes: 88 additions & 0 deletions app/mcp/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { MCPClientLogger } from "./logger";
import { McpRequestMessage } from "./types";
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: true,
// },
},
},
);
await client.connect(transport);
return client;
}

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;
}

/** Execute a request */
export async function executeRequest(
client: Client,
request: McpRequestMessage,
) {
return client.request(request, z.any());
}
31 changes: 31 additions & 0 deletions app/mcp/example.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { createClient, listPrimitives } from "@/app/mcp/client";
import { MCPClientLogger } from "@/app/mcp/logger";
import conf from "./mcp_config.json";

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

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

const client = await createClient(conf.mcpServers.everything, "everything");
const primitives = await listPrimitives(client);

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

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

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

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

main().catch((error) => {
logger.error(error);
process.exit(1);
});
65 changes: 65 additions & 0 deletions app/mcp/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// ANSI color codes for terminal output
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.print(colors.blue, message);
}

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

error(message: any) {
this.print(colors.red, message);
}

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

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

/**
* Format message to string, if message is object, convert to JSON string
*/
private formatMessage(message: any): string {
return typeof message === "object"
? JSON.stringify(message, null, 2)
: message;
}

/**
* Print formatted message to console
*/
private print(color: string, message: any) {
const formattedMessage = this.formatMessage(message);
const logMessage = `${color}${colors.bright}[${this.prefix}]${colors.reset} ${formattedMessage}`;

// 只使用 console.log,这样日志会显示在 Tauri 的终端中
console.log(logMessage);
}
}
16 changes: 16 additions & 0 deletions app/mcp/mcp_config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"/Users/kadxy/Desktop"
]
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Remove hardcoded filesystem path.

The filesystem server configuration contains a hardcoded path /Users/kadxy/Desktop which will not work across different environments and could pose security risks.

Consider using environment variables or configuration parameters for the path:

-        "/Users/kadxy/Desktop"
+        "${MCP_FILESYSTEM_PATH}"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"filesystem": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"/Users/kadxy/Desktop"
]
},
"filesystem": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"${MCP_FILESYSTEM_PATH}"
]
},

"everything": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-everything"]
}
}
}
61 changes: 61 additions & 0 deletions app/mcp/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// ref: https://spec.modelcontextprotocol.io/specification/basic/messages/

import { z } from "zod";

export interface McpRequestMessage {
jsonrpc?: "2.0";
id?: string | number;
method: "tools/call" | string;
params?: {
[key: string]: unknown;
};
}

export const McpRequestMessageSchema: z.ZodType<McpRequestMessage> = z.object({
jsonrpc: z.literal("2.0").optional(),
id: z.union([z.string(), z.number()]).optional(),
method: z.string(),
params: z.record(z.unknown()).optional(),
});

export interface McpResponseMessage {
jsonrpc?: "2.0";
id?: string | number;
result?: {
[key: string]: unknown;
};
error?: {
code: number;
message: string;
data?: unknown;
};
}

export const McpResponseMessageSchema: z.ZodType<McpResponseMessage> = z.object(
{
jsonrpc: z.literal("2.0").optional(),
id: z.union([z.string(), z.number()]).optional(),
result: z.record(z.unknown()).optional(),
error: z
.object({
code: z.number(),
message: z.string(),
data: z.unknown().optional(),
})
.optional(),
},
);

export interface McpNotifications {
jsonrpc?: "2.0";
method: string;
params?: {
[key: string]: unknown;
};
}

export const McpNotificationsSchema: z.ZodType<McpNotifications> = z.object({
jsonrpc: z.literal("2.0").optional(),
method: z.string(),
params: z.record(z.unknown()).optional(),
});
11 changes: 11 additions & 0 deletions app/mcp/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export function isMcpJson(content: string) {
return content.match(/```json:mcp:(\w+)([\s\S]*?)```/);
}

export function extractMcpJson(content: string) {
const match = content.match(/```json:mcp:(\w+)([\s\S]*?)```/);
if (match) {
return { clientId: match[1], mcp: JSON.parse(match[2]) };
}
return null;
}
5 changes: 3 additions & 2 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { Analytics } from "@vercel/analytics/react";

import { Home } from "./components/home";

import { getServerSideConfig } from "./config/server";
import { initializeMcpClients } from "./mcp/actions";

const serverConfig = getServerSideConfig();

export default async function App() {
await initializeMcpClients();

return (
<>
<Home />
Expand Down
Loading