🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@metalift/mcp

Package Overview
Dependencies
Maintainers
1
Versions
11
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@metalift/mcp - npm Package Compare versions

Comparing version
1.0.7
to
1.0.10
+6
dist/scrape-format.d.ts
export type ScrapeResponseDetail = "compact" | "standard" | "full";
/** Matches API COMPACT_BODY_MAX_CHARS — keep in sync with scrape_response.py */
export declare const COMPACT_BODY_MAX_CHARS = 16000;
export declare function resolveScrapeResponseDetail(result: Record<string, unknown>, fallback?: ScrapeResponseDetail): ScrapeResponseDetail;
export declare function formatScrapeResponse(result: Record<string, unknown>, detail?: ScrapeResponseDetail): string;
export declare function formatBatchScrapeResponse(result: Record<string, unknown>, detail?: ScrapeResponseDetail): string;
/** Matches API COMPACT_BODY_MAX_CHARS — keep in sync with scrape_response.py */
export const COMPACT_BODY_MAX_CHARS = 16_000;
export function resolveScrapeResponseDetail(result, fallback = "compact") {
const detail = result.response_detail;
if (detail === "compact" || detail === "standard" || detail === "full") {
return detail;
}
return fallback;
}
function formatMetadataBlock(metadata, detail) {
const lines = [];
const title = typeof metadata.title === "string" ? metadata.title : "Untitled";
lines.push(`# ${title}`);
const sourceUrl = typeof metadata.source_url === "string" ? metadata.source_url : "";
if (sourceUrl)
lines.push(`URL: ${sourceUrl}`);
if (typeof metadata.status_code === "number") {
lines.push(`HTTP: ${metadata.status_code}`);
}
if (detail !== "compact") {
if (typeof metadata.description === "string" && metadata.description) {
lines.push(`Description: ${metadata.description.slice(0, 400)}`);
}
if (typeof metadata.strategy === "string") {
lines.push(`Strategy: ${metadata.strategy}`);
}
if (typeof metadata.links_total === "number") {
lines.push(`Links: ${metadata.links_total} total`);
}
else if (Array.isArray(metadata.links) && metadata.links.length > 0) {
lines.push(`Links: ${metadata.links.length} included`);
}
}
return lines;
}
export function formatScrapeResponse(result, detail = resolveScrapeResponseDetail(result)) {
if (detail === "full") {
return JSON.stringify(result, null, 2);
}
const success = result.success === true;
const credits = typeof result.credits_charged === "number" ? result.credits_charged : undefined;
const data = result.data;
const metadata = (data?.metadata ?? {});
const markdown = typeof data?.markdown === "string" ? data.markdown : "";
const text = typeof data?.text === "string" ? data.text : "";
const body = markdown || text;
const lines = [
...formatMetadataBlock(metadata, detail),
credits !== undefined ? `Credits: ${credits}` : "",
`Detail: ${detail}`,
"",
].filter(Boolean);
if (!success) {
const err = typeof result.error === "string" ? result.error : "Scrape failed";
lines.push(`Error: ${err}`);
return lines.join("\n");
}
if (body) {
lines.push(body);
}
else {
lines.push("(No markdown/text content in response.)");
}
if (detail === "compact") {
lines.push("");
lines.push("Summarize in plain language. Use response_detail=standard for full page text or full for raw JSON + all links.");
}
else {
lines.push("");
lines.push("Summarize in plain language. Use response_detail=full for complete JSON including all links.");
}
return lines.join("\n");
}
export function formatBatchScrapeResponse(result, detail = resolveScrapeResponseDetail(result)) {
if (detail === "full") {
return JSON.stringify(result, null, 2);
}
const pages = Array.isArray(result.data) ? result.data : [];
const credits = typeof result.credits_charged === "number" ? result.credits_charged : undefined;
const lines = [
`Batch scrape: ${pages.length} page(s)`,
credits !== undefined ? `Credits: ${credits}` : "",
`Detail: ${detail}`,
"",
].filter(Boolean);
for (let i = 0; i < pages.length; i++) {
lines.push(`--- Page ${i + 1} ---`);
lines.push(formatScrapeResponse({ success: true, response_detail: detail, data: pages[i] }, detail));
lines.push("");
}
return lines.join("\n").trim();
}
import assert from "node:assert/strict";
import test from "node:test";
import { formatScrapeResponse, resolveScrapeResponseDetail, } from "./scrape-format.js";
test("resolveScrapeResponseDetail reads API field", () => {
assert.equal(resolveScrapeResponseDetail({ response_detail: "full" }), "full");
assert.equal(resolveScrapeResponseDetail({}), "compact");
});
test("formatScrapeResponse compact omits links from output", () => {
const out = formatScrapeResponse({
success: true,
response_detail: "compact",
credits_charged: 1,
data: {
metadata: {
title: "Docker Docs",
source_url: "https://docs.docker.com/",
status_code: 200,
links: ["https://example.com/a"],
},
markdown: "# Hello",
},
});
assert.match(out, /Detail: compact/);
assert.match(out, /# Hello/);
assert.doesNotMatch(out, /example\.com\/a/);
});
test("formatScrapeResponse full returns JSON", () => {
const payload = {
success: true,
response_detail: "full",
data: { markdown: "# Hi", metadata: { links: ["https://x.com"] } },
};
const out = formatScrapeResponse(payload, "full");
assert.match(out, /"links"/);
assert.match(out, /https:\/\/x\.com/);
});
test("formatScrapeResponse standard includes extra metadata lines", () => {
const out = formatScrapeResponse({
success: true,
response_detail: "standard",
data: {
metadata: {
title: "Page",
source_url: "https://example.com",
description: "A description",
strategy: "article",
links_total: 100,
},
markdown: "body",
},
});
assert.match(out, /Description:/);
assert.match(out, /Strategy: article/);
assert.match(out, /Links: 100 total/);
});
export declare const SEARCH_UNAVAILABLE_DOCS = "https://metalift.ai/docs/release-notes";
export declare function searchUnavailableMessage(apiUrl: string, statusCode?: number): string;
export declare function formatSearchApiError(apiUrl: string, body: unknown, status: number): string | null;
export const SEARCH_UNAVAILABLE_DOCS = "https://metalift.ai/docs/release-notes";
export function searchUnavailableMessage(apiUrl, statusCode) {
const status = statusCode ? ` (HTTP ${statusCode})` : "";
return (`Web search is not available on ${apiUrl}${status}. ` +
"POST /v1/search was not found or search is disabled on this API host. " +
"Metalift Cloud (https://api.metalift.ai) may not have search deployed yet — " +
"use a self-hosted scrape API with SEARCH_ENABLED=true, or check " +
`${SEARCH_UNAVAILABLE_DOCS} for rollout status.`);
}
export function formatSearchApiError(apiUrl, body, status) {
if (status === 404) {
return searchUnavailableMessage(apiUrl, status);
}
if (status === 503) {
const detail = typeof body === "object" && body !== null && "detail" in body
? String(body.detail ?? "")
: "";
if (/search|searxng/i.test(detail)) {
return searchUnavailableMessage(apiUrl, status);
}
}
return null;
}
+3
-1

@@ -0,1 +1,2 @@

import { formatSearchApiError } from "./search-errors.js";
const B2B_ATTESTATION_HEADER = "X-B2B-Attestation";

@@ -83,3 +84,4 @@ const B2B_ATTESTATION_VALUE = "I-confirm-B2B-use";

if (!response.ok) {
throw new Error(formatApiError(body, response.status));
const searchError = path === "/v1/search" ? formatSearchApiError(this.apiUrl, body, response.status) : null;
throw new Error(searchError || formatApiError(body, response.status));
}

@@ -86,0 +88,0 @@ return withBilling(body, billingFromResponse(response));

@@ -6,4 +6,5 @@ #!/usr/bin/env node

import { MetaliftClient } from "./client.js";
import { buildWebSearchRequest, WEB_SEARCH_RESULT_LIMIT } from "./web-search.js";
import { formatBatchScrapeResponse, formatScrapeResponse } from "./scrape-format.js";
import { normalizeScrapeArgs } from "./scrape-args.js";
import { buildWebSearchRequest, formatWebSearchResponse, WEB_SEARCH_RESULT_LIMIT } from "./web-search.js";
const COMPLIANCE_NOTICE = "You are solely responsible for complying with website terms, robots.txt, copyright, and data protection laws when using scraped content.";

@@ -20,9 +21,15 @@ const SERVER_INSTRUCTIONS = `Metalift provides web search and web scraping as separate, independently billed tools.

- Fetches page content (markdown, html, text). Billed per URL (static=1, JS=5, premium=10+ credits).
- Use after search when the user or agent needs full page content from specific URLs.
- response_detail controls how much is returned (default compact):
• compact — truncated markdown (~16k chars), minimal metadata, no links (best for LLM context)
• standard — full markdown/text, metadata with up to 25 links
• full — complete JSON payload including all links and html when requested
- Use compact unless you need full page text or link extraction.
Recommended agent workflow: metalift_web_search → pick 0–N relevant URLs from snippets → metalift_scrape chosen URLs only.`;
Recommended agent workflow: metalift_web_search → answer from snippets when possible → metalift_scrape (compact) only when snippets are insufficient.
For simple factual questions (versions, definitions, current events), prefer answering directly from search snippets. Do not scrape unless snippets are insufficient. Never paste raw scrape JSON to the user — summarize the answer in plain language.`;
const client = new MetaliftClient();
const server = new McpServer({
name: "metalift",
version: "1.0.7",
version: "1.0.10",
}, {

@@ -33,5 +40,9 @@ instructions: SERVER_INSTRUCTIONS,

title: "Scrape URL",
description: `Scrape a single URL into markdown, HTML, or text for LLM context. Separate from web search — use after metalift_web_search when full page content is needed. 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}`,
description: `Scrape a single URL into markdown, HTML, or text for LLM context. Separate from web search — use after metalift_web_search when full page content is needed. Default response_detail=compact (truncated markdown, no links). Use standard for full page text or full for complete JSON + all links. Default scrape path: 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: {
url: z.string().url(),
response_detail: z
.enum(["compact", "standard", "full"])
.optional()
.describe("Response depth: compact (default, ~16k chars, no links), standard (full markdown + capped links), full (complete JSON)."),
formats: z.array(z.enum(["markdown", "html", "text", "json"])).optional(),

@@ -57,5 +68,12 @@ render: z.enum(["static", "dynamic", "auto"]).optional(),

}, async (args) => {
const result = await client.scrape(normalizeScrapeArgs(args));
const normalized = normalizeScrapeArgs(args);
const detail = normalized.response_detail ?? "compact";
const result = await client.scrape({ ...normalized, response_detail: detail });
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
content: [
{
type: "text",
text: formatScrapeResponse(result, detail),
},
],
};

@@ -74,2 +92,3 @@ });

only_main_content: z.boolean().optional(),
response_detail: z.enum(["compact", "standard", "full"]).optional(),
})

@@ -80,5 +99,15 @@ .optional(),

}, async (args) => {
const result = await client.batch(args);
const scrapeOptions = args.scrape_options ?? {};
const detail = scrapeOptions.response_detail ?? "compact";
const result = await client.batch({
...args,
scrape_options: { ...scrapeOptions, response_detail: detail },
});
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
content: [
{
type: "text",
text: formatBatchScrapeResponse(result, detail),
},
],
};

@@ -120,3 +149,3 @@ });

title: "Web Search",
description: `Search the web and return up to ${WEB_SEARCH_RESULT_LIMIT} SERP results (title, url, snippet, engine, score). Costs 2 credits per search. Returns search snippets only — not page content. Do not auto-scrape results; pick relevant URLs and call metalift_scrape separately for full content.`,
description: `Search the web and return up to ${WEB_SEARCH_RESULT_LIMIT} SERP results (title, url, snippet, engine, score). Costs 2 credits per search. Returns search snippets only — not page content. Answer simple questions from snippets; do not auto-scrape. Call metalift_scrape separately only when full page content is required.`,
inputSchema: {

@@ -131,3 +160,3 @@ query: z.string().min(1).max(512),

return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
content: [{ type: "text", text: formatWebSearchResponse(result) }],
};

@@ -134,0 +163,0 @@ });

export type ScrapeFormat = "markdown" | "html" | "text" | "json";
export type ScrapeRender = "static" | "dynamic" | "auto";
export type ScrapeProxy = "auto" | "direct" | "residential" | "datacenter";
export type ScrapeResponseDetail = "compact" | "standard" | "full";
export interface ScrapeArgs {

@@ -16,2 +17,3 @@ url: string;

cookie_header?: string;
response_detail?: ScrapeResponseDetail;
}

@@ -18,0 +20,0 @@ /** Default timeout for the fast direct static markdown path. */

@@ -52,4 +52,5 @@ /** Default timeout for the fast direct static markdown path. */

render: "auto",
response_detail: args.response_detail ?? "compact",
timeout_ms: args.timeout_ms ?? FAST_SCRAPE_TIMEOUT_MS,
};
}

@@ -11,2 +11,3 @@ import assert from "node:assert/strict";

render: "auto",
response_detail: "compact",
timeout_ms: FAST_SCRAPE_TIMEOUT_MS,

@@ -13,0 +14,0 @@ });

@@ -8,6 +8,7 @@ export declare const WEB_SEARCH_RESULT_LIMIT = 10;

}): {
language: string;
limit: number;
query: string;
categories?: string[];
language?: string;
};
export declare function formatWebSearchResponse(result: Record<string, unknown>): string;
export const WEB_SEARCH_RESULT_LIMIT = 10;
export const WEB_SEARCH_CREDITS = 2;
export function buildWebSearchRequest(args) {
return { ...args, limit: WEB_SEARCH_RESULT_LIMIT };
return {
...args,
language: args.language ?? "en",
limit: WEB_SEARCH_RESULT_LIMIT,
};
}
export function formatWebSearchResponse(result) {
const results = Array.isArray(result.results) ? result.results : [];
const query = typeof result.query === "string" ? result.query : "";
const credits = typeof result.credits_charged === "number" ? result.credits_charged : WEB_SEARCH_CREDITS;
const lines = [`Query: ${query}`, `Results: ${results.length} (${credits} credits)`, ""];
if (results.length === 0) {
lines.push("No relevant results. Try rephrasing the query.");
return lines.join("\n");
}
for (let i = 0; i < results.length; i++) {
const row = results[i];
const title = typeof row.title === "string" ? row.title : "Untitled";
const url = typeof row.url === "string" ? row.url : "";
const snippet = typeof row.snippet === "string" ? row.snippet : "";
lines.push(`${i + 1}. ${title}`);
lines.push(` URL: ${url}`);
if (snippet) {
lines.push(` ${snippet.slice(0, 320)}${snippet.length > 320 ? "…" : ""}`);
}
lines.push("");
}
lines.push("Answer simple questions from these snippets when possible. Only call metalift_scrape when full page content is required.");
return lines.join("\n");
}
import assert from "node:assert/strict";
import test from "node:test";
import { buildWebSearchRequest, WEB_SEARCH_RESULT_LIMIT } from "./web-search.js";
test("buildWebSearchRequest always uses top-10 limit", () => {
import { buildWebSearchRequest, formatWebSearchResponse, WEB_SEARCH_RESULT_LIMIT } from "./web-search.js";
test("buildWebSearchRequest always uses top-10 limit and defaults language to en", () => {
assert.equal(WEB_SEARCH_RESULT_LIMIT, 10);
assert.deepEqual(buildWebSearchRequest({ query: "docker docs" }), {
query: "docker docs",
language: "en",
limit: 10,

@@ -12,1 +13,17 @@ });

});
test("formatWebSearchResponse renders readable snippets", () => {
const text = formatWebSearchResponse({
query: "docker docs",
credits_charged: 2,
results: [
{
title: "Docker Docs",
url: "https://docs.docker.com/",
snippet: "Official Docker documentation.",
},
],
});
assert.match(text, /Query: docker docs/);
assert.match(text, /Docker Docs/);
assert.match(text, /Answer simple questions from these snippets/);
});
{
"name": "@metalift/mcp",
"mcpName": "io.github.MetaLift-AI/metalift",
"version": "1.0.7",
"version": "1.0.10",
"description": "Metalift MCP server for AI agents",

@@ -16,3 +16,3 @@ "license": "SEE LICENSE IN LICENSE",

"dev": "tsx src/index.ts",
"test": "npm run build && node --test dist/client.test.js dist/scrape-args.test.js dist/web-search.test.js",
"test": "npm run build && node --test dist/client.test.js dist/scrape-args.test.js dist/scrape-format.test.js dist/web-search.test.js",
"prepublishOnly": "npm run build && node ../../scripts/npm-prepare-publish.mjs strip",

@@ -19,0 +19,0 @@ "postpublish": "node ../../scripts/npm-prepare-publish.mjs restore"