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 all 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
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"
]
},
"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