@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 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.0 → https://url2md.io; eip155:8453 (real money); paying from 0x0972…7a36, at most $0.10 per call and $1.00 in total
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 | 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. |
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 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
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
- | 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. |
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 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.
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
- ## Working on it
408
+ ## Checking what you are about to run
376
409
 
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`.
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 install # in mcp/
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`). Source: <https://github.com/glassyeah/url2md>.
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.0";
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.0",
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
- "url": "https://url2md.io"
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": "./dist/index.js"
23
+ "url2md": "dist/index.js"
29
24
  },
30
25
  "files": [
31
26
  "dist",