feat: simple MCP example

This commit is contained in:
Kadxy
2024-12-28 14:31:43 +08:00
parent e1c7c54dfa
commit c3108ad333
10 changed files with 413 additions and 9 deletions

33
app/mcp/actions.ts Normal file
View 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
View 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
View 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
View 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
View 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>",
},
},
};