import type { ExtensionAPI } from "@elyracode/coding-agent"; import { Type } from "typebox"; export default function (elyra: ExtensionAPI): void { // ── Tool 1: execute_http_request ── // "Intelligent Postman" -- send HTTP requests and analyze responses elyra.registerTool({ name: "execute_http_request", label: "Execute HTTP Request", description: "Send an HTTP request to any URL and return the response. " + "Use this to test API endpoints, verify responses, debug issues. " + "Supports GET, POST, PUT, PATCH, DELETE with headers, body, and auth. " + "Returns status code, headers, and body. Useful for testing local APIs (localhost).", parameters: Type.Object({ url: Type.String({ description: "The URL to request (e.g., http://localhost:8000/api/users)" }), method: Type.Optional( Type.Union( [Type.Literal("GET"), Type.Literal("POST"), Type.Literal("PUT"), Type.Literal("PATCH"), Type.Literal("DELETE"), Type.Literal("HEAD"), Type.Literal("OPTIONS")], { description: "HTTP method (default: GET)" }, ), ), headers: Type.Optional( Type.Record(Type.String(), Type.String(), { description: "Request headers (e.g., { \"Authorization\": \"Bearer token\", \"Content-Type\": \"application/json\" })", }), ), body: Type.Optional( Type.String({ description: "Request body (JSON string for POST/PUT/PATCH)" }), ), timeout: Type.Optional( Type.Number({ description: "Timeout in milliseconds (default: 30000)" }), ), }), execute: async (_toolCallId, params) => { try { const method = params.method ?? "GET"; const headers: Record = { "User-Agent": "elyra-http-tools", ...params.headers, }; // Auto-set Content-Type for body requests if not specified if (params.body && !headers["Content-Type"] && !headers["content-type"]) { headers["Content-Type"] = "application/json"; } const response = await fetch(params.url, { method, headers, body: params.body ?? undefined, signal: AbortSignal.timeout(params.timeout ?? 30000), }); const responseHeaders: Record = {}; response.headers.forEach((value, key) => { responseHeaders[key] = value; }); const contentType = response.headers.get("content-type") ?? ""; let body: string; if (contentType.includes("application/json")) { const json = await response.json(); body = JSON.stringify(json, null, 2); } else { body = await response.text(); } // Truncate large responses const truncated = body.length > 50000 ? `${body.slice(0, 50000)}\n\n... (truncated, ${body.length} chars total)` : body; const lines: string[] = [ `${method} ${params.url}`, `Status: ${response.status} ${response.statusText}`, "", "Response Headers:", ...Object.entries(responseHeaders).map(([k, v]) => ` ${k}: ${v}`), "", "Body:", truncated, ]; return { content: [{ type: "text", text: lines.join("\n") }], details: { status: response.status, statusText: response.statusText, contentType, }, }; } catch (error) { const msg = error instanceof Error ? error.message : String(error); return { content: [{ type: "text", text: `HTTP request failed: ${msg}` }], details: {}, }; } }, }); // ── Tool 2: read_url_content ── // Fetch a URL and convert to readable text (strips HTML tags) elyra.registerTool({ name: "read_url_content", label: "Read URL Content", description: "Fetch a web page or documentation URL and return its text content. " + "HTML is stripped to readable text. Use this to read live documentation, " + "blog posts, changelogs, or any web content that may be newer than your training data. " + "Especially useful for reading framework docs (Laravel, React, etc.) to get up-to-date information.", parameters: Type.Object({ url: Type.String({ description: "The URL to fetch (e.g., https://laravel.com/docs/13.x/reverb)" }), selector: Type.Optional( Type.String({ description: "CSS-like content hint: 'main', 'article', 'body'. Used to focus on main content and skip navigation/footer (default: auto-detect)", }), ), }), execute: async (_toolCallId, params) => { try { const response = await fetch(params.url, { headers: { "User-Agent": "elyra-http-tools (documentation reader)", Accept: "text/html, text/plain, application/json, text/markdown", }, signal: AbortSignal.timeout(30000), }); if (!response.ok) { return { content: [{ type: "text", text: `Failed to fetch ${params.url}: ${response.status} ${response.statusText}` }], details: {}, }; } const contentType = response.headers.get("content-type") ?? ""; let text: string; if (contentType.includes("application/json")) { const json = await response.json(); text = JSON.stringify(json, null, 2); } else { const html = await response.text(); if (contentType.includes("text/html")) { text = htmlToText(html, params.selector); } else { text = html; } } // Truncate const truncated = text.length > 80000 ? `${text.slice(0, 80000)}\n\n... (truncated, ${text.length} chars total)` : text; return { content: [{ type: "text", text: `# Content from ${params.url}\n\n${truncated}` }], details: { url: params.url, length: text.length }, }; } catch (error) { const msg = error instanceof Error ? error.message : String(error); return { content: [{ type: "text", text: `Failed to read ${params.url}: ${msg}` }], details: {}, }; } }, }); // ── Tool 3: inspect_openapi ── // Fetch and parse an OpenAPI/Swagger spec elyra.registerTool({ name: "inspect_openapi", label: "Inspect OpenAPI Spec", description: "Fetch an OpenAPI/Swagger specification from a URL and return a structured summary " + "of all endpoints, methods, parameters, request bodies, and response schemas. " + "Use this when the user wants to build an API client, generate SDK code, " + "or understand a third-party API structure.", parameters: Type.Object({ url: Type.String({ description: "URL to the OpenAPI/Swagger JSON or YAML spec (e.g., https://petstore.swagger.io/v2/swagger.json)", }), filter: Type.Optional( Type.String({ description: "Filter endpoints by path prefix (e.g., '/users', '/orders'). Omit for all endpoints.", }), ), }), execute: async (_toolCallId, params) => { try { const response = await fetch(params.url, { headers: { "User-Agent": "elyra-http-tools", Accept: "application/json, application/yaml, text/yaml", }, signal: AbortSignal.timeout(30000), }); if (!response.ok) { return { content: [{ type: "text", text: `Failed to fetch spec: ${response.status} ${response.statusText}` }], details: {}, }; } const spec = await response.json() as Record; const info = spec.info as Record | undefined; const paths = spec.paths as Record> | undefined; if (!paths) { return { content: [{ type: "text", text: "Invalid OpenAPI spec: no 'paths' found." }], details: {}, }; } const lines: string[] = [ `# OpenAPI: ${(info?.title as string) ?? "Unknown API"}`, `Version: ${(info?.version as string) ?? "unknown"}`, `Description: ${(info?.description as string) ?? "none"}`, `Base URL: ${extractBaseUrl(spec)}`, "", ]; let endpointCount = 0; for (const [path, methods] of Object.entries(paths)) { if (params.filter && !path.startsWith(params.filter)) continue; for (const [method, details] of Object.entries(methods)) { if (method.startsWith("x-") || typeof details !== "object" || !details) continue; const op = details as Record; endpointCount++; const summary = (op.summary as string) ?? ""; const operationId = (op.operationId as string) ?? ""; lines.push(`## ${method.toUpperCase()} ${path}`); if (summary) lines.push(`Summary: ${summary}`); if (operationId) lines.push(`Operation: ${operationId}`); // Parameters const parameters = op.parameters as Array> | undefined; if (parameters && parameters.length > 0) { lines.push("Parameters:"); for (const param of parameters) { const required = param.required ? " (required)" : ""; lines.push(` - ${param.name} [${param.in}]: ${param.description ?? (param.schema as Record)?.type ?? "unknown"}${required}`); } } // Request body const requestBody = op.requestBody as Record | undefined; if (requestBody) { const content = requestBody.content as Record> | undefined; if (content) { const jsonSchema = content["application/json"]?.schema; if (jsonSchema) { lines.push(`Request Body: ${JSON.stringify(jsonSchema, null, 2).slice(0, 500)}`); } } } // Response codes const responses = op.responses as Record> | undefined; if (responses) { const codes = Object.keys(responses).join(", "); lines.push(`Responses: ${codes}`); } lines.push(""); } } lines.unshift(`Endpoints: ${endpointCount}${params.filter ? ` (filtered by ${params.filter})` : ""}`); const text = lines.join("\n"); const truncated = text.length > 80000 ? `${text.slice(0, 80000)}\n\n... (truncated)` : text; return { content: [{ type: "text", text: truncated }], details: { endpointCount, title: info?.title }, }; } catch (error) { const msg = error instanceof Error ? error.message : String(error); return { content: [{ type: "text", text: `Failed to parse OpenAPI spec: ${msg}` }], details: {}, }; } }, }); // ── Command: /http ── elyra.registerCommand("http", { description: "HTTP tools: test APIs, fetch docs, parse OpenAPI specs", handler: async (_args: string, ctx) => { const options = [ "Test API -- send an HTTP request to test an endpoint", "Read URL -- fetch web content or documentation", "OpenAPI -- parse a Swagger/OpenAPI spec", ]; const selected = await ctx.ui.select("HTTP Tools", options); if (!selected) return; if (selected.startsWith("Test API")) { elyra.sendUserMessage("I want to test an API endpoint. Ask me for the URL, method, and any headers or body to send."); } else if (selected.startsWith("Read URL")) { elyra.sendUserMessage("I want to read content from a URL. Ask me which URL to fetch."); } else if (selected.startsWith("OpenAPI")) { elyra.sendUserMessage("I want to parse an OpenAPI spec. Ask me for the URL to the spec."); } }, }); } /** * Strip HTML tags and extract readable text content. * Simple but effective for documentation pages. */ function htmlToText(html: string, selectorHint?: string): string { let content = html; // Try to extract main content area if (selectorHint) { const tagMatch = content.match(new RegExp(`<${selectorHint}[^>]*>([\\s\\S]*?)<\\/${selectorHint}>`, "i")); if (tagMatch) content = tagMatch[1]; } else { // Auto-detect: prefer
, then
, then for (const tag of ["main", "article"]) { const match = content.match(new RegExp(`<${tag}[^>]*>([\\s\\S]*?)<\\/${tag}>`, "i")); if (match) { content = match[1]; break; } } } // Remove script and style blocks content = content.replace(/]*>[\s\S]*?<\/script>/gi, ""); content = content.replace(/]*>[\s\S]*?<\/style>/gi, ""); content = content.replace(/]*>[\s\S]*?<\/nav>/gi, ""); content = content.replace(/]*>[\s\S]*?<\/footer>/gi, ""); content = content.replace(/]*>[\s\S]*?<\/header>/gi, ""); // Convert common elements to text content = content.replace(/]*>([\s\S]*?)<\/h[1-6]>/gi, "\n## $1\n"); content = content.replace(/]*>([\s\S]*?)<\/p>/gi, "\n$1\n"); content = content.replace(/]*>([\s\S]*?)<\/li>/gi, "- $1\n"); content = content.replace(//gi, "\n"); content = content.replace(/]*>([\s\S]*?)<\/pre>/gi, "\n```\n$1\n```\n"); content = content.replace(/]*>([\s\S]*?)<\/code>/gi, "`$1`"); content = content.replace(/]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi, "$2 ($1)"); content = content.replace(/]*>([\s\S]*?)<\/strong>/gi, "**$1**"); content = content.replace(/]*>([\s\S]*?)<\/em>/gi, "*$1*"); // Strip remaining HTML tags content = content.replace(/<[^>]+>/g, ""); // Clean up HTML entities content = content.replace(/&/g, "&"); content = content.replace(/</g, "<"); content = content.replace(/>/g, ">"); content = content.replace(/"/g, '"'); content = content.replace(/'/g, "'"); content = content.replace(/ /g, " "); // Clean up whitespace content = content.replace(/\n{3,}/g, "\n\n"); content = content.trim(); return content; } function extractBaseUrl(spec: Record): string { // OpenAPI 3.x const servers = spec.servers as Array> | undefined; if (servers && servers.length > 0) { return (servers[0].url as string) ?? ""; } // Swagger 2.x const host = spec.host as string | undefined; const basePath = spec.basePath as string | undefined; const schemes = spec.schemes as string[] | undefined; if (host) { const scheme = schemes?.[0] ?? "https"; return `${scheme}://${host}${basePath ?? ""}`; } return ""; }