New:Microsoft Teams Notifications Are Now Available in Socket.Learn more
Get Started

@url2md-io/mcp

Package Overview
Dependencies
Maintainers
1
Versions
6
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@url2md-io/mcp

MCP server for url2md: PDFs and x402-tolled pages as clean Markdown, JavaScript rendering included, paid per call from your own wallet.

npmnpm
Version
0.1.4
Version published
Weekly downloads
58
-92.03%
Maintainers
1
Weekly downloads
 
Created
Source

@url2md-io/mcp

The official package is @url2md-io/mcp — with the @url2md-io scope. The unscoped name url2md-mcp on npm is someone else's package, published by an unrelated person in May 2026. It is not ours, we have no control over it, and we cannot see what it does with what it is given. Running npx url2md-mcp runs their code — and this tool is set up by handing it a wallet private key. Give the key only to @url2md-io/mcp. If the name you are about to install has no scope in front of it, it is the wrong one.

An MCP server that gives your agent one tool: PDFs and pages that charge for access, as clean Markdown.

One conversion engine handles every kind, so the output is the same shape whichever it was: PDFs, pages that only appear once JavaScript has run (a headless browser is used only when the page needs one), and ordinary HTML. robots.txt is honoured per RFC 9309. There is no account, no sign-up and no API key. Each call is paid for on-chain, from a wallet you control, at $0.005 — half a cent — in USDC on Base, and the settlement receipt comes back with the Markdown. When the page itself demands an x402 payment before it will hand anything over, url2md pays that for you and the cost is included in what you pay.

What you need before you start

  • Node 22.18 or newer. Check with node --version. Nothing else: npx fetches the package and the published files are plain JavaScript, so there is nothing for you to build.
  • A wallet private key, holding USDC on Base. This is the part that is different from a normal MCP server, so it is worth being precise:
    • It must be a raw private key: 0x followed by 64 hex characters. Not a seed phrase.
    • The wallet needs USDC only — no ETH. The payment facilitator submits the transfer and pays the gas. A wallet with $2 of USDC and nothing else works.
    • Use a fresh wallet with a small balance. Anything with this key can spend the USDC in it. Do not use a wallet you keep anything else in.
    • Get USDC on Base by bridging, or by buying on an exchange that withdraws to Base directly (Coinbase does). The USDC contract on Base is 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913.

If you would rather try it without spending real money, see Trying it on testnet below — the test network's USDC is free.

Install

Nothing to install. Every example below runs the server with npx, which fetches it on first use and caches it:

npx @url2md-io/mcp

Mind the scope — @url2md-io/mcp, never the unscoped url2md-mcp, which is someone else's package. Run it once by hand like that and it will refuse to start, with CONFIG_MISSING_KEY. That is the answer you want: the package is there and the only thing missing is the key, which the next step sets up.

To pin a version, or to keep it off the network at start-up, install it instead:

npm install -g @url2md-io/mcp    # then the command is: url2md

Put the key in a file, not in a config

You can pass the key in URL2MD_PRIVATE_KEY, and the examples below show both ways. Prefer the file:

umask 077
printf '0xYOUR_PRIVATE_KEY' > ~/.url2md-key    # no trailing newline needed; one is fine
chmod 600 ~/.url2md-key

Then set URL2MD_PRIVATE_KEY_FILE=/Users/you/.url2md-key instead of the key itself. A path is not a secret. The key itself, put anywhere else, ends up somewhere you did not intend:

  • claude mcp add … --env URL2MD_PRIVATE_KEY=0x… puts it in your shell history, and then in ~/.claude.json in plaintext.
  • Any -e URL2MD_PRIVATE_KEY=0x… on a command line is visible to every process on the machine through ps aux for as long as it runs.

URL2MD_PRIVATE_KEY_FILE wins if both are set.

Add it to your MCP client

Claude Code

claude mcp add url2md \
  --env URL2MD_PRIVATE_KEY_FILE=/Users/you/.url2md-key \
  --env URL2MD_BASE_URL=https://url2md.io \
  -- npx -y @url2md-io/mcp

Claude Desktop, Cursor, Windsurf, and anything else that reads a JSON config

Add this to the mcpServers object of your client's config file (Claude Desktop: ~/Library/Application Support/Claude/claude_desktop_config.json on macOS, %APPDATA%\Claude\claude_desktop_config.json on Windows):

{
  "mcpServers": {
    "url2md": {
      "command": "npx",
      "args": ["-y", "@url2md-io/mcp"],
      "env": {
        "URL2MD_PRIVATE_KEY_FILE": "/Users/you/.url2md-key",
        "URL2MD_BASE_URL": "https://url2md.io"
      }
    }
  }
}

If the server never appears, the cause is almost always PATH. Desktop clients on macOS are launched by the window manager with a minimal PATH that usually does not include a version-managed Node, so a bare "npx" resolves to nothing. Use an absolute path for commandwhich npx prints it — and keep args as they are. Restart the client after editing the file.

Check it before you wire it up

You can talk to the server yourself with the reference MCP inspector, which needs nothing installed:

npx @modelcontextprotocol/inspector --cli npx -y @url2md-io/mcp \
  -e URL2MD_PRIVATE_KEY_FILE=/Users/you/.url2md-key \
  --method tools/list

The server prints one line to stderr when it starts, naming the deployment, the network and whether it is real money, the wallet address it will pay from, and the per-call limit:

@url2md-io/mcp 0.1.4 → https://url2md.io; eip155:8453 (real money); paying from 0x0972…7a36, at most $0.10 per call and $1.00 in total

Your private key is never printed, logged, or sent anywhere except as the signature on a payment.

The tool

fetch_markdown

ParameterWhat it does
urlRequired. The absolute http(s) URL to convert.
renderOptional, default auto. auto: use a headless browser only if the page looks client-rendered. static: never render, take the HTML as served. browser: always render.

render=browser costs the same but is slower, and can fail on pages a plain fetch would have handled. auto is right almost always.

What comes back — the Markdown as the tool's text, and this as structured content:

{
  "markdown": "# Example Domain\n\nThis domain is for use in documentation examples…",
  "url": "https://example.com/",
  "finalUrl": "https://example.com/",
  "title": "Example Domain",
  "source": "static",
  "words": 24,
  "tokensEstimate": 42,
  "warnings": [],
  "payment": {
    "amountUsd": "0.005",
    "amountAtomic": "5000",
    "network": "eip155:8453",
    "payTo": "0xa09aE66Ec8664475697FfC0eb45aa3Fe52Aa6030",
    "settled": true,
    "payer": "0x…your wallet",
    "transaction": "0x3bd94a…",
    "explorerUrl": "https://basescan.org/tx/0x3bd94a…",
    "ledgerId": "e17a62c0-0801-4043-a58a-da135c1343ae"
  },
  "paymentRounds": 1,
  "committedUsd": "0.005",
  "signed": [
    {
      "amountUsd": "0.005",
      "amountAtomic": "5000",
      "network": "eip155:8453",
      "payTo": "0xa09aE66Ec8664475697FfC0eb45aa3Fe52Aa6030",
      "payer": "0x…your wallet",
      "nonce": "0xdccc5af7…",
      "validBefore": "2026-09-07T05:17:26.000Z"
    }
  ]
}

source says how the Markdown was produced: static, browser, pdf, origin-markdown or text. Fields that do not apply are left out rather than set to null.

payment is there whenever you were charged, even if the service answered without confirming the settlement. The amountUsd, network, payTo and payer are always what this client signed — never what the service's receipt claims. If the receipt disagrees with any of them, warnings names both figures and the signed one stands. settled: false means no usable settlement came back at all; transaction is then absent and warnings tells you to check the wallet on-chain.

committedUsd is everything the call authorized, and signed lists each authorization with the nonce that identifies it on-chain. On an ordinary call committedUsd equals payment.amountUsd. On a tolled call it is higher: the first authorization is replaced by the quote, only the quote is settled, and the service still holds the first one — warnings says so, with both numbers.

Read warnings. It carries the service's own notes too — a browser render that failed and fell back to the static result, for instance. Anything you must not miss is also written above the Markdown in the tool's text, because many MCP clients show a model only the text and drop the structured content: an unconfirmed payment appears there as a PAYMENT NOT CONFIRMED line, and every warning as a NOTE: line. A clean, settled call has no preamble at all.

What a call costs

$0.005 per call, whatever the page. The wallet is debited once, when the call succeeds — or, for a page that needs a browser, just before the browser starts.

Which failures are free, exactly. A malformed URL, an unsupported scheme and a blocked host are refused on the unpaid request: nothing is signed and details is empty. A missing URL is the one exception, and it costs nothing either: the bare path answers a payment challenge like every other x402 endpoint, so this client signs a payment and is then told INVALID_URL — the service settles nothing for a call it answered with an error, so the authorization is wasted, not spent. Everything else — ROBOTS_DISALLOWED, ROBOTS_UNREACHABLE (which is where a mistyped domain lands), a page that times out, a render that fails — is discovered after the payment gate, so a signed authorization has already left this process. url2md does not settle a payment for a call it answered with an error, and the ledger row would say needs_refund if it did; but this client cannot see the chain and does not claim the money is safe. Every such failure therefore comes back with details.committedUsd, and details.signed[] carrying each authorization's amount, recipient, EIP-3009 nonce and expiry — that nonce is what USDC emits as AuthorizationUsed when a payment is actually redeemed, so you can check for yourself. An agent that retries a mistyped domain in a loop signs $0.005 each time until URL2MD_MAX_SPEND_USD stops it.

Pages that charge their own toll. Some pages are themselves behind an x402 paywall. url2md can pay them for you: it quotes you the base price plus the page's toll plus 25 %, you pay that, and it pays the origin from its own wallet. Those calls come back with toll filled in and paymentRounds: 2. Tolls above $0.05 are refused rather than passed on.

Two ceilings. One call will not commit more than $0.10 in total, however many rounds it takes — comfortably above the most a legitimate call can cost (base + the $0.05 toll cap + margin ≈ $0.068), and low enough that a surprising price is refused instead of paid. And the whole process will not commit more than $1.00 for as long as it runs, which is what bounds an agent stuck in a retry loop; restarting the server resets it. Raise either with URL2MD_MAX_PRICE_USD and URL2MD_MAX_SPEND_USD. Neither check can be turned off, though setting the numbers absurdly high has the same effect, so the server says so on startup if you do.

What is checked before anything is signed. A 402 is written by whoever answers your request, so the terms in it are not evidence of anything on their own. Before signing, this client fetches the service's own published terms from GET / (free, unpaid) and refuses the payment unless the 402 matches them:

CheckRefused with
The service publishes a payTo, an asset and a network at allUNPUBLISHED_TERMS
The recipient is the payTo the service publishesPAYTO_MISMATCH
The token is the asset the service publishesUNSUPPORTED_PAYMENT_TERMS
The network is the one the service publishesUNSUPPORTED_PAYMENT_TERMS
The scheme is exactUNSUPPORTED_PAYMENT_TERMS
The authorization is redeemable for at most an hourUNSUPPORTED_PAYMENT_TERMS
The whole call stays at or under your per-call ceilingPRICE_ABOVE_LIMIT
This process has not spent its session budgetSPEND_LIMIT_REACHED

These fail closed: a service that publishes no receiving address gets no signature, rather than an unchecked one. The amount is not compared with the published price for the toll round, because a quote is legitimately higher than the base price — but a first 402 above the published price is reported in warnings even when it is inside your ceiling, since that is a service charging something other than what it advertises.

URL2MD_BASE_URL is the whole trust anchor. Every check above compares the 402 with what that URL published moments earlier. It stops a service contradicting itself; it cannot stop a service that lies consistently, because an impostor writes both halves. So: point it at a deployment you trust, and nothing else. It must be https:// (only a loopback address may be plain http), and the paid request is pinned to that origin — a redirect to another host is refused (REDIRECT_REFUSED) rather than followed, because following it would hand your signed authorization to whoever the redirect names.

The ceiling bounds the whole call, so a two-round tolled call cannot commit twice the number you set — and what a call committed in total is always reported, as committedUsd on success and details.committedUsd on failure.

If a call fails after a payment was signed, the error carries details.signedButUnconfirmed, details.committedUsd, and details.signed — one entry per authorization, with the amount, the recipient, the network, the EIP-3009 nonce and when it stops being redeemable. A signature that has left this process is money you have committed whether or not the service ever redeems it, so it is never omitted from a failure.

Configuration

One of the two key variables is required. Everything else has a default.

URL2MD_PRIVATE_KEY_FILE

Path to a file containing the key. Preferred — a path is not a secret, and a key on a command line is visible to every process on the machine. Wins if both key variables are set.

URL2MD_PRIVATE_KEY

The key itself: 0x followed by 64 hex characters. Use the file instead where you can.

URL2MD_MAX_PRICE_USD — default 0.10

The most this server will commit for one call, in USD, across every round of that call. A tolled page takes two rounds; this ceiling covers both.

URL2MD_MAX_SPEND_USD — default 1.00

The most this server will commit in total, for as long as the process runs. Restarting resets it.

URL2MD_BASE_URL — default https://url2md.io

The url2md deployment to call. Must be https:// (only a loopback address may be plain http), because this is the origin every 402 is checked against.

URL2MD_TIMEOUT_MS — default 90000

How long to wait for one conversion, in milliseconds. 90 s is the default because that is what the service's own bounds add up to: 20 s for the fetch (robots.txt, every redirect hop and the body run on one deadline) and, when the page needs a headless browser, 55 s more for the render (30 s of navigation plus 25 s of action and slack) — 75 s in the worst case, plus room for the network and the payment round trips.

Do not set it to 30 s because you read that a render gets 30 s: that is the navigation timeout inside the browser, not the wall clock. Giving up early does not cancel the call, and on a page that needed a browser your payment has already settled by the time the render starts.

When something goes wrong

Failures come back as MCP error results (isError: true) whose text is a JSON object, so an agent can branch on the code rather than read prose:

{ "error": { "code": "BLOCKED_HOST", "message": "localhost is not a public host", "retryable": false, "httpStatus": 400 } }
CodeWhat it means, and what to do
CONFIG_MISSING_KEY, CONFIG_BAD_KEY, CONFIG_BAD_KEY_FILENo key, not a private key, or a key file that could not be read. The server refuses to start (exit 2). Set URL2MD_PRIVATE_KEY_FILE to a readable file holding 0x + 64 hex characters.
CONFIG_BAD_LIMIT, CONFIG_BAD_BASE_URL, CONFIG_BAD_TIMEOUTA setting the server will not guess at — including a URL2MD_BASE_URL that is not https://. It refuses to start. Fix the value named in the message.
INVALID_URL, UNSUPPORTED_SCHEMEThe URL is missing, or is not an absolute http(s) URL. Fix the URL.
BLOCKED_HOSTPrivate, loopback, link-local or internal host. Public URLs only — this is deliberate.
ROBOTS_DISALLOWEDThe origin's robots.txt forbids it, and url2md obeys robots.txt — so there is nothing to retry. Checked after the payment gate, so an authorization was signed: read details.committedUsd.
PRICE_ABOVE_LIMITThe call, in total, would cost more than URL2MD_MAX_PRICE_USD. Read details.totalUsd; raise the limit if it is reasonable.
SPEND_LIMIT_REACHEDThis server has committed its whole URL2MD_MAX_SPEND_USD session budget. Nothing was signed. Raise it and restart, or look at why the agent is spending.
UNPUBLISHED_TERMSThe service asks for payment but publishes no receiving address, asset or network to check its 402 against. Nothing was signed. Check URL2MD_BASE_URL. A real url2md deployment publishes all three at GET /.
PAYTO_MISMATCHThe 402 asked you to pay an address the service does not publish. Nothing was signed. Do not raise any limit. Check you are pointed at the right URL2MD_BASE_URL; if you are, stop and investigate.
UNSUPPORTED_PAYMENT_TERMSThe 402's network, asset or scheme is not the one the service publishes, or this client cannot pay it. Nothing was signed. As above.
PAYMENT_NOT_SIGNEDThe payment could not be signed — usually the x402 client's own spend controls refusing an unrecognised token. Read the message; nothing was sent.
ROBOTS_UNREACHABLEThe origin's robots.txt could not be fetched. RFC 9309 requires treating that as disallow. Checked after the payment gate, so an authorization was signed — read details.committedUsd. Retryable, but each retry signs another payment: a nonexistent domain lands here too, so check the URL before looping.
SERVICE_UNREACHABLE, TIMEOUTurl2md itself did not answer, or not in time. Retryable. Check URL2MD_BASE_URL.
BAD_RESPONSE, PAYMENT_NOT_ACCEPTEDurl2md answered something this client could not use, or kept asking for payment after two rounds. Nothing further was signed. Report it.
REDIRECT_REFUSEDThe service tried to redirect the request to another host. The request — and the signed authorization it carried — was not followed there. Check URL2MD_BASE_URL. If it is right, stop and investigate: an honest deployment does not do this.
TOO_MANY_REDIRECTSMore than three redirects inside the service's own origin. Check URL2MD_BASE_URL.
UPSTREAM_PAYMENT_REQUIRED, UPSTREAM_PAYMENT_FAILEDThe page charges its own toll and url2md could not pass it through, or the origin refused url2md's payment. If details.callerCharged is true you were charged; quote details.ledgerId.
INTERNALAn unexpected failure inside this server. Report it.
UPSTREAM_TOLL_TOO_HIGHThe page charges a toll above url2md's $0.05 cap, so it is not available through url2md. Nothing to do.
PAYMENT_REJECTEDThe service refused the payment when it tried to settle it. Usually an empty wallet. Check the wallet has USDC on the network the startup line names.
PAYMENT_REQUIREDThe service asked for payment but does not publish x402 pricing, so this client will not guess at terms. Check URL2MD_BASE_URL.
UPSTREAM_STATUSThe origin answered a status url2md cannot convert. details.status is the origin's own: 404 or 5xx means the page is broken or gone, but 403 usually means the origin refused url2md — a bot wall, not a page failure — and retrying changes nothing. On a 429 the origin is asking for a slower pace: details.retryAfter carries its Retry-After when it sent one, and that is the one case where waiting and trying again is worth a second payment.
RENDER_FAILED, UPSTREAM_TIMEOUTThe page itself failed to load or render in time. retryable says whether trying again is worth it.
RATE_LIMITED60 calls a minute, 6 browser renders a minute. Wait.

url2md's own error codes are documented in full in the service's OpenAPI document, https://url2md.io/openapi.json; any of them can appear here unchanged.

If you were charged and got nothing, the error carries details.callerCharged: true and details.ledgerId. url2md keeps a row for every settled payment and reconciles it against the chain; quote the ledger id to hello@url2md.io.

Trying it on testnet

Base Sepolia USDC is free, so you can exercise the whole thing without spending anything. Note that this changes the payment network to Base Sepolia (eip155:84532): a wallet funded with USDC on Base mainnet cannot pay the testnet deployment, and vice versa. The startup line names the network it will pay on, so you can check before you spend.

npx @modelcontextprotocol/inspector --cli npx -y @url2md-io/mcp \
  -e URL2MD_PRIVATE_KEY_FILE=/Users/you/.url2md-testnet-key \
  -e URL2MD_BASE_URL=https://url2md-testnet.url2md.workers.dev \
  --method tools/call --tool-name fetch_markdown --tool-arg url=https://example.com

Fund a throwaway wallet from the Coinbase Base Sepolia faucet. The testnet deployment is the same code as production, on the test network. Set the same URL2MD_BASE_URL in your client config to point it at testnet permanently.

This local server, or the hosted connector

This package is the local lane: it runs beside your client, holds your wallet key, and pays each call itself, which is what lets it pay a page's own x402 toll too (quoted up front, settled after your payment). Running it beside your client keeps the key on your machine and the payment yours.

The service also serves a hosted connector on its own origin — POST /mcp, Streamable HTTP behind OAuth 2.1 with a GitHub sign-in — for clients that hold no wallet, such as claude.ai's custom connectors. It is free to the signed-in account within a published allowance, and it never pays a toll: a page that charges one comes back as an error pointing at this package and at the paid HTTP endpoint. Where a deployment has it switched on, GET / lists it under clients.mcp.remote with the URL and the allowance. Use this package when you want a toll paid, when you want no account in the loop, or when you need more than the allowance.

The key is read once at startup and used for one thing: signing EIP-3009 USDC transfer authorizations for the exact amount the 402 asks for. It is never written to stdout (which is the MCP wire), to stderr, to an error message, or into a tool result. The only thing derived from it that ever leaves the process is the public address.

Checking what you are about to run

The service's repository is private, so this README and the published tarball are the whole public record — which is the reason the README documents every check, every error code and everything the key is used for, rather than pointing at code you cannot open. The tarball is small and readable, and you can have it without installing anything:

npm pack @url2md-io/mcp     # downloads the tarball; tar -xzf it and read dist/

dist/ is plain JavaScript compiled from TypeScript, a few hundred lines. dist/client.js is where the key is used and where every 402 is checked against what the service publishes.

About url2md

https://url2md.io — the service itself, with the same facts as JSON (GET /), Markdown (/llms.txt) and OpenAPI (/openapi.json). Questions: hello@url2md.io.

MIT.

Keywords

mcp

FAQs

Package last updated on 13 Sep 2026

Related posts