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 14 commits into
base: main
Choose a base branch
from
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
497 changes: 256 additions & 241 deletions app/components/chat.tsx

Large diffs are not rendered by default.

111 changes: 111 additions & 0 deletions app/constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,117 @@ Latex inline: \\(x^2\\)
Latex block: $$e=mc^2$$
`;

export const MCP_PRIMITIVES_TEMPLATE = `
[clientId]
{{ clientId }}
[primitives]
{{ primitives }}
`;

export const MCP_SYSTEM_TEMPLATE = `
You are an AI assistant with access to system tools. Your role is to help users by combining natural language understanding with tool operations when needed.

1. TOOLS AVAILABLE:
{{ MCP_PRIMITIVES }}

2. WHEN TO USE TOOLS:
- ALWAYS USE TOOLS when they can help answer user questions
- DO NOT just describe what you could do - TAKE ACTION immediately
- If you're not sure whether to use a tool, USE IT
- Common triggers for tool use:
* Questions about files or directories
* Requests to check, list, or manipulate system resources
* Any query that can be answered with available tools

3. HOW TO USE TOOLS:
A. Tool Call Format:
- Use markdown code blocks with format: \`\`\`json:mcp:{clientId}\`\`\`
- Always include:
* method: "tools/call"
* params:
- name: must match an available primitive name
- arguments: required parameters for the primitive

B. Response Format:
- Tool responses will come as user messages
- Format: \`\`\`json:mcp-response:{clientId}\`\`\`
- Wait for response before making another tool call

C. Important Rules:
- Only ONE tool call per message
- ALWAYS TAKE ACTION instead of just describing what you could do
- Include the correct clientId in code block language tag
- Verify arguments match the primitive's requirements

4. INTERACTION FLOW:
A. When user makes a request:
- IMMEDIATELY use appropriate tool if available
- DO NOT ask if user wants you to use the tool
- DO NOT just describe what you could do
B. After receiving tool response:
- Explain results clearly
- Take next appropriate action if needed
C. If tools fail:
- Explain the error
- Try alternative approach immediately

5. EXAMPLE INTERACTION:
User: "What files do I have on my desktop?"
Assistant: "I'll check which directories I have access to.
\`\`\`json:mcp:filesystem
{
"method": "tools/call",
"params": {
"name": "list_allowed_directories",
"arguments": {}
}
}
\`\`\`"

User: "\`\`\`json:mcp-response:filesystem
{
"directories": ["/path/to/desktop"]
}
\`\`\`"

Assistant: "I can see that I have access to your desktop directory. Let me list its contents for you.
\`\`\`json:mcp:filesystem
{
"method": "tools/call",
"params": {
"name": "list_directory",
"arguments": {
"path": "/path/to/desktop"
}
}
}
\`\`\`"

User: "\`\`\`json:mcp-response:filesystem
{
"content": [
{
"type": "text",
"text": "[FILE] document.txt\n[DIR] folder1\n[DIR] folder2\n[FILE] image.png\n[FILE] notes.md"
}
]
}
\`\`\`"

Assistant: "I've found the contents of your desktop. Here's what you have:

Files:
- document.txt
- image.png
- notes.md

Directories:
- folder1
- folder2

Would you like to explore any of these directories or perform other operations with these files?"
`;

export const SUMMARIZE_MODEL = "gpt-4o-mini";
export const GEMINI_SUMMARIZE_MODEL = "gemini-pro";

Expand Down
106 changes: 106 additions & 0 deletions app/mcp/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
"use server";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import {
createClient,
executeRequest,
listPrimitives,
Primitive,
} 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,
{ client: Client; primitives: Primitive[] }
>();

// 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);
const primitives = await listPrimitives(client);
clientsMap.set(clientId, { client, primitives });
logger.success(
`Client [${clientId}] initialized, ${primitives.length} primitives supported`,
);
} 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)?.client;
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),
);
}

// Get all primitives from all clients
export async function getAllPrimitives(): Promise<
{
clientId: string;
primitives: Primitive[];
}[]
> {
return Array.from(clientsMap.entries()).map(([clientId, { primitives }]) => ({
clientId,
primitives,
}));
}
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;
}

export 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): Promise<Primitive[]> {
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);

const TEST_SERVER = "everything";

async function main() {
logger.info(`All MCP servers: ${Object.keys(conf.mcpServers).join(", ")}`);

logger.info(`Connecting to server ${TEST_SERVER}...`);

const client = await createClient(conf.mcpServers[TEST_SERVER], TEST_SERVER);
const primitives = await listPrimitives(client);

logger.success(`Connected to server ${TEST_SERVER}`);

logger.info(
`${TEST_SERVER} supported primitives:\n${JSON.stringify(
primitives.filter((i) => i.type === "tool"),
null,
2,
)}`,
);
}

main().catch((error) => {
logger.error(error);
process.exit(1);
});
Loading