rrcrawl 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/.env.example ADDED
@@ -0,0 +1,21 @@
1
+ # auto uses OneCLI when ONECLI_URL is present, otherwise provider API keys.
2
+ RRCRAWL_AUTH_MODE=auto
3
+
4
+ # Optional comma-separated provider allowlist.
5
+ # In env mode the default is every provider with a configured key.
6
+ # In onecli mode the default is all providers.
7
+ RRCRAWL_PROVIDERS=firecrawl,tavily,scrapedo
8
+
9
+ FIRECRAWL_API_KEY=
10
+ TAVILY_API_KEY=
11
+ SCRAPEDO_API_TOKEN=
12
+
13
+ # Optional operational tuning.
14
+ RRCRAWL_REQUEST_TIMEOUT_MS=60000
15
+ RRCRAWL_CRAWL_TIMEOUT_MS=180000
16
+ RRCRAWL_POLL_INTERVAL_MS=2000
17
+
18
+ # Optional API endpoint overrides.
19
+ FIRECRAWL_API_URL=https://api.firecrawl.dev
20
+ TAVILY_API_URL=https://api.tavily.com
21
+ SCRAPEDO_API_URL=https://api.scrape.do/
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 rrcrawl contributors
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,176 @@
1
+ # rrcrawl
2
+
3
+ A tiny stdio MCP server that gives agents two web-content tools:
4
+
5
+ - `scrape`: fetch one URL as Markdown, round-robin across Firecrawl, Tavily,
6
+ and Scrape.do.
7
+ - `crawl`: crawl multiple pages as Markdown, round-robin across Firecrawl and
8
+ Tavily. Scrape.do is intentionally excluded because it has no native
9
+ multi-page crawl API.
10
+
11
+ Each call advances its tool's round-robin cursor. If the selected provider
12
+ fails, rrcrawl tries every other provider in that tool's pool once.
13
+
14
+ ## Requirements
15
+
16
+ - Node.js 22 or newer
17
+ - At least one provider credential, or a OneCLI gateway configuration
18
+
19
+ ## Run with npx
20
+
21
+ Once the package is published, no local installation is needed:
22
+
23
+ ```bash
24
+ npx -y rrcrawl@latest
25
+ ```
26
+
27
+ An MCP client configuration can launch it directly:
28
+
29
+ ```json
30
+ {
31
+ "mcpServers": {
32
+ "rrcrawl": {
33
+ "command": "npx",
34
+ "args": ["-y", "rrcrawl@latest"],
35
+ "env": {
36
+ "FIRECRAWL_API_KEY": "fc-...",
37
+ "TAVILY_API_KEY": "tvly-...",
38
+ "SCRAPEDO_API_TOKEN": "..."
39
+ }
40
+ }
41
+ }
42
+ }
43
+ ```
44
+
45
+ With OneCLI:
46
+
47
+ ```bash
48
+ RRCRAWL_AUTH_MODE=onecli onecli run -- npx -y rrcrawl@latest
49
+ ```
50
+
51
+ ## Local development
52
+
53
+ ```bash
54
+ npm install
55
+ npm run build
56
+ ```
57
+
58
+ Copy `.env.example` to `.env` for local configuration. `.env` is loaded
59
+ automatically and is ignored by Git.
60
+
61
+ ## Authentication modes
62
+
63
+ `RRCRAWL_AUTH_MODE` accepts:
64
+
65
+ - `env`: read `FIRECRAWL_API_KEY`, `TAVILY_API_KEY`, and
66
+ `SCRAPEDO_API_TOKEN`.
67
+ - `onecli`: omit provider credentials from requests and rely on OneCLI's
68
+ transparent gateway to inject them.
69
+ - `auto` (default): use `onecli` when `ONECLI_URL` is present; otherwise use
70
+ `env`.
71
+
72
+ `RRCRAWL_PROVIDERS` optionally restricts the active providers:
73
+
74
+ ```dotenv
75
+ RRCRAWL_PROVIDERS=firecrawl,tavily,scrapedo
76
+ ```
77
+
78
+ In `env` mode, providers without credentials are disabled unless explicitly
79
+ listed, in which case startup fails with a useful configuration error.
80
+
81
+ ### OneCLI
82
+
83
+ Configure OneCLI secrets for the provider API hosts:
84
+
85
+ - `api.firecrawl.dev`: inject `Authorization: Bearer {secret}` for `/v2/*`.
86
+ - `api.tavily.com`: inject `Authorization: Bearer {secret}` for `/extract` and
87
+ `/crawl`.
88
+ - `api.scrape.do`: inject the `token` query parameter for `/*`.
89
+
90
+ Then run the built server through the gateway:
91
+
92
+ ```bash
93
+ RRCRAWL_AUTH_MODE=onecli onecli run -- node dist/index.js
94
+ ```
95
+
96
+ No placeholder provider keys are required: rrcrawl omits the credential fields
97
+ in OneCLI mode.
98
+
99
+ When `HTTPS_PROXY` or `HTTP_PROXY` is set (as OneCLI's gateway does), rrcrawl
100
+ installs a matching proxy dispatcher on startup so all provider calls route
101
+ through the gateway. Node's global `fetch` does not honor these variables on
102
+ its own, so this is required behind an egress-locked gateway. `NODE_EXTRA_CA_CERTS`
103
+ (also injected by the gateway) is honored automatically for the gateway's CA.
104
+
105
+ ## Local MCP client configuration
106
+
107
+ After `npm run build`, configure an MCP client with:
108
+
109
+ ```json
110
+ {
111
+ "mcpServers": {
112
+ "rrcrawl": {
113
+ "command": "node",
114
+ "args": ["/absolute/path/to/rrcrawl/dist/index.js"],
115
+ "env": {
116
+ "RRCRAWL_AUTH_MODE": "env",
117
+ "FIRECRAWL_API_KEY": "fc-...",
118
+ "TAVILY_API_KEY": "tvly-...",
119
+ "SCRAPEDO_API_TOKEN": "..."
120
+ }
121
+ }
122
+ }
123
+ }
124
+ ```
125
+
126
+ For `.env` configuration, set the MCP server's working directory to this
127
+ project or pass the variables explicitly.
128
+
129
+ ## Tool schemas
130
+
131
+ ### `scrape`
132
+
133
+ ```json
134
+ { "url": "https://example.com/article" }
135
+ ```
136
+
137
+ Returns the selected provider, canonical URL when available, optional title,
138
+ and Markdown.
139
+
140
+ ### `crawl`
141
+
142
+ ```json
143
+ {
144
+ "url": "https://example.com/docs",
145
+ "limit": 10,
146
+ "maxDepth": 1,
147
+ "includePaths": ["/docs/.*"],
148
+ "allowExternal": false,
149
+ "instructions": "Return API reference pages"
150
+ }
151
+ ```
152
+
153
+ `limit` is capped at 100 and `maxDepth` at 5 to bound cost and response size.
154
+
155
+ ## Development
156
+
157
+ ```bash
158
+ npm test
159
+ npm run check
160
+ npm run build
161
+ ```
162
+
163
+ Tests use injected HTTP fakes and never call paid provider APIs.
164
+
165
+ ## Publishing
166
+
167
+ The package name `rrcrawl` was unclaimed on npm when this project was created.
168
+ Availability is only guaranteed after the first successful publish.
169
+
170
+ ```bash
171
+ npm login
172
+ npm publish
173
+ ```
174
+
175
+ `prepublishOnly` runs tests and type-checking, while `prepack` always rebuilds
176
+ the distributable executable.
@@ -0,0 +1,24 @@
1
+ import type { ProviderName } from "./types.js";
2
+ export type AuthMode = "env" | "onecli";
3
+ export interface AppConfig {
4
+ authMode: AuthMode;
5
+ providers: ProviderName[];
6
+ requestTimeoutMs: number;
7
+ crawlTimeoutMs: number;
8
+ pollIntervalMs: number;
9
+ firecrawl: {
10
+ apiUrl: string;
11
+ apiKey?: string;
12
+ };
13
+ tavily: {
14
+ apiUrl: string;
15
+ apiKey?: string;
16
+ };
17
+ scrapedo: {
18
+ apiUrl: string;
19
+ apiToken?: string;
20
+ };
21
+ }
22
+ type Environment = Record<string, string | undefined>;
23
+ export declare function loadConfig(env?: Environment): AppConfig;
24
+ export {};
package/dist/config.js ADDED
@@ -0,0 +1,96 @@
1
+ const providerNames = new Set([
2
+ "firecrawl",
3
+ "tavily",
4
+ "scrapedo",
5
+ ]);
6
+ function positiveInteger(env, name, fallback) {
7
+ const value = env[name];
8
+ if (!value)
9
+ return fallback;
10
+ // Plain base-10 digits only: reject hex, exponent, leading +/- and whitespace
11
+ // that Number() would silently accept.
12
+ if (!/^[0-9]+$/.test(value.trim())) {
13
+ throw new Error(`${name} must be a positive integer`);
14
+ }
15
+ const parsed = Number.parseInt(value.trim(), 10);
16
+ if (!Number.isInteger(parsed) || parsed <= 0) {
17
+ throw new Error(`${name} must be a positive integer`);
18
+ }
19
+ return parsed;
20
+ }
21
+ function parseAuthMode(env) {
22
+ const configured = env.RRCRAWL_AUTH_MODE ?? "auto";
23
+ if (configured === "auto") {
24
+ return env.ONECLI_URL ? "onecli" : "env";
25
+ }
26
+ if (configured !== "env" && configured !== "onecli") {
27
+ throw new Error("RRCRAWL_AUTH_MODE must be auto, env, or onecli");
28
+ }
29
+ return configured;
30
+ }
31
+ function parseProviderList(value) {
32
+ const result = value
33
+ .split(",")
34
+ .map((provider) => provider.trim().toLowerCase())
35
+ .filter(Boolean);
36
+ for (const provider of result) {
37
+ if (!providerNames.has(provider)) {
38
+ throw new Error(`Unknown provider in RRCRAWL_PROVIDERS: ${provider}`);
39
+ }
40
+ }
41
+ return [...new Set(result)];
42
+ }
43
+ export function loadConfig(env = process.env) {
44
+ const authMode = parseAuthMode(env);
45
+ const firecrawlKey = env.FIRECRAWL_API_KEY;
46
+ const tavilyKey = env.TAVILY_API_KEY;
47
+ const scrapedoToken = env.SCRAPEDO_API_TOKEN ?? env.SCRAPE_DO_API_TOKEN ?? env.SCRAPE_DO_TOKEN;
48
+ const configuredProviders = env.RRCRAWL_PROVIDERS
49
+ ? parseProviderList(env.RRCRAWL_PROVIDERS)
50
+ : undefined;
51
+ let providers;
52
+ if (configuredProviders) {
53
+ providers = configuredProviders;
54
+ }
55
+ else if (authMode === "onecli") {
56
+ providers = ["firecrawl", "tavily", "scrapedo"];
57
+ }
58
+ else {
59
+ providers = [
60
+ ...(firecrawlKey ? ["firecrawl"] : []),
61
+ ...(tavilyKey ? ["tavily"] : []),
62
+ ...(scrapedoToken ? ["scrapedo"] : []),
63
+ ];
64
+ }
65
+ if (providers.length === 0) {
66
+ throw new Error("No providers configured. Set provider API keys or use RRCRAWL_AUTH_MODE=onecli.");
67
+ }
68
+ if (authMode === "env") {
69
+ const missing = providers.filter((provider) => (provider === "firecrawl" && !firecrawlKey) ||
70
+ (provider === "tavily" && !tavilyKey) ||
71
+ (provider === "scrapedo" && !scrapedoToken));
72
+ if (missing.length > 0) {
73
+ throw new Error(`Missing credentials for configured providers: ${missing.join(", ")}`);
74
+ }
75
+ }
76
+ return {
77
+ authMode,
78
+ providers,
79
+ requestTimeoutMs: positiveInteger(env, "RRCRAWL_REQUEST_TIMEOUT_MS", 60_000),
80
+ crawlTimeoutMs: positiveInteger(env, "RRCRAWL_CRAWL_TIMEOUT_MS", 180_000),
81
+ pollIntervalMs: positiveInteger(env, "RRCRAWL_POLL_INTERVAL_MS", 2_000),
82
+ firecrawl: {
83
+ apiUrl: (env.FIRECRAWL_API_URL ?? "https://api.firecrawl.dev").replace(/\/$/, ""),
84
+ apiKey: authMode === "env" ? firecrawlKey : undefined,
85
+ },
86
+ tavily: {
87
+ apiUrl: (env.TAVILY_API_URL ?? "https://api.tavily.com").replace(/\/$/, ""),
88
+ apiKey: authMode === "env" ? tavilyKey : undefined,
89
+ },
90
+ scrapedo: {
91
+ apiUrl: env.SCRAPEDO_API_URL ?? "https://api.scrape.do/",
92
+ apiToken: authMode === "env" ? scrapedoToken : undefined,
93
+ },
94
+ };
95
+ }
96
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAiBA,MAAM,aAAa,GAAG,IAAI,GAAG,CAAe;IAC1C,WAAW;IACX,QAAQ;IACR,UAAU;CACX,CAAC,CAAC;AAEH,SAAS,eAAe,CACtB,GAAgB,EAChB,IAAY,EACZ,QAAgB;IAEhB,MAAM,KAAK,GAAG,GAAG,CAAC,IAAI,CAAC,CAAC;IACxB,IAAI,CAAC,KAAK;QAAE,OAAO,QAAQ,CAAC;IAC5B,8EAA8E;IAC9E,uCAAuC;IACvC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC;QACnC,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,6BAA6B,CAAC,CAAC;IACxD,CAAC;IACD,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;IACjD,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,MAAM,IAAI,CAAC,EAAE,CAAC;QAC7C,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,6BAA6B,CAAC,CAAC;IACxD,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAS,aAAa,CAAC,GAAgB;IACrC,MAAM,UAAU,GAAG,GAAG,CAAC,iBAAiB,IAAI,MAAM,CAAC;IACnD,IAAI,UAAU,KAAK,MAAM,EAAE,CAAC;QAC1B,OAAO,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC;IAC3C,CAAC;IACD,IAAI,UAAU,KAAK,KAAK,IAAI,UAAU,KAAK,QAAQ,EAAE,CAAC;QACpD,MAAM,IAAI,KAAK,CAAC,gDAAgD,CAAC,CAAC;IACpE,CAAC;IACD,OAAO,UAAU,CAAC;AACpB,CAAC;AAED,SAAS,iBAAiB,CAAC,KAAa;IACtC,MAAM,MAAM,GAAG,KAAK;SACjB,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;SAChD,MAAM,CAAC,OAAO,CAAC,CAAC;IAEnB,KAAK,MAAM,QAAQ,IAAI,MAAM,EAAE,CAAC;QAC9B,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,QAAwB,CAAC,EAAE,CAAC;YACjD,MAAM,IAAI,KAAK,CAAC,0CAA0C,QAAQ,EAAE,CAAC,CAAC;QACxE,CAAC;IACH,CAAC;IACD,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,CAAmB,CAAC;AAChD,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,MAAmB,OAAO,CAAC,GAAG;IACvD,MAAM,QAAQ,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC;IACpC,MAAM,YAAY,GAAG,GAAG,CAAC,iBAAiB,CAAC;IAC3C,MAAM,SAAS,GAAG,GAAG,CAAC,cAAc,CAAC;IACrC,MAAM,aAAa,GACjB,GAAG,CAAC,kBAAkB,IAAI,GAAG,CAAC,mBAAmB,IAAI,GAAG,CAAC,eAAe,CAAC;IAE3E,MAAM,mBAAmB,GAAG,GAAG,CAAC,iBAAiB;QAC/C,CAAC,CAAC,iBAAiB,CAAC,GAAG,CAAC,iBAAiB,CAAC;QAC1C,CAAC,CAAC,SAAS,CAAC;IAEd,IAAI,SAAyB,CAAC;IAC9B,IAAI,mBAAmB,EAAE,CAAC;QACxB,SAAS,GAAG,mBAAmB,CAAC;IAClC,CAAC;SAAM,IAAI,QAAQ,KAAK,QAAQ,EAAE,CAAC;QACjC,SAAS,GAAG,CAAC,WAAW,EAAE,QAAQ,EAAE,UAAU,CAAC,CAAC;IAClD,CAAC;SAAM,CAAC;QACN,SAAS,GAAG;YACV,GAAG,CAAC,YAAY,CAAC,CAAC,CAAE,CAAC,WAAW,CAAW,CAAC,CAAC,CAAC,EAAE,CAAC;YACjD,GAAG,CAAC,SAAS,CAAC,CAAC,CAAE,CAAC,QAAQ,CAAW,CAAC,CAAC,CAAC,EAAE,CAAC;YAC3C,GAAG,CAAC,aAAa,CAAC,CAAC,CAAE,CAAC,UAAU,CAAW,CAAC,CAAC,CAAC,EAAE,CAAC;SAClD,CAAC;IACJ,CAAC;IAED,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC3B,MAAM,IAAI,KAAK,CACb,iFAAiF,CAClF,CAAC;IACJ,CAAC;IAED,IAAI,QAAQ,KAAK,KAAK,EAAE,CAAC;QACvB,MAAM,OAAO,GAAG,SAAS,CAAC,MAAM,CAC9B,CAAC,QAAQ,EAAE,EAAE,CACX,CAAC,QAAQ,KAAK,WAAW,IAAI,CAAC,YAAY,CAAC;YAC3C,CAAC,QAAQ,KAAK,QAAQ,IAAI,CAAC,SAAS,CAAC;YACrC,CAAC,QAAQ,KAAK,UAAU,IAAI,CAAC,aAAa,CAAC,CAC9C,CAAC;QACF,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACvB,MAAM,IAAI,KAAK,CACb,iDAAiD,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CACtE,CAAC;QACJ,CAAC;IACH,CAAC;IAED,OAAO;QACL,QAAQ;QACR,SAAS;QACT,gBAAgB,EAAE,eAAe,CAC/B,GAAG,EACH,4BAA4B,EAC5B,MAAM,CACP;QACD,cAAc,EAAE,eAAe,CAC7B,GAAG,EACH,0BAA0B,EAC1B,OAAO,CACR;QACD,cAAc,EAAE,eAAe,CAC7B,GAAG,EACH,0BAA0B,EAC1B,KAAK,CACN;QACD,SAAS,EAAE;YACT,MAAM,EAAE,CAAC,GAAG,CAAC,iBAAiB,IAAI,2BAA2B,CAAC,CAAC,OAAO,CACpE,KAAK,EACL,EAAE,CACH;YACD,MAAM,EAAE,QAAQ,KAAK,KAAK,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,SAAS;SACtD;QACD,MAAM,EAAE;YACN,MAAM,EAAE,CAAC,GAAG,CAAC,cAAc,IAAI,wBAAwB,CAAC,CAAC,OAAO,CAC9D,KAAK,EACL,EAAE,CACH;YACD,MAAM,EAAE,QAAQ,KAAK,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS;SACnD;QACD,QAAQ,EAAE;YACR,MAAM,EAAE,GAAG,CAAC,gBAAgB,IAAI,wBAAwB;YACxD,QAAQ,EAAE,QAAQ,KAAK,KAAK,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,SAAS;SACzD;KACF,CAAC;AACJ,CAAC"}
package/dist/http.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ export type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
2
+ export declare class HttpError extends Error {
3
+ readonly status: number;
4
+ readonly responseBody: string;
5
+ constructor(status: number, responseBody: string, url: string);
6
+ }
7
+ export declare class QuotaExceededError extends Error {
8
+ readonly provider: string;
9
+ readonly status: number;
10
+ constructor(provider: string, status: number);
11
+ }
12
+ export declare function rethrowQuota(provider: string, error: unknown, quotaStatuses: readonly number[]): never;
13
+ export declare function isAbsoluteHttpUrl(value: string | undefined): value is string;
14
+ export declare function requestJson<T>(fetchFn: FetchLike, url: string, init: RequestInit, timeoutMs: number): Promise<T>;
15
+ export declare function requestText(fetchFn: FetchLike, url: string, init: RequestInit, timeoutMs: number): Promise<string>;
package/dist/http.js ADDED
@@ -0,0 +1,78 @@
1
+ export class HttpError extends Error {
2
+ status;
3
+ responseBody;
4
+ constructor(status, responseBody, url) {
5
+ super(`HTTP ${status} from ${url}: ${responseBody.slice(0, 500)}`);
6
+ this.status = status;
7
+ this.responseBody = responseBody;
8
+ this.name = "HttpError";
9
+ }
10
+ }
11
+ // Signals that a provider has exhausted its quota/credits (as opposed to a
12
+ // transient rate limit). The router disables the provider for the rest of the
13
+ // process when it sees this.
14
+ export class QuotaExceededError extends Error {
15
+ provider;
16
+ status;
17
+ constructor(provider, status) {
18
+ super(`${provider} quota exhausted (HTTP ${status})`);
19
+ this.provider = provider;
20
+ this.status = status;
21
+ this.name = "QuotaExceededError";
22
+ }
23
+ }
24
+ // Rethrows an error, converting an HttpError with a hard quota/credit status
25
+ // into a QuotaExceededError; every other error is rethrown unchanged.
26
+ export function rethrowQuota(provider, error, quotaStatuses) {
27
+ if (error instanceof HttpError && quotaStatuses.includes(error.status)) {
28
+ throw new QuotaExceededError(provider, error.status);
29
+ }
30
+ throw error;
31
+ }
32
+ // True only for an absolute http(s) URL — the shape pageSchema.url() accepts.
33
+ // Guards against relative/garbage provider URLs that would otherwise fail
34
+ // output validation outside the tool handler as an unrecoverable McpError.
35
+ export function isAbsoluteHttpUrl(value) {
36
+ if (!value)
37
+ return false;
38
+ try {
39
+ const { protocol } = new URL(value);
40
+ return protocol === "http:" || protocol === "https:";
41
+ }
42
+ catch {
43
+ return false;
44
+ }
45
+ }
46
+ // Reads the full body under the same timeout/abort as the request so a stalled
47
+ // body can't hang forever after the headers arrive.
48
+ async function requestBody(fetchFn, url, init, timeoutMs) {
49
+ const controller = new AbortController();
50
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
51
+ try {
52
+ const response = await fetchFn(url, {
53
+ ...init,
54
+ signal: controller.signal,
55
+ });
56
+ const body = await response.text();
57
+ if (!response.ok) {
58
+ throw new HttpError(response.status, body, url);
59
+ }
60
+ return body;
61
+ }
62
+ finally {
63
+ clearTimeout(timer);
64
+ }
65
+ }
66
+ export async function requestJson(fetchFn, url, init, timeoutMs) {
67
+ const body = await requestBody(fetchFn, url, init, timeoutMs);
68
+ try {
69
+ return JSON.parse(body);
70
+ }
71
+ catch {
72
+ throw new Error(`Invalid JSON from ${url}: ${body.slice(0, 500)}`);
73
+ }
74
+ }
75
+ export async function requestText(fetchFn, url, init, timeoutMs) {
76
+ return requestBody(fetchFn, url, init, timeoutMs);
77
+ }
78
+ //# sourceMappingURL=http.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.js","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import "dotenv/config";
package/dist/index.js ADDED
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env node
2
+ import "dotenv/config";
3
+ import { createRequire } from "node:module";
4
+ import { loadConfig } from "./config.js";
5
+ import { configureProxy } from "./proxy.js";
6
+ import { runServer } from "./server.js";
7
+ const require = createRequire(import.meta.url);
8
+ const { version } = require("../package.json");
9
+ async function main() {
10
+ const arguments_ = new Set(process.argv.slice(2));
11
+ if (arguments_.has("--version") || arguments_.has("-v")) {
12
+ console.log(version);
13
+ return;
14
+ }
15
+ if (arguments_.has("--help") || arguments_.has("-h")) {
16
+ console.log(`rrcrawl ${version}
17
+
18
+ Tiny stdio MCP server for round-robin web scraping and crawling.
19
+
20
+ Usage:
21
+ rrcrawl
22
+ rrcrawl --help
23
+ rrcrawl --version
24
+
25
+ Configuration is read from environment variables and .env.
26
+ See https://www.npmjs.com/package/rrcrawl for details.`);
27
+ return;
28
+ }
29
+ configureProxy();
30
+ await runServer(loadConfig());
31
+ }
32
+ main().catch((error) => {
33
+ console.error(error instanceof Error ? error.message : String(error));
34
+ process.exitCode = 1;
35
+ });
36
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,eAAe,CAAC;AACvB,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC,MAAM,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC/C,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,iBAAiB,CAAwB,CAAC;AAEtE,KAAK,UAAU,IAAI;IACjB,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAClD,IAAI,UAAU,CAAC,GAAG,CAAC,WAAW,CAAC,IAAI,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;QACxD,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACrB,OAAO;IACT,CAAC;IACD,IAAI,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;QACrD,OAAO,CAAC,GAAG,CAAC,WAAW,OAAO;;;;;;;;;;uDAUqB,CAAC,CAAC;QACrD,OAAO;IACT,CAAC;IACD,cAAc,EAAE,CAAC;IACjB,MAAM,SAAS,CAAC,UAAU,EAAE,CAAC,CAAC;AAChC,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;IAC9B,OAAO,CAAC,KAAK,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IACtE,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;AACvB,CAAC,CAAC,CAAC"}
@@ -0,0 +1,25 @@
1
+ import { type FetchLike } from "../http.js";
2
+ import type { CrawlProvider, CrawlRequest, CrawlResult, ScrapeProvider, ScrapeRequest, ScrapeResult } from "../types.js";
3
+ export interface FirecrawlOptions {
4
+ apiUrl: string;
5
+ apiKey?: string;
6
+ requestTimeoutMs: number;
7
+ crawlTimeoutMs: number;
8
+ pollIntervalMs: number;
9
+ fetchFn?: FetchLike;
10
+ sleep?: (milliseconds: number) => Promise<void>;
11
+ }
12
+ export declare class FirecrawlProvider implements ScrapeProvider, CrawlProvider {
13
+ private readonly options;
14
+ readonly name: "firecrawl";
15
+ private readonly fetchFn;
16
+ private readonly sleep;
17
+ constructor(options: FirecrawlOptions);
18
+ private headers;
19
+ scrape(request: ScrapeRequest): Promise<ScrapeResult>;
20
+ crawl(request: CrawlRequest): Promise<CrawlResult>;
21
+ private collectDocuments;
22
+ private json;
23
+ private documentUrl;
24
+ private page;
25
+ }
@@ -0,0 +1,135 @@
1
+ import { isAbsoluteHttpUrl, requestJson, rethrowQuota, } from "../http.js";
2
+ export class FirecrawlProvider {
3
+ options;
4
+ name = "firecrawl";
5
+ fetchFn;
6
+ sleep;
7
+ constructor(options) {
8
+ this.options = options;
9
+ this.fetchFn = options.fetchFn ?? fetch;
10
+ this.sleep =
11
+ options.sleep ??
12
+ ((milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)));
13
+ }
14
+ headers() {
15
+ return {
16
+ "content-type": "application/json",
17
+ ...(this.options.apiKey
18
+ ? { authorization: `Bearer ${this.options.apiKey}` }
19
+ : {}),
20
+ };
21
+ }
22
+ async scrape(request) {
23
+ const response = await this.json(`${this.options.apiUrl}/v2/scrape`, {
24
+ method: "POST",
25
+ headers: this.headers(),
26
+ body: JSON.stringify({
27
+ url: request.url,
28
+ formats: ["markdown"],
29
+ onlyMainContent: true,
30
+ }),
31
+ });
32
+ if (response.success === false) {
33
+ throw new Error(`Firecrawl scrape failed: ${response.error ?? "unknown error"}`);
34
+ }
35
+ if (!response.data?.markdown) {
36
+ throw new Error("Firecrawl returned no markdown content");
37
+ }
38
+ const url = this.documentUrl(response.data);
39
+ const page = this.page(response.data, isAbsoluteHttpUrl(url) ? url : request.url);
40
+ return { provider: this.name, ...page };
41
+ }
42
+ async crawl(request) {
43
+ const started = await this.json(`${this.options.apiUrl}/v2/crawl`, {
44
+ method: "POST",
45
+ headers: this.headers(),
46
+ body: JSON.stringify({
47
+ url: request.url,
48
+ limit: request.limit,
49
+ maxDiscoveryDepth: request.maxDepth,
50
+ includePaths: request.includePaths.length > 0 ? request.includePaths : undefined,
51
+ allowExternalLinks: request.allowExternal,
52
+ prompt: request.instructions,
53
+ scrapeOptions: {
54
+ formats: ["markdown"],
55
+ onlyMainContent: true,
56
+ },
57
+ }),
58
+ });
59
+ if (started.success === false) {
60
+ throw new Error(`Firecrawl crawl failed to start: ${started.error ?? "unknown error"}`);
61
+ }
62
+ if (!started.id) {
63
+ throw new Error("Firecrawl did not return a crawl job id");
64
+ }
65
+ const deadline = Date.now() + this.options.crawlTimeoutMs;
66
+ while (Date.now() < deadline) {
67
+ const status = await this.json(`${this.options.apiUrl}/v2/crawl/${encodeURIComponent(started.id)}`, { method: "GET", headers: this.headers() });
68
+ if (status.status === "completed") {
69
+ const documents = await this.collectDocuments(status, request.limit);
70
+ // Each crawl page must carry its own absolute URL. If Firecrawl omits
71
+ // it we drop the page rather than mislabel every one with the crawl root.
72
+ const pages = documents
73
+ .map((document) => {
74
+ const url = this.documentUrl(document);
75
+ return document.markdown && isAbsoluteHttpUrl(url)
76
+ ? this.page(document, url)
77
+ : undefined;
78
+ })
79
+ .filter((page) => page !== undefined);
80
+ if (pages.length === 0) {
81
+ throw new Error("Firecrawl crawl completed without page content");
82
+ }
83
+ return { provider: this.name, pages };
84
+ }
85
+ if (status.status === "failed" || status.status === "cancelled") {
86
+ throw new Error(`Firecrawl crawl ${status.status}: ${status.error ?? "unknown error"}`);
87
+ }
88
+ await this.sleep(this.options.pollIntervalMs);
89
+ }
90
+ throw new Error(`Firecrawl crawl timed out after ${this.options.crawlTimeoutMs}ms`);
91
+ }
92
+ // Firecrawl paginates crawl results: `next` holds the URL of the following
93
+ // batch (returned when the response exceeds ~10MB). Follow it until null so
94
+ // large crawls don't silently return only the first batch. Bounded by the
95
+ // requested page limit — there can't be more result pages than documents.
96
+ async collectDocuments(first, limit) {
97
+ const documents = [...(first.data ?? [])];
98
+ let next = first.next;
99
+ for (let i = 0; next && documents.length < limit && i < limit; i += 1) {
100
+ const batch = await this.json(next, {
101
+ method: "GET",
102
+ headers: this.headers(),
103
+ });
104
+ documents.push(...(batch.data ?? []));
105
+ next = batch.next;
106
+ }
107
+ return documents;
108
+ }
109
+ // 402 Payment Required is Firecrawl's insufficient-credits signal (permanent).
110
+ // 429 rate limits are transient and deliberately left to normal failover.
111
+ async json(url, init) {
112
+ try {
113
+ return await requestJson(this.fetchFn, url, init, this.options.requestTimeoutMs);
114
+ }
115
+ catch (error) {
116
+ rethrowQuota(this.name, error, [402]);
117
+ }
118
+ }
119
+ documentUrl(document) {
120
+ return (document.metadata?.sourceURL ??
121
+ document.metadata?.sourceUrl ??
122
+ document.metadata?.url ??
123
+ undefined);
124
+ }
125
+ page(document, url) {
126
+ return {
127
+ url,
128
+ ...(document.metadata?.title
129
+ ? { title: document.metadata.title }
130
+ : {}),
131
+ markdown: document.markdown ?? "",
132
+ };
133
+ }
134
+ }
135
+ //# sourceMappingURL=firecrawl.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"firecrawl.js","sourceRoot":"","sources":["../../src/providers/firecrawl.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ import { type FetchLike } from "../http.js";
2
+ import type { ScrapeProvider, ScrapeRequest, ScrapeResult } from "../types.js";
3
+ export interface ScrapeDoOptions {
4
+ apiUrl: string;
5
+ apiToken?: string;
6
+ requestTimeoutMs: number;
7
+ fetchFn?: FetchLike;
8
+ }
9
+ export declare class ScrapeDoProvider implements ScrapeProvider {
10
+ private readonly options;
11
+ readonly name: "scrapedo";
12
+ private readonly fetchFn;
13
+ constructor(options: ScrapeDoOptions);
14
+ scrape(request: ScrapeRequest): Promise<ScrapeResult>;
15
+ }
@@ -0,0 +1,51 @@
1
+ import { HttpError, QuotaExceededError, requestText, } from "../http.js";
2
+ export class ScrapeDoProvider {
3
+ options;
4
+ name = "scrapedo";
5
+ fetchFn;
6
+ constructor(options) {
7
+ this.options = options;
8
+ this.fetchFn = options.fetchFn ?? fetch;
9
+ }
10
+ async scrape(request) {
11
+ const endpoint = new URL(this.options.apiUrl);
12
+ endpoint.searchParams.set("url", request.url);
13
+ endpoint.searchParams.set("output", "markdown");
14
+ if (this.options.apiToken) {
15
+ endpoint.searchParams.set("token", this.options.apiToken);
16
+ }
17
+ // Scrape.do returns the *target's* HTTP status. Distinguish its own
18
+ // control-plane errors from the scraped page's status:
19
+ // 402 -> out of credits (permanent quota)
20
+ // 400/401/429 -> Scrape.do rejected the request (transient failover)
21
+ // everything else (403/404/410/5xx) is the target page's own status;
22
+ // Scrape.do still returns its rendered content, so use it.
23
+ let markdown;
24
+ try {
25
+ markdown = await requestText(this.fetchFn, endpoint.toString(), { method: "GET", headers: { accept: "text/markdown, text/plain, */*" } }, this.options.requestTimeoutMs);
26
+ }
27
+ catch (error) {
28
+ if (error instanceof HttpError) {
29
+ if (error.status === 402) {
30
+ throw new QuotaExceededError(this.name, 402);
31
+ }
32
+ if (error.status === 400 || error.status === 401 || error.status === 429) {
33
+ throw error;
34
+ }
35
+ markdown = error.responseBody;
36
+ }
37
+ else {
38
+ throw error;
39
+ }
40
+ }
41
+ if (!markdown.trim()) {
42
+ throw new Error("Scrape.do returned no markdown content");
43
+ }
44
+ return {
45
+ provider: this.name,
46
+ url: request.url,
47
+ markdown,
48
+ };
49
+ }
50
+ }
51
+ //# sourceMappingURL=scrapedo.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scrapedo.js","sourceRoot":"","sources":["../../src/providers/scrapedo.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,SAAS,EACT,kBAAkB,EAClB,WAAW,GAEZ,MAAM,YAAY,CAAC;AAcpB,MAAM,OAAO,gBAAgB;IAIE;IAHpB,IAAI,GAAG,UAAmB,CAAC;IACnB,OAAO,CAAY;IAEpC,YAA6B,OAAwB;QAAxB,YAAO,GAAP,OAAO,CAAiB;QACnD,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,KAAK,CAAC;IAC1C,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,OAAsB;QACjC,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QAC9C,QAAQ,CAAC,YAAY,CAAC,GAAG,CAAC,KAAK,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC;QAC9C,QAAQ,CAAC,YAAY,CAAC,GAAG,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;QAChD,IAAI,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC;YAC1B,QAAQ,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,EAAE,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QAC5D,CAAC;QAED,oEAAoE;QACpE,uDAAuD;QACvD,4CAA4C;QAC5C,uEAAuE;QACvE,uEAAuE;QACvE,6DAA6D;QAC7D,IAAI,QAAgB,CAAC;QACrB,IAAI,CAAC;YACH,QAAQ,GAAG,MAAM,WAAW,CAC1B,IAAI,CAAC,OAAO,EACZ,QAAQ,CAAC,QAAQ,EAAE,EACnB,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,gCAAgC,EAAE,EAAE,EACxE,IAAI,CAAC,OAAO,CAAC,gBAAgB,CAC9B,CAAC;QACJ,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,KAAK,YAAY,SAAS,EAAE,CAAC;gBAC/B,IAAI,KAAK,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;oBACzB,MAAM,IAAI,kBAAkB,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;gBAC/C,CAAC;gBACD,IAAI,KAAK,CAAC,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;oBACzE,MAAM,KAAK,CAAC;gBACd,CAAC;gBACD,QAAQ,GAAG,KAAK,CAAC,YAAY,CAAC;YAChC,CAAC;iBAAM,CAAC;gBACN,MAAM,KAAK,CAAC;YACd,CAAC;QACH,CAAC;QACD,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC;YACrB,MAAM,IAAI,KAAK,CAAC,wCAAwC,CAAC,CAAC;QAC5D,CAAC;QACD,OAAO;YACL,QAAQ,EAAE,IAAI,CAAC,IAAI;YACnB,GAAG,EAAE,OAAO,CAAC,GAAG;YAChB,QAAQ;SACT,CAAC;IACJ,CAAC;CACF"}
@@ -0,0 +1,18 @@
1
+ import { type FetchLike } from "../http.js";
2
+ import type { CrawlProvider, CrawlRequest, CrawlResult, ScrapeProvider, ScrapeRequest, ScrapeResult } from "../types.js";
3
+ export interface TavilyOptions {
4
+ apiUrl: string;
5
+ apiKey?: string;
6
+ requestTimeoutMs: number;
7
+ fetchFn?: FetchLike;
8
+ }
9
+ export declare class TavilyProvider implements ScrapeProvider, CrawlProvider {
10
+ private readonly options;
11
+ readonly name: "tavily";
12
+ private readonly fetchFn;
13
+ constructor(options: TavilyOptions);
14
+ private headers;
15
+ scrape(request: ScrapeRequest): Promise<ScrapeResult>;
16
+ crawl(request: CrawlRequest): Promise<CrawlResult>;
17
+ private post;
18
+ }
@@ -0,0 +1,74 @@
1
+ import { isAbsoluteHttpUrl, requestJson, rethrowQuota, } from "../http.js";
2
+ export class TavilyProvider {
3
+ options;
4
+ name = "tavily";
5
+ fetchFn;
6
+ constructor(options) {
7
+ this.options = options;
8
+ this.fetchFn = options.fetchFn ?? fetch;
9
+ }
10
+ headers() {
11
+ return {
12
+ "content-type": "application/json",
13
+ accept: "application/json",
14
+ ...(this.options.apiKey
15
+ ? { authorization: `Bearer ${this.options.apiKey}` }
16
+ : {}),
17
+ };
18
+ }
19
+ async scrape(request) {
20
+ const response = await this.post("/extract", {
21
+ urls: [request.url],
22
+ format: "markdown",
23
+ extract_depth: "basic",
24
+ });
25
+ const result = response.results?.[0];
26
+ if (!result?.raw_content) {
27
+ throw new Error("Tavily returned no markdown content");
28
+ }
29
+ return {
30
+ provider: this.name,
31
+ url: isAbsoluteHttpUrl(result.url) ? result.url : request.url,
32
+ markdown: result.raw_content,
33
+ };
34
+ }
35
+ async crawl(request) {
36
+ const response = await this.post("/crawl", {
37
+ url: request.url,
38
+ max_depth: request.maxDepth,
39
+ limit: request.limit,
40
+ select_paths: request.includePaths.length > 0 ? request.includePaths : undefined,
41
+ allow_external: request.allowExternal,
42
+ instructions: request.instructions,
43
+ format: "markdown",
44
+ extract_depth: "basic",
45
+ });
46
+ // Each crawl page must carry its own absolute URL; drop malformed ones
47
+ // rather than emit a relative URL (crashes output validation) or duplicate
48
+ // the crawl root across pages.
49
+ const pages = (response.results ?? [])
50
+ .map((result) => result.raw_content && isAbsoluteHttpUrl(result.url)
51
+ ? { url: result.url, markdown: result.raw_content }
52
+ : undefined)
53
+ .filter((page) => page !== undefined);
54
+ if (pages.length === 0) {
55
+ throw new Error("Tavily crawl returned no page content");
56
+ }
57
+ return { provider: this.name, pages };
58
+ }
59
+ // 432 (plan usage exceeded) and 433 (out of credits), plus 402, are Tavily's
60
+ // permanent quota signals. 429 is a transient rate limit, left to failover.
61
+ async post(path, body) {
62
+ try {
63
+ return await requestJson(this.fetchFn, `${this.options.apiUrl}${path}`, {
64
+ method: "POST",
65
+ headers: this.headers(),
66
+ body: JSON.stringify(body),
67
+ }, this.options.requestTimeoutMs);
68
+ }
69
+ catch (error) {
70
+ rethrowQuota(this.name, error, [402, 432, 433]);
71
+ }
72
+ }
73
+ }
74
+ //# sourceMappingURL=tavily.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tavily.js","sourceRoot":"","sources":["../../src/providers/tavily.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ type Environment = Record<string, string | undefined>;
2
+ export declare function proxyUrl(env: Environment): string | undefined;
3
+ export declare function configureProxy(env?: Environment): string | undefined;
4
+ export {};
package/dist/proxy.js ADDED
@@ -0,0 +1,23 @@
1
+ import { ProxyAgent, setGlobalDispatcher } from "undici";
2
+ // Node's global fetch (undici) ignores HTTPS_PROXY/HTTP_PROXY. Behind an egress
3
+ // proxy (e.g. the OneCLI gateway) every request must be routed through a
4
+ // ProxyAgent explicitly, or it bypasses the proxy and gets blocked.
5
+ // ponytail: NO_PROXY is not honored — this server only talks to provider hosts,
6
+ // all of which go through the gateway. Parse NO_PROXY if that ever changes.
7
+ export function proxyUrl(env) {
8
+ return (env.HTTPS_PROXY ??
9
+ env.https_proxy ??
10
+ env.HTTP_PROXY ??
11
+ env.http_proxy ??
12
+ undefined);
13
+ }
14
+ // Installs a global proxy dispatcher when a proxy is configured. Returns the
15
+ // proxy URL that was applied, or undefined when none is set.
16
+ export function configureProxy(env = process.env) {
17
+ const url = proxyUrl(env);
18
+ if (url) {
19
+ setGlobalDispatcher(new ProxyAgent(url));
20
+ }
21
+ return url;
22
+ }
23
+ //# sourceMappingURL=proxy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proxy.js","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,mBAAmB,EAAE,MAAM,QAAQ,CAAC;AAIzD,gFAAgF;AAChF,yEAAyE;AACzE,oEAAoE;AACpE,gFAAgF;AAChF,4EAA4E;AAC5E,MAAM,UAAU,QAAQ,CAAC,GAAgB;IACvC,OAAO,CACL,GAAG,CAAC,WAAW;QACf,GAAG,CAAC,WAAW;QACf,GAAG,CAAC,UAAU;QACd,GAAG,CAAC,UAAU;QACd,SAAS,CACV,CAAC;AACJ,CAAC;AAED,6EAA6E;AAC7E,6DAA6D;AAC7D,MAAM,UAAU,cAAc,CAC5B,MAAmB,OAAO,CAAC,GAAG;IAE9B,MAAM,GAAG,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC;IAC1B,IAAI,GAAG,EAAE,CAAC;QACR,mBAAmB,CAAC,IAAI,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC;IAC3C,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
@@ -0,0 +1,26 @@
1
+ import type { CrawlProvider, CrawlRequest, CrawlResult, ProviderName, ScrapeProvider, ScrapeRequest, ScrapeResult } from "./types.js";
2
+ export declare class AllProvidersFailedError extends Error {
3
+ readonly operation: "scrape" | "crawl";
4
+ readonly failures: Array<{
5
+ provider: ProviderName;
6
+ message: string;
7
+ }>;
8
+ constructor(operation: "scrape" | "crawl", failures: Array<{
9
+ provider: ProviderName;
10
+ message: string;
11
+ }>);
12
+ }
13
+ export declare class ServiceUnavailableError extends Error {
14
+ readonly operation: "scrape" | "crawl";
15
+ constructor(operation: "scrape" | "crawl");
16
+ }
17
+ export declare class RoundRobinRouter {
18
+ private readonly scrapeProviders;
19
+ private readonly crawlProviders;
20
+ private readonly cursor;
21
+ private readonly disabled;
22
+ constructor(scrapeProviders: ScrapeProvider[], crawlProviders: CrawlProvider[]);
23
+ scrape(request: ScrapeRequest): Promise<ScrapeResult>;
24
+ crawl(request: CrawlRequest): Promise<CrawlResult>;
25
+ private tryProviders;
26
+ }
package/dist/router.js ADDED
@@ -0,0 +1,81 @@
1
+ import { QuotaExceededError } from "./http.js";
2
+ export class AllProvidersFailedError extends Error {
3
+ operation;
4
+ failures;
5
+ constructor(operation, failures) {
6
+ super(`All ${operation} providers failed: ${failures
7
+ .map(({ provider, message }) => `${provider}: ${message}`)
8
+ .join("; ")}`);
9
+ this.operation = operation;
10
+ this.failures = failures;
11
+ this.name = "AllProvidersFailedError";
12
+ }
13
+ }
14
+ // No provider is available to serve the request: every configured provider has
15
+ // been permanently disabled after exhausting its quota.
16
+ export class ServiceUnavailableError extends Error {
17
+ operation;
18
+ constructor(operation) {
19
+ super(`Service unavailable: all ${operation} providers have exhausted their quota`);
20
+ this.operation = operation;
21
+ this.name = "ServiceUnavailableError";
22
+ }
23
+ }
24
+ export class RoundRobinRouter {
25
+ scrapeProviders;
26
+ crawlProviders;
27
+ cursor = {
28
+ scrape: 0,
29
+ crawl: 0,
30
+ };
31
+ // Providers that hit a hard quota. Account-level: disabling here removes the
32
+ // provider from both scrape and crawl rotations until the process restarts.
33
+ disabled = new Set();
34
+ constructor(scrapeProviders, crawlProviders) {
35
+ this.scrapeProviders = scrapeProviders;
36
+ this.crawlProviders = crawlProviders;
37
+ if (scrapeProviders.length === 0) {
38
+ throw new Error("At least one scrape provider is required");
39
+ }
40
+ }
41
+ scrape(request) {
42
+ return this.tryProviders("scrape", this.scrapeProviders, (provider) => provider.scrape(request));
43
+ }
44
+ crawl(request) {
45
+ if (this.crawlProviders.length === 0) {
46
+ throw new Error("No crawl-capable provider configured; enable Firecrawl or Tavily");
47
+ }
48
+ return this.tryProviders("crawl", this.crawlProviders, (provider) => provider.crawl(request));
49
+ }
50
+ async tryProviders(operation, providers, execute) {
51
+ const active = providers.filter((provider) => !this.disabled.has(provider.name));
52
+ if (active.length === 0) {
53
+ throw new ServiceUnavailableError(operation);
54
+ }
55
+ const start = this.cursor[operation] % active.length;
56
+ this.cursor[operation] += 1;
57
+ const failures = [];
58
+ for (let offset = 0; offset < active.length; offset += 1) {
59
+ const provider = active[(start + offset) % active.length];
60
+ try {
61
+ return await execute(provider);
62
+ }
63
+ catch (error) {
64
+ if (error instanceof QuotaExceededError) {
65
+ this.disabled.add(provider.name);
66
+ }
67
+ failures.push({
68
+ provider: provider.name,
69
+ message: error instanceof Error ? error.message : String(error),
70
+ });
71
+ }
72
+ }
73
+ // If the quota errors just disabled the last provider, surface unavailable
74
+ // rather than a generic all-failed error.
75
+ if (providers.every((provider) => this.disabled.has(provider.name))) {
76
+ throw new ServiceUnavailableError(operation);
77
+ }
78
+ throw new AllProvidersFailedError(operation, failures);
79
+ }
80
+ }
81
+ //# sourceMappingURL=router.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"router.js","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAW/C,MAAM,OAAO,uBAAwB,SAAQ,KAAK;IAErC;IACA;IAFX,YACW,SAA6B,EAC7B,QAA4D;QAErE,KAAK,CACH,OAAO,SAAS,sBAAsB,QAAQ;aAC3C,GAAG,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,GAAG,QAAQ,KAAK,OAAO,EAAE,CAAC;aACzD,IAAI,CAAC,IAAI,CAAC,EAAE,CAChB,CAAC;QAPO,cAAS,GAAT,SAAS,CAAoB;QAC7B,aAAQ,GAAR,QAAQ,CAAoD;QAOrE,IAAI,CAAC,IAAI,GAAG,yBAAyB,CAAC;IACxC,CAAC;CACF;AAED,+EAA+E;AAC/E,wDAAwD;AACxD,MAAM,OAAO,uBAAwB,SAAQ,KAAK;IAC3B;IAArB,YAAqB,SAA6B;QAChD,KAAK,CACH,4BAA4B,SAAS,uCAAuC,CAC7E,CAAC;QAHiB,cAAS,GAAT,SAAS,CAAoB;QAIhD,IAAI,CAAC,IAAI,GAAG,yBAAyB,CAAC;IACxC,CAAC;CACF;AAED,MAAM,OAAO,gBAAgB;IAUR;IACA;IAVF,MAAM,GAAuC;QAC5D,MAAM,EAAE,CAAC;QACT,KAAK,EAAE,CAAC;KACT,CAAC;IACF,6EAA6E;IAC7E,4EAA4E;IAC3D,QAAQ,GAAG,IAAI,GAAG,EAAgB,CAAC;IAEpD,YACmB,eAAiC,EACjC,cAA+B;QAD/B,oBAAe,GAAf,eAAe,CAAkB;QACjC,mBAAc,GAAd,cAAc,CAAiB;QAEhD,IAAI,eAAe,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACjC,MAAM,IAAI,KAAK,CAAC,0CAA0C,CAAC,CAAC;QAC9D,CAAC;IACH,CAAC;IAED,MAAM,CAAC,OAAsB;QAC3B,OAAO,IAAI,CAAC,YAAY,CAAC,QAAQ,EAAE,IAAI,CAAC,eAAe,EAAE,CAAC,QAAQ,EAAE,EAAE,CACpE,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,CACzB,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,OAAqB;QACzB,IAAI,IAAI,CAAC,cAAc,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACrC,MAAM,IAAI,KAAK,CACb,kEAAkE,CACnE,CAAC;QACJ,CAAC;QACD,OAAO,IAAI,CAAC,YAAY,CAAC,OAAO,EAAE,IAAI,CAAC,cAAc,EAAE,CAAC,QAAQ,EAAE,EAAE,CAClE,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,CACxB,CAAC;IACJ,CAAC;IAEO,KAAK,CAAC,YAAY,CACxB,SAA6B,EAC7B,SAAsB,EACtB,OAAkD;QAElD,MAAM,MAAM,GAAG,SAAS,CAAC,MAAM,CAC7B,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,CAChD,CAAC;QACF,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACxB,MAAM,IAAI,uBAAuB,CAAC,SAAS,CAAC,CAAC;QAC/C,CAAC;QAED,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC;QACrD,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;QAC5B,MAAM,QAAQ,GAAuD,EAAE,CAAC;QAExE,KAAK,IAAI,MAAM,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,IAAI,CAAC,EAAE,CAAC;YACzD,MAAM,QAAQ,GAAG,MAAM,CAAC,CAAC,KAAK,GAAG,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;YAC1D,IAAI,CAAC;gBACH,OAAO,MAAM,OAAO,CAAC,QAAQ,CAAC,CAAC;YACjC,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,IAAI,KAAK,YAAY,kBAAkB,EAAE,CAAC;oBACxC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;gBACnC,CAAC;gBACD,QAAQ,CAAC,IAAI,CAAC;oBACZ,QAAQ,EAAE,QAAQ,CAAC,IAAI;oBACvB,OAAO,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;iBAChE,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QAED,2EAA2E;QAC3E,0CAA0C;QAC1C,IAAI,SAAS,CAAC,KAAK,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;YACpE,MAAM,IAAI,uBAAuB,CAAC,SAAS,CAAC,CAAC;QAC/C,CAAC;QACD,MAAM,IAAI,uBAAuB,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;IACzD,CAAC;CACF"}
@@ -0,0 +1,6 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { AppConfig } from "./config.js";
3
+ import { RoundRobinRouter } from "./router.js";
4
+ export declare function createRouter(config: AppConfig): RoundRobinRouter;
5
+ export declare function createServer(router: RoundRobinRouter): McpServer;
6
+ export declare function runServer(config: AppConfig): Promise<void>;
package/dist/server.js ADDED
@@ -0,0 +1,160 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { z } from "zod";
4
+ import { FirecrawlProvider } from "./providers/firecrawl.js";
5
+ import { ScrapeDoProvider } from "./providers/scrapedo.js";
6
+ import { TavilyProvider } from "./providers/tavily.js";
7
+ import { RoundRobinRouter } from "./router.js";
8
+ const pageSchema = z.object({
9
+ url: z.string().url(),
10
+ title: z.string().optional(),
11
+ markdown: z.string(),
12
+ });
13
+ function toErrorResult(error) {
14
+ return {
15
+ content: [
16
+ {
17
+ type: "text",
18
+ text: error instanceof Error ? error.message : String(error),
19
+ },
20
+ ],
21
+ isError: true,
22
+ };
23
+ }
24
+ export function createRouter(config) {
25
+ const scrapeProviders = [];
26
+ const crawlProviders = [];
27
+ if (config.providers.includes("firecrawl")) {
28
+ const provider = new FirecrawlProvider({
29
+ ...config.firecrawl,
30
+ requestTimeoutMs: config.requestTimeoutMs,
31
+ crawlTimeoutMs: config.crawlTimeoutMs,
32
+ pollIntervalMs: config.pollIntervalMs,
33
+ });
34
+ scrapeProviders.push(provider);
35
+ crawlProviders.push(provider);
36
+ }
37
+ if (config.providers.includes("tavily")) {
38
+ const provider = new TavilyProvider({
39
+ ...config.tavily,
40
+ requestTimeoutMs: config.requestTimeoutMs,
41
+ });
42
+ scrapeProviders.push(provider);
43
+ crawlProviders.push(provider);
44
+ }
45
+ if (config.providers.includes("scrapedo")) {
46
+ scrapeProviders.push(new ScrapeDoProvider({
47
+ ...config.scrapedo,
48
+ requestTimeoutMs: config.requestTimeoutMs,
49
+ }));
50
+ }
51
+ return new RoundRobinRouter(scrapeProviders, crawlProviders);
52
+ }
53
+ export function createServer(router) {
54
+ const server = new McpServer({
55
+ name: "rrcrawl",
56
+ version: "0.1.0",
57
+ });
58
+ server.registerTool("scrape", {
59
+ title: "Scrape one URL",
60
+ description: "Fetch one URL as normalized Markdown. Requests rotate across all configured providers and fail over on errors.",
61
+ inputSchema: {
62
+ url: z.string().url().describe("HTTP or HTTPS URL to scrape"),
63
+ },
64
+ outputSchema: pageSchema.extend({
65
+ provider: z.enum(["firecrawl", "tavily", "scrapedo"]),
66
+ }),
67
+ annotations: {
68
+ readOnlyHint: true,
69
+ idempotentHint: true,
70
+ openWorldHint: true,
71
+ },
72
+ }, async ({ url }) => {
73
+ try {
74
+ const result = await router.scrape({ url });
75
+ return {
76
+ content: [{ type: "text", text: JSON.stringify(result) }],
77
+ structuredContent: {
78
+ provider: result.provider,
79
+ url: result.url,
80
+ ...(result.title ? { title: result.title } : {}),
81
+ markdown: result.markdown,
82
+ },
83
+ };
84
+ }
85
+ catch (error) {
86
+ return toErrorResult(error);
87
+ }
88
+ });
89
+ server.registerTool("crawl", {
90
+ title: "Crawl a website",
91
+ description: "Crawl multiple pages as normalized Markdown. Requests rotate between configured crawl-capable providers (Firecrawl and Tavily) and fail over on errors.",
92
+ inputSchema: {
93
+ url: z.string().url().describe("Root HTTP or HTTPS URL"),
94
+ limit: z
95
+ .number()
96
+ .int()
97
+ .min(1)
98
+ .max(100)
99
+ .default(10)
100
+ .describe("Maximum number of pages"),
101
+ maxDepth: z
102
+ .number()
103
+ .int()
104
+ .min(1)
105
+ .max(5)
106
+ .default(1)
107
+ .describe("Maximum link depth from the root URL"),
108
+ includePaths: z
109
+ .array(z.string())
110
+ .max(20)
111
+ .default([])
112
+ .describe("Provider-compatible regex path filters"),
113
+ allowExternal: z
114
+ .boolean()
115
+ .default(false)
116
+ .describe("Allow pages outside the root domain"),
117
+ instructions: z
118
+ .string()
119
+ .max(2_000)
120
+ .optional()
121
+ .describe("Natural-language guidance for selecting pages"),
122
+ },
123
+ outputSchema: z.object({
124
+ provider: z.enum(["firecrawl", "tavily"]),
125
+ pages: z.array(pageSchema),
126
+ }),
127
+ annotations: {
128
+ readOnlyHint: true,
129
+ idempotentHint: true,
130
+ openWorldHint: true,
131
+ },
132
+ }, async ({ url, limit, maxDepth, includePaths, allowExternal, instructions, }) => {
133
+ try {
134
+ const result = await router.crawl({
135
+ url,
136
+ limit,
137
+ maxDepth,
138
+ includePaths,
139
+ allowExternal,
140
+ instructions,
141
+ });
142
+ return {
143
+ content: [{ type: "text", text: JSON.stringify(result) }],
144
+ structuredContent: {
145
+ provider: result.provider,
146
+ pages: result.pages,
147
+ },
148
+ };
149
+ }
150
+ catch (error) {
151
+ return toErrorResult(error);
152
+ }
153
+ });
154
+ return server;
155
+ }
156
+ export async function runServer(config) {
157
+ const server = createServer(createRouter(config));
158
+ await server.connect(new StdioServerTransport());
159
+ }
160
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ export type ProviderName = "firecrawl" | "tavily" | "scrapedo";
2
+ export interface Page {
3
+ url: string;
4
+ title?: string;
5
+ markdown: string;
6
+ }
7
+ export interface ScrapeRequest {
8
+ url: string;
9
+ }
10
+ export interface ScrapeResult extends Page {
11
+ provider: ProviderName;
12
+ }
13
+ export interface CrawlRequest {
14
+ url: string;
15
+ limit: number;
16
+ maxDepth: number;
17
+ includePaths: string[];
18
+ allowExternal: boolean;
19
+ instructions?: string;
20
+ }
21
+ export interface CrawlResult {
22
+ provider: ProviderName;
23
+ pages: Page[];
24
+ }
25
+ export interface ScrapeProvider {
26
+ readonly name: ProviderName;
27
+ scrape(request: ScrapeRequest): Promise<ScrapeResult>;
28
+ }
29
+ export interface CrawlProvider {
30
+ readonly name: ProviderName;
31
+ crawl(request: CrawlRequest): Promise<CrawlResult>;
32
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "rrcrawl",
3
+ "version": "0.1.0",
4
+ "description": "Tiny round-robin crawl MCP backed by Firecrawl, Tavily, and Scrape.do",
5
+ "license": "MIT",
6
+ "author": "Roman Shterenzon <roman.shterenzon@gmail.com>",
7
+ "homepage": "https://github.com/romanbsd/rrcrawl#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/romanbsd/rrcrawl.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/romanbsd/rrcrawl/issues"
14
+ },
15
+ "keywords": [
16
+ "mcp",
17
+ "model-context-protocol",
18
+ "scrape",
19
+ "crawl",
20
+ "firecrawl",
21
+ "tavily",
22
+ "scrape.do"
23
+ ],
24
+ "type": "module",
25
+ "bin": {
26
+ "rrcrawl": "./dist/index.js"
27
+ },
28
+ "files": [
29
+ "dist",
30
+ "README.md",
31
+ ".env.example"
32
+ ],
33
+ "scripts": {
34
+ "build": "tsc -p tsconfig.json",
35
+ "check": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json --noEmit",
36
+ "start": "node dist/index.js",
37
+ "dev": "tsx src/index.ts",
38
+ "test": "vitest run",
39
+ "smoke": "tsx scripts/smoke.ts",
40
+ "prepack": "npm run build",
41
+ "prepublishOnly": "npm test && npm run check"
42
+ },
43
+ "engines": {
44
+ "node": ">=22"
45
+ },
46
+ "dependencies": {
47
+ "@modelcontextprotocol/sdk": "1.29.0",
48
+ "dotenv": "^17.2.3",
49
+ "undici": "^8.8.0",
50
+ "zod": "^3.25.76"
51
+ },
52
+ "devDependencies": {
53
+ "@types/node": "^24.10.1",
54
+ "tsx": "^4.21.0",
55
+ "typescript": "^5.9.3",
56
+ "vitest": "^4.0.16"
57
+ },
58
+ "overrides": {
59
+ "@hono/node-server": "2.0.11"
60
+ },
61
+ "publishConfig": {
62
+ "access": "public"
63
+ }
64
+ }