UNPKG

@sylphlab/mcp-net-core

Version:

Core logic for MCP network tools

472 lines (461 loc) 18 kB
// src/tools/getPublicIpTool.ts import { defineTool } from "@sylphlab/mcp-core"; import { jsonPart } from "@sylphlab/mcp-core"; import { z as z2 } from "zod"; // src/tools/getPublicIpTool.schema.ts import { z } from "zod"; var GetPublicIpToolInputSchema = z.object({ id: z.string().optional() // Keep id for correlation if used in batch by server }); // src/tools/getPublicIpTool.ts var GetPublicIpResultSchema = z2.object({ id: z2.string().optional(), success: z2.boolean(), ip: z2.string().optional(), error: z2.string().optional(), suggestion: z2.string().optional() }); var GetPublicIpOutputSchema = z2.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 = 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 [jsonPart(results, GetPublicIpOutputSchema)]; } }); // src/tools/getInterfacesTool.ts import os from "node:os"; import { defineTool as defineTool2 } from "@sylphlab/mcp-core"; import { jsonPart as jsonPart2 } from "@sylphlab/mcp-core"; import { z as z4 } from "zod"; // src/tools/getInterfacesTool.schema.ts import { z as z3 } from "zod"; var GetInterfacesToolInputSchema = z3.object({ id: z3.string().optional() // Keep id for correlation if used in batch by server }); // src/tools/getInterfacesTool.ts var GetInterfacesResultSchema = z4.object({ id: z4.string().optional(), success: z4.boolean(), // Using z.custom for complex os types, refine if needed result: z4.custom().optional(), error: z4.string().optional(), suggestion: z4.string().optional() }); var GetInterfacesOutputSchema = z4.array(GetInterfacesResultSchema); var getInterfacesTool = defineTool2({ 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 = os.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 [jsonPart2(results, GetInterfacesOutputSchema)]; } }); // src/tools/downloadTool.ts import { defineTool as defineTool3 } from "@sylphlab/mcp-core"; import { jsonPart as jsonPart3, validateAndResolvePath } from "@sylphlab/mcp-core"; import { z as z6 } from "zod"; // src/tools/downloadTool.schema.ts import { z as z5 } from "zod"; var DownloadItemSchema = z5.object({ id: z5.string().optional(), url: z5.string().url("Invalid URL format."), destinationPath: z5.string().min(1, "Destination path cannot be empty."), overwrite: z5.boolean().default(false) }); var downloadToolInputSchema = z5.object({ items: z5.array(DownloadItemSchema).min(1, "At least one download item is required.") }); // src/tools/downloadTool.ts import * as fs from "node:fs"; import * as fsp from "node:fs/promises"; import * as https from "node:https"; import * as path from "node:path"; import { pipeline } from "node:stream/promises"; var DownloadResultItemSchema = z6.object({ id: z6.string().optional(), path: z6.string(), success: z6.boolean(), message: z6.string().optional(), error: z6.string().optional(), suggestion: z6.string().optional() }); var DownloadToolOutputSchema = z6.array(DownloadResultItemSchema); async function processSingleDownload(item, options) { const { id, url, destinationPath, overwrite } = item; const { workspaceRoot, allowOutsideWorkspace } = options; let absoluteDestPath; try { const validationResult = 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 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 = defineTool3({ 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 [jsonPart3(results, DownloadToolOutputSchema)]; } }); // src/tools/fetchTool.ts import { defineTool as defineTool4 } from "@sylphlab/mcp-core"; import { jsonPart as jsonPart4 } from "@sylphlab/mcp-core"; import { z as z8 } from "zod"; // src/tools/fetchTool.schema.ts import { z as z7 } from "zod"; var HttpMethodSchema = z7.enum(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]); var ResponseTypeSchema = z7.enum(["text", "json", "ignore"]); var FetchItemSchema = z7.object({ id: z7.string().optional(), url: z7.string().url("Invalid URL format."), method: HttpMethodSchema.default("GET"), headers: z7.record(z7.string()).optional(), // Record<string, string> body: z7.string().optional(), // Body is expected as a string responseType: ResponseTypeSchema.default("text") }); var fetchToolInputSchema = z7.object({ items: z7.array(FetchItemSchema).min(1, "At least one fetch item is required.") }); // src/tools/fetchTool.ts var FetchResultItemSchema = z8.object({ id: z8.string().optional(), success: z8.boolean(), status: z8.number().optional(), statusText: z8.string().optional(), headers: z8.record(z8.string()).optional(), body: z8.unknown().optional(), error: z8.string().optional(), suggestion: z8.string().optional() }); var FetchToolOutputSchema = z8.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 = defineTool4({ 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 [jsonPart4(results, FetchToolOutputSchema)]; } }); export { GetInterfacesToolInputSchema, GetPublicIpToolInputSchema, downloadTool, downloadToolInputSchema, fetchTool, getInterfacesTool, getPublicIpTool }; //# sourceMappingURL=index.js.map