skim-mcp 0.2.0 → 0.2.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 +36 -4
- package/dist/index.js +123 -12
- package/dist/src/index.d.ts +2 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
[](https://registry.modelcontextprotocol.io/v0/servers?search=skim402)
|
|
7
7
|
[](LICENSE)
|
|
8
8
|
|
|
9
|
-
`skim-mcp` is the official Model Context Protocol server for [Skim](https://skim402.com) — the clean reader API for AI agents. It exposes `read_url`, `read_urls` (batch), `extract_url` (structured / table), `watch_urls`, and `check_watch`. The default path is a card-plan API key (`SKIM_API_KEY`); x402 wallet pay stays optional.
|
|
9
|
+
`skim-mcp` is the official Model Context Protocol server for [Skim](https://skim402.com) — the clean reader API for AI agents. It exposes `read_url`, `read_urls` (batch), `extract_url` (structured / table), `crawl_url`, `read_pdf`, `watch_urls`, and `check_watch`. The default path is a card-plan API key (`SKIM_API_KEY`); x402 wallet pay stays optional.
|
|
10
10
|
|
|
11
11
|
> **See it before you wire it:** [try Skim free in your browser](https://freeskims.skim402.com) — 10 free skims a day, no signup. Paste a URL, see exactly what your agent gets back.
|
|
12
12
|
|
|
@@ -137,17 +137,45 @@ Structured JSON from a page. Pass a JSON Schema, or a preset: `article`, `produc
|
|
|
137
137
|
|
|
138
138
|
Presets are sent as schemas on those extract routes (card lane has no live `/api/t/extract/{preset}` today). Align with skim402-web if that splits later.
|
|
139
139
|
|
|
140
|
+
### `crawl_url`
|
|
141
|
+
|
|
142
|
+
Crawl a site (origin or start URL) and return clean Markdown per page. Discovers `sitemap.xml` / `robots.txt` sitemaps plus same-origin links. Cap 25 pages. **1 credit per successful page**; failed pages are not charged.
|
|
143
|
+
|
|
144
|
+
**Input:** `{ "url": "https://example.com", "maxPages": 10 }`
|
|
145
|
+
|
|
146
|
+
Optional: `stripLinks`, `stripImages` (passed through to each page read). Bare hosts like `example.com` are treated as `https://example.com`.
|
|
147
|
+
|
|
148
|
+
**Route:** `POST /api/t/crawl` (API key). No x402 `/v1` twin — wallet-only configs get a clear error; set `SKIM_API_KEY`.
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
Crawl https://example.com (max 10 pages) and list the page titles.
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### `read_pdf`
|
|
155
|
+
|
|
156
|
+
Fetch a public PDF URL and return clean Markdown plus an optional bookmark outline. Text comes only from the file. Image-only scans return **422** (no OCR). Files larger than **8 MB** return **413**. **3 credits**; failed conversions are not charged.
|
|
157
|
+
|
|
158
|
+
**Input:** `{ "url": "https://example.com/paper.pdf" }`
|
|
159
|
+
|
|
160
|
+
Optional: `outline` (default `true`).
|
|
161
|
+
|
|
162
|
+
**Route:** `POST /api/t/read-pdf` (API key). No x402 `/v1` twin — set `SKIM_API_KEY`.
|
|
163
|
+
|
|
164
|
+
```
|
|
165
|
+
Read the PDF at https://example.com/paper.pdf and summarize the outline.
|
|
166
|
+
```
|
|
167
|
+
|
|
140
168
|
### `watch_urls` / `check_watch`
|
|
141
169
|
|
|
142
170
|
Register 1–20 URLs, then poll for content diffs. `watch_id` is a secret.
|
|
143
171
|
|
|
144
172
|
**Input:** `{ "urls": ["https://competitor.com/pricing"], "note": "pricing" }` then `{ "watch_id": "w_…" }` (optional `status_only: true`)
|
|
145
173
|
|
|
146
|
-
**Routes (API key
|
|
174
|
+
**Routes (API key):** `POST /api/t/watch` · `GET /api/t/watch/diff?id=` · `GET /api/t/watch/status?id=`
|
|
147
175
|
|
|
148
|
-
**Routes (wallet
|
|
176
|
+
**Routes (wallet):** `POST /api/v2/watch` · `GET /api/v2/watch/diff?id=` · `GET /api/v2/watch/status?id=`
|
|
149
177
|
|
|
150
|
-
`/api/t/watch*` is
|
|
178
|
+
Card-lane `/api/t/watch*` is live (POST without a key returns `401`). Optional HTTPS `webhookUrl` is supported by the API; this MCP tool still sends `{ urls, note? }`.
|
|
151
179
|
|
|
152
180
|
### Example agent prompts
|
|
153
181
|
|
|
@@ -158,6 +186,10 @@ Read these three pages and compare their pricing: https://a.example/pricing http
|
|
|
158
186
|
|
|
159
187
|
Extract the product name, price, and availability from https://example.com/products/notebook as JSON.
|
|
160
188
|
|
|
189
|
+
Crawl https://example.com (max 10 pages) and list the page titles.
|
|
190
|
+
|
|
191
|
+
Read the PDF at https://example.com/paper.pdf and summarize it.
|
|
192
|
+
|
|
161
193
|
Watch https://competitor.com/pricing and https://competitor.com/changelog, then check the watch for changes.
|
|
162
194
|
```
|
|
163
195
|
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
4
4
|
import { privateKeyToAccount } from "viem/accounts";
|
|
5
5
|
import { wrapFetchWithPayment } from "x402-fetch";
|
|
6
6
|
import { z } from "zod";
|
|
7
|
-
const VERSION = "0.1
|
|
7
|
+
const VERSION = "0.2.1";
|
|
8
8
|
const BASE_URL = (process.env.SKIM_API_URL ?? "https://skim402.com").replace(/\/+$/, "");
|
|
9
9
|
const API_KEY = process.env.SKIM_API_KEY ?? "";
|
|
10
10
|
const PRIVATE_KEY = process.env.SKIM_WALLET_PRIVATE_KEY ?? "";
|
|
@@ -151,6 +151,9 @@ function ok(text) {
|
|
|
151
151
|
function authMissing() {
|
|
152
152
|
return fail("Skim needs a payment method. Set SKIM_API_KEY (card plan, free tier at skim402.com/pricing) or SKIM_WALLET_PRIVATE_KEY (Base wallet with USDC). Card is easier — no crypto setup required.");
|
|
153
153
|
}
|
|
154
|
+
function cardLaneOnly(tool, path) {
|
|
155
|
+
return fail(`${tool} is card-lane only (${path}) — there is no x402 /v1 twin. Set SKIM_API_KEY (sk402_..., free tier at skim402.com/pricing). Wallet pay still works for read_url, read_urls, extract_url, and watch.`);
|
|
156
|
+
}
|
|
154
157
|
function buildUrl(path, query) {
|
|
155
158
|
const url = new URL(path, `${BASE_URL}/`);
|
|
156
159
|
if (query) {
|
|
@@ -208,6 +211,42 @@ function formatRead(data) {
|
|
|
208
211
|
const frontmatter = metaLines.length > 0 ? `---\n${metaLines.join("\n")}\n---\n\n` : "";
|
|
209
212
|
return frontmatter + (data.markdown ?? data.text ?? "");
|
|
210
213
|
}
|
|
214
|
+
function formatCrawl(data) {
|
|
215
|
+
const meta = {};
|
|
216
|
+
if (data.url)
|
|
217
|
+
meta.url = data.url;
|
|
218
|
+
if (data.origin)
|
|
219
|
+
meta.origin = data.origin;
|
|
220
|
+
if (data.pageCount != null)
|
|
221
|
+
meta.pageCount = data.pageCount;
|
|
222
|
+
if (data.discovered != null)
|
|
223
|
+
meta.discovered = data.discovered;
|
|
224
|
+
if (data.capped != null)
|
|
225
|
+
meta.capped = data.capped;
|
|
226
|
+
if (data.maxPages != null)
|
|
227
|
+
meta.maxPages = data.maxPages;
|
|
228
|
+
if (data.sources?.length)
|
|
229
|
+
meta.sources = data.sources.join(", ");
|
|
230
|
+
if (data.charged != null)
|
|
231
|
+
meta.charged = data.charged;
|
|
232
|
+
if (data.fetchedAt)
|
|
233
|
+
meta.fetchedAt = data.fetchedAt;
|
|
234
|
+
const blocks = (data.pages ?? []).map((page) => {
|
|
235
|
+
if (page.ok !== false && (page.markdown || page.title)) {
|
|
236
|
+
const heading = page.title ? `${page.title} — ${page.url}` : page.url;
|
|
237
|
+
return `## ${heading}\n\n${page.markdown ?? ""}`;
|
|
238
|
+
}
|
|
239
|
+
const err = page.error;
|
|
240
|
+
const detail = typeof err === "string"
|
|
241
|
+
? err
|
|
242
|
+
: err
|
|
243
|
+
? `${err.status ?? ""} ${err.message ?? ""}`.trim()
|
|
244
|
+
: "unknown error";
|
|
245
|
+
return `## ${page.url}\n\nERROR: ${detail}`;
|
|
246
|
+
});
|
|
247
|
+
return (formatRead({ metadata: meta, markdown: blocks.join("\n\n---\n\n") }) ||
|
|
248
|
+
JSON.stringify(data, null, 2));
|
|
249
|
+
}
|
|
211
250
|
const server = new McpServer({
|
|
212
251
|
name: "skim-mcp",
|
|
213
252
|
version: VERSION,
|
|
@@ -316,7 +355,87 @@ server.tool("extract_url", "Extract structured JSON (or tables) from a URL via S
|
|
|
316
355
|
return fail(requestFailedMessage(err));
|
|
317
356
|
}
|
|
318
357
|
});
|
|
319
|
-
server.tool("
|
|
358
|
+
server.tool("crawl_url", "Crawl a site (origin or start URL) and return clean Markdown for the important pages. Discovers sitemap.xml / robots.txt sitemaps plus same-origin links. Cap 25 pages. 1 credit per successful page; failed pages are not charged. Card lane only: POST /api/t/crawl. No x402 /v1 twin.", {
|
|
359
|
+
url: z
|
|
360
|
+
.string()
|
|
361
|
+
.min(1)
|
|
362
|
+
.describe("Site origin or start URL. Bare hosts like example.com are treated as https://example.com."),
|
|
363
|
+
maxPages: z
|
|
364
|
+
.number()
|
|
365
|
+
.int()
|
|
366
|
+
.min(1)
|
|
367
|
+
.max(25)
|
|
368
|
+
.optional()
|
|
369
|
+
.describe("Optional page cap, 1–25. Default 25."),
|
|
370
|
+
stripLinks: z
|
|
371
|
+
.boolean()
|
|
372
|
+
.optional()
|
|
373
|
+
.describe("If true, flatten markdown links to their anchor text on every page."),
|
|
374
|
+
stripImages: z
|
|
375
|
+
.boolean()
|
|
376
|
+
.optional()
|
|
377
|
+
.describe("If true, drop image markup from every page."),
|
|
378
|
+
}, async ({ url, maxPages, stripLinks, stripImages }) => {
|
|
379
|
+
if (!hasAuth)
|
|
380
|
+
return authMissing();
|
|
381
|
+
if (!cardLane)
|
|
382
|
+
return cardLaneOnly("crawl_url", "POST /api/t/crawl");
|
|
383
|
+
try {
|
|
384
|
+
const res = await skimFetch("POST", "/api/t/crawl", {
|
|
385
|
+
body: {
|
|
386
|
+
url,
|
|
387
|
+
...(maxPages !== undefined ? { maxPages } : {}),
|
|
388
|
+
...(stripLinks !== undefined ? { stripLinks } : {}),
|
|
389
|
+
...(stripImages !== undefined ? { stripImages } : {}),
|
|
390
|
+
},
|
|
391
|
+
});
|
|
392
|
+
const data = (await readJson(res));
|
|
393
|
+
return ok(formatCrawl(data));
|
|
394
|
+
}
|
|
395
|
+
catch (err) {
|
|
396
|
+
return fail(requestFailedMessage(err));
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
server.tool("read_pdf", "Fetch a public PDF URL and return clean Markdown plus an optional bookmark outline. Text comes only from the file — nothing is invented. Image-only scans return 422 (no OCR). Files larger than 8 MB return 413. 3 credits; failed conversions are not charged. Card lane only: POST /api/t/read-pdf. No x402 /v1 twin.", {
|
|
400
|
+
url: z
|
|
401
|
+
.string()
|
|
402
|
+
.url()
|
|
403
|
+
.describe("Absolute http(s) PDF URL to fetch and convert."),
|
|
404
|
+
outline: z
|
|
405
|
+
.boolean()
|
|
406
|
+
.optional()
|
|
407
|
+
.describe("If true (default), include the PDF bookmark outline when present. Set false to omit it."),
|
|
408
|
+
}, async ({ url, outline }) => {
|
|
409
|
+
if (!hasAuth)
|
|
410
|
+
return authMissing();
|
|
411
|
+
if (!cardLane)
|
|
412
|
+
return cardLaneOnly("read_pdf", "POST /api/t/read-pdf");
|
|
413
|
+
try {
|
|
414
|
+
const res = await skimFetch("POST", "/api/t/read-pdf", {
|
|
415
|
+
body: {
|
|
416
|
+
url,
|
|
417
|
+
...(outline !== undefined ? { outline } : {}),
|
|
418
|
+
},
|
|
419
|
+
});
|
|
420
|
+
const data = (await readJson(res));
|
|
421
|
+
return ok(formatRead({
|
|
422
|
+
markdown: data.markdown,
|
|
423
|
+
text: data.text,
|
|
424
|
+
metadata: {
|
|
425
|
+
...(data.url ? { url: data.url } : {}),
|
|
426
|
+
...(data.finalUrl ? { finalUrl: data.finalUrl } : {}),
|
|
427
|
+
...(data.pageCount != null ? { pageCount: data.pageCount } : {}),
|
|
428
|
+
...(data.charged != null ? { charged: data.charged } : {}),
|
|
429
|
+
...(data.fetchedAt ? { fetchedAt: data.fetchedAt } : {}),
|
|
430
|
+
...(data.outline != null ? { outline: data.outline } : {}),
|
|
431
|
+
},
|
|
432
|
+
}));
|
|
433
|
+
}
|
|
434
|
+
catch (err) {
|
|
435
|
+
return fail(requestFailedMessage(err));
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
server.tool("watch_urls", "Register a private Skim Watch on 1–20 URLs. Returns a watch_id (treat it as a secret) used with check_watch. First check baselines each page; later checks report content diffs. Card lane: POST /api/t/watch. Wallet lane: POST /api/v2/watch.", {
|
|
320
439
|
urls: z
|
|
321
440
|
.array(z.string().url())
|
|
322
441
|
.min(1)
|
|
@@ -338,11 +457,7 @@ server.tool("watch_urls", "Register a private Skim Watch on 1–20 URLs. Returns
|
|
|
338
457
|
return ok(JSON.stringify(data, null, 2));
|
|
339
458
|
}
|
|
340
459
|
catch (err) {
|
|
341
|
-
|
|
342
|
-
if (cardLane && /404/.test(msg)) {
|
|
343
|
-
return fail(`${msg}\n\nExpected card-lane path is POST /api/t/watch (see skim402-web Signals: POST /t/watch). That route is not live on skim402.com yet — align with skim402-web rather than inventing a different protocol. Wallet-lane POST /api/v2/watch is already live.`);
|
|
344
|
-
}
|
|
345
|
-
return fail(msg);
|
|
460
|
+
return fail(requestFailedMessage(err));
|
|
346
461
|
}
|
|
347
462
|
});
|
|
348
463
|
server.tool("check_watch", "Poll a Skim Watch for content changes (or fetch registration status). Pass the watch_id from watch_urls. Card lane: GET /api/t/watch/diff?id= (or /status). Wallet lane: GET /api/v2/watch/diff?id= (or /status, which is free).", {
|
|
@@ -367,11 +482,7 @@ server.tool("check_watch", "Poll a Skim Watch for content changes (or fetch regi
|
|
|
367
482
|
return ok(JSON.stringify(data, null, 2));
|
|
368
483
|
}
|
|
369
484
|
catch (err) {
|
|
370
|
-
|
|
371
|
-
if (cardLane && /404/.test(msg)) {
|
|
372
|
-
return fail(`${msg}\n\nExpected card-lane paths are GET /api/t/watch/diff?id= and GET /api/t/watch/status?id= (skim402-web). Those routes are not live on skim402.com yet. Wallet-lane GET /api/v2/watch/diff and /status are already live.`);
|
|
373
|
-
}
|
|
374
|
-
return fail(msg);
|
|
485
|
+
return fail(requestFailedMessage(err));
|
|
375
486
|
}
|
|
376
487
|
});
|
|
377
488
|
const transport = new StdioServerTransport();
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skim-mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"mcpName": "io.github.JessieJanie/skim402",
|
|
5
|
-
"description": "MCP server for Skim — clean web reader for AI agents. Card API key or x402 wallet. Batch, extract, watch.",
|
|
5
|
+
"description": "MCP server for Skim — clean web reader for AI agents. Card API key or x402 wallet. Batch, extract, crawl, PDF, watch.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
8
|
"skim-mcp": "dist/index.js"
|