feat: Create all MCP Servers at startup

This commit is contained in:
Kadxy 2024-12-28 21:06:26 +08:00
parent c3108ad333
commit 664879b9df
11 changed files with 134 additions and 165 deletions

View File

@ -1 +1,2 @@
public/serviceWorker.js public/serviceWorker.js
app/mcp/mcp_config.json

View File

@ -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),
);
}

View File

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

View File

@ -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) => {

View File

@ -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
View 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"]
}
}
}

View File

@ -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>",
},
},
};

View File

@ -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 />

View File

@ -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);

View File

@ -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",

View File

@ -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"