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 +21 -0
- package/LICENSE +21 -0
- package/README.md +176 -0
- package/dist/config.d.ts +24 -0
- package/dist/config.js +96 -0
- package/dist/config.js.map +1 -0
- package/dist/http.d.ts +15 -0
- package/dist/http.js +78 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +36 -0
- package/dist/index.js.map +1 -0
- package/dist/providers/firecrawl.d.ts +25 -0
- package/dist/providers/firecrawl.js +135 -0
- package/dist/providers/firecrawl.js.map +1 -0
- package/dist/providers/scrapedo.d.ts +15 -0
- package/dist/providers/scrapedo.js +51 -0
- package/dist/providers/scrapedo.js.map +1 -0
- package/dist/providers/tavily.d.ts +18 -0
- package/dist/providers/tavily.js +74 -0
- package/dist/providers/tavily.js.map +1 -0
- package/dist/proxy.d.ts +4 -0
- package/dist/proxy.js +23 -0
- package/dist/proxy.js.map +1 -0
- package/dist/router.d.ts +26 -0
- package/dist/router.js +81 -0
- package/dist/router.js.map +1 -0
- package/dist/server.d.ts +6 -0
- package/dist/server.js +160 -0
- package/dist/server.js.map +1 -0
- package/dist/types.d.ts +32 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +64 -0
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.
|
package/dist/config.d.ts
ADDED
|
@@ -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":"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"}
|
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
|
package/dist/http.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.js","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAKA,MAAM,OAAO,SAAU,SAAQ,KAAK;IAEvB;IACA;IAFX,YACW,MAAc,EACd,YAAoB,EAC7B,GAAW;QAEX,KAAK,CAAC,QAAQ,MAAM,SAAS,GAAG,KAAK,YAAY,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;QAJ1D,WAAM,GAAN,MAAM,CAAQ;QACd,iBAAY,GAAZ,YAAY,CAAQ;QAI7B,IAAI,CAAC,IAAI,GAAG,WAAW,CAAC;IAC1B,CAAC;CACF;AAED,2EAA2E;AAC3E,8EAA8E;AAC9E,6BAA6B;AAC7B,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IAEhC;IACA;IAFX,YACW,QAAgB,EAChB,MAAc;QAEvB,KAAK,CAAC,GAAG,QAAQ,0BAA0B,MAAM,GAAG,CAAC,CAAC;QAH7C,aAAQ,GAAR,QAAQ,CAAQ;QAChB,WAAM,GAAN,MAAM,CAAQ;QAGvB,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;IACnC,CAAC;CACF;AAED,6EAA6E;AAC7E,sEAAsE;AACtE,MAAM,UAAU,YAAY,CAC1B,QAAgB,EAChB,KAAc,EACd,aAAgC;IAEhC,IAAI,KAAK,YAAY,SAAS,IAAI,aAAa,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC;QACvE,MAAM,IAAI,kBAAkB,CAAC,QAAQ,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACvD,CAAC;IACD,MAAM,KAAK,CAAC;AACd,CAAC;AAED,8EAA8E;AAC9E,0EAA0E;AAC1E,2EAA2E;AAC3E,MAAM,UAAU,iBAAiB,CAAC,KAAyB;IACzD,IAAI,CAAC,KAAK;QAAE,OAAO,KAAK,CAAC;IACzB,IAAI,CAAC;QACH,MAAM,EAAE,QAAQ,EAAE,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC;QACpC,OAAO,QAAQ,KAAK,OAAO,IAAI,QAAQ,KAAK,QAAQ,CAAC;IACvD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,+EAA+E;AAC/E,oDAAoD;AACpD,KAAK,UAAU,WAAW,CACxB,OAAkB,EAClB,GAAW,EACX,IAAiB,EACjB,SAAiB;IAEjB,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;IACzC,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,SAAS,CAAC,CAAC;IAE9D,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,GAAG,EAAE;YAClC,GAAG,IAAI;YACP,MAAM,EAAE,UAAU,CAAC,MAAM;SAC1B,CAAC,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;QACnC,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;YACjB,MAAM,IAAI,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC;QAClD,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;YAAS,CAAC;QACT,YAAY,CAAC,KAAK,CAAC,CAAC;IACtB,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,OAAkB,EAClB,GAAW,EACX,IAAiB,EACjB,SAAiB;IAEjB,MAAM,IAAI,GAAG,MAAM,WAAW,CAAC,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,SAAS,CAAC,CAAC;IAC9D,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAM,CAAC;IAC/B,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CAAC,qBAAqB,GAAG,KAAK,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;IACrE,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,OAAkB,EAClB,GAAW,EACX,IAAiB,EACjB,SAAiB;IAEjB,OAAO,WAAW,CAAC,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,SAAS,CAAC,CAAC;AACpD,CAAC"}
|
package/dist/index.d.ts
ADDED
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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"}
|
package/dist/proxy.d.ts
ADDED
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"}
|
package/dist/router.d.ts
ADDED
|
@@ -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":"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"}
|
package/dist/server.d.ts
ADDED
|
@@ -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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"}
|
package/dist/types.d.ts
ADDED
|
@@ -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 @@
|
|
|
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
|
+
}
|