Skip to main content
Glama
builtin-tools.ts13 kB
/** * 内置 MCP 工具实现 * * 解决 stdio 冲突问题: * 当 code-mode 作为 MCP server 运行时,其 stdio 已被父进程占用, * 无法再通过 stdio 连接其他 MCP servers。 * * 此模块提供内置实现,直接在进程内执行,避免 stdio 冲突。 */ import * as fs from "fs/promises"; import * as path from "path"; import { stat } from "fs/promises"; const DEBUG = process.env.SANDBOX_DEBUG === "true"; /** * 模拟 MCP Client 接口 */ export interface BuiltinMCPClient { callTool(params: { name: string; arguments: any }): Promise<any>; listTools(): Promise<{ tools: Array<{ name: string; description?: string; inputSchema?: any }>; }>; } /** * Filesystem 工具实现 */ export class FilesystemTools implements BuiltinMCPClient { private allowedPaths: string[]; constructor(allowedPaths: string[] = [process.cwd()]) { this.allowedPaths = allowedPaths; } async listTools() { return { tools: [ { name: "read_file", description: "Read complete contents of a file", inputSchema: { type: "object", properties: { path: { type: "string", description: "Path to file" }, }, required: ["path"], }, }, { name: "read_multiple_files", description: "Read multiple files simultaneously", inputSchema: { type: "object", properties: { paths: { type: "array", items: { type: "string" } }, }, required: ["paths"], }, }, { name: "write_file", description: "Create new file or overwrite existing", inputSchema: { type: "object", properties: { path: { type: "string" }, content: { type: "string" }, }, required: ["path", "content"], }, }, { name: "edit_file", description: "Edit file with diff-based changes", inputSchema: { type: "object", properties: { path: { type: "string" }, edits: { type: "array" }, dryRun: { type: "boolean" }, }, required: ["path", "edits"], }, }, { name: "create_directory", description: "Create new directory or ensure it exists", inputSchema: { type: "object", properties: { path: { type: "string" }, }, required: ["path"], }, }, { name: "list_directory", description: "List directory contents", inputSchema: { type: "object", properties: { path: { type: "string" }, }, required: ["path"], }, }, { name: "move_file", description: "Move or rename files and directories", inputSchema: { type: "object", properties: { source: { type: "string" }, destination: { type: "string" }, }, required: ["source", "destination"], }, }, { name: "search_files", description: "Search for files matching pattern", inputSchema: { type: "object", properties: { path: { type: "string" }, pattern: { type: "string" }, }, required: ["path", "pattern"], }, }, { name: "get_file_info", description: "Get metadata about file or directory", inputSchema: { type: "object", properties: { path: { type: "string" }, }, required: ["path"], }, }, { name: "list_allowed_directories", description: "List directories available to access", inputSchema: { type: "object", properties: {} }, }, ], }; } async callTool(params: { name: string; arguments: any }): Promise<any> { const { name, arguments: args } = params; try { switch (name) { case "read_file": return await this.readFile(args.path); case "read_multiple_files": return await this.readMultipleFiles(args.paths); case "write_file": return await this.writeFile(args.path, args.content); case "create_directory": return await this.createDirectory(args.path); case "list_directory": return await this.listDirectory(args.path); case "move_file": return await this.moveFile(args.source, args.destination); case "search_files": return await this.searchFiles(args.path, args.pattern); case "get_file_info": return await this.getFileInfo(args.path); case "list_allowed_directories": return await this.listAllowedDirectories(); default: throw new Error(`Unknown tool: ${name}`); } } catch (error: any) { if (DEBUG) console.error(`[builtin-filesystem] ${name} error:`, error.message); throw error; } } private async readFile(filePath: string) { const content = await fs.readFile(filePath, "utf-8"); return { content: [{ type: "text", text: content }], }; } private async readMultipleFiles(paths: string[]) { const results = await Promise.all( paths.map(async (path) => { try { const content = await fs.readFile(path, "utf-8"); return { path, content }; } catch (error: any) { return { path, error: error.message }; } }), ); return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }], }; } private async writeFile(filePath: string, content: string) { // 确保父目录存在 const dirPath = path.dirname(filePath); await fs.mkdir(dirPath, { recursive: true }); await fs.writeFile(filePath, content, "utf-8"); return { content: [{ type: "text", text: `Successfully wrote to ${filePath}` }], }; } private async createDirectory(dirPath: string) { await fs.mkdir(dirPath, { recursive: true }); return { content: [ { type: "text", text: `Successfully created directory ${dirPath}` }, ], }; } private async listDirectory(dirPath: string) { const entries = await fs.readdir(dirPath, { withFileTypes: true }); const items = entries.map((entry) => ({ name: entry.name, type: entry.isDirectory() ? "directory" : "file", })); return { content: [{ type: "text", text: JSON.stringify(items, null, 2) }], }; } private async moveFile(source: string, destination: string) { await fs.rename(source, destination); return { content: [ { type: "text", text: `Successfully moved ${source} to ${destination}`, }, ], }; } private async searchFiles(basePath: string, pattern: string) { const results: string[] = []; const regex = new RegExp(pattern); async function walk(dir: string) { const entries = await fs.readdir(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { await walk(fullPath); } else if (regex.test(entry.name)) { results.push(fullPath); } } } await walk(basePath); return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }], }; } private async getFileInfo(filePath: string) { const stats = await stat(filePath); const info = { size: stats.size, created: stats.birthtime, modified: stats.mtime, accessed: stats.atime, isDirectory: stats.isDirectory(), isFile: stats.isFile(), permissions: stats.mode, }; return { content: [{ type: "text", text: JSON.stringify(info, null, 2) }], }; } private async listAllowedDirectories() { return { content: [ { type: "text", text: JSON.stringify(this.allowedPaths, null, 2) }, ], }; } } /** * Fetch 工具实现 */ export class FetchTools implements BuiltinMCPClient { async listTools() { return { tools: [ { name: "fetch", description: "Fetch URL and extract contents as markdown", inputSchema: { type: "object", properties: { url: { type: "string", description: "URL to fetch" }, max_length: { type: "number", description: "Maximum characters to return", }, start_index: { type: "number", description: "Starting character index", }, raw: { type: "boolean", description: "Return raw HTML without simplification", }, }, required: ["url"], }, }, ], }; } async callTool(params: { name: string; arguments: any }): Promise<any> { if (params.name === "fetch") { return await this.fetch(params.arguments); } throw new Error(`Unknown tool: ${params.name}`); } private async fetch(args: { url: string; max_length?: number; start_index?: number; raw?: boolean; }) { try { // 使用 Node.js 内置 https/http 模块 const https = await import("https"); const http = await import("http"); const { URL } = await import("url"); const parsedUrl = new URL(args.url); const isHttps = parsedUrl.protocol === "https:"; const client = isHttps ? https.default : http.default; // 检查是否有代理环境变量(由 srt 设置) const proxyUrl = process.env.HTTPS_PROXY || process.env.HTTP_PROXY || process.env.ALL_PROXY; let agent = undefined; if (proxyUrl && isHttps) { // 动态导入 https-proxy-agent 以支持 srt 沙箱网络访问 const { HttpsProxyAgent } = await import("https-proxy-agent"); agent = new HttpsProxyAgent(proxyUrl); if (DEBUG) console.error(`[builtin-fetch] using proxy: ${proxyUrl}`); } return new Promise((resolve, reject) => { const options: any = agent ? { agent } : {}; const req = client.get(args.url, options, (res) => { const chunks: Buffer[] = []; res.on("data", (chunk: Buffer) => { chunks.push(chunk); }); res.on("end", () => { try { let text = Buffer.concat(chunks).toString("utf-8"); // 应用偏移和长度限制 const startIndex = args.start_index || 0; const maxLength = args.max_length || 50000; if (startIndex > 0) { text = text.substring(startIndex); } if (text.length > maxLength) { text = text.substring(0, maxLength); } // 如果不是 raw 模式,简单清理 HTML const contentType = res.headers["content-type"]; if (!args.raw && contentType?.includes("text/html")) { // 基础 HTML → Markdown 转换 text = text .replace( /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "", ) .replace( /<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, "", ) .replace(/<[^>]+>/g, "") .replace(/\n\s*\n\s*\n/g, "\n\n") .trim(); } resolve({ content: [{ type: "text", text }], }); } catch (error: any) { reject(error); } }); }); req.on("error", (error: Error) => { reject(error); }); req.setTimeout(8000, () => { req.destroy(); reject(new Error("Request timeout (8s)")); }); }); } catch (error: any) { if (DEBUG) console.error("[builtin-fetch] error:", error.message); throw new Error(`Fetch failed: ${error.message}`); } } } /** * 创建内置工具集合 */ export function createBuiltinTools(): Map<string, BuiltinMCPClient> { const tools = new Map<string, BuiltinMCPClient>(); tools.set("filesystem", new FilesystemTools([process.cwd()])); tools.set("fetch", new FetchTools()); if (DEBUG) { console.error("✅ 内置工具已初始化: filesystem, fetch"); } return tools; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/cexll/code-mode-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server