@url2md-io/mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +392 -0
- package/dist/client.js +611 -0
- package/dist/index.js +288 -0
- package/package.json +58 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 url2md
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
# @url2md-io/mcp
|
|
2
|
+
|
|
3
|
+
> **The official package is `@url2md-io/mcp` — with the `@url2md-io` scope.** The unscoped name
|
|
4
|
+
> `url2md-mcp` on npm is **someone else's package**, published by an unrelated person in May 2026.
|
|
5
|
+
> It is not ours, we have no control over it, and we cannot see what it does with what it is given.
|
|
6
|
+
> Running `npx url2md-mcp` runs their code — and this tool is set up by handing it a wallet private
|
|
7
|
+
> key. Give the key only to `@url2md-io/mcp`. If the name you are about to install has no scope in
|
|
8
|
+
> front of it, it is the wrong one.
|
|
9
|
+
|
|
10
|
+
An MCP server that gives your agent one tool: **turn any URL into clean Markdown**.
|
|
11
|
+
|
|
12
|
+
It handles ordinary pages, JavaScript-rendered pages (a headless browser is used only when the page
|
|
13
|
+
needs one) and PDFs. There is no account, no sign-up and no API key. Each call is paid for on-chain,
|
|
14
|
+
from a wallet you control, at **$0.005** — half a cent — in USDC on Base. The settlement receipt
|
|
15
|
+
comes back with the Markdown.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## What you need before you start
|
|
20
|
+
|
|
21
|
+
1. **Node 22.18 or newer.** Check with `node --version`. Nothing else: `npx` fetches the package
|
|
22
|
+
and the published files are plain JavaScript, so there is nothing for you to build.
|
|
23
|
+
2. **A wallet private key, holding USDC on Base.** This is the part that is different from a normal
|
|
24
|
+
MCP server, so it is worth being precise:
|
|
25
|
+
- It must be a raw private key: `0x` followed by 64 hex characters. Not a seed phrase.
|
|
26
|
+
- The wallet needs **USDC only — no ETH**. The payment facilitator submits the transfer and pays
|
|
27
|
+
the gas. A wallet with $2 of USDC and nothing else works.
|
|
28
|
+
- **Use a fresh wallet with a small balance.** Anything with this key can spend the USDC in it.
|
|
29
|
+
Do not use a wallet you keep anything else in.
|
|
30
|
+
- Get USDC on Base by bridging, or by buying on an exchange that withdraws to Base directly
|
|
31
|
+
(Coinbase does). The USDC contract on Base is `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`.
|
|
32
|
+
|
|
33
|
+
If you would rather try it without spending real money, see [Trying it on
|
|
34
|
+
testnet](#trying-it-on-testnet) below — the test network's USDC is free.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
Nothing to install. Every example below runs the server with `npx`, which fetches it on first use
|
|
41
|
+
and caches it:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npx @url2md-io/mcp
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Mind the scope — `@url2md-io/mcp`, never the unscoped `url2md-mcp`, which is someone else's
|
|
48
|
+
package. Run it once by hand like that and it will refuse to start, with `CONFIG_MISSING_KEY`.
|
|
49
|
+
That is the answer you want: the package is there and the only thing missing is the key, which the
|
|
50
|
+
next step sets up.
|
|
51
|
+
|
|
52
|
+
To pin a version, or to keep it off the network at start-up, install it instead:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npm install -g @url2md-io/mcp # then the command is: url2md
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Put the key in a file, not in a config
|
|
59
|
+
|
|
60
|
+
You can pass the key in `URL2MD_PRIVATE_KEY`, and the examples below show both ways. Prefer the
|
|
61
|
+
file:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
umask 077
|
|
65
|
+
printf '0xYOUR_PRIVATE_KEY' > ~/.url2md-key # no trailing newline needed; one is fine
|
|
66
|
+
chmod 600 ~/.url2md-key
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Then set **`URL2MD_PRIVATE_KEY_FILE=/Users/you/.url2md-key`** instead of the key itself. A path is
|
|
70
|
+
not a secret. The key itself, put anywhere else, ends up somewhere you did not intend:
|
|
71
|
+
|
|
72
|
+
- `claude mcp add … --env URL2MD_PRIVATE_KEY=0x…` puts it in your shell history, and then in
|
|
73
|
+
`~/.claude.json` in plaintext.
|
|
74
|
+
- Any `-e URL2MD_PRIVATE_KEY=0x…` on a command line is visible to every process on the machine
|
|
75
|
+
through `ps aux` for as long as it runs.
|
|
76
|
+
|
|
77
|
+
`URL2MD_PRIVATE_KEY_FILE` wins if both are set.
|
|
78
|
+
|
|
79
|
+
## Add it to your MCP client
|
|
80
|
+
|
|
81
|
+
### Claude Code
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
claude mcp add url2md \
|
|
85
|
+
--env URL2MD_PRIVATE_KEY_FILE=/Users/you/.url2md-key \
|
|
86
|
+
--env URL2MD_BASE_URL=https://url2md.io \
|
|
87
|
+
-- npx -y @url2md-io/mcp
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Claude Desktop, Cursor, Windsurf, and anything else that reads a JSON config
|
|
91
|
+
|
|
92
|
+
Add this to the `mcpServers` object of your client's config file (Claude Desktop:
|
|
93
|
+
`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS,
|
|
94
|
+
`%APPDATA%\Claude\claude_desktop_config.json` on Windows):
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{
|
|
98
|
+
"mcpServers": {
|
|
99
|
+
"url2md": {
|
|
100
|
+
"command": "npx",
|
|
101
|
+
"args": ["-y", "@url2md-io/mcp"],
|
|
102
|
+
"env": {
|
|
103
|
+
"URL2MD_PRIVATE_KEY_FILE": "/Users/you/.url2md-key",
|
|
104
|
+
"URL2MD_BASE_URL": "https://url2md.io"
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
If the server never appears, the cause is almost always `PATH`. Desktop clients on macOS are
|
|
112
|
+
launched by the window manager with a minimal `PATH` that usually does not include a
|
|
113
|
+
version-managed Node, so a bare `"npx"` resolves to nothing. Use an absolute path for `command` —
|
|
114
|
+
`which npx` prints it — and keep `args` as they are. Restart the client after editing the file.
|
|
115
|
+
|
|
116
|
+
### Check it before you wire it up
|
|
117
|
+
|
|
118
|
+
You can talk to the server yourself with the reference MCP inspector, which needs nothing installed:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
npx @modelcontextprotocol/inspector --cli npx -y @url2md-io/mcp \
|
|
122
|
+
-e URL2MD_PRIVATE_KEY_FILE=/Users/you/.url2md-key \
|
|
123
|
+
--method tools/list
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The server prints one line to stderr when it starts, naming the deployment, **the network and
|
|
127
|
+
whether it is real money**, the wallet address it will pay from, and the per-call limit:
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
@url2md-io/mcp 0.1.0 → https://url2md.io; eip155:8453 (real money); paying from 0x0972…7a36, at most $0.10 per call and $1.00 in total
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Your private key is never printed, logged, or sent anywhere except as the signature on a payment.
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## The tool
|
|
138
|
+
|
|
139
|
+
### `fetch_markdown`
|
|
140
|
+
|
|
141
|
+
| Parameter | Required | Default | What it does |
|
|
142
|
+
|---|---|---|---|
|
|
143
|
+
| `url` | yes | — | The absolute `http(s)` URL to convert. |
|
|
144
|
+
| `render` | no | `auto` | `auto`: use a headless browser only if the page looks client-rendered. `static`: never render, take the HTML as served. `browser`: always render. |
|
|
145
|
+
|
|
146
|
+
`render=browser` costs the same but is slower, and can fail on pages a plain fetch would have
|
|
147
|
+
handled. `auto` is right almost always.
|
|
148
|
+
|
|
149
|
+
**What comes back** — the Markdown as the tool's text, and this as structured content:
|
|
150
|
+
|
|
151
|
+
```json
|
|
152
|
+
{
|
|
153
|
+
"markdown": "# Example Domain\n\nThis domain is for use in documentation examples…",
|
|
154
|
+
"url": "https://example.com/",
|
|
155
|
+
"finalUrl": "https://example.com/",
|
|
156
|
+
"title": "Example Domain",
|
|
157
|
+
"source": "static",
|
|
158
|
+
"words": 24,
|
|
159
|
+
"tokensEstimate": 42,
|
|
160
|
+
"warnings": [],
|
|
161
|
+
"payment": {
|
|
162
|
+
"amountUsd": "0.005",
|
|
163
|
+
"amountAtomic": "5000",
|
|
164
|
+
"network": "eip155:8453",
|
|
165
|
+
"payTo": "0xa09aE66Ec8664475697FfC0eb45aa3Fe52Aa6030",
|
|
166
|
+
"settled": true,
|
|
167
|
+
"payer": "0x…your wallet",
|
|
168
|
+
"transaction": "0x3bd94a…",
|
|
169
|
+
"explorerUrl": "https://basescan.org/tx/0x3bd94a…",
|
|
170
|
+
"ledgerId": "e17a62c0-0801-4043-a58a-da135c1343ae"
|
|
171
|
+
},
|
|
172
|
+
"paymentRounds": 1,
|
|
173
|
+
"committedUsd": "0.005",
|
|
174
|
+
"signed": [
|
|
175
|
+
{
|
|
176
|
+
"amountUsd": "0.005",
|
|
177
|
+
"amountAtomic": "5000",
|
|
178
|
+
"network": "eip155:8453",
|
|
179
|
+
"payTo": "0xa09aE66Ec8664475697FfC0eb45aa3Fe52Aa6030",
|
|
180
|
+
"payer": "0x…your wallet",
|
|
181
|
+
"nonce": "0xdccc5af7…",
|
|
182
|
+
"validBefore": "2026-09-07T05:17:26.000Z"
|
|
183
|
+
}
|
|
184
|
+
]
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
`source` says how the Markdown was produced: `static`, `browser`, `pdf`, `origin-markdown` or
|
|
189
|
+
`text`. Fields that do not apply are left out rather than set to `null`.
|
|
190
|
+
|
|
191
|
+
**`payment` is there whenever you were charged**, even if the service answered without confirming
|
|
192
|
+
the settlement. The `amountUsd`, `network`, `payTo` and `payer` are always **what this client
|
|
193
|
+
signed** — never what the service's receipt claims. If the receipt disagrees with any of them,
|
|
194
|
+
`warnings` names both figures and the signed one stands. `settled: false` means no usable
|
|
195
|
+
settlement came back at all; `transaction` is then absent and `warnings` tells you to check the
|
|
196
|
+
wallet on-chain.
|
|
197
|
+
|
|
198
|
+
`committedUsd` is everything the call authorized, and `signed` lists each authorization with the
|
|
199
|
+
nonce that identifies it on-chain. On an ordinary call `committedUsd` equals `payment.amountUsd`.
|
|
200
|
+
On a **tolled** call it is higher: the first authorization is replaced by the quote, only the quote
|
|
201
|
+
is settled, and the service still holds the first one — `warnings` says so, with both numbers.
|
|
202
|
+
|
|
203
|
+
Read `warnings`. It carries the service's own notes too — a browser render that failed and fell
|
|
204
|
+
back to the static result, for instance. Anything you must not miss is also written **above the
|
|
205
|
+
Markdown in the tool's text**, because many MCP clients show a model only the text and drop the
|
|
206
|
+
structured content: an unconfirmed payment appears there as a `PAYMENT NOT CONFIRMED` line, and
|
|
207
|
+
every warning as a `NOTE:` line. A clean, settled call has no preamble at all.
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## What a call costs
|
|
212
|
+
|
|
213
|
+
**$0.005** per call, whatever the page. The wallet is debited once, when the call succeeds — or, for
|
|
214
|
+
a page that needs a browser, just before the browser starts.
|
|
215
|
+
|
|
216
|
+
**Which failures are free, exactly.** A bad URL, an unsupported scheme and a blocked host are
|
|
217
|
+
refused on the *unpaid* request: nothing is signed and `details` is empty. Everything else —
|
|
218
|
+
`ROBOTS_DISALLOWED`, `ROBOTS_UNREACHABLE` (which is where a mistyped domain lands), a page that
|
|
219
|
+
times out, a render that fails — is discovered **after** the payment gate, so a signed
|
|
220
|
+
authorization has already left this process. url2md does not settle a payment for a call it
|
|
221
|
+
answered with an error, and the ledger row would say `needs_refund` if it did; but this client
|
|
222
|
+
cannot see the chain and does not claim the money is safe. Every such failure therefore comes back
|
|
223
|
+
with `details.committedUsd`, and `details.signed[]` carrying each authorization's amount,
|
|
224
|
+
recipient, EIP-3009 nonce and expiry — that nonce is what USDC emits as `AuthorizationUsed` when a
|
|
225
|
+
payment is actually redeemed, so you can check for yourself. An agent that retries a mistyped
|
|
226
|
+
domain in a loop signs $0.005 each time until `URL2MD_MAX_SPEND_USD` stops it.
|
|
227
|
+
|
|
228
|
+
**Pages that charge their own toll.** Some pages are themselves behind an x402 paywall. url2md can
|
|
229
|
+
pay them for you: it quotes you the base price plus the page's toll plus 25 %, you pay that, and it
|
|
230
|
+
pays the origin from its own wallet. Those calls come back with `toll` filled in and
|
|
231
|
+
`paymentRounds: 2`. Tolls above $0.05 are refused rather than passed on.
|
|
232
|
+
|
|
233
|
+
**Two ceilings.** One call will not commit more than **$0.10** in total, however many rounds it
|
|
234
|
+
takes — comfortably above the most a legitimate call can cost (base + the $0.05 toll cap + margin
|
|
235
|
+
≈ $0.068), and low enough that a surprising price is refused instead of paid. And the whole process
|
|
236
|
+
will not commit more than **$1.00** for as long as it runs, which is what bounds an agent stuck in a
|
|
237
|
+
retry loop; restarting the server resets it. Raise either with `URL2MD_MAX_PRICE_USD` and
|
|
238
|
+
`URL2MD_MAX_SPEND_USD`. Neither check can be turned off, though setting the numbers absurdly high
|
|
239
|
+
has the same effect, so the server says so on startup if you do.
|
|
240
|
+
|
|
241
|
+
**What is checked before anything is signed.** A `402` is written by whoever answers your request,
|
|
242
|
+
so the terms in it are not evidence of anything on their own. Before signing, this client fetches
|
|
243
|
+
the service's own published terms from `GET /` (free, unpaid) and refuses the payment unless the
|
|
244
|
+
402 matches them:
|
|
245
|
+
|
|
246
|
+
| Check | Refused with |
|
|
247
|
+
|---|---|
|
|
248
|
+
| The service publishes a `payTo`, an asset and a network at all | `UNPUBLISHED_TERMS` |
|
|
249
|
+
| The recipient is the `payTo` the service publishes | `PAYTO_MISMATCH` |
|
|
250
|
+
| The token is the asset the service publishes | `UNSUPPORTED_PAYMENT_TERMS` |
|
|
251
|
+
| The network is the one the service publishes | `UNSUPPORTED_PAYMENT_TERMS` |
|
|
252
|
+
| The scheme is `exact` | `UNSUPPORTED_PAYMENT_TERMS` |
|
|
253
|
+
| The authorization is redeemable for at most an hour | `UNSUPPORTED_PAYMENT_TERMS` |
|
|
254
|
+
| The **whole call** stays at or under your per-call ceiling | `PRICE_ABOVE_LIMIT` |
|
|
255
|
+
| This process has not spent its session budget | `SPEND_LIMIT_REACHED` |
|
|
256
|
+
|
|
257
|
+
These **fail closed**: a service that publishes no receiving address gets no signature, rather than
|
|
258
|
+
an unchecked one. The amount is not compared with the published price for the *toll* round, because
|
|
259
|
+
a quote is legitimately higher than the base price — but a **first** 402 above the published price
|
|
260
|
+
is reported in `warnings` even when it is inside your ceiling, since that is a service charging
|
|
261
|
+
something other than what it advertises.
|
|
262
|
+
|
|
263
|
+
**`URL2MD_BASE_URL` is the whole trust anchor.** Every check above compares the 402 with what *that
|
|
264
|
+
URL* published moments earlier. It stops a service contradicting itself; it cannot stop a service
|
|
265
|
+
that lies consistently, because an impostor writes both halves. So: point it at a deployment you
|
|
266
|
+
trust, and nothing else. It must be `https://` (only a loopback address may be plain `http`), and
|
|
267
|
+
the paid request is pinned to that origin — a redirect to another host is refused
|
|
268
|
+
(`REDIRECT_REFUSED`) rather than followed, because following it would hand your signed
|
|
269
|
+
authorization to whoever the redirect names.
|
|
270
|
+
|
|
271
|
+
The ceiling bounds the whole call, so a two-round tolled call cannot commit twice the number you
|
|
272
|
+
set — and what a call committed in total is always reported, as `committedUsd` on success and
|
|
273
|
+
`details.committedUsd` on failure.
|
|
274
|
+
|
|
275
|
+
**If a call fails after a payment was signed**, the error carries `details.signedButUnconfirmed`,
|
|
276
|
+
`details.committedUsd`, and `details.signed` — one entry per authorization, with the amount, the
|
|
277
|
+
recipient, the network, the EIP-3009 nonce and when it stops being redeemable. A signature that has
|
|
278
|
+
left this process is money you have committed whether or not the service ever redeems it, so it is
|
|
279
|
+
never omitted from a failure.
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
## Configuration
|
|
284
|
+
|
|
285
|
+
| Variable | Required | Default | What it is |
|
|
286
|
+
|---|---|---|---|
|
|
287
|
+
| `URL2MD_PRIVATE_KEY_FILE` | one of these two | — | Path to a file containing the key. **Preferred** — a path is not a secret. Wins if both are set. |
|
|
288
|
+
| `URL2MD_PRIVATE_KEY` | one of these two | — | The key itself: `0x` + 64 hex characters. |
|
|
289
|
+
| `URL2MD_MAX_PRICE_USD` | no | `0.10` | Most this server will commit for **one call**, in USD, across every round of it. |
|
|
290
|
+
| `URL2MD_MAX_SPEND_USD` | no | `1.00` | Most this server will commit **in total**, for as long as the process runs. Restarting resets it. |
|
|
291
|
+
| `URL2MD_BASE_URL` | no | `https://url2md.io` | The url2md deployment to call. |
|
|
292
|
+
| `URL2MD_TIMEOUT_MS` | no | `90000` | How long to wait for one conversion. |
|
|
293
|
+
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
## When something goes wrong
|
|
297
|
+
|
|
298
|
+
Failures come back as MCP error results (`isError: true`) whose text is a JSON object, so an agent
|
|
299
|
+
can branch on the code rather than read prose:
|
|
300
|
+
|
|
301
|
+
```json
|
|
302
|
+
{ "error": { "code": "BLOCKED_HOST", "message": "localhost is not a public host", "retryable": false, "httpStatus": 400 } }
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
| Code | What happened | What to do |
|
|
306
|
+
|---|---|---|
|
|
307
|
+
| `CONFIG_MISSING_KEY`, `CONFIG_BAD_KEY`, `CONFIG_BAD_KEY_FILE` | No 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. |
|
|
308
|
+
| `CONFIG_BAD_LIMIT`, `CONFIG_BAD_BASE_URL`, `CONFIG_BAD_TIMEOUT` | A 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. |
|
|
309
|
+
| `INVALID_URL`, `UNSUPPORTED_SCHEME` | The URL is not an absolute `http(s)` URL. | Fix the URL. |
|
|
310
|
+
| `BLOCKED_HOST` | Private, loopback, link-local or internal host. | Public URLs only — this is deliberate. |
|
|
311
|
+
| `ROBOTS_DISALLOWED` | The origin's robots.txt forbids it. Checked after the payment gate, so an authorization was signed — read `details.committedUsd`. | Nothing; url2md obeys robots.txt. |
|
|
312
|
+
| `PRICE_ABOVE_LIMIT` | The call, in total, would cost more than `URL2MD_MAX_PRICE_USD`. | Read `details.totalUsd`; raise the limit if it is reasonable. |
|
|
313
|
+
| `SPEND_LIMIT_REACHED` | This 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. |
|
|
314
|
+
| `UNPUBLISHED_TERMS` | The 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 /`. |
|
|
315
|
+
| `PAYTO_MISMATCH` | The 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. |
|
|
316
|
+
| `UNSUPPORTED_PAYMENT_TERMS` | The 402's network, asset or scheme is not the one the service publishes, or this client cannot pay it. Nothing was signed. | As above. |
|
|
317
|
+
| `PAYMENT_NOT_SIGNED` | The payment could not be signed — usually the x402 client's own spend controls refusing an unrecognised token. | Read the message; nothing was sent. |
|
|
318
|
+
| `ROBOTS_UNREACHABLE` | The 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. |
|
|
319
|
+
| `SERVICE_UNREACHABLE`, `TIMEOUT` | url2md itself did not answer, or not in time. | Retryable. Check `URL2MD_BASE_URL`. |
|
|
320
|
+
| `BAD_RESPONSE`, `PAYMENT_NOT_ACCEPTED` | url2md answered something this client could not use, or kept asking for payment after two rounds. | Nothing further was signed. Report it. |
|
|
321
|
+
| `REDIRECT_REFUSED` | The 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. |
|
|
322
|
+
| `TOO_MANY_REDIRECTS` | More than three redirects inside the service's own origin. | Check `URL2MD_BASE_URL`. |
|
|
323
|
+
| `UPSTREAM_PAYMENT_REQUIRED`, `UPSTREAM_PAYMENT_FAILED` | The 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`. |
|
|
324
|
+
| `INTERNAL` | An unexpected failure inside this server. | Report it. |
|
|
325
|
+
| `UPSTREAM_TOLL_TOO_HIGH` | The page charges a toll above url2md's $0.05 cap. | Nothing; that page is not available through url2md. |
|
|
326
|
+
| `PAYMENT_REJECTED` | The 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. |
|
|
327
|
+
| `PAYMENT_REQUIRED` | The service asked for payment but does not publish x402 pricing, so this client will not guess at terms. | Check `URL2MD_BASE_URL`. |
|
|
328
|
+
| `RENDER_FAILED`, `UPSTREAM_STATUS`, `UPSTREAM_TIMEOUT` | The page itself failed. | `retryable` says whether trying again is worth it. |
|
|
329
|
+
| `RATE_LIMITED` | 60 calls a minute, 6 browser renders a minute. | Wait. |
|
|
330
|
+
|
|
331
|
+
url2md's own error codes are documented in full in the service's
|
|
332
|
+
[`FEATURES.md`](https://github.com/glassyeah/url2md/blob/main/FEATURES.md) and `openapi.json`; any
|
|
333
|
+
of them can appear here unchanged.
|
|
334
|
+
|
|
335
|
+
**If you were charged and got nothing**, the error carries `details.callerCharged: true` and
|
|
336
|
+
`details.ledgerId`. url2md keeps a row for every settled payment and reconciles it against the chain;
|
|
337
|
+
quote the ledger id to <hello@url2md.io>.
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
## Trying it on testnet
|
|
342
|
+
|
|
343
|
+
Base Sepolia USDC is free, so you can exercise the whole thing without spending anything. Note that
|
|
344
|
+
this changes the **payment network** to Base Sepolia (`eip155:84532`): a wallet funded with USDC on
|
|
345
|
+
Base mainnet cannot pay the testnet deployment, and vice versa. The startup line names the network
|
|
346
|
+
it will pay on, so you can check before you spend.
|
|
347
|
+
|
|
348
|
+
```bash
|
|
349
|
+
npx @modelcontextprotocol/inspector --cli npx -y @url2md-io/mcp \
|
|
350
|
+
-e URL2MD_PRIVATE_KEY_FILE=/Users/you/.url2md-testnet-key \
|
|
351
|
+
-e URL2MD_BASE_URL=https://url2md-testnet.url2md.workers.dev \
|
|
352
|
+
--method tools/call --tool-name fetch_markdown --tool-arg url=https://example.com
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
Fund a throwaway wallet from the [Coinbase Base Sepolia faucet](https://portal.cdp.coinbase.com/products/faucet).
|
|
356
|
+
The testnet deployment is the same code as production, on the test network. Set the same
|
|
357
|
+
`URL2MD_BASE_URL` in your client config to point it at testnet permanently.
|
|
358
|
+
|
|
359
|
+
---
|
|
360
|
+
|
|
361
|
+
## Why this is a local server and not a hosted one
|
|
362
|
+
|
|
363
|
+
Because a hosted server would have no wallet of yours to pay with. It would pay from its own and
|
|
364
|
+
then have to bill you some other way — an account, an API key, an invoice — which is the exact thing
|
|
365
|
+
x402 removes. Running it beside your client keeps the key on your machine and the payment yours.
|
|
366
|
+
|
|
367
|
+
The key is read once at startup and used for one thing: signing
|
|
368
|
+
[EIP-3009](https://eips.ethereum.org/EIPS/eip-3009) USDC transfer authorizations for the exact
|
|
369
|
+
amount the 402 asks for. It is never written to stdout (which is the MCP wire), to stderr, to an
|
|
370
|
+
error message, or into a tool result. The only thing derived from it that ever leaves the process is
|
|
371
|
+
the public address.
|
|
372
|
+
|
|
373
|
+
---
|
|
374
|
+
|
|
375
|
+
## Working on it
|
|
376
|
+
|
|
377
|
+
The source is the `mcp/` directory of the service repository. There is no build step — Node runs the
|
|
378
|
+
TypeScript directly, which is why `engines.node` is `>=22.18`.
|
|
379
|
+
|
|
380
|
+
```bash
|
|
381
|
+
npm install # in mcp/
|
|
382
|
+
npm run typecheck # tsc
|
|
383
|
+
npm test # vitest
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
## About url2md
|
|
387
|
+
|
|
388
|
+
<https://url2md.io> — the service itself, with the same facts as JSON (`GET /`), Markdown
|
|
389
|
+
(`/llms.txt`) and OpenAPI (`/openapi.json`). Source: <https://github.com/glassyeah/url2md>.
|
|
390
|
+
Questions: <hello@url2md.io>.
|
|
391
|
+
|
|
392
|
+
MIT.
|