mirror of
https://github.com/Yidadaa/ChatGPT-Next-Web.git
synced 2025-08-08 13:38:48 +08:00
feat: simple MCP example
This commit is contained in:
33
app/mcp/actions.ts
Normal file
33
app/mcp/actions.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
"use server";
|
||||
|
||||
import { createClient, executeRequest } from "./client";
|
||||
import { MCPClientLogger } from "./logger";
|
||||
import { MCP_CONF } from "@/app/mcp/mcp_config";
|
||||
|
||||
const logger = new MCPClientLogger("MCP Server");
|
||||
|
||||
let fsClient: any = null;
|
||||
|
||||
async function initFileSystemClient() {
|
||||
if (!fsClient) {
|
||||
fsClient = await createClient(MCP_CONF.filesystem, "fs");
|
||||
logger.success("FileSystem client initialized");
|
||||
}
|
||||
return fsClient;
|
||||
}
|
||||
|
||||
export async function executeMcpAction(request: any) {
|
||||
"use server";
|
||||
|
||||
try {
|
||||
if (!fsClient) {
|
||||
await initFileSystemClient();
|
||||
}
|
||||
|
||||
logger.info("Executing MCP request for fs");
|
||||
return await executeRequest(fsClient, request);
|
||||
} catch (error) {
|
||||
logger.error(`MCP execution error: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
87
app/mcp/client.ts
Normal file
87
app/mcp/client.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
||||
import { MCPClientLogger } from "./logger";
|
||||
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 indicates whether the client will emit notifications when the list of roots changes.
|
||||
// listChanged 指示客户端在根列表更改时是否发出通知。
|
||||
listChanged: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
await client.connect(transport);
|
||||
return client;
|
||||
}
|
||||
|
||||
interface Primitive {
|
||||
type: "resource" | "tool" | "prompt";
|
||||
value: any;
|
||||
}
|
||||
|
||||
/** 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;
|
||||
}
|
||||
|
||||
export async function executeRequest(client: Client, request: any) {
|
||||
const r = client.request(request, z.any());
|
||||
console.log(r);
|
||||
return r;
|
||||
}
|
92
app/mcp/example.ts
Normal file
92
app/mcp/example.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { createClient, listPrimitives } from "@/app/mcp/client";
|
||||
import { MCPClientLogger } from "@/app/mcp/logger";
|
||||
import { z } from "zod";
|
||||
import { MCP_CONF } from "@/app/mcp/mcp_config";
|
||||
|
||||
const logger = new MCPClientLogger("MCP FS 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() {
|
||||
logger.info("Connecting to server...");
|
||||
|
||||
const client = await createClient(MCP_CONF.filesystem, "fs");
|
||||
const primitives = await listPrimitives(client);
|
||||
|
||||
logger.success(`Connected to server fs`);
|
||||
|
||||
logger.info(
|
||||
`server capabilities: ${Object.keys(
|
||||
client.getServerCapabilities() ?? [],
|
||||
).join(", ")}`,
|
||||
);
|
||||
|
||||
logger.debug("Server supports the following primitives:");
|
||||
|
||||
primitives.forEach((primitive) => {
|
||||
logger.debug("\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) => {
|
||||
logger.error(error);
|
||||
process.exit(1);
|
||||
});
|
60
app/mcp/logger.ts
Normal file
60
app/mcp/logger.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
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.log(colors.blue, message);
|
||||
}
|
||||
|
||||
success(message: any) {
|
||||
this.log(colors.green, message);
|
||||
}
|
||||
|
||||
error(message: any) {
|
||||
const formattedMessage = this.formatMessage(message);
|
||||
console.error(
|
||||
`${colors.red}${colors.bright}[${this.prefix}]${colors.reset} ${formattedMessage}`,
|
||||
);
|
||||
}
|
||||
|
||||
warn(message: any) {
|
||||
this.log(colors.yellow, message);
|
||||
}
|
||||
|
||||
debug(message: any) {
|
||||
if (this.debugMode) {
|
||||
this.log(colors.dim, message);
|
||||
}
|
||||
}
|
||||
|
||||
private formatMessage(message: any): string {
|
||||
return typeof message === "object"
|
||||
? JSON.stringify(message, null, 2)
|
||||
: message;
|
||||
}
|
||||
|
||||
private log(color: string, message: any) {
|
||||
const formattedMessage = this.formatMessage(message);
|
||||
console.log(
|
||||
`${color}${colors.bright}[${this.prefix}]${colors.reset} ${formattedMessage}`,
|
||||
);
|
||||
}
|
||||
}
|
40
app/mcp/mcp_config.ts
Normal file
40
app/mcp/mcp_config.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
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>",
|
||||
},
|
||||
},
|
||||
};
|
Reference in New Issue
Block a user