@url2md-io/mcp 0.1.0 → 0.1.2
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/README.md +87 -52
- package/dist/client.js +3 -0
- package/dist/index.js +1 -1
- package/package.json +3 -8
package/README.md
CHANGED
|
@@ -127,7 +127,7 @@ The server prints one line to stderr when it starts, naming the deployment, **th
|
|
|
127
127
|
whether it is real money**, the wallet address it will pay from, and the per-call limit:
|
|
128
128
|
|
|
129
129
|
```
|
|
130
|
-
@url2md-io/mcp 0.1.
|
|
130
|
+
@url2md-io/mcp 0.1.2 → https://url2md.io; eip155:8453 (real money); paying from 0x0972…7a36, at most $0.10 per call and $1.00 in total
|
|
131
131
|
```
|
|
132
132
|
|
|
133
133
|
Your private key is never printed, logged, or sent anywhere except as the signature on a payment.
|
|
@@ -138,10 +138,10 @@ Your private key is never printed, logged, or sent anywhere except as the signat
|
|
|
138
138
|
|
|
139
139
|
### `fetch_markdown`
|
|
140
140
|
|
|
141
|
-
| Parameter |
|
|
142
|
-
|
|
143
|
-
| `url` |
|
|
144
|
-
| `render` |
|
|
141
|
+
| Parameter | What it does |
|
|
142
|
+
|---|---|
|
|
143
|
+
| `url` | **Required.** The absolute `http(s)` URL to convert. |
|
|
144
|
+
| `render` | Optional, 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. |
|
|
145
145
|
|
|
146
146
|
`render=browser` costs the same but is slower, and can fail on pages a plain fetch would have
|
|
147
147
|
handled. `auto` is right almost always.
|
|
@@ -213,8 +213,12 @@ every warning as a `NOTE:` line. A clean, settled call has no preamble at all.
|
|
|
213
213
|
**$0.005** per call, whatever the page. The wallet is debited once, when the call succeeds — or, for
|
|
214
214
|
a page that needs a browser, just before the browser starts.
|
|
215
215
|
|
|
216
|
-
**Which failures are free, exactly.** A
|
|
217
|
-
refused on the *unpaid* request: nothing is signed and `details` is empty.
|
|
216
|
+
**Which failures are free, exactly.** A malformed URL, an unsupported scheme and a blocked host are
|
|
217
|
+
refused on the *unpaid* request: nothing is signed and `details` is empty. A **missing** URL is the
|
|
218
|
+
one exception, and it costs nothing either: the bare path answers a payment challenge like every
|
|
219
|
+
other x402 endpoint, so this client signs a payment and is then told `INVALID_URL` — the service
|
|
220
|
+
settles nothing for a call it answered with an error, so the authorization is wasted, not spent.
|
|
221
|
+
Everything else —
|
|
218
222
|
`ROBOTS_DISALLOWED`, `ROBOTS_UNREACHABLE` (which is where a mistyped domain lands), a page that
|
|
219
223
|
times out, a render that fails — is discovered **after** the payment gate, so a signed
|
|
220
224
|
authorization has already left this process. url2md does not settle a payment for a call it
|
|
@@ -282,14 +286,43 @@ never omitted from a failure.
|
|
|
282
286
|
|
|
283
287
|
## Configuration
|
|
284
288
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
289
|
+
One of the two key variables is required. Everything else has a default.
|
|
290
|
+
|
|
291
|
+
#### `URL2MD_PRIVATE_KEY_FILE`
|
|
292
|
+
|
|
293
|
+
Path to a file containing the key. **Preferred** — a path is not a secret, and a key on a command
|
|
294
|
+
line is visible to every process on the machine. Wins if both key variables are set.
|
|
295
|
+
|
|
296
|
+
#### `URL2MD_PRIVATE_KEY`
|
|
297
|
+
|
|
298
|
+
The key itself: `0x` followed by 64 hex characters. Use the file instead where you can.
|
|
299
|
+
|
|
300
|
+
#### `URL2MD_MAX_PRICE_USD` — default `0.10`
|
|
301
|
+
|
|
302
|
+
The most this server will commit for **one call**, in USD, across every round of that call. A
|
|
303
|
+
tolled page takes two rounds; this ceiling covers both.
|
|
304
|
+
|
|
305
|
+
#### `URL2MD_MAX_SPEND_USD` — default `1.00`
|
|
306
|
+
|
|
307
|
+
The most this server will commit **in total**, for as long as the process runs. Restarting resets
|
|
308
|
+
it.
|
|
309
|
+
|
|
310
|
+
#### `URL2MD_BASE_URL` — default `https://url2md.io`
|
|
311
|
+
|
|
312
|
+
The url2md deployment to call. Must be `https://` (only a loopback address may be plain `http`),
|
|
313
|
+
because this is the origin every 402 is checked against.
|
|
314
|
+
|
|
315
|
+
#### `URL2MD_TIMEOUT_MS` — default `90000`
|
|
316
|
+
|
|
317
|
+
How long to wait for one conversion, in milliseconds. 90 s is the default because that is what the
|
|
318
|
+
service's own bounds add up to: 20 s for the fetch (robots.txt, every redirect hop and the body run
|
|
319
|
+
on one deadline) and, when the page needs a headless browser, 55 s more for the render (30 s of
|
|
320
|
+
navigation plus 25 s of action and slack) — 75 s in the worst case, plus room for the network and
|
|
321
|
+
the payment round trips.
|
|
322
|
+
|
|
323
|
+
Do not set it to 30 s because you read that a render gets 30 s: that is the navigation timeout
|
|
324
|
+
*inside* the browser, not the wall clock. Giving up early does not cancel the call, and on a page
|
|
325
|
+
that needed a browser your payment has already settled by the time the render starts.
|
|
293
326
|
|
|
294
327
|
---
|
|
295
328
|
|
|
@@ -302,35 +335,35 @@ can branch on the code rather than read prose:
|
|
|
302
335
|
{ "error": { "code": "BLOCKED_HOST", "message": "localhost is not a public host", "retryable": false, "httpStatus": 400 } }
|
|
303
336
|
```
|
|
304
337
|
|
|
305
|
-
| Code | What
|
|
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).
|
|
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.
|
|
309
|
-
| `INVALID_URL`, `UNSUPPORTED_SCHEME` | The URL is not an absolute `http(s)` URL.
|
|
310
|
-
| `BLOCKED_HOST` | Private, loopback, link-local or internal host.
|
|
311
|
-
| `ROBOTS_DISALLOWED` | The origin's robots.txt forbids it. Checked after the payment gate, so an authorization was signed
|
|
312
|
-
| `PRICE_ABOVE_LIMIT` | The call, in total, would cost more than `URL2MD_MAX_PRICE_USD`.
|
|
313
|
-
| `SPEND_LIMIT_REACHED` | This server has committed its whole `URL2MD_MAX_SPEND_USD` session budget. **Nothing was signed.**
|
|
314
|
-
| `UNPUBLISHED_TERMS` | The service asks for payment but publishes no receiving address, asset or network to check its 402 against. **Nothing was signed.**
|
|
315
|
-
| `PAYTO_MISMATCH` | The 402 asked you to pay an address the service does not publish. **Nothing was signed.**
|
|
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.
|
|
317
|
-
| `PAYMENT_NOT_SIGNED` | The payment could not be signed — usually the x402 client's own spend controls refusing an unrecognised token.
|
|
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`.
|
|
319
|
-
| `SERVICE_UNREACHABLE`, `TIMEOUT` | url2md itself did not answer, or not in time.
|
|
320
|
-
| `BAD_RESPONSE`, `PAYMENT_NOT_ACCEPTED` | url2md answered something this client could not use, or kept asking for payment after two rounds.
|
|
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.
|
|
322
|
-
| `TOO_MANY_REDIRECTS` | More than three redirects inside the service's own origin.
|
|
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.
|
|
324
|
-
| `INTERNAL` | An unexpected failure inside this server.
|
|
325
|
-
| `UPSTREAM_TOLL_TOO_HIGH` | The page charges a toll above url2md's $0.05 cap
|
|
326
|
-
| `PAYMENT_REJECTED` | The service refused the payment when it tried to settle it. Usually an empty wallet.
|
|
327
|
-
| `PAYMENT_REQUIRED` | The service asked for payment but does not publish x402 pricing, so this client will not guess at terms.
|
|
328
|
-
| `
|
|
329
|
-
| `
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
of them can appear here unchanged.
|
|
338
|
+
| Code | What it means, and what to do |
|
|
339
|
+
|---|---|
|
|
340
|
+
| `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. |
|
|
341
|
+
| `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. |
|
|
342
|
+
| `INVALID_URL`, `UNSUPPORTED_SCHEME` | The URL is missing, or is not an absolute `http(s)` URL. Fix the URL. |
|
|
343
|
+
| `BLOCKED_HOST` | Private, loopback, link-local or internal host. Public URLs only — this is deliberate. |
|
|
344
|
+
| `ROBOTS_DISALLOWED` | The 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`. |
|
|
345
|
+
| `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. |
|
|
346
|
+
| `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. |
|
|
347
|
+
| `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 /`. |
|
|
348
|
+
| `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. |
|
|
349
|
+
| `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. |
|
|
350
|
+
| `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. |
|
|
351
|
+
| `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. |
|
|
352
|
+
| `SERVICE_UNREACHABLE`, `TIMEOUT` | url2md itself did not answer, or not in time. Retryable. Check `URL2MD_BASE_URL`. |
|
|
353
|
+
| `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. |
|
|
354
|
+
| `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. |
|
|
355
|
+
| `TOO_MANY_REDIRECTS` | More than three redirects inside the service's own origin. Check `URL2MD_BASE_URL`. |
|
|
356
|
+
| `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`. |
|
|
357
|
+
| `INTERNAL` | An unexpected failure inside this server. Report it. |
|
|
358
|
+
| `UPSTREAM_TOLL_TOO_HIGH` | The page charges a toll above url2md's $0.05 cap, so it is not available through url2md. Nothing to do. |
|
|
359
|
+
| `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. |
|
|
360
|
+
| `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`. |
|
|
361
|
+
| `UPSTREAM_STATUS` | The 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. |
|
|
362
|
+
| `RENDER_FAILED`, `UPSTREAM_TIMEOUT` | The page itself failed to load or render in time. `retryable` says whether trying again is worth it. |
|
|
363
|
+
| `RATE_LIMITED` | 60 calls a minute, 6 browser renders a minute. Wait. |
|
|
364
|
+
|
|
365
|
+
url2md's own error codes are documented in full in the service's OpenAPI document,
|
|
366
|
+
<https://url2md.io/openapi.json>; any of them can appear here unchanged.
|
|
334
367
|
|
|
335
368
|
**If you were charged and got nothing**, the error carries `details.callerCharged: true` and
|
|
336
369
|
`details.ledgerId`. url2md keeps a row for every settled payment and reconciles it against the chain;
|
|
@@ -372,21 +405,23 @@ the public address.
|
|
|
372
405
|
|
|
373
406
|
---
|
|
374
407
|
|
|
375
|
-
##
|
|
408
|
+
## Checking what you are about to run
|
|
376
409
|
|
|
377
|
-
The
|
|
378
|
-
|
|
410
|
+
The service's repository is private, so this README and the published tarball are the whole public
|
|
411
|
+
record — which is the reason the README documents every check, every error code and everything the
|
|
412
|
+
key is used for, rather than pointing at code you cannot open. The tarball is small and readable,
|
|
413
|
+
and you can have it without installing anything:
|
|
379
414
|
|
|
380
415
|
```bash
|
|
381
|
-
npm
|
|
382
|
-
npm run typecheck # tsc
|
|
383
|
-
npm test # vitest
|
|
416
|
+
npm pack @url2md-io/mcp # downloads the tarball; tar -xzf it and read dist/
|
|
384
417
|
```
|
|
385
418
|
|
|
419
|
+
`dist/` is plain JavaScript compiled from TypeScript, a few hundred lines. `dist/client.js` is
|
|
420
|
+
where the key is used and where every 402 is checked against what the service publishes.
|
|
421
|
+
|
|
386
422
|
## About url2md
|
|
387
423
|
|
|
388
424
|
<https://url2md.io> — the service itself, with the same facts as JSON (`GET /`), Markdown
|
|
389
|
-
(`/llms.txt`) and OpenAPI (`/openapi.json`).
|
|
390
|
-
Questions: <hello@url2md.io>.
|
|
425
|
+
(`/llms.txt`) and OpenAPI (`/openapi.json`). Questions: <hello@url2md.io>.
|
|
391
426
|
|
|
392
427
|
MIT.
|
package/dist/client.js
CHANGED
|
@@ -120,6 +120,9 @@ export function readConfig(env, readFile = defaultReadFile) {
|
|
|
120
120
|
throw new Url2mdToolError("CONFIG_BAD_BASE_URL", `URL2MD_BASE_URL must be https:// (got ${JSON.stringify(baseUrl)}). This URL is what every 402 is checked against, so anyone who can rewrite the traffic can rewrite the terms this client verifies. Only a loopback address may be plain http.`);
|
|
121
121
|
}
|
|
122
122
|
const timeoutRaw = (env["URL2MD_TIMEOUT_MS"] ?? "").trim();
|
|
123
|
+
// 90 s: the service's own worst case (20 s fetch + 55 s browser wall clock = 75 s, its `LIMITS.worstCaseMs`)
|
|
124
|
+
// plus slack for the network and the payment round trips. It is the same number as the service's
|
|
125
|
+
// `CLIENT_TIMEOUT_MS`, kept in step by a drift guard because this package cannot import from that one.
|
|
123
126
|
const timeoutMs = timeoutRaw ? Number(timeoutRaw) : 90_000;
|
|
124
127
|
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
125
128
|
throw new Url2mdToolError("CONFIG_BAD_TIMEOUT", `URL2MD_TIMEOUT_MS must be a positive number of milliseconds; got ${JSON.stringify(timeoutRaw)}.`);
|
package/dist/index.js
CHANGED
|
@@ -18,7 +18,7 @@ import { Budget, ceilingLooksWrong, CEILING_WORTH_MENTIONING_USD, convert, disco
|
|
|
18
18
|
* message or into a tool result.
|
|
19
19
|
*/
|
|
20
20
|
export const SERVER_NAME = "url2md";
|
|
21
|
-
export const SERVER_VERSION = "0.1.
|
|
21
|
+
export const SERVER_VERSION = "0.1.2";
|
|
22
22
|
export const TOOL_NAME = "fetch_markdown";
|
|
23
23
|
export const inputSchema = z.object({
|
|
24
24
|
url: z.string().describe("The absolute http(s) URL to convert. Public hosts only: private, loopback and link-local addresses are refused, and the origin's robots.txt is obeyed."),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@url2md-io/mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "MCP server for url2md — any URL in, clean Markdown out. Pays per call over x402 from your own wallet.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
@@ -11,13 +11,8 @@
|
|
|
11
11
|
"url-to-markdown"
|
|
12
12
|
],
|
|
13
13
|
"homepage": "https://url2md.io",
|
|
14
|
-
"repository": {
|
|
15
|
-
"type": "git",
|
|
16
|
-
"url": "https://github.com/glassyeah/url2md.git",
|
|
17
|
-
"directory": "mcp"
|
|
18
|
-
},
|
|
19
14
|
"bugs": {
|
|
20
|
-
"
|
|
15
|
+
"email": "hello@url2md.io"
|
|
21
16
|
},
|
|
22
17
|
"license": "MIT",
|
|
23
18
|
"type": "module",
|
|
@@ -25,7 +20,7 @@
|
|
|
25
20
|
"node": ">=22.18"
|
|
26
21
|
},
|
|
27
22
|
"bin": {
|
|
28
|
-
"url2md": "
|
|
23
|
+
"url2md": "dist/index.js"
|
|
29
24
|
},
|
|
30
25
|
"files": [
|
|
31
26
|
"dist",
|