@metalift/mcp
Advanced tools
+38
-11
@@ -22,2 +22,31 @@ const B2B_ATTESTATION_HEADER = "X-B2B-Attestation"; | ||
| } | ||
| function formatApiError(body, status) { | ||
| if (typeof body === "object" && body !== null) { | ||
| const record = body; | ||
| const detail = record.detail; | ||
| if (typeof detail === "string" && detail) { | ||
| return detail; | ||
| } | ||
| if (typeof detail === "object" && detail !== null) { | ||
| const detailRecord = detail; | ||
| const message = detailRecord.message; | ||
| const code = detailRecord.code; | ||
| if (typeof message === "string" && message) { | ||
| return typeof code === "string" ? `${message} (${code})` : message; | ||
| } | ||
| } | ||
| const error = record.error; | ||
| if (typeof error === "string" && error) { | ||
| return error; | ||
| } | ||
| } | ||
| if (typeof body === "string" && body.trim()) { | ||
| const trimmed = body.trim(); | ||
| if (trimmed.startsWith("<!DOCTYPE") || trimmed.startsWith("<html")) { | ||
| return `Request failed: ${status} (received HTML instead of JSON — check METALIFT_API_URL points to the scrape API, e.g. http://localhost:8080)`; | ||
| } | ||
| return trimmed.slice(0, 500); | ||
| } | ||
| return `Request failed: ${status}`; | ||
| } | ||
| export class MetaliftClient { | ||
@@ -46,14 +75,12 @@ apiUrl; | ||
| }); | ||
| const body = (await response.json()); | ||
| const rawBody = await response.text(); | ||
| let body; | ||
| try { | ||
| body = rawBody ? JSON.parse(rawBody) : {}; | ||
| } | ||
| catch { | ||
| throw new Error(formatApiError(rawBody, response.status)); | ||
| } | ||
| if (!response.ok) { | ||
| const detail = typeof body === "object" && body !== null && "detail" in body | ||
| ? body.detail | ||
| : undefined; | ||
| const error = typeof body === "object" && body !== null && "error" in body | ||
| ? body.error | ||
| : undefined; | ||
| const message = (typeof detail === "string" && detail) || | ||
| (typeof error === "string" && error) || | ||
| `Request failed: ${response.status}`; | ||
| throw new Error(message); | ||
| throw new Error(formatApiError(body, response.status)); | ||
| } | ||
@@ -60,0 +87,0 @@ return withBilling(body, billingFromResponse(response)); |
+2
-2
@@ -15,3 +15,3 @@ #!/usr/bin/env node | ||
| title: "Scrape URL", | ||
| description: `Scrape a single URL into markdown, HTML, or text for LLM context. Default: fast direct static article extraction (strategy=article, render=static, proxy=direct, 10s timeout). For WAF, SPA, retail, or JS-heavy pages pass strategy=auto or a specific strategy (spa, cloudflare, retail). Response includes credits_charged based on actual usage (static=1, JS=5, premium=10+). ${COMPLIANCE_NOTICE}`, | ||
| description: `Scrape a single URL into markdown, HTML, or text for LLM context. Default: fast direct static article extraction (strategy=article, render=static, proxy=direct, 10s timeout). For full page HTML on static sites use strategy=download with formats=["html"] (1 credit, all tiers). strategy=raw and full-page HTML without download require Enterprise tier. For WAF, SPA, retail, or JS-heavy pages pass strategy=auto or a specific strategy (spa, cloudflare, retail). Response includes credits_charged based on actual usage (static=1, JS=5, premium=10+). ${COMPLIANCE_NOTICE}`, | ||
| inputSchema: { | ||
@@ -29,3 +29,3 @@ url: z.string().url(), | ||
| .optional() | ||
| .describe("Scrape strategy: auto, article, spa, cloudflare, authenticated, listing, retail, jsonld, raw, or comma-separated chain. See /v1/strategies for protection levels and credit estimates."), | ||
| .describe("Scrape strategy: auto, article, spa, cloudflare, authenticated, listing, retail, jsonld, download, raw, or comma-separated chain. Use download for full static HTML. See /v1/strategies for protection levels and credit estimates."), | ||
| cookies: z.record(z.string()).optional(), | ||
@@ -32,0 +32,0 @@ cookie_header: z |
+26
-2
| /** Default timeout for the fast direct static markdown path. */ | ||
| export const FAST_SCRAPE_TIMEOUT_MS = 10_000; | ||
| function wantsFullPageHtml(args) { | ||
| return args.formats?.includes("html") === true && args.only_main_content === false; | ||
| } | ||
| function shouldAutoDownload(args) { | ||
| if (!wantsFullPageHtml(args)) { | ||
| return false; | ||
| } | ||
| const primary = (args.strategy ?? "auto").split(",")[0]?.trim().toLowerCase(); | ||
| if (primary === "raw" || primary === "download") { | ||
| return false; | ||
| } | ||
| if (primary && primary !== "auto" && primary !== "article") { | ||
| return false; | ||
| } | ||
| return true; | ||
| } | ||
| function isMarkdownOnlyFormats(formats) { | ||
@@ -11,2 +27,11 @@ return formats === undefined || (formats.length === 1 && formats[0] === "markdown"); | ||
| export function normalizeScrapeArgs(args) { | ||
| if (shouldAutoDownload(args)) { | ||
| return { | ||
| ...args, | ||
| strategy: "download", | ||
| render: args.render ?? "static", | ||
| proxy: args.proxy ?? "direct", | ||
| timeout_ms: args.timeout_ms ?? FAST_SCRAPE_TIMEOUT_MS, | ||
| }; | ||
| } | ||
| const hasAdvancedOpts = args.strategy !== undefined || | ||
@@ -27,6 +52,5 @@ args.render !== undefined || | ||
| strategy: "article", | ||
| render: "static", | ||
| proxy: "direct", | ||
| render: "auto", | ||
| timeout_ms: args.timeout_ms ?? FAST_SCRAPE_TIMEOUT_MS, | ||
| }; | ||
| } |
@@ -10,4 +10,3 @@ import assert from "node:assert/strict"; | ||
| strategy: "article", | ||
| render: "static", | ||
| proxy: "direct", | ||
| render: "auto", | ||
| timeout_ms: FAST_SCRAPE_TIMEOUT_MS, | ||
@@ -64,2 +63,24 @@ }); | ||
| }); | ||
| test("normalizeScrapeArgs auto-selects download for full-page HTML", () => { | ||
| const args = normalizeScrapeArgs({ | ||
| url: BASE_URL, | ||
| formats: ["html"], | ||
| only_main_content: false, | ||
| }); | ||
| assert.equal(args.strategy, "download"); | ||
| assert.equal(args.render, "static"); | ||
| assert.equal(args.proxy, "direct"); | ||
| assert.equal(args.timeout_ms, FAST_SCRAPE_TIMEOUT_MS); | ||
| }); | ||
| test("normalizeScrapeArgs preserves explicit cloudflare for full-page HTML", () => { | ||
| const args = normalizeScrapeArgs({ | ||
| url: BASE_URL, | ||
| formats: ["html"], | ||
| only_main_content: false, | ||
| strategy: "cloudflare", | ||
| render: "dynamic", | ||
| }); | ||
| assert.equal(args.strategy, "cloudflare"); | ||
| assert.equal(args.render, "dynamic"); | ||
| }); | ||
| test("normalizeScrapeArgs preserves non-markdown formats", () => { | ||
@@ -77,4 +98,3 @@ const args = normalizeScrapeArgs({ url: BASE_URL, formats: ["html"] }); | ||
| assert.equal(args.strategy, "article"); | ||
| assert.equal(args.render, "static"); | ||
| assert.equal(args.proxy, "direct"); | ||
| assert.equal(args.render, "auto"); | ||
| }); |
+1
-1
| { | ||
| "name": "@metalift/mcp", | ||
| "mcpName": "io.github.MetaLift-AI/metalift", | ||
| "version": "1.0.5", | ||
| "version": "1.0.6", | ||
| "description": "Metalift MCP server for AI agents", | ||
@@ -6,0 +6,0 @@ "license": "SEE LICENSE IN LICENSE", |
Unidentified License
LicenseSomething that seems like a license was found, but its contents could not be matched with a known license.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Unidentified License
LicenseSomething that seems like a license was found, but its contents could not be matched with a known license.
30718
9.2%616
13.03%3
-25%