UNPKG

@sylphlab/mcp-net-core

Version:

Core logic for MCP network tools

515 lines (502 loc) 20.8 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { GetInterfacesToolInputSchema: () => GetInterfacesToolInputSchema, GetPublicIpToolInputSchema: () => GetPublicIpToolInputSchema, downloadTool: () => downloadTool, downloadToolInputSchema: () => downloadToolInputSchema, fetchTool: () => fetchTool, getInterfacesTool: () => getInterfacesTool, getPublicIpTool: () => getPublicIpTool }); module.exports = __toCommonJS(index_exports); // src/tools/getPublicIpTool.ts var import_mcp_core = require("@sylphlab/mcp-core"); var import_mcp_core2 = require("@sylphlab/mcp-core"); var import_zod2 = require("zod"); // src/tools/getPublicIpTool.schema.ts var import_zod = require("zod"); var GetPublicIpToolInputSchema = import_zod.z.object({ id: import_zod.z.string().optional() // Keep id for correlation if used in batch by server }); // src/tools/getPublicIpTool.ts var GetPublicIpResultSchema = import_zod2.z.object({ id: import_zod2.z.string().optional(), success: import_zod2.z.boolean(), ip: import_zod2.z.string().optional(), error: import_zod2.z.string().optional(), suggestion: import_zod2.z.string().optional() }); var GetPublicIpOutputSchema = import_zod2.z.array(GetPublicIpResultSchema); async function fetchPublicIp() { try { const response = await fetch("https://ipinfo.io/json"); if (!response.ok) throw new Error(`ipinfo.io HTTP error! status: ${response.status}`); const data = await response.json(); if (typeof data?.ip !== "string") throw new Error("IP address not found or invalid in response from ipinfo.io"); return { ip: data.ip, error: null }; } catch (e) { const errorMsg = `Failed to fetch public IP from ipinfo.io: ${e instanceof Error ? e.message : String(e)}`; try { const fallbackResponse = await fetch("https://api.ipify.org?format=json"); if (!fallbackResponse.ok) throw new Error(`api.ipify.org HTTP error! status: ${fallbackResponse.status}`); const fallbackData = await fallbackResponse.json(); if (typeof fallbackData?.ip !== "string") throw new Error("IP address not found or invalid in fallback response from api.ipify.org"); return { ip: fallbackData.ip, error: null }; } catch (fallbackError) { const fallbackErrorMsg = `Fallback failed: ${fallbackError instanceof Error ? fallbackError.message : String(fallbackError)}`; return { ip: null, error: `${errorMsg}. ${fallbackErrorMsg}` }; } } } var getPublicIpTool = (0, import_mcp_core.defineTool)({ name: "getPublicIp", description: "Retrieves the public IP address of the machine running the MCP server.", inputSchema: GetPublicIpToolInputSchema, outputSchema: GetPublicIpOutputSchema, // Use the array schema execute: async (input, _options) => { const parsed = GetPublicIpToolInputSchema.safeParse(input); if (!parsed.success) { const errorMessages = Object.entries(parsed.error.flatten().fieldErrors).map(([field, messages]) => `${field}: ${messages.join(", ")}`).join("; "); throw new Error(`Input validation failed: ${errorMessages}`); } const { id } = parsed.data; const results = []; let ip; let error; let suggestion; let success = false; try { const publicIpInfo = await fetchPublicIp(); if (publicIpInfo.error) { throw new Error(publicIpInfo.error); } if (publicIpInfo.ip) { ip = publicIpInfo.ip; success = true; } else { throw new Error("Public IP could not be determined despite no specific error reported."); } } catch (e) { success = false; error = e instanceof Error ? e.message : "Unknown error retrieving public IP"; suggestion = "Check network connectivity and reachability of public IP services (ipinfo.io, api.ipify.org)."; ip = void 0; } results.push({ id, success, ip, error, suggestion }); return [(0, import_mcp_core2.jsonPart)(results, GetPublicIpOutputSchema)]; } }); // src/tools/getInterfacesTool.ts var import_node_os = __toESM(require("os"), 1); var import_mcp_core3 = require("@sylphlab/mcp-core"); var import_mcp_core4 = require("@sylphlab/mcp-core"); var import_zod4 = require("zod"); // src/tools/getInterfacesTool.schema.ts var import_zod3 = require("zod"); var GetInterfacesToolInputSchema = import_zod3.z.object({ id: import_zod3.z.string().optional() // Keep id for correlation if used in batch by server }); // src/tools/getInterfacesTool.ts var GetInterfacesResultSchema = import_zod4.z.object({ id: import_zod4.z.string().optional(), success: import_zod4.z.boolean(), // Using z.custom for complex os types, refine if needed result: import_zod4.z.custom().optional(), error: import_zod4.z.string().optional(), suggestion: import_zod4.z.string().optional() }); var GetInterfacesOutputSchema = import_zod4.z.array(GetInterfacesResultSchema); var getInterfacesTool = (0, import_mcp_core3.defineTool)({ name: "getInterfaces", description: "Retrieves details about the network interfaces on the machine.", inputSchema: GetInterfacesToolInputSchema, outputSchema: GetInterfacesOutputSchema, // Use the array schema execute: async (input, _options) => { const parsed = GetInterfacesToolInputSchema.safeParse(input); if (!parsed.success) { const errorMessages = Object.entries(parsed.error.flatten().fieldErrors).map(([field, messages]) => `${field}: ${messages.join(", ")}`).join("; "); throw new Error(`Input validation failed: ${errorMessages}`); } const { id } = parsed.data; const results = []; let interfaces; let error; let suggestion; let success = false; try { interfaces = import_node_os.default.networkInterfaces(); if (!interfaces || Object.keys(interfaces).length === 0) { throw new Error("No network interfaces found on the system."); } success = true; suggestion = "Result contains local network interface details."; } catch (e) { success = false; error = e instanceof Error ? e.message : "Unknown error retrieving network interfaces"; suggestion = "Check system permissions or if network interfaces are available."; interfaces = void 0; } results.push({ id, success, result: interfaces, error, suggestion }); return [(0, import_mcp_core4.jsonPart)(results, GetInterfacesOutputSchema)]; } }); // src/tools/downloadTool.ts var import_mcp_core5 = require("@sylphlab/mcp-core"); var import_mcp_core6 = require("@sylphlab/mcp-core"); var import_zod6 = require("zod"); // src/tools/downloadTool.schema.ts var import_zod5 = require("zod"); var DownloadItemSchema = import_zod5.z.object({ id: import_zod5.z.string().optional(), url: import_zod5.z.string().url("Invalid URL format."), destinationPath: import_zod5.z.string().min(1, "Destination path cannot be empty."), overwrite: import_zod5.z.boolean().default(false) }); var downloadToolInputSchema = import_zod5.z.object({ items: import_zod5.z.array(DownloadItemSchema).min(1, "At least one download item is required.") }); // src/tools/downloadTool.ts var fs = __toESM(require("fs"), 1); var fsp = __toESM(require("fs/promises"), 1); var https = __toESM(require("https"), 1); var path = __toESM(require("path"), 1); var import_promises = require("stream/promises"); var DownloadResultItemSchema = import_zod6.z.object({ id: import_zod6.z.string().optional(), path: import_zod6.z.string(), success: import_zod6.z.boolean(), message: import_zod6.z.string().optional(), error: import_zod6.z.string().optional(), suggestion: import_zod6.z.string().optional() }); var DownloadToolOutputSchema = import_zod6.z.array(DownloadResultItemSchema); async function processSingleDownload(item, options) { const { id, url, destinationPath, overwrite } = item; const { workspaceRoot, allowOutsideWorkspace } = options; let absoluteDestPath; try { const validationResult = (0, import_mcp_core6.validateAndResolvePath)( destinationPath, workspaceRoot, allowOutsideWorkspace ); if (typeof validationResult !== "string") { const error = validationResult?.error ?? "Unknown path validation error"; const suggestion = validationResult?.suggestion ?? "Review path and workspace settings."; throw new Error(`Path validation failed: ${error} ${suggestion}`); } absoluteDestPath = validationResult; const destDir = path.dirname(absoluteDestPath); await fsp.mkdir(destDir, { recursive: true }); try { await fsp.access(absoluteDestPath); if (!overwrite) { throw new Error( `File already exists at '${destinationPath}'. Use overwrite: true to replace.` ); } await fsp.unlink(absoluteDestPath); } catch (error) { const isEnoent = error && typeof error === "object" && "code" in error && error.code === "ENOENT"; if (!isEnoent) { throw error; } } const response = await new Promise((resolve, reject) => { let redirectCount = 0; const maxRedirects = 5; const makeRequest = (currentUrl) => { const request = https.get(currentUrl, (res) => { const statusCode = res.statusCode ?? 0; if (statusCode >= 300 && statusCode < 400 && res.headers.location) { redirectCount++; if (redirectCount > maxRedirects) { reject(new Error("Exceeded maximum redirects (5).")); res.resume(); return; } res.resume(); makeRequest(res.headers.location); return; } if (statusCode < 200 || statusCode >= 300) { let errorBody = ""; res.on("data", (chunk) => { if (errorBody.length < 500) errorBody += chunk.toString(); }); res.on("end", () => { reject( new Error( `Download failed. Status Code: ${statusCode}. ${errorBody.substring(0, 100)}` ) ); }); res.resume(); return; } resolve(res); }); request.on("error", (err) => { reject(new Error(`Network request failed: ${err.message}`)); }); request.setTimeout(3e4, () => { request.destroy(new Error("Request timed out after 30 seconds")); }); request.end(); }; makeRequest(url); }); const fileStream = fs.createWriteStream(absoluteDestPath); try { await (0, import_promises.pipeline)(response, fileStream); } catch (pipeError) { throw new Error( `File write failed: ${pipeError instanceof Error ? pipeError.message : String(pipeError)}` ); } const successMsg = `Successfully downloaded '${url}' to '${destinationPath}'.`; return { id, path: destinationPath, success: true, message: successMsg }; } catch (error) { const errorMsg = `Download failed for item ${id ?? url}: ${error instanceof Error ? error.message : String(error)}`; if (absoluteDestPath) { try { await fsp.unlink(absoluteDestPath); } catch (cleanupError) { if (cleanupError && typeof cleanupError === "object" && "code" in cleanupError && cleanupError.code !== "ENOENT") { } } } let suggestion; if (error instanceof Error) { if (error.message?.includes("ENOTFOUND") || error.message?.includes("ECONNREFUSED") || error.message?.includes("Network request failed") || error.message?.includes("timed out")) { suggestion = "Check the URL and network connectivity."; } else if (error.message?.includes("EACCES")) { suggestion = "Check file system write permissions for the destination directory."; } else if (error.message?.includes("File already exists")) { suggestion = "Set overwrite: true if you want to replace the existing file."; } else if (error.message?.includes("Path validation failed")) { suggestion = error.message.split("Path validation failed: ")[1]?.split("Suggestion: ")[1] ?? "Check path validity and workspace settings."; } else if (error.message?.includes("File write failed")) { suggestion = "Check disk space and file system permissions."; } else { suggestion = "Review the error message and input parameters."; } } return { id, path: destinationPath, success: false, error: errorMsg, // message: errorMsg, // Keep message for success only suggestion }; } } var downloadTool = (0, import_mcp_core5.defineTool)({ name: "downloadTool", description: "Downloads one or more files from URLs to specified paths within the workspace.", inputSchema: downloadToolInputSchema, outputSchema: DownloadToolOutputSchema, // Use the array schema execute: async (input, options) => { const parsed = downloadToolInputSchema.safeParse(input); if (!parsed.success) { const errorMessages = Object.entries(parsed.error.flatten().fieldErrors).map(([field, messages]) => `${field}: ${messages.join(", ")}`).join("; "); throw new Error(`Input validation failed: ${errorMessages}`); } if (!options?.workspaceRoot) { throw new Error("Workspace root is not available in options."); } const { items } = parsed.data; const results = []; for (const item of items) { const result = await processSingleDownload(item, options); results.push(result); } return [(0, import_mcp_core6.jsonPart)(results, DownloadToolOutputSchema)]; } }); // src/tools/fetchTool.ts var import_mcp_core7 = require("@sylphlab/mcp-core"); var import_mcp_core8 = require("@sylphlab/mcp-core"); var import_zod8 = require("zod"); // src/tools/fetchTool.schema.ts var import_zod7 = require("zod"); var HttpMethodSchema = import_zod7.z.enum(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]); var ResponseTypeSchema = import_zod7.z.enum(["text", "json", "ignore"]); var FetchItemSchema = import_zod7.z.object({ id: import_zod7.z.string().optional(), url: import_zod7.z.string().url("Invalid URL format."), method: HttpMethodSchema.default("GET"), headers: import_zod7.z.record(import_zod7.z.string()).optional(), // Record<string, string> body: import_zod7.z.string().optional(), // Body is expected as a string responseType: ResponseTypeSchema.default("text") }); var fetchToolInputSchema = import_zod7.z.object({ items: import_zod7.z.array(FetchItemSchema).min(1, "At least one fetch item is required.") }); // src/tools/fetchTool.ts var FetchResultItemSchema = import_zod8.z.object({ id: import_zod8.z.string().optional(), success: import_zod8.z.boolean(), status: import_zod8.z.number().optional(), statusText: import_zod8.z.string().optional(), headers: import_zod8.z.record(import_zod8.z.string()).optional(), body: import_zod8.z.unknown().optional(), error: import_zod8.z.string().optional(), suggestion: import_zod8.z.string().optional() }); var FetchToolOutputSchema = import_zod8.z.array(FetchResultItemSchema); async function processSingleFetch(item) { const { id, url, method = "GET", headers = {}, body, responseType = "text" } = item; const resultItem = { id, success: false }; try { const requestOptions = { method, headers // Cast headers }; if (body && ["POST", "PUT", "PATCH"].includes(method)) { requestOptions.body = body; if (!requestOptions.headers || !requestOptions.headers["Content-Type"] && !requestOptions.headers["content-type"]) { if (typeof requestOptions.headers !== "object" || requestOptions.headers === null || Array.isArray(requestOptions.headers)) { requestOptions.headers = {}; } requestOptions.headers["Content-Type"] = "application/json"; } } const response = await fetch(url, requestOptions); resultItem.status = response.status; resultItem.statusText = response.statusText; const responseHeaders = {}; response.headers.forEach((value, key) => { responseHeaders[key] = value; }); resultItem.headers = responseHeaders; let responseBody = null; let bodyForError = null; try { bodyForError = await response.clone().text(); } catch { } if (!response.ok) { let errorDetail = ""; if (typeof bodyForError === "string" && bodyForError.length > 0) { errorDetail = ` - Body: ${bodyForError.substring(0, 150)}`; } throw new Error(`HTTP error! Status: ${response.status}${errorDetail}`); } try { if (responseType === "json") { responseBody = await response.json(); } else if (responseType === "text") { responseBody = bodyForError; } else { responseBody = null; } } catch (parseError) { throw new Error( `Failed to parse response body as ${responseType}: ${parseError instanceof Error ? parseError.message : String(parseError)}` ); } resultItem.body = responseBody; resultItem.success = true; } catch (e) { const errorMsg = e instanceof Error ? e.message : "Unknown error"; resultItem.error = `Fetch failed for ${url}: ${errorMsg}`; if (errorMsg.includes("Failed to parse response body")) { resultItem.suggestion = `The server responded with status ${resultItem.status}, but the response body was not valid ${responseType}. Check the 'responseType' parameter or the server's response format.`; } else if (errorMsg.includes("HTTP error")) { resultItem.suggestion = "The server returned an error status code. Check the error message body (if available) and the request details (URL, method, headers, body)."; } else if (errorMsg.includes("fetch") || errorMsg.includes("Network request failed")) { resultItem.suggestion = "Check URL, network connection, DNS resolution, and potential CORS issues if running in a browser."; } else { resultItem.suggestion = "Check URL, network connection, method, headers, body, and potential CORS issues."; } resultItem.success = false; } return resultItem; } var fetchTool = (0, import_mcp_core7.defineTool)({ name: "fetch", // Keep tool name simple description: "Performs one or more HTTP fetch requests sequentially.", inputSchema: fetchToolInputSchema, outputSchema: FetchToolOutputSchema, // Use the array schema execute: async (input, _options) => { const parsed = fetchToolInputSchema.safeParse(input); if (!parsed.success) { const errorMessages = Object.entries(parsed.error.flatten().fieldErrors).map(([field, messages]) => `${field}: ${messages.join(", ")}`).join("; "); throw new Error(`Input validation failed: ${errorMessages}`); } const { items } = parsed.data; const results = []; for (const item of items) { const result = await processSingleFetch(item); results.push(result); } return [(0, import_mcp_core8.jsonPart)(results, FetchToolOutputSchema)]; } }); // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { GetInterfacesToolInputSchema, GetPublicIpToolInputSchema, downloadTool, downloadToolInputSchema, fetchTool, getInterfacesTool, getPublicIpTool }); //# sourceMappingURL=index.cjs.map