🎩 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.2
to
1.0.5
+1
dist/client.test.d.ts
export {};
import assert from "node:assert/strict";
import test from "node:test";
import { billingFromResponse, withBilling } from "./client.js";
test("billingFromResponse parses credit headers", () => {
const response = new Response("{}", {
headers: {
"X-Metalift-Credits-Charged": "5",
"X-Metalift-Credits-Estimated": "10",
},
});
assert.deepEqual(billingFromResponse(response), {
credits_charged: 5,
credits_estimated: 10,
});
});
test("billingFromResponse returns null when headers are missing", () => {
const response = new Response("{}");
assert.deepEqual(billingFromResponse(response), {
credits_charged: null,
credits_estimated: null,
});
});
test("withBilling merges billing fields into API payload", () => {
const merged = withBilling({ success: true }, { credits_charged: 1, credits_estimated: null });
assert.equal(merged.success, true);
assert.equal(merged.credits_charged, 1);
assert.equal(merged.credits_estimated, null);
});
export type ScrapeFormat = "markdown" | "html" | "text" | "json";
export type ScrapeRender = "static" | "dynamic" | "auto";
export type ScrapeProxy = "auto" | "direct" | "residential" | "datacenter";
export interface ScrapeArgs {
url: string;
formats?: ScrapeFormat[];
render?: ScrapeRender;
only_main_content?: boolean;
timeout_ms?: number;
wait_for?: string;
screenshot?: boolean;
proxy?: ScrapeProxy;
strategy?: string;
cookies?: Record<string, string>;
cookie_header?: string;
}
/** Default timeout for the fast direct static markdown path. */
export declare const FAST_SCRAPE_TIMEOUT_MS = 10000;
/**
* Apply fast defaults for plain URL-to-markdown scrapes.
* Preserves full auto/browser/proxy behavior when the caller opts in explicitly.
*/
export declare function normalizeScrapeArgs(args: ScrapeArgs): ScrapeArgs;
/** Default timeout for the fast direct static markdown path. */
export const FAST_SCRAPE_TIMEOUT_MS = 10_000;
function isMarkdownOnlyFormats(formats) {
return formats === undefined || (formats.length === 1 && formats[0] === "markdown");
}
/**
* Apply fast defaults for plain URL-to-markdown scrapes.
* Preserves full auto/browser/proxy behavior when the caller opts in explicitly.
*/
export function normalizeScrapeArgs(args) {
const hasAdvancedOpts = args.strategy !== undefined ||
args.render !== undefined ||
args.proxy !== undefined ||
args.screenshot !== undefined ||
args.wait_for !== undefined ||
args.cookies !== undefined ||
args.cookie_header !== undefined ||
!isMarkdownOnlyFormats(args.formats);
if (hasAdvancedOpts) {
return { ...args };
}
return {
...args,
formats: args.formats ?? ["markdown"],
strategy: "article",
render: "static",
proxy: "direct",
timeout_ms: args.timeout_ms ?? FAST_SCRAPE_TIMEOUT_MS,
};
}
import assert from "node:assert/strict";
import test from "node:test";
import { FAST_SCRAPE_TIMEOUT_MS, normalizeScrapeArgs } from "./scrape-args.js";
const BASE_URL = "https://example.com/docs";
test("normalizeScrapeArgs applies fast defaults for plain markdown scrape", () => {
assert.deepEqual(normalizeScrapeArgs({ url: BASE_URL }), {
url: BASE_URL,
formats: ["markdown"],
strategy: "article",
render: "static",
proxy: "direct",
timeout_ms: FAST_SCRAPE_TIMEOUT_MS,
});
});
test("normalizeScrapeArgs preserves explicit timeout_ms", () => {
assert.equal(normalizeScrapeArgs({ url: BASE_URL, timeout_ms: 30_000 }).timeout_ms, 30_000);
});
test("normalizeScrapeArgs preserves explicit strategy auto", () => {
const args = normalizeScrapeArgs({ url: BASE_URL, strategy: "auto" });
assert.equal(args.strategy, "auto");
assert.equal(args.render, undefined);
assert.equal(args.proxy, undefined);
});
test("normalizeScrapeArgs preserves explicit specialized strategy", () => {
const args = normalizeScrapeArgs({ url: BASE_URL, strategy: "cloudflare" });
assert.equal(args.strategy, "cloudflare");
assert.equal(args.render, undefined);
});
test("normalizeScrapeArgs preserves explicit render auto", () => {
const args = normalizeScrapeArgs({ url: BASE_URL, render: "auto" });
assert.equal(args.render, "auto");
assert.equal(args.strategy, undefined);
});
test("normalizeScrapeArgs preserves explicit render dynamic", () => {
const args = normalizeScrapeArgs({ url: BASE_URL, render: "dynamic" });
assert.equal(args.render, "dynamic");
});
test("normalizeScrapeArgs preserves explicit proxy auto", () => {
const args = normalizeScrapeArgs({ url: BASE_URL, proxy: "auto" });
assert.equal(args.proxy, "auto");
});
test("normalizeScrapeArgs preserves explicit premium proxy", () => {
const args = normalizeScrapeArgs({ url: BASE_URL, proxy: "residential" });
assert.equal(args.proxy, "residential");
});
test("normalizeScrapeArgs preserves screenshot requests", () => {
const args = normalizeScrapeArgs({ url: BASE_URL, screenshot: true });
assert.equal(args.screenshot, true);
assert.equal(args.strategy, undefined);
});
test("normalizeScrapeArgs preserves wait_for selector", () => {
const args = normalizeScrapeArgs({ url: BASE_URL, wait_for: "main" });
assert.equal(args.wait_for, "main");
});
test("normalizeScrapeArgs preserves cookies", () => {
const args = normalizeScrapeArgs({ url: BASE_URL, cookies: { sid: "abc" } });
assert.deepEqual(args.cookies, { sid: "abc" });
});
test("normalizeScrapeArgs preserves cookie_header", () => {
const args = normalizeScrapeArgs({ url: BASE_URL, cookie_header: "sid=abc" });
assert.equal(args.cookie_header, "sid=abc");
});
test("normalizeScrapeArgs preserves non-markdown formats", () => {
const args = normalizeScrapeArgs({ url: BASE_URL, formats: ["html"] });
assert.deepEqual(args.formats, ["html"]);
assert.equal(args.strategy, undefined);
});
test("normalizeScrapeArgs preserves multi-format requests", () => {
const args = normalizeScrapeArgs({ url: BASE_URL, formats: ["markdown", "html"] });
assert.deepEqual(args.formats, ["markdown", "html"]);
});
test("normalizeScrapeArgs treats explicit markdown-only formats as fast path", () => {
const args = normalizeScrapeArgs({ url: BASE_URL, formats: ["markdown"] });
assert.equal(args.strategy, "article");
assert.equal(args.render, "static");
assert.equal(args.proxy, "direct");
});
+18
-11

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

import type { ScrapeArgs } from "./scrape-args.js";
export interface MetaliftClientOptions {

@@ -5,2 +6,8 @@ apiUrl?: string;

}
export interface BillingMeta {
credits_charged: number | null;
credits_estimated: number | null;
}
export declare function billingFromResponse(response: Response): BillingMeta;
export declare function withBilling<T extends Record<string, unknown>>(body: T, billing: BillingMeta): T & BillingMeta;
export declare class MetaliftClient {

@@ -11,21 +18,21 @@ private apiUrl;

private headers;
request<T>(path: string, init?: RequestInit): Promise<T>;
scrape(params: Record<string, unknown>): Promise<unknown>;
batch(params: Record<string, unknown>): Promise<unknown>;
crawl(params: Record<string, unknown>): Promise<unknown>;
map(params: Record<string, unknown>): Promise<unknown>;
jobStatus(jobId: string): Promise<unknown>;
request<T extends Record<string, unknown>>(path: string, init?: RequestInit): Promise<T & BillingMeta>;
scrape(params: ScrapeArgs | Record<string, unknown>): Promise<Record<string, unknown> & BillingMeta>;
batch(params: Record<string, unknown>): Promise<Record<string, unknown> & BillingMeta>;
crawl(params: Record<string, unknown>): Promise<Record<string, unknown> & BillingMeta>;
map(params: Record<string, unknown>): Promise<Record<string, unknown> & BillingMeta>;
jobStatus(jobId: string): Promise<Record<string, unknown> & BillingMeta>;
listStrategies(): Promise<{
success: boolean;
strategies: unknown[];
}>;
} & BillingMeta>;
listProtectionTypes(): Promise<{
success: boolean;
protection_types: unknown[];
}>;
} & BillingMeta>;
listSessions(): Promise<{
success: boolean;
sessions: unknown[];
}>;
warmSession(params: Record<string, unknown>): Promise<unknown>;
} & BillingMeta>;
warmSession(params: Record<string, unknown>): Promise<Record<string, unknown> & BillingMeta>;
health(): Promise<{

@@ -35,3 +42,3 @@ status: string;

browser_ready?: boolean;
}>;
} & BillingMeta>;
}

@@ -0,1 +1,22 @@

const B2B_ATTESTATION_HEADER = "X-B2B-Attestation";
const B2B_ATTESTATION_VALUE = "I-confirm-B2B-use";
const CLIENT_ID_HEADER = "X-Metalift-Client";
const MCP_CLIENT_VERSION = "1.0.3";
const CREDITS_CHARGED_HEADER = "X-Metalift-Credits-Charged";
const CREDITS_ESTIMATED_HEADER = "X-Metalift-Credits-Estimated";
function parseHeaderInt(value) {
if (value == null || value === "")
return null;
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : null;
}
export function billingFromResponse(response) {
return {
credits_charged: parseHeaderInt(response.headers.get(CREDITS_CHARGED_HEADER)),
credits_estimated: parseHeaderInt(response.headers.get(CREDITS_ESTIMATED_HEADER)),
};
}
export function withBilling(body, billing) {
return { ...body, ...billing };
}
export class MetaliftClient {

@@ -9,3 +30,7 @@ apiUrl;

headers() {
const headers = { "Content-Type": "application/json" };
const headers = {
"Content-Type": "application/json",
[B2B_ATTESTATION_HEADER]: B2B_ATTESTATION_VALUE,
[CLIENT_ID_HEADER]: `mcp/${MCP_CLIENT_VERSION}`,
};
if (this.apiKey) {

@@ -21,7 +46,16 @@ headers.Authorization = `Bearer ${this.apiKey}`;

});
const body = await response.json();
const body = (await response.json());
if (!response.ok) {
throw new Error(body.detail || body.error || `Request failed: ${response.status}`);
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);
}
return body;
return withBilling(body, billingFromResponse(response));
}

@@ -28,0 +62,0 @@ scrape(params) {

@@ -6,2 +6,3 @@ #!/usr/bin/env node

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

@@ -15,3 +16,3 @@ const client = new MetaliftClient();

title: "Scrape URL",
description: `Scrape a single URL into markdown, HTML, or text for LLM context. ${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 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: {

@@ -39,3 +40,3 @@ url: z.string().url(),

}, async (args) => {
const result = await client.scrape(args);
const result = await client.scrape(normalizeScrapeArgs(args));
return {

@@ -47,3 +48,3 @@ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],

title: "Batch Scrape URLs",
description: `Scrape multiple URLs in parallel. ${COMPLIANCE_NOTICE}`,
description: `Scrape multiple URLs in parallel. Response includes credits_charged (per-page usage billing). ${COMPLIANCE_NOTICE}`,
inputSchema: {

@@ -69,3 +70,3 @@ urls: z.array(z.string().url()).min(1).max(100),

title: "Crawl Website",
description: "Crawl a website starting from a URL and return markdown for discovered pages.",
description: "Crawl a website starting from a URL and return markdown for discovered pages. Job creation shows credits_estimated; poll metalift_job_status for credits_charged as pages complete.",
inputSchema: {

@@ -72,0 +73,0 @@ url: z.string().url(),

{
"name": "@metalift/mcp",
"mcpName": "io.github.MetaLift-AI/metalift",
"version": "1.0.2",
"version": "1.0.5",
"description": "Metalift MCP server for AI agents",

@@ -16,2 +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",
"prepublishOnly": "npm run build && node ../../scripts/npm-prepare-publish.mjs strip",

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

@@ -16,5 +16,5 @@ # @metalift/mcp

## Install (after npm publish)
## Install
Add to your MCP client config:
Published on npm as `@metalift/mcp`. Default config uses `npx`:

@@ -36,2 +36,14 @@ ```json

**Corporate Windows / SSL inspection:** if `npx` fails with `UNABLE_TO_VERIFY_LEAF_SIGNATURE`, install locally and use `node` — see [examples/cursor-mcp-local.json](../../examples/cursor-mcp-local.json) and [MCP setup troubleshooting](../../packages/platform-web/docs/mcp-setup.md#troubleshooting).
## Troubleshooting
| Error | Cause | Fix |
|-------|-------|-----|
| `Failed to acquire MessagePort` | Cursor IDE bug on Windows | Reload window, restart Cursor — [details](../../packages/platform-web/docs/mcp-setup.md#failed-to-acquire-messageport-cursor--vs-code) |
| `UNABLE_TO_VERIFY_LEAF_SIGNATURE` | Corporate SSL inspection blocks npm | Local install + `node` path — [details](../../packages/platform-web/docs/mcp-setup.md#unable_to_verify_leaf_signature-npm--npx) |
| 401 / 402 at runtime | Auth or billing | Check API key and subscription |
Full guide: [packages/platform-web/docs/mcp-setup.md](../../packages/platform-web/docs/mcp-setup.md).
## Environment variables

@@ -48,3 +60,3 @@

|------|-------------|
| `metalift_scrape` | Scrape a single URL |
| `metalift_scrape` | Scrape a single URL (default: fast direct static markdown; use `strategy: "auto"` for WAF/SPA/retail) |
| `metalift_batch_scrape` | Scrape multiple URLs |

@@ -51,0 +63,0 @@ | `metalift_crawl` | Crawl a website |