skim-mcp 0.2.0 → 0.2.3
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 +52 -4
- package/dist/index.js +166 -12
- package/dist/src/index.d.ts +2 -0
- package/package.json +9 -10
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 `
|
|
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`, `check_watch`, and `poll_signal`. 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,59 @@ 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=`
|
|
175
|
+
|
|
176
|
+
**Routes (wallet):** `POST /api/v2/watch` · `GET /api/v2/watch/diff?id=` · `GET /api/v2/watch/status?id=`
|
|
177
|
+
|
|
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? }`.
|
|
179
|
+
|
|
180
|
+
### `poll_signal`
|
|
181
|
+
|
|
182
|
+
Poll a [Skim Signal](https://skim402.com/signals) and return the latest structured items (title, summary, source, timestamp, link, and entities). **2 credits** per successful poll; failed polls are refunded.
|
|
183
|
+
|
|
184
|
+
**Requires `SKIM_API_KEY`.** The card-lane routes are `GET /api/t/signal/{slug}/latest?limit=` and `GET /api/t/feeds/x402/latest?limit=` for the x402 ecosystem feed.
|
|
147
185
|
|
|
148
|
-
**
|
|
186
|
+
**Input:** `{ "slug": "ai-news", "limit": 20 }`
|
|
149
187
|
|
|
150
|
-
|
|
188
|
+
Optional documented filters: `forms` (SEC filings and campaign finance), `categories` (deals), `fields` (research), `states` (film incentives), and `committees` (campaign finance).
|
|
189
|
+
|
|
190
|
+
```
|
|
191
|
+
Poll the ai-news Signal for the latest 20 items.
|
|
192
|
+
```
|
|
151
193
|
|
|
152
194
|
### Example agent prompts
|
|
153
195
|
|
|
@@ -158,6 +200,12 @@ Read these three pages and compare their pricing: https://a.example/pricing http
|
|
|
158
200
|
|
|
159
201
|
Extract the product name, price, and availability from https://example.com/products/notebook as JSON.
|
|
160
202
|
|
|
203
|
+
Crawl https://example.com (max 10 pages) and list the page titles.
|
|
204
|
+
|
|
205
|
+
Read the PDF at https://example.com/paper.pdf and summarize it.
|
|
206
|
+
|
|
207
|
+
Poll the ai-news Signal for the latest 20 items.
|
|
208
|
+
|
|
161
209
|
Watch https://competitor.com/pricing and https://competitor.com/changelog, then check the watch for changes.
|
|
162
210
|
```
|
|
163
211
|
|
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.
|
|
7
|
+
const VERSION = "0.2.3";
|
|
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,17 @@ 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
|
+
}
|
|
157
|
+
const SIGNAL_SLUGS = [
|
|
158
|
+
"ai-news", "sec-filings", "crypto-news", "macro", "security", "regulations",
|
|
159
|
+
"courts", "recalls", "deals", "launches", "trending", "research", "energy",
|
|
160
|
+
"entertainment", "studio-jobs", "campaign-finance", "film-incentives", "x402",
|
|
161
|
+
];
|
|
162
|
+
function signalPollPath(slug) {
|
|
163
|
+
return slug === "x402" ? "/api/t/feeds/x402/latest" : `/api/t/signal/${slug}/latest`;
|
|
164
|
+
}
|
|
154
165
|
function buildUrl(path, query) {
|
|
155
166
|
const url = new URL(path, `${BASE_URL}/`);
|
|
156
167
|
if (query) {
|
|
@@ -208,6 +219,42 @@ function formatRead(data) {
|
|
|
208
219
|
const frontmatter = metaLines.length > 0 ? `---\n${metaLines.join("\n")}\n---\n\n` : "";
|
|
209
220
|
return frontmatter + (data.markdown ?? data.text ?? "");
|
|
210
221
|
}
|
|
222
|
+
function formatCrawl(data) {
|
|
223
|
+
const meta = {};
|
|
224
|
+
if (data.url)
|
|
225
|
+
meta.url = data.url;
|
|
226
|
+
if (data.origin)
|
|
227
|
+
meta.origin = data.origin;
|
|
228
|
+
if (data.pageCount != null)
|
|
229
|
+
meta.pageCount = data.pageCount;
|
|
230
|
+
if (data.discovered != null)
|
|
231
|
+
meta.discovered = data.discovered;
|
|
232
|
+
if (data.capped != null)
|
|
233
|
+
meta.capped = data.capped;
|
|
234
|
+
if (data.maxPages != null)
|
|
235
|
+
meta.maxPages = data.maxPages;
|
|
236
|
+
if (data.sources?.length)
|
|
237
|
+
meta.sources = data.sources.join(", ");
|
|
238
|
+
if (data.charged != null)
|
|
239
|
+
meta.charged = data.charged;
|
|
240
|
+
if (data.fetchedAt)
|
|
241
|
+
meta.fetchedAt = data.fetchedAt;
|
|
242
|
+
const blocks = (data.pages ?? []).map((page) => {
|
|
243
|
+
if (page.ok !== false && (page.markdown || page.title)) {
|
|
244
|
+
const heading = page.title ? `${page.title} — ${page.url}` : page.url;
|
|
245
|
+
return `## ${heading}\n\n${page.markdown ?? ""}`;
|
|
246
|
+
}
|
|
247
|
+
const err = page.error;
|
|
248
|
+
const detail = typeof err === "string"
|
|
249
|
+
? err
|
|
250
|
+
: err
|
|
251
|
+
? `${err.status ?? ""} ${err.message ?? ""}`.trim()
|
|
252
|
+
: "unknown error";
|
|
253
|
+
return `## ${page.url}\n\nERROR: ${detail}`;
|
|
254
|
+
});
|
|
255
|
+
return (formatRead({ metadata: meta, markdown: blocks.join("\n\n---\n\n") }) ||
|
|
256
|
+
JSON.stringify(data, null, 2));
|
|
257
|
+
}
|
|
211
258
|
const server = new McpServer({
|
|
212
259
|
name: "skim-mcp",
|
|
213
260
|
version: VERSION,
|
|
@@ -316,7 +363,87 @@ server.tool("extract_url", "Extract structured JSON (or tables) from a URL via S
|
|
|
316
363
|
return fail(requestFailedMessage(err));
|
|
317
364
|
}
|
|
318
365
|
});
|
|
319
|
-
server.tool("
|
|
366
|
+
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.", {
|
|
367
|
+
url: z
|
|
368
|
+
.string()
|
|
369
|
+
.min(1)
|
|
370
|
+
.describe("Site origin or start URL. Bare hosts like example.com are treated as https://example.com."),
|
|
371
|
+
maxPages: z
|
|
372
|
+
.number()
|
|
373
|
+
.int()
|
|
374
|
+
.min(1)
|
|
375
|
+
.max(25)
|
|
376
|
+
.optional()
|
|
377
|
+
.describe("Optional page cap, 1–25. Default 25."),
|
|
378
|
+
stripLinks: z
|
|
379
|
+
.boolean()
|
|
380
|
+
.optional()
|
|
381
|
+
.describe("If true, flatten markdown links to their anchor text on every page."),
|
|
382
|
+
stripImages: z
|
|
383
|
+
.boolean()
|
|
384
|
+
.optional()
|
|
385
|
+
.describe("If true, drop image markup from every page."),
|
|
386
|
+
}, async ({ url, maxPages, stripLinks, stripImages }) => {
|
|
387
|
+
if (!hasAuth)
|
|
388
|
+
return authMissing();
|
|
389
|
+
if (!cardLane)
|
|
390
|
+
return cardLaneOnly("crawl_url", "POST /api/t/crawl");
|
|
391
|
+
try {
|
|
392
|
+
const res = await skimFetch("POST", "/api/t/crawl", {
|
|
393
|
+
body: {
|
|
394
|
+
url,
|
|
395
|
+
...(maxPages !== undefined ? { maxPages } : {}),
|
|
396
|
+
...(stripLinks !== undefined ? { stripLinks } : {}),
|
|
397
|
+
...(stripImages !== undefined ? { stripImages } : {}),
|
|
398
|
+
},
|
|
399
|
+
});
|
|
400
|
+
const data = (await readJson(res));
|
|
401
|
+
return ok(formatCrawl(data));
|
|
402
|
+
}
|
|
403
|
+
catch (err) {
|
|
404
|
+
return fail(requestFailedMessage(err));
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
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.", {
|
|
408
|
+
url: z
|
|
409
|
+
.string()
|
|
410
|
+
.url()
|
|
411
|
+
.describe("Absolute http(s) PDF URL to fetch and convert."),
|
|
412
|
+
outline: z
|
|
413
|
+
.boolean()
|
|
414
|
+
.optional()
|
|
415
|
+
.describe("If true (default), include the PDF bookmark outline when present. Set false to omit it."),
|
|
416
|
+
}, async ({ url, outline }) => {
|
|
417
|
+
if (!hasAuth)
|
|
418
|
+
return authMissing();
|
|
419
|
+
if (!cardLane)
|
|
420
|
+
return cardLaneOnly("read_pdf", "POST /api/t/read-pdf");
|
|
421
|
+
try {
|
|
422
|
+
const res = await skimFetch("POST", "/api/t/read-pdf", {
|
|
423
|
+
body: {
|
|
424
|
+
url,
|
|
425
|
+
...(outline !== undefined ? { outline } : {}),
|
|
426
|
+
},
|
|
427
|
+
});
|
|
428
|
+
const data = (await readJson(res));
|
|
429
|
+
return ok(formatRead({
|
|
430
|
+
markdown: data.markdown,
|
|
431
|
+
text: data.text,
|
|
432
|
+
metadata: {
|
|
433
|
+
...(data.url ? { url: data.url } : {}),
|
|
434
|
+
...(data.finalUrl ? { finalUrl: data.finalUrl } : {}),
|
|
435
|
+
...(data.pageCount != null ? { pageCount: data.pageCount } : {}),
|
|
436
|
+
...(data.charged != null ? { charged: data.charged } : {}),
|
|
437
|
+
...(data.fetchedAt ? { fetchedAt: data.fetchedAt } : {}),
|
|
438
|
+
...(data.outline != null ? { outline: data.outline } : {}),
|
|
439
|
+
},
|
|
440
|
+
}));
|
|
441
|
+
}
|
|
442
|
+
catch (err) {
|
|
443
|
+
return fail(requestFailedMessage(err));
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
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
447
|
urls: z
|
|
321
448
|
.array(z.string().url())
|
|
322
449
|
.min(1)
|
|
@@ -338,11 +465,7 @@ server.tool("watch_urls", "Register a private Skim Watch on 1–20 URLs. Returns
|
|
|
338
465
|
return ok(JSON.stringify(data, null, 2));
|
|
339
466
|
}
|
|
340
467
|
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);
|
|
468
|
+
return fail(requestFailedMessage(err));
|
|
346
469
|
}
|
|
347
470
|
});
|
|
348
471
|
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 +490,42 @@ server.tool("check_watch", "Poll a Skim Watch for content changes (or fetch regi
|
|
|
367
490
|
return ok(JSON.stringify(data, null, 2));
|
|
368
491
|
}
|
|
369
492
|
catch (err) {
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
493
|
+
return fail(requestFailedMessage(err));
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
server.tool("poll_signal", "Poll a Skim Signal feed and return the latest structured items. 2 credits per successful poll; failed polls are refunded. Requires SKIM_API_KEY because the wallet lane uses a v2 402 handshake that x402-fetch does not support for these GET feeds.", {
|
|
497
|
+
slug: z.enum(SIGNAL_SLUGS).describe("Signal slug. Use x402 for the ecosystem feed."),
|
|
498
|
+
limit: z.number().int().min(1).max(100).optional().describe("Max items, newest first. Default 50, capped at 100."),
|
|
499
|
+
forms: z.string().optional().describe("Comma-separated form filter for sec-filings or campaign-finance."),
|
|
500
|
+
categories: z.string().optional().describe("Comma-separated category filter for deals."),
|
|
501
|
+
fields: z.string().optional().describe("Comma-separated arXiv field filter for research."),
|
|
502
|
+
states: z.string().optional().describe("Comma-separated two-letter state filter for film-incentives."),
|
|
503
|
+
committees: z.string().optional().describe("Comma-separated committee-name filter for campaign-finance."),
|
|
504
|
+
}, async ({ slug, limit, forms, categories, fields, states, committees }) => {
|
|
505
|
+
if (!hasAuth)
|
|
506
|
+
return authMissing();
|
|
507
|
+
if (!cardLane) {
|
|
508
|
+
return fail("poll_signal is card-lane only. Set SKIM_API_KEY (sk402_..., free tier at skim402.com/pricing).");
|
|
509
|
+
}
|
|
510
|
+
try {
|
|
511
|
+
const query = {};
|
|
512
|
+
if (limit !== undefined)
|
|
513
|
+
query.limit = String(limit);
|
|
514
|
+
if (forms)
|
|
515
|
+
query.forms = forms;
|
|
516
|
+
if (categories)
|
|
517
|
+
query.categories = categories;
|
|
518
|
+
if (fields)
|
|
519
|
+
query.fields = fields;
|
|
520
|
+
if (states)
|
|
521
|
+
query.states = states;
|
|
522
|
+
if (committees)
|
|
523
|
+
query.committees = committees;
|
|
524
|
+
const res = await skimFetch("GET", signalPollPath(slug), { query });
|
|
525
|
+
return ok(JSON.stringify(await readJson(res), null, 2));
|
|
526
|
+
}
|
|
527
|
+
catch (err) {
|
|
528
|
+
return fail(requestFailedMessage(err));
|
|
375
529
|
}
|
|
376
530
|
});
|
|
377
531
|
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.3",
|
|
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, signals.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
8
|
"skim-mcp": "dist/index.js"
|
|
@@ -13,13 +13,6 @@
|
|
|
13
13
|
"README.md",
|
|
14
14
|
"LICENSE"
|
|
15
15
|
],
|
|
16
|
-
"scripts": {
|
|
17
|
-
"build": "tsc -p tsconfig.build.json && node -e \"require('fs').chmodSync('dist/index.js', 0o755)\"",
|
|
18
|
-
"typecheck": "tsc --noEmit",
|
|
19
|
-
"dev": "tsx src/index.ts",
|
|
20
|
-
"prepublishOnly": "pnpm build",
|
|
21
|
-
"postinstall": "npm install -g tsx@4.21.0"
|
|
22
|
-
},
|
|
23
16
|
"dependencies": {
|
|
24
17
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
25
18
|
"viem": "^2.21.0",
|
|
@@ -56,5 +49,11 @@
|
|
|
56
49
|
"license": "MIT",
|
|
57
50
|
"engines": {
|
|
58
51
|
"node": ">=18"
|
|
52
|
+
},
|
|
53
|
+
"scripts": {
|
|
54
|
+
"build": "tsc -p tsconfig.build.json && node -e \"require('fs').chmodSync('dist/index.js', 0o755)\"",
|
|
55
|
+
"typecheck": "tsc --noEmit",
|
|
56
|
+
"dev": "tsx src/index.ts",
|
|
57
|
+
"postinstall": "npm install -g tsx@4.21.0"
|
|
59
58
|
}
|
|
60
|
-
}
|
|
59
|
+
}
|