mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2025-05-20 04:30:17 +09:00
feat: Create all MCP Servers at startup
This commit is contained in:
parent
c3108ad333
commit
664879b9df
@ -1 +1,2 @@
|
|||||||
public/serviceWorker.js
|
public/serviceWorker.js
|
||||||
|
app/mcp/mcp_config.json
|
@ -2,32 +2,76 @@
|
|||||||
|
|
||||||
import { createClient, executeRequest } from "./client";
|
import { createClient, executeRequest } from "./client";
|
||||||
import { MCPClientLogger } from "./logger";
|
import { MCPClientLogger } from "./logger";
|
||||||
import { MCP_CONF } from "@/app/mcp/mcp_config";
|
import conf from "./mcp_config.json";
|
||||||
|
|
||||||
const logger = new MCPClientLogger("MCP Server");
|
const logger = new MCPClientLogger("MCP Server");
|
||||||
|
|
||||||
let fsClient: any = null;
|
// Use Map to store all clients
|
||||||
|
const clientsMap = new Map<string, any>();
|
||||||
|
|
||||||
async function initFileSystemClient() {
|
// Whether initialized
|
||||||
if (!fsClient) {
|
let initialized = false;
|
||||||
fsClient = await createClient(MCP_CONF.filesystem, "fs");
|
|
||||||
logger.success("FileSystem client initialized");
|
// Store failed clients
|
||||||
|
let errorClients: string[] = [];
|
||||||
|
|
||||||
|
// Initialize all configured clients
|
||||||
|
export async function initializeMcpClients() {
|
||||||
|
// If already initialized, return
|
||||||
|
if (initialized) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
return fsClient;
|
|
||||||
|
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(",")}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function executeMcpAction(request: any) {
|
// Execute MCP request
|
||||||
"use server";
|
export async function executeMcpAction(clientId: string, request: any) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!fsClient) {
|
// Find the corresponding client
|
||||||
await initFileSystemClient();
|
const client = clientsMap.get(clientId);
|
||||||
|
if (!client) {
|
||||||
|
logger.error(`Client ${clientId} not found`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info("Executing MCP request for fs");
|
logger.info(`Executing MCP request for ${clientId}`);
|
||||||
return await executeRequest(fsClient, request);
|
// Execute request and return result
|
||||||
|
return await executeRequest(client, request);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`MCP execution error: ${error}`);
|
logger.error(`MCP execution error: ${error}`);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get all available client IDs
|
||||||
|
export async function getAvailableClients() {
|
||||||
|
return Array.from(clientsMap.keys()).filter(
|
||||||
|
(clientId) => !errorClients.includes(clientId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -29,11 +29,9 @@ export async function createClient(
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
capabilities: {
|
capabilities: {
|
||||||
roots: {
|
// roots: {
|
||||||
// listChanged indicates whether the client will emit notifications when the list of roots changes.
|
// listChanged: true,
|
||||||
// listChanged 指示客户端在根列表更改时是否发出通知。
|
// },
|
||||||
listChanged: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -80,8 +78,7 @@ export async function listPrimitives(client: Client) {
|
|||||||
return primitives;
|
return primitives;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Execute a request */
|
||||||
export async function executeRequest(client: Client, request: any) {
|
export async function executeRequest(client: Client, request: any) {
|
||||||
const r = client.request(request, z.any());
|
return client.request(request, z.any());
|
||||||
console.log(r);
|
|
||||||
return r;
|
|
||||||
}
|
}
|
||||||
|
@ -1,35 +1,16 @@
|
|||||||
import { createClient, listPrimitives } from "@/app/mcp/client";
|
import { createClient, listPrimitives } from "@/app/mcp/client";
|
||||||
import { MCPClientLogger } from "@/app/mcp/logger";
|
import { MCPClientLogger } from "@/app/mcp/logger";
|
||||||
import { z } from "zod";
|
import conf from "./mcp_config.json";
|
||||||
import { MCP_CONF } from "@/app/mcp/mcp_config";
|
|
||||||
|
|
||||||
const logger = new MCPClientLogger("MCP FS Example", true);
|
const logger = new MCPClientLogger("MCP Server 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() {
|
async function main() {
|
||||||
logger.info("Connecting to server...");
|
logger.info("Connecting to server...");
|
||||||
|
|
||||||
const client = await createClient(MCP_CONF.filesystem, "fs");
|
const client = await createClient(conf.mcpServers.everything, "everything");
|
||||||
const primitives = await listPrimitives(client);
|
const primitives = await listPrimitives(client);
|
||||||
|
|
||||||
logger.success(`Connected to server fs`);
|
logger.success(`Connected to server everything`);
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`server capabilities: ${Object.keys(
|
`server capabilities: ${Object.keys(
|
||||||
@ -37,53 +18,11 @@ async function main() {
|
|||||||
).join(", ")}`,
|
).join(", ")}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
logger.debug("Server supports the following primitives:");
|
logger.info("Server supports the following primitives:");
|
||||||
|
|
||||||
primitives.forEach((primitive) => {
|
primitives.forEach((primitive) => {
|
||||||
logger.debug("\n" + JSON.stringify(primitive, null, 2));
|
logger.info("\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) => {
|
main().catch((error) => {
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
// ANSI color codes for terminal output
|
||||||
const colors = {
|
const colors = {
|
||||||
reset: "\x1b[0m",
|
reset: "\x1b[0m",
|
||||||
bright: "\x1b[1m",
|
bright: "\x1b[1m",
|
||||||
@ -21,40 +22,44 @@ export class MCPClientLogger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
info(message: any) {
|
info(message: any) {
|
||||||
this.log(colors.blue, message);
|
this.print(colors.blue, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
success(message: any) {
|
success(message: any) {
|
||||||
this.log(colors.green, message);
|
this.print(colors.green, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
error(message: any) {
|
error(message: any) {
|
||||||
const formattedMessage = this.formatMessage(message);
|
this.print(colors.red, message);
|
||||||
console.error(
|
|
||||||
`${colors.red}${colors.bright}[${this.prefix}]${colors.reset} ${formattedMessage}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
warn(message: any) {
|
warn(message: any) {
|
||||||
this.log(colors.yellow, message);
|
this.print(colors.yellow, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
debug(message: any) {
|
debug(message: any) {
|
||||||
if (this.debugMode) {
|
if (this.debugMode) {
|
||||||
this.log(colors.dim, message);
|
this.print(colors.dim, message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format message to string, if message is object, convert to JSON string
|
||||||
|
*/
|
||||||
private formatMessage(message: any): string {
|
private formatMessage(message: any): string {
|
||||||
return typeof message === "object"
|
return typeof message === "object"
|
||||||
? JSON.stringify(message, null, 2)
|
? JSON.stringify(message, null, 2)
|
||||||
: message;
|
: message;
|
||||||
}
|
}
|
||||||
|
|
||||||
private log(color: string, message: any) {
|
/**
|
||||||
|
* Print formatted message to console
|
||||||
|
*/
|
||||||
|
private print(color: string, message: any) {
|
||||||
const formattedMessage = this.formatMessage(message);
|
const formattedMessage = this.formatMessage(message);
|
||||||
console.log(
|
const logMessage = `${color}${colors.bright}[${this.prefix}]${colors.reset} ${formattedMessage}`;
|
||||||
`${color}${colors.bright}[${this.prefix}]${colors.reset} ${formattedMessage}`,
|
|
||||||
);
|
// 只使用 console.log,这样日志会显示在 Tauri 的终端中
|
||||||
|
console.log(logMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
16
app/mcp/mcp_config.json
Normal file
16
app/mcp/mcp_config.json
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"filesystem": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": [
|
||||||
|
"-y",
|
||||||
|
"@modelcontextprotocol/server-filesystem",
|
||||||
|
"/Users/kadxy/Desktop"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"everything": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["-y", "@modelcontextprotocol/server-everything"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,40 +0,0 @@
|
|||||||
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>",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
@ -1,12 +1,13 @@
|
|||||||
import { Analytics } from "@vercel/analytics/react";
|
import { Analytics } from "@vercel/analytics/react";
|
||||||
|
|
||||||
import { Home } from "./components/home";
|
import { Home } from "./components/home";
|
||||||
|
|
||||||
import { getServerSideConfig } from "./config/server";
|
import { getServerSideConfig } from "./config/server";
|
||||||
|
import { initializeMcpClients } from "./mcp/actions";
|
||||||
|
|
||||||
const serverConfig = getServerSideConfig();
|
const serverConfig = getServerSideConfig();
|
||||||
|
|
||||||
export default async function App() {
|
export default async function App() {
|
||||||
|
await initializeMcpClients();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Home />
|
<Home />
|
||||||
|
@ -356,6 +356,27 @@ export const useChatStore = createPersistStore(
|
|||||||
|
|
||||||
onNewMessage(message: ChatMessage, targetSession: ChatSession) {
|
onNewMessage(message: ChatMessage, targetSession: ChatSession) {
|
||||||
get().updateTargetSession(targetSession, (session) => {
|
get().updateTargetSession(targetSession, (session) => {
|
||||||
|
// Check and process MCP JSON
|
||||||
|
const content =
|
||||||
|
typeof message.content === "string" ? message.content : "";
|
||||||
|
const mcpMatch = content.match(/```json:mcp:(\w+)([\s\S]*?)```/);
|
||||||
|
if (mcpMatch) {
|
||||||
|
try {
|
||||||
|
const clientId = mcpMatch[1];
|
||||||
|
const mcp = JSON.parse(mcpMatch[2]);
|
||||||
|
console.log("[MCP Request]", clientId, mcp);
|
||||||
|
// Execute MCP action
|
||||||
|
executeMcpAction(clientId, mcp)
|
||||||
|
.then((result) => {
|
||||||
|
console.log("[MCP Response]", result);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("[MCP Error]", error);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[MCP Error]", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
session.messages = session.messages.concat();
|
session.messages = session.messages.concat();
|
||||||
session.lastUpdate = Date.now();
|
session.lastUpdate = Date.now();
|
||||||
});
|
});
|
||||||
@ -429,22 +450,6 @@ export const useChatStore = createPersistStore(
|
|||||||
async onFinish(message) {
|
async onFinish(message) {
|
||||||
botMessage.streaming = false;
|
botMessage.streaming = false;
|
||||||
if (message) {
|
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.content = message;
|
||||||
botMessage.date = new Date().toLocaleString();
|
botMessage.date = new Date().toLocaleString();
|
||||||
get().onNewMessage(botMessage, session);
|
get().onNewMessage(botMessage, session);
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
"export:dev": "concurrently -r \"yarn mask:watch\" \"cross-env BUILD_MODE=export BUILD_APP=1 next dev\"",
|
"export:dev": "concurrently -r \"yarn mask:watch\" \"cross-env BUILD_MODE=export BUILD_APP=1 next dev\"",
|
||||||
"app:dev": "concurrently -r \"yarn mask:watch\" \"yarn tauri dev\"",
|
"app:dev": "concurrently -r \"yarn mask:watch\" \"yarn tauri dev\"",
|
||||||
"app:build": "yarn mask && yarn tauri build",
|
"app:build": "yarn mask && yarn tauri build",
|
||||||
|
"app:clear": "yarn tauri dev",
|
||||||
"prompts": "node ./scripts/fetch-prompts.mjs",
|
"prompts": "node ./scripts/fetch-prompts.mjs",
|
||||||
"prepare": "husky install",
|
"prepare": "husky install",
|
||||||
"proxy-dev": "sh ./scripts/init-proxy.sh && proxychains -f ./scripts/proxychains.conf yarn dev",
|
"proxy-dev": "sh ./scripts/init-proxy.sh && proxychains -f ./scripts/proxychains.conf yarn dev",
|
||||||
@ -58,7 +59,7 @@
|
|||||||
"zustand": "^4.3.8"
|
"zustand": "^4.3.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/api": "^1.6.0",
|
"@tauri-apps/api": "^2.1.1",
|
||||||
"@tauri-apps/cli": "1.5.11",
|
"@tauri-apps/cli": "1.5.11",
|
||||||
"@testing-library/dom": "^10.4.0",
|
"@testing-library/dom": "^10.4.0",
|
||||||
"@testing-library/jest-dom": "^6.6.3",
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
|
@ -2038,10 +2038,10 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
tslib "^2.4.0"
|
tslib "^2.4.0"
|
||||||
|
|
||||||
"@tauri-apps/api@^1.6.0":
|
"@tauri-apps/api@^2.1.1":
|
||||||
version "1.6.0"
|
version "2.1.1"
|
||||||
resolved "https://registry.npmjs.org/@tauri-apps/api/-/api-1.6.0.tgz#745b7e4e26782c3b2ad9510d558fa5bb2cf29186"
|
resolved "https://registry.yarnpkg.com/@tauri-apps/api/-/api-2.1.1.tgz#77d4ddb683d31072de4e6a47c8613d9db011652b"
|
||||||
integrity sha512-rqI++FWClU5I2UBp4HXFvl+sBWkdigBkxnpJDQUWttNyG7IZP4FwQGhTNL5EOw0vI8i6eSAJ5frLqO7n7jbJdg==
|
integrity sha512-fzUfFFKo4lknXGJq8qrCidkUcKcH2UHhfaaCNt4GzgzGaW2iS26uFOg4tS3H4P8D6ZEeUxtiD5z0nwFF0UN30A==
|
||||||
|
|
||||||
"@tauri-apps/cli-darwin-arm64@1.5.11":
|
"@tauri-apps/cli-darwin-arm64@1.5.11":
|
||||||
version "1.5.11"
|
version "1.5.11"
|
||||||
|
Loading…
Reference in New Issue
Block a user