Ctrl K
ComunidadGuías…

lib/website-inspector.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { emptyDiagnostics } from "@/lib/diagnostics";
import { analyzeWebsiteWithPlaywright, assertPublicWebsiteUrl, redactWebsiteUrlForLog, type PageControl } from "@/lib/runtime/playwright-website";
import type { GeneratedMcpServer, GenerationDiagnostics, McpTool, OpenApiEndpoint } from "@/lib/types";

export type WebsiteMcpResult = {
  generated: GeneratedMcpServer;
  endpointMap: OpenApiEndpoint[];
  diagnostics: GenerationDiagnostics;
  sourceUrl: string;
  pageTitle: string;
};

const MAX_TOOLS = 12;

function slug(value: string) {
  return value
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, "_")
    .replace(/^_+|_+$/g, "")
    .slice(0, 42) || "element";
}

function safeArgumentName(value: string, fallback: string, seen: Set<string>) {
  const base = slug(value).replace(/^(\d)/, "field_$1") || fallback;
  const reserved = new Set(["__proto__", "constructor", "prototype", "instruction"]);
  const safeBase = reserved.has(base) ? `${base}_value` : base;
  let candidate = safeBase;
  let suffix = 2;
  while (seen.has(candidate)) {
    candidate = `${safeBase}_${suffix}`;
    suffix += 1;
  }
  seen.add(candidate);
  return candidate;
}

function inputSchemaFor(control: PageControl) {
  const properties: Record<string, unknown> = {
    instruction: {
      type: "string",
      description: "Optional instruction for the browser runtime before executing this website workflow.",
    },
  };
  const required: string[] = [];
  const seen = new Set(Object.keys(properties));

  const inputs = control.inputs ?? [];
  for (let index = 0; index < inputs.length; index += 1) {
    const input = inputs[index];
    const originalName = input.name || `field_${index + 1}`;
    const name = safeArgumentName(originalName, `field_${index + 1}`, seen);
    properties[name] = {
      type: input.type === "number" ? "number" : "string",
      description: `Value for ${originalName}.`,
      "x-astrail-name": originalName,
      "x-astrail-in": "browser_form",
    };
    if (input.required) required.push(name);
  }

  return { type: "object", properties, required };
}

function buildTools(controls: PageControl[], title: string): { tools: McpTool[]; endpointMap: OpenApiEndpoint[] } {
  const deduped = new Map<string, PageControl>();
  for (const control of controls) {
    const key = `${control.kind}:${slug(control.label)}`;
    if (!deduped.has(key)) deduped.set(key, control);
  }

  const selected = Array.from(deduped.values()).slice(0, MAX_TOOLS);
  const tools = selected.map((control) => {
    const name = `browser_${control.kind}_${slug(control.label)}`;
    const readOnly = control.kind === "open_page" || control.kind === "follow_link"
      || (control.kind === "submit_form" && (control.method ?? "GET").toUpperCase() === "GET");
    return {
      name,
      description: `Browser workflow for ${title}: ${control.label}. Safe public reads execute in Playwright; interactive/auth flows require runtime review.`,
      input_schema: inputSchemaFor(control),
      method: "BROWSER",
      path: control.selector ?? control.href ?? "/",
      annotations: { readOnlyHint: readOnly },
      policy: readOnly ? "allow" as const : "approval" as const,
    };
  });

  const endpointMap = selected.map((control, index) => {
    const readOnly = control.kind === "open_page" || control.kind === "follow_link"
      || (control.kind === "submit_form" && (control.method ?? "GET").toUpperCase() === "GET");
    return {
      method: "BROWSER",
      path: control.selector ?? control.href ?? "/",
      runtime_kind: "browser" as const,
      browser_action: control.kind,
      selector: control.selector,
      target_url: control.href ?? control.action ?? null,
      tool_name: tools[index].name,
      operation_id: tools[index].name,
      summary: control.label,
      description: tools[index].description,
      parameters: control.inputs?.map((input) => ({
        name: input.name,
        in: "browser_form",
        required: input.required,
        schema: { type: input.type === "number" ? "number" : "string" },
      })) ?? [],
      request_body: control.kind === "submit_form" ? { method: control.method ?? "GET" } : undefined,
      responses: {
        website_browser_runtime: { description: "Playwright-backed public page read for open/link/GET form workflows." },
        browser_runtime_required: { description: "Requires reviewed isolated Playwright execution for interactive workflows." },
      },
      requires_auth: false,
      operation_kind: readOnly ? "read" as const : "write" as const,
      action_class: readOnly ? "read" as const : "write" as const,
      policy: readOnly ? "allow" as const : "approval" as const,
    };
  });

  return { tools, endpointMap };
}

function generatedCodeFor(name: string, tools: McpTool[]) {
  const toolNames = tools.map((tool) => tool.name).join(", ");
  return `import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const server = new McpServer({ name: ${JSON.stringify(name)}, version: "0.1.0" });

// Website-to-MCP hosted runtime template.
// Tools discovered: ${toolNames}
// Hosted execution uses Astrail's Playwright-backed runtime, not generated JS eval.

server.tool("browser_runtime_status", "Check whether this website MCP server is attached to Astrail's browser runtime.", z.object({}), async () => ({
  content: [{ type: "text", text: "Website to MCP template installed. Hosted execution is handled by Astrail's Playwright runtime." }],
}));

const transport = new StdioServerTransport();
await server.connect(transport);
`;
}

export async function inspectWebsiteForMcp(inputUrl: string): Promise<WebsiteMcpResult> {
  const input = assertPublicWebsiteUrl(inputUrl);
  const traceId = `analysis_${Date.now().toString(36)}`;
  const diagnostics = emptyDiagnostics(input.toString());
  diagnostics.discovery_method = "playwright_website_inspection";

  const analysis = await analyzeWebsiteWithPlaywright(input.toString(), traceId);
  const controls: PageControl[] = [
    { kind: "open_page", label: "open page", selector: "body", href: analysis.finalUrl },
    ...analysis.forms,
    ...analysis.buttons,
    ...analysis.links,
  ];
  const { tools, endpointMap } = buildTools(controls, analysis.title);

  diagnostics.spec_size_bytes = analysis.visibleTextSummary.length;
  diagnostics.endpoint_count = endpointMap.length;
  diagnostics.tools_generated = tools.length;
  diagnostics.selected_group = "Browser workflows";
  diagnostics.warnings.push("Website-to-MCP is experimental. Playwright can execute safe public reads; auth, sessions, JS-only clicks, and complex workflows require sandbox review.");
  diagnostics.raw = [
    `Input URL checked: ${redactWebsiteUrlForLog(input.toString())}.`,
    `Final URL: ${redactWebsiteUrlForLog(analysis.finalUrl)}.`,
    `Page title: ${analysis.title}.`,
    `Links found: ${analysis.links.length}.`,
    `Forms found: ${analysis.forms.length}.`,
    `Buttons found: ${analysis.buttons.length}.`,
    `Screenshot: ${analysis.screenshotPath ?? "unavailable"}.`,
    `Visible text summary: ${analysis.visibleTextSummary.slice(0, 240)}.`,
    ...analysis.actionHistory.map((item) => `Action: ${item}`),
  ];
  diagnostics.trace.push(
    { label: "Website opened with Playwright", status: "passed", detail: analysis.finalUrl },
    { label: `${controls.length} browser actions detected`, status: "passed", detail: "links, buttons, forms, inputs" },
    { label: `${tools.length} MCP tools generated`, status: "passed" },
    { label: "Screenshot captured", status: analysis.screenshotPath ? "passed" : "warning", detail: analysis.screenshotPath ?? "Screenshot unavailable" },
    { label: "Experimental Playwright runtime enabled", status: "warning", detail: "Safe public reads work; complex workflows need sandbox review." }
  );
  diagnostics.timestamps.completed_at = new Date().toISOString();

  const name = `${slug(analysis.title).replace(/_/g, "-") || input.hostname}-browser-server`;
  return {
    generated: {
      name,
      description: `Playwright-backed MCP workflow candidates discovered from ${analysis.title}.`,
      tools,
      generated_code: generatedCodeFor(name, tools),
    },
    endpointMap,
    diagnostics,
    sourceUrl: input.toString(),
    pageTitle: analysis.title,
  };
}