@fouradata/mcp 0.1.1
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/LICENSE +21 -0
- package/README.md +138 -0
- package/bin/foura-mcp.js +2 -0
- package/dist/auth.d.ts +2 -0
- package/dist/auth.js +32 -0
- package/dist/auth.js.map +1 -0
- package/dist/http.d.ts +1 -0
- package/dist/http.js +71 -0
- package/dist/http.js.map +1 -0
- package/dist/resources.d.ts +27 -0
- package/dist/resources.js +89 -0
- package/dist/resources.js.map +1 -0
- package/dist/safe-target.d.ts +5 -0
- package/dist/safe-target.js +118 -0
- package/dist/safe-target.js.map +1 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +17 -0
- package/dist/server.js.map +1 -0
- package/dist/stdio.d.ts +2 -0
- package/dist/stdio.js +7 -0
- package/dist/stdio.js.map +1 -0
- package/dist/tools/browser.d.ts +2 -0
- package/dist/tools/browser.js +127 -0
- package/dist/tools/browser.js.map +1 -0
- package/dist/tools/proxy.d.ts +2 -0
- package/dist/tools/proxy.js +171 -0
- package/dist/tools/proxy.js.map +1 -0
- package/dist/tools/single.d.ts +2 -0
- package/dist/tools/single.js +158 -0
- package/dist/tools/single.js.map +1 -0
- package/package.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 FourA
|
|
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,138 @@
|
|
|
1
|
+
# @fouradata/mcp
|
|
2
|
+
|
|
3
|
+
[FourA Web Scraping API](https://foura.ai/) as three [Model Context Protocol](https://modelcontextprotocol.io) tools. Plug it into Claude Desktop, Claude Code, Cursor, Windsurf, or any other MCP client and you can fetch arbitrary public web pages, bypass anti-bot challenges, and render JavaScript-heavy sites — all without writing a line of integration code.
|
|
4
|
+
|
|
5
|
+
Three tools, one API key.
|
|
6
|
+
|
|
7
|
+
## Quick Start — hosted
|
|
8
|
+
|
|
9
|
+
Grab a key at [foura.ai/dashboard#api-keys](https://foura.ai/dashboard#api-keys) (one click, shown once on creation, format `pk_live_...`). Then drop this into your MCP client's config:
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{
|
|
13
|
+
"mcpServers": {
|
|
14
|
+
"foura": {
|
|
15
|
+
"url": "https://mcp.foura.ai/mcp",
|
|
16
|
+
"headers": {
|
|
17
|
+
"Authorization": "Bearer pk_live_..."
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
That's it. Restart the client and `foura_single`, `foura_proxy`, `foura_browser` show up in your tool list.
|
|
25
|
+
|
|
26
|
+
Same JSON works in every major client — just point it at the right file:
|
|
27
|
+
|
|
28
|
+
| Client | Where the config lives |
|
|
29
|
+
|---|---|
|
|
30
|
+
| Claude Desktop (macOS) | `~/Library/Application Support/Claude/claude_desktop_config.json` |
|
|
31
|
+
| Claude Desktop (Windows) | `%APPDATA%\Claude\claude_desktop_config.json` |
|
|
32
|
+
| Claude Code | `claude mcp add --transport http foura https://mcp.foura.ai/mcp --header "Authorization: Bearer pk_live_..."` |
|
|
33
|
+
| Cursor | `~/.cursor/mcp.json` |
|
|
34
|
+
| Windsurf | `~/.codeium/windsurf/mcp_config.json` |
|
|
35
|
+
| VS Code (MCP extension) | `.vscode/mcp.json` in your workspace |
|
|
36
|
+
|
|
37
|
+
## Quick Start — local stdio
|
|
38
|
+
|
|
39
|
+
Prefer to run the server in-process on your machine? Use npx:
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"mcpServers": {
|
|
44
|
+
"foura": {
|
|
45
|
+
"command": "npx",
|
|
46
|
+
"args": ["-y", "@fouradata/mcp"],
|
|
47
|
+
"env": {
|
|
48
|
+
"FOURA_API_KEY": "pk_live_..."
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The package downloads and runs on first launch. No global install needed.
|
|
56
|
+
|
|
57
|
+
## The Three Tools
|
|
58
|
+
|
|
59
|
+
### `foura_single` — fast HTTP
|
|
60
|
+
|
|
61
|
+
One HTTP request, response back. Typically 200ms–2s. Use it for static pages, JSON APIs, server-rendered HTML — the bread and butter of scraping. Set `unblocker: true` if the target is picky about wire-level signals.
|
|
62
|
+
|
|
63
|
+
```jsonc
|
|
64
|
+
{
|
|
65
|
+
"method": "GET",
|
|
66
|
+
"url": "https://example.com",
|
|
67
|
+
"unblocker": true
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
You can pass custom headers, a body, per-stage timeouts, redirect controls, JSON auto-parse, a binary-buffer mode, and built-in response validation (`validate.status.accept`, `validate.data.fail`, and so on). If `foura_single` comes back with a 403 or captcha, escalate to `foura_proxy`. If the page needs JavaScript, escalate to `foura_browser`.
|
|
72
|
+
|
|
73
|
+
### `foura_proxy` — rotating proxies with retry
|
|
74
|
+
|
|
75
|
+
Same shape as `foura_single`, but routed through a pool of proxies with automatic retry on failure. Per-host scoring picks the proxies most likely to succeed against this particular target, so you're not burning attempts on known-bad routes.
|
|
76
|
+
|
|
77
|
+
```jsonc
|
|
78
|
+
{
|
|
79
|
+
"maxTries": 5,
|
|
80
|
+
"request": {
|
|
81
|
+
"method": "GET",
|
|
82
|
+
"url": "https://example.com/pricing",
|
|
83
|
+
"unblocker": true
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Typical latency 1–5s. The response includes the proxy ID that succeeded and the total number of tries — handy when you're debugging why a site keeps blocking you.
|
|
89
|
+
|
|
90
|
+
### `foura_browser` — full browser session
|
|
91
|
+
|
|
92
|
+
A real browser session. JavaScript runs, the DOM finishes rendering, cookies come back with the response. Use it when the page is a single-page app, when content lazy-loads after first paint, or when there's an anti-bot challenge that needs a real browser to clear.
|
|
93
|
+
|
|
94
|
+
```jsonc
|
|
95
|
+
{
|
|
96
|
+
"url": "https://example.com/spa",
|
|
97
|
+
"timeout_ms": 15000,
|
|
98
|
+
"checkText": "data-table"
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Slowest of the three (2–10s), but it's the only tool that handles JavaScript end-to-end. `checkText` fails the request if a substring you expect doesn't appear in the rendered HTML — useful when a page returns 200 but the actual content is missing.
|
|
103
|
+
|
|
104
|
+
## Authentication
|
|
105
|
+
|
|
106
|
+
Your `Bearer` token (or the `FOURA_API_KEY` env var in stdio mode) forwards to the FourA API as `X-API-Key`. One key, all three tools.
|
|
107
|
+
|
|
108
|
+
Keys are managed in the [dashboard](https://foura.ai/dashboard#api-keys) — shown once on creation, rotate or deactivate any time. Errors come back to the MCP client with the full envelope (`retryAfter`, `current.{concurrency,rpm}`, `limits.maxRpm`) so the LLM can self-throttle without you stepping in. Full reference: [foura.ai/docs/api/errors](https://foura.ai/docs/api/errors).
|
|
109
|
+
|
|
110
|
+
## Limits
|
|
111
|
+
|
|
112
|
+
- **Large responses get offloaded.** Anything above 50 KB is cached on the MCP server and returned as a `resource_link` with a preview. Your client calls `resources/read` to pull the full body only when it actually needs it. Cached payloads expire after an hour.
|
|
113
|
+
- **Private targets are refused.** Requests to private or reserved IP ranges (RFC 5735, 6598, and IPv6 reserved blocks) are blocked at the MCP layer. We only forward to public hosts.
|
|
114
|
+
- **Rate limits** are enforced by the FourA API per service. Concurrency + RPM. Details at [foura.ai/docs/api/rate-limits](https://foura.ai/docs/api/rate-limits).
|
|
115
|
+
|
|
116
|
+
## Self-Hosting
|
|
117
|
+
|
|
118
|
+
The MCP server runs in one container, statelessly — each request brings its own key, so there's no session state, no sticky load balancing, nothing to coordinate. Scale horizontally behind any load balancer.
|
|
119
|
+
|
|
120
|
+
Configurable environment:
|
|
121
|
+
|
|
122
|
+
| Variable | Default | Purpose |
|
|
123
|
+
|---|---|---|
|
|
124
|
+
| `PORT` | `3076` | HTTP listen port |
|
|
125
|
+
| `FOURA_API_BASE` | `https://api.foura.ai/api` | Upstream FourA REST base URL |
|
|
126
|
+
| `FOURA_MCP_PAYLOADS_DIR` | `/data/payloads` | Where >50 KB responses are cached on disk |
|
|
127
|
+
|
|
128
|
+
A public GitHub mirror lands with `v1.0`; until then the source lives in a private repo. Ping `support@foura.ai` if you need early access to the container image.
|
|
129
|
+
|
|
130
|
+
## License
|
|
131
|
+
|
|
132
|
+
MIT. See [`LICENSE`](./LICENSE).
|
|
133
|
+
|
|
134
|
+
## Links
|
|
135
|
+
|
|
136
|
+
- API documentation: <https://foura.ai/docs>
|
|
137
|
+
- MCP specification: <https://modelcontextprotocol.io>
|
|
138
|
+
- Get a key: <https://foura.ai/dashboard#api-keys>
|
package/bin/foura-mcp.js
ADDED
package/dist/auth.d.ts
ADDED
package/dist/auth.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
/**
|
|
3
|
+
* Shared FourA auth.
|
|
4
|
+
*
|
|
5
|
+
* The API key authenticates the CALLER, not the endpoint — one key opens
|
|
6
|
+
* /single/, /proxy/, and /browser/. So this lives in one place and is
|
|
7
|
+
* imported by every tool. (Schemas, paths, and per-endpoint behavior remain
|
|
8
|
+
* fully duplicated across tool files — see
|
|
9
|
+
* feedback_foura_endpoints_independent_schemas.)
|
|
10
|
+
*
|
|
11
|
+
* Dual-mode:
|
|
12
|
+
* - stdio: the user sets FOURA_API_KEY in env (e.g. via claude_desktop_config).
|
|
13
|
+
* - HTTP: each incoming /mcp request supplies its own key via
|
|
14
|
+
* Authorization: Bearer pk_live_..., which the transport scopes into
|
|
15
|
+
* AsyncLocalStorage before invoking the tool handler.
|
|
16
|
+
*/
|
|
17
|
+
const apiKeyContext = new AsyncLocalStorage();
|
|
18
|
+
export function withApiKey(key, fn) {
|
|
19
|
+
return apiKeyContext.run(key, fn);
|
|
20
|
+
}
|
|
21
|
+
export function getApiKey() {
|
|
22
|
+
const fromContext = apiKeyContext.getStore();
|
|
23
|
+
if (fromContext)
|
|
24
|
+
return fromContext;
|
|
25
|
+
const fromEnv = process.env.FOURA_API_KEY;
|
|
26
|
+
if (fromEnv)
|
|
27
|
+
return fromEnv;
|
|
28
|
+
throw new Error("FOURA_API_KEY not provided. In stdio mode set the FOURA_API_KEY env var. " +
|
|
29
|
+
"In HTTP mode send Authorization: Bearer pk_live_... Get a key at " +
|
|
30
|
+
"https://foura.ai/dashboard#api-keys");
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=auth.js.map
|
package/dist/auth.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAErD;;;;;;;;;;;;;;GAcG;AACH,MAAM,aAAa,GAAG,IAAI,iBAAiB,EAAU,CAAC;AAEtD,MAAM,UAAU,UAAU,CAAI,GAAW,EAAE,EAAoB;IAC7D,OAAO,aAAa,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;AACpC,CAAC;AAED,MAAM,UAAU,SAAS;IACvB,MAAM,WAAW,GAAG,aAAa,CAAC,QAAQ,EAAE,CAAC;IAC7C,IAAI,WAAW;QAAE,OAAO,WAAW,CAAC;IACpC,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC;IAC1C,IAAI,OAAO;QAAE,OAAO,OAAO,CAAC;IAC5B,MAAM,IAAI,KAAK,CACb,2EAA2E;QACzE,mEAAmE;QACnE,qCAAqC,CACxC,CAAC;AACJ,CAAC"}
|
package/dist/http.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/http.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
3
|
+
import { createServer } from "./server.js";
|
|
4
|
+
import { withApiKey } from "./auth.js";
|
|
5
|
+
const PORT = Number(process.env.PORT ?? 3076);
|
|
6
|
+
const SERVER_VERSION = "0.1.1";
|
|
7
|
+
const app = express();
|
|
8
|
+
app.use(express.json({ limit: "4mb" }));
|
|
9
|
+
app.get("/healthz", (_req, res) => {
|
|
10
|
+
res.json({ ok: true, name: "foura-mcp", version: SERVER_VERSION });
|
|
11
|
+
});
|
|
12
|
+
function extractBearer(req) {
|
|
13
|
+
const auth = req.header("authorization");
|
|
14
|
+
if (auth?.toLowerCase().startsWith("bearer "))
|
|
15
|
+
return auth.slice(7).trim();
|
|
16
|
+
const xKey = req.header("x-api-key");
|
|
17
|
+
if (xKey)
|
|
18
|
+
return xKey.trim();
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
app.post("/mcp", async (req, res) => {
|
|
22
|
+
const apiKey = extractBearer(req);
|
|
23
|
+
if (!apiKey) {
|
|
24
|
+
res.status(401).json({
|
|
25
|
+
jsonrpc: "2.0",
|
|
26
|
+
error: {
|
|
27
|
+
code: -32001,
|
|
28
|
+
message: "Missing API key. Send 'Authorization: Bearer pk_live_...' with each request. " +
|
|
29
|
+
"Get a key at https://foura.ai/dashboard#api-keys",
|
|
30
|
+
},
|
|
31
|
+
id: null,
|
|
32
|
+
});
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const mcp = createServer();
|
|
37
|
+
const transport = new StreamableHTTPServerTransport({
|
|
38
|
+
sessionIdGenerator: undefined, // stateless — fresh transport per request
|
|
39
|
+
});
|
|
40
|
+
res.on("close", () => {
|
|
41
|
+
transport.close();
|
|
42
|
+
mcp.close();
|
|
43
|
+
});
|
|
44
|
+
await mcp.connect(transport);
|
|
45
|
+
await withApiKey(apiKey, () => transport.handleRequest(req, res, req.body));
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
console.error("[foura-mcp] /mcp handler error:", err);
|
|
49
|
+
if (!res.headersSent) {
|
|
50
|
+
res.status(500).json({
|
|
51
|
+
jsonrpc: "2.0",
|
|
52
|
+
error: { code: -32603, message: "Internal server error" },
|
|
53
|
+
id: null,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
// Stateless mode: GET (server-initiated SSE) and DELETE (session teardown) not supported.
|
|
59
|
+
const methodNotAllowed = (_req, res) => {
|
|
60
|
+
res.status(405).json({
|
|
61
|
+
jsonrpc: "2.0",
|
|
62
|
+
error: { code: -32000, message: "Method not allowed in stateless mode. Use POST /mcp." },
|
|
63
|
+
id: null,
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
app.get("/mcp", methodNotAllowed);
|
|
67
|
+
app.delete("/mcp", methodNotAllowed);
|
|
68
|
+
app.listen(PORT, "0.0.0.0", () => {
|
|
69
|
+
console.error(`[foura-mcp] HTTP listening on :${PORT}`);
|
|
70
|
+
});
|
|
71
|
+
//# 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":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
/**
|
|
3
|
+
* Resources — offload large response bodies (>= THRESHOLD bytes) onto host
|
|
4
|
+
* disk and return a MCP resource_link instead of inlining megabytes into the
|
|
5
|
+
* LLM context.
|
|
6
|
+
*
|
|
7
|
+
* Transport-level cross-cutting concern (same carve-out as auth.ts and
|
|
8
|
+
* safe-target.ts). Per-tool product code stays per-file: each tool decides
|
|
9
|
+
* which response field is the "large payload" (single/proxy use `data`,
|
|
10
|
+
* browser uses `body`).
|
|
11
|
+
*/
|
|
12
|
+
export declare const THRESHOLD_BYTES = 50000;
|
|
13
|
+
export declare const PAYLOADS_DIR: string;
|
|
14
|
+
export interface StoredPayload {
|
|
15
|
+
uri: string;
|
|
16
|
+
name: string;
|
|
17
|
+
mimeType: string;
|
|
18
|
+
size: number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Write `data` to disk and return a resource_link descriptor.
|
|
22
|
+
*
|
|
23
|
+
* Caller has already decided this payload is big enough to offload (use
|
|
24
|
+
* `THRESHOLD_BYTES` for the size check).
|
|
25
|
+
*/
|
|
26
|
+
export declare function storePayload(data: Buffer | string, mimeType: string, suggestedName: string): Promise<StoredPayload>;
|
|
27
|
+
export declare function registerResourceHandler(server: McpServer): void;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { mkdir, writeFile, readFile } from "node:fs/promises";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { ResourceTemplate, } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6
|
+
/**
|
|
7
|
+
* Resources — offload large response bodies (>= THRESHOLD bytes) onto host
|
|
8
|
+
* disk and return a MCP resource_link instead of inlining megabytes into the
|
|
9
|
+
* LLM context.
|
|
10
|
+
*
|
|
11
|
+
* Transport-level cross-cutting concern (same carve-out as auth.ts and
|
|
12
|
+
* safe-target.ts). Per-tool product code stays per-file: each tool decides
|
|
13
|
+
* which response field is the "large payload" (single/proxy use `data`,
|
|
14
|
+
* browser uses `body`).
|
|
15
|
+
*/
|
|
16
|
+
export const THRESHOLD_BYTES = 50_000;
|
|
17
|
+
export const PAYLOADS_DIR = process.env.FOURA_MCP_PAYLOADS_DIR ?? path.join(tmpdir(), "foura-mcp-payloads");
|
|
18
|
+
const URI_PREFIX = "foura-mcp://payload/";
|
|
19
|
+
let initPromise = null;
|
|
20
|
+
function ensureDir() {
|
|
21
|
+
if (!initPromise)
|
|
22
|
+
initPromise = mkdir(PAYLOADS_DIR, { recursive: true }).then(() => undefined);
|
|
23
|
+
return initPromise;
|
|
24
|
+
}
|
|
25
|
+
const SAFE_UUID = /^[0-9a-f-]{36}$/i;
|
|
26
|
+
/**
|
|
27
|
+
* Write `data` to disk and return a resource_link descriptor.
|
|
28
|
+
*
|
|
29
|
+
* Caller has already decided this payload is big enough to offload (use
|
|
30
|
+
* `THRESHOLD_BYTES` for the size check).
|
|
31
|
+
*/
|
|
32
|
+
export async function storePayload(data, mimeType, suggestedName) {
|
|
33
|
+
await ensureDir();
|
|
34
|
+
const uuid = randomUUID();
|
|
35
|
+
const isBinary = Buffer.isBuffer(data);
|
|
36
|
+
const buf = isBinary ? data : Buffer.from(data, "utf8");
|
|
37
|
+
const dataPath = path.join(PAYLOADS_DIR, `${uuid}.bin`);
|
|
38
|
+
const metaPath = path.join(PAYLOADS_DIR, `${uuid}.meta.json`);
|
|
39
|
+
const meta = {
|
|
40
|
+
mimeType,
|
|
41
|
+
originalName: suggestedName,
|
|
42
|
+
size: buf.byteLength,
|
|
43
|
+
storedAt: new Date().toISOString(),
|
|
44
|
+
binary: isBinary,
|
|
45
|
+
};
|
|
46
|
+
await Promise.all([
|
|
47
|
+
writeFile(dataPath, buf),
|
|
48
|
+
writeFile(metaPath, JSON.stringify(meta), { encoding: "utf8" }),
|
|
49
|
+
]);
|
|
50
|
+
return {
|
|
51
|
+
uri: `${URI_PREFIX}${uuid}`,
|
|
52
|
+
name: suggestedName,
|
|
53
|
+
mimeType,
|
|
54
|
+
size: buf.byteLength,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export function registerResourceHandler(server) {
|
|
58
|
+
server.registerResource("payload", new ResourceTemplate(`${URI_PREFIX}{uuid}`, { list: undefined }), {
|
|
59
|
+
title: "Cached foura-mcp response payload",
|
|
60
|
+
description: "A large response body (>=50KB) returned by an earlier foura-mcp tool call, " +
|
|
61
|
+
"stored on disk for follow-up reads instead of inlined into context.",
|
|
62
|
+
}, async (uri, { uuid }) => {
|
|
63
|
+
const uuidStr = Array.isArray(uuid) ? uuid[0] : uuid;
|
|
64
|
+
if (!uuidStr || !SAFE_UUID.test(uuidStr)) {
|
|
65
|
+
throw new Error(`Invalid payload UUID: ${uuidStr}`);
|
|
66
|
+
}
|
|
67
|
+
const metaPath = path.join(PAYLOADS_DIR, `${uuidStr}.meta.json`);
|
|
68
|
+
const dataPath = path.join(PAYLOADS_DIR, `${uuidStr}.bin`);
|
|
69
|
+
const metaRaw = await readFile(metaPath, "utf8");
|
|
70
|
+
const meta = JSON.parse(metaRaw);
|
|
71
|
+
const buf = await readFile(dataPath);
|
|
72
|
+
return {
|
|
73
|
+
contents: [
|
|
74
|
+
meta.binary
|
|
75
|
+
? {
|
|
76
|
+
uri: uri.href,
|
|
77
|
+
mimeType: meta.mimeType,
|
|
78
|
+
blob: buf.toString("base64"),
|
|
79
|
+
}
|
|
80
|
+
: {
|
|
81
|
+
uri: uri.href,
|
|
82
|
+
mimeType: meta.mimeType,
|
|
83
|
+
text: buf.toString("utf8"),
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=resources.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resources.js","sourceRoot":"","sources":["../src/resources.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { lookup } from "node:dns/promises";
|
|
2
|
+
import { isIPv4, isIPv6 } from "node:net";
|
|
3
|
+
function ipv4ToInt(addr) {
|
|
4
|
+
const parts = addr.split(".");
|
|
5
|
+
if (parts.length !== 4)
|
|
6
|
+
throw new Error(`bad IPv4: ${addr}`);
|
|
7
|
+
let n = 0;
|
|
8
|
+
for (const p of parts) {
|
|
9
|
+
const v = Number(p);
|
|
10
|
+
if (!Number.isInteger(v) || v < 0 || v > 255)
|
|
11
|
+
throw new Error(`bad IPv4 octet: ${addr}`);
|
|
12
|
+
n = n * 256 + v;
|
|
13
|
+
}
|
|
14
|
+
return n >>> 0;
|
|
15
|
+
}
|
|
16
|
+
// RFC 5735 + RFC 6598 (CGNAT) reserved IPv4 blocks.
|
|
17
|
+
const V4_RESERVED = [
|
|
18
|
+
{ base: ipv4ToInt("0.0.0.0"), prefix: 8 }, // "this network"
|
|
19
|
+
{ base: ipv4ToInt("10.0.0.0"), prefix: 8 }, // RFC1918 private
|
|
20
|
+
{ base: ipv4ToInt("100.64.0.0"), prefix: 10 }, // CGNAT (RFC 6598)
|
|
21
|
+
{ base: ipv4ToInt("127.0.0.0"), prefix: 8 }, // loopback
|
|
22
|
+
{ base: ipv4ToInt("169.254.0.0"), prefix: 16 }, // link-local
|
|
23
|
+
{ base: ipv4ToInt("172.16.0.0"), prefix: 12 }, // RFC1918 private
|
|
24
|
+
{ base: ipv4ToInt("192.0.0.0"), prefix: 24 }, // IETF protocol
|
|
25
|
+
{ base: ipv4ToInt("192.0.2.0"), prefix: 24 }, // TEST-NET-1
|
|
26
|
+
{ base: ipv4ToInt("192.88.99.0"), prefix: 24 }, // 6to4 anycast (deprecated)
|
|
27
|
+
{ base: ipv4ToInt("192.168.0.0"), prefix: 16 }, // RFC1918 private
|
|
28
|
+
{ base: ipv4ToInt("198.18.0.0"), prefix: 15 }, // benchmarking
|
|
29
|
+
{ base: ipv4ToInt("198.51.100.0"), prefix: 24 }, // TEST-NET-2
|
|
30
|
+
{ base: ipv4ToInt("203.0.113.0"), prefix: 24 }, // TEST-NET-3
|
|
31
|
+
{ base: ipv4ToInt("224.0.0.0"), prefix: 4 }, // multicast
|
|
32
|
+
{ base: ipv4ToInt("240.0.0.0"), prefix: 4 }, // reserved
|
|
33
|
+
{ base: ipv4ToInt("255.255.255.255"), prefix: 32 }, // broadcast
|
|
34
|
+
];
|
|
35
|
+
function isReservedV4(addr) {
|
|
36
|
+
let n;
|
|
37
|
+
try {
|
|
38
|
+
n = ipv4ToInt(addr);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
for (const b of V4_RESERVED) {
|
|
44
|
+
const mask = b.prefix === 0 ? 0 : (0xffffffff << (32 - b.prefix)) >>> 0;
|
|
45
|
+
if ((n & mask) === (b.base & mask))
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
function isReservedV6(addr) {
|
|
51
|
+
const a = addr.toLowerCase();
|
|
52
|
+
if (a === "::" || a === "::1")
|
|
53
|
+
return true;
|
|
54
|
+
const firstGroup = a.split(":")[0] ?? "";
|
|
55
|
+
// ULA fc00::/7 — first hex of first group is f, second is c or d
|
|
56
|
+
if (/^f[cd][0-9a-f]{0,2}$/.test(firstGroup))
|
|
57
|
+
return true;
|
|
58
|
+
// link-local fe80::/10 — first group starts fe8, fe9, fea, feb
|
|
59
|
+
if (/^fe[89ab][0-9a-f]{0,1}$/.test(firstGroup))
|
|
60
|
+
return true;
|
|
61
|
+
// documentation 2001:db8::/32
|
|
62
|
+
if (/^2001:0?db8(:|$)/.test(a))
|
|
63
|
+
return true;
|
|
64
|
+
// IPv4-mapped: ::ffff:x.x.x.x — check the embedded v4
|
|
65
|
+
const v4mapped = /^::ffff:([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)$/.exec(a);
|
|
66
|
+
if (v4mapped?.[1])
|
|
67
|
+
return isReservedV4(v4mapped[1]);
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
export class SsrfBlockedError extends Error {
|
|
71
|
+
hostInfo;
|
|
72
|
+
constructor(hostInfo) {
|
|
73
|
+
super(`Refusing to fetch ${hostInfo}: target resolves to a private or reserved IP range. ` +
|
|
74
|
+
`The FourA scraping API only forwards requests to public internet hosts.`);
|
|
75
|
+
this.hostInfo = hostInfo;
|
|
76
|
+
this.name = "SsrfBlockedError";
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
export async function assertPublicTarget(rawUrl) {
|
|
80
|
+
let url;
|
|
81
|
+
try {
|
|
82
|
+
url = new URL(rawUrl);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
throw new SsrfBlockedError(`invalid URL: ${rawUrl}`);
|
|
86
|
+
}
|
|
87
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
88
|
+
throw new SsrfBlockedError(`unsupported scheme ${url.protocol}`);
|
|
89
|
+
}
|
|
90
|
+
// Strip the [...] brackets from a bare IPv6 hostname
|
|
91
|
+
const host = url.hostname.replace(/^\[|\]$/g, "");
|
|
92
|
+
if (isIPv4(host)) {
|
|
93
|
+
if (isReservedV4(host))
|
|
94
|
+
throw new SsrfBlockedError(host);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (isIPv6(host)) {
|
|
98
|
+
if (isReservedV6(host))
|
|
99
|
+
throw new SsrfBlockedError(host);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
let addrs;
|
|
103
|
+
try {
|
|
104
|
+
addrs = await lookup(host, { all: true, verbatim: true });
|
|
105
|
+
}
|
|
106
|
+
catch (e) {
|
|
107
|
+
throw new SsrfBlockedError(`DNS lookup failed for ${host}: ${e.message}`);
|
|
108
|
+
}
|
|
109
|
+
if (addrs.length === 0) {
|
|
110
|
+
throw new SsrfBlockedError(`${host} resolved to no addresses`);
|
|
111
|
+
}
|
|
112
|
+
for (const a of addrs) {
|
|
113
|
+
const bad = a.family === 4 ? isReservedV4(a.address) : isReservedV6(a.address);
|
|
114
|
+
if (bad)
|
|
115
|
+
throw new SsrfBlockedError(`${host} → ${a.address}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=safe-target.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"safe-target.js","sourceRoot":"","sources":["../src/safe-target.ts"],"names":[],"mappings":"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"}
|
package/dist/server.d.ts
ADDED
package/dist/server.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { registerSingleTool } from "./tools/single.js";
|
|
3
|
+
import { registerProxyTool } from "./tools/proxy.js";
|
|
4
|
+
import { registerBrowserTool } from "./tools/browser.js";
|
|
5
|
+
import { registerResourceHandler } from "./resources.js";
|
|
6
|
+
export function createServer() {
|
|
7
|
+
const server = new McpServer({
|
|
8
|
+
name: "foura-mcp",
|
|
9
|
+
version: "0.1.1",
|
|
10
|
+
});
|
|
11
|
+
registerSingleTool(server);
|
|
12
|
+
registerProxyTool(server);
|
|
13
|
+
registerBrowserTool(server);
|
|
14
|
+
registerResourceHandler(server);
|
|
15
|
+
return server;
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAC;AAEzD,MAAM,UAAU,YAAY;IAC1B,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;QAC3B,IAAI,EAAE,WAAW;QACjB,OAAO,EAAE,OAAO;KACjB,CAAC,CAAC;IAEH,kBAAkB,CAAC,MAAM,CAAC,CAAC;IAC3B,iBAAiB,CAAC,MAAM,CAAC,CAAC;IAC1B,mBAAmB,CAAC,MAAM,CAAC,CAAC;IAC5B,uBAAuB,CAAC,MAAM,CAAC,CAAC;IAEhC,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
package/dist/stdio.d.ts
ADDED
package/dist/stdio.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { createServer } from "./server.js";
|
|
4
|
+
const server = createServer();
|
|
5
|
+
const transport = new StdioServerTransport();
|
|
6
|
+
await server.connect(transport);
|
|
7
|
+
//# sourceMappingURL=stdio.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stdio.js","sourceRoot":"","sources":["../src/stdio.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,MAAM,MAAM,GAAG,YAAY,EAAE,CAAC;AAC9B,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;AAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { request } from "undici";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { getApiKey } from "../auth.js";
|
|
4
|
+
import { assertPublicTarget, SsrfBlockedError } from "../safe-target.js";
|
|
5
|
+
import { storePayload, THRESHOLD_BYTES } from "../resources.js";
|
|
6
|
+
function extractContentTypeFromObject(headers) {
|
|
7
|
+
if (!headers || typeof headers !== "object")
|
|
8
|
+
return null;
|
|
9
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
10
|
+
if (k.toLowerCase() === "content-type" && typeof v === "string") {
|
|
11
|
+
return v.split(";")[0]?.trim() ?? null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
const BROWSER_API_URL = (process.env.FOURA_API_BASE ?? "https://api.foura.ai/api") + "/browser/";
|
|
17
|
+
const BrowserCookieInputSchema = z.object({
|
|
18
|
+
name: z.string(),
|
|
19
|
+
value: z.string(),
|
|
20
|
+
domain: z.string().optional(),
|
|
21
|
+
});
|
|
22
|
+
const browserInputShape = {
|
|
23
|
+
url: z.string().url().describe("Target URL to load in a full browser session"),
|
|
24
|
+
headers: z
|
|
25
|
+
.record(z.string(), z.string())
|
|
26
|
+
.optional()
|
|
27
|
+
.describe("Custom HTTP headers as a key-value object. Note: object shape, not [name, value] tuples."),
|
|
28
|
+
cookies: z
|
|
29
|
+
.array(BrowserCookieInputSchema)
|
|
30
|
+
.optional()
|
|
31
|
+
.describe("Cookies to set before navigation: [{ name, value, domain? }]"),
|
|
32
|
+
userAgent: z.string().optional().describe("Override the browser's User-Agent string"),
|
|
33
|
+
proxy: z.string().optional().describe("Optional proxy URL for the browser (http://, socks5://)"),
|
|
34
|
+
timeout_ms: z
|
|
35
|
+
.number()
|
|
36
|
+
.int()
|
|
37
|
+
.min(0)
|
|
38
|
+
.max(120_000)
|
|
39
|
+
.optional()
|
|
40
|
+
.describe("Page load timeout in ms (default 30000, max 120000)"),
|
|
41
|
+
checkStatus: z
|
|
42
|
+
.number()
|
|
43
|
+
.int()
|
|
44
|
+
.optional()
|
|
45
|
+
.describe("Expected HTTP status code; request fails if the page returns a different status"),
|
|
46
|
+
checkText: z
|
|
47
|
+
.string()
|
|
48
|
+
.optional()
|
|
49
|
+
.describe("Substring that must appear in the rendered HTML for the request to count as success"),
|
|
50
|
+
};
|
|
51
|
+
export function registerBrowserTool(server) {
|
|
52
|
+
server.registerTool("foura_browser", {
|
|
53
|
+
title: "FourA — full browser navigation",
|
|
54
|
+
description: "Load a page in a full browser session. JavaScript executes, the DOM renders fully, and cookies " +
|
|
55
|
+
"come back with the response. Use for single-page apps, lazy-loaded content, or pages behind " +
|
|
56
|
+
"anti-bot challenges like Cloudflare. Slowest of the three tools (typically 2–10s). For plain " +
|
|
57
|
+
"static pages prefer foura_single; for static pages where you're being blocked prefer foura_proxy.",
|
|
58
|
+
inputSchema: browserInputShape,
|
|
59
|
+
}, async (input) => {
|
|
60
|
+
try {
|
|
61
|
+
await assertPublicTarget(input.url);
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
if (e instanceof SsrfBlockedError) {
|
|
65
|
+
return { isError: true, content: [{ type: "text", text: e.message }] };
|
|
66
|
+
}
|
|
67
|
+
throw e;
|
|
68
|
+
}
|
|
69
|
+
const res = await request(BROWSER_API_URL, {
|
|
70
|
+
method: "POST",
|
|
71
|
+
headers: {
|
|
72
|
+
"X-API-Key": getApiKey(),
|
|
73
|
+
"Content-Type": "application/json",
|
|
74
|
+
"User-Agent": "foura-mcp/0.1.1 (browser)",
|
|
75
|
+
},
|
|
76
|
+
body: JSON.stringify(input),
|
|
77
|
+
});
|
|
78
|
+
const text = await res.body.text();
|
|
79
|
+
let parsed;
|
|
80
|
+
try {
|
|
81
|
+
parsed = JSON.parse(text);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return {
|
|
85
|
+
isError: true,
|
|
86
|
+
content: [
|
|
87
|
+
{
|
|
88
|
+
type: "text",
|
|
89
|
+
text: `FourA browser — non-JSON response (${res.statusCode}): ${text.slice(0, 200)}`,
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
95
|
+
const e = parsed;
|
|
96
|
+
const hint = e.retryAfter !== undefined ? ` Retry after ${e.retryAfter}s.` : "";
|
|
97
|
+
return {
|
|
98
|
+
isError: true,
|
|
99
|
+
content: [
|
|
100
|
+
{
|
|
101
|
+
type: "text",
|
|
102
|
+
text: `FourA browser error (${res.statusCode}): ${e.error ?? "Unknown"}.${hint}\n\n${JSON.stringify(parsed, null, 2)}`,
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const parsedObj = parsed;
|
|
108
|
+
const body = parsedObj.body;
|
|
109
|
+
if (typeof body === "string" && Buffer.byteLength(body, "utf8") >= THRESHOLD_BYTES) {
|
|
110
|
+
const ct = extractContentTypeFromObject(parsedObj.headers) ?? "text/html";
|
|
111
|
+
const stored = await storePayload(body, ct, "rendered-page.html");
|
|
112
|
+
const preview = body.slice(0, 500);
|
|
113
|
+
const trimmed = { ...parsedObj, body: `<offloaded ${stored.size} bytes — see resource ${stored.uri}>` };
|
|
114
|
+
return {
|
|
115
|
+
content: [
|
|
116
|
+
{ type: "text", text: JSON.stringify(trimmed, null, 2) },
|
|
117
|
+
{ type: "text", text: `Rendered page preview (${(stored.size / 1024).toFixed(1)} KB total):\n${preview}` },
|
|
118
|
+
{ type: "resource_link", uri: stored.uri, name: stored.name, mimeType: stored.mimeType },
|
|
119
|
+
],
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
content: [{ type: "text", text: JSON.stringify(parsed, null, 2) }],
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=browser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser.js","sourceRoot":"","sources":["../../src/tools/browser.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { request } from "undici";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { getApiKey } from "../auth.js";
|
|
4
|
+
import { assertPublicTarget, SsrfBlockedError } from "../safe-target.js";
|
|
5
|
+
import { storePayload, THRESHOLD_BYTES } from "../resources.js";
|
|
6
|
+
function extractContentTypeFromHeaderInfo(headers) {
|
|
7
|
+
if (!Array.isArray(headers))
|
|
8
|
+
return null;
|
|
9
|
+
for (const h of headers) {
|
|
10
|
+
if (h && typeof h === "object") {
|
|
11
|
+
const ct = h["content-type"];
|
|
12
|
+
if (typeof ct === "string")
|
|
13
|
+
return ct.split(";")[0]?.trim() ?? null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
const PROXY_API_URL = (process.env.FOURA_API_BASE ?? "https://api.foura.ai/api") + "/proxy/";
|
|
19
|
+
const ProxyHttpMethod = z.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
|
|
20
|
+
const ProxyValidateSchema = z
|
|
21
|
+
.object({
|
|
22
|
+
status: z
|
|
23
|
+
.object({
|
|
24
|
+
accept: z.array(z.number().int()).optional().describe("HTTP status codes to treat as success"),
|
|
25
|
+
fail: z.array(z.number().int()).optional().describe("HTTP status codes to treat as failure"),
|
|
26
|
+
})
|
|
27
|
+
.optional(),
|
|
28
|
+
headers: z
|
|
29
|
+
.object({
|
|
30
|
+
accept: z.record(z.string(), z.string()).optional(),
|
|
31
|
+
fail: z.record(z.string(), z.string()).optional(),
|
|
32
|
+
})
|
|
33
|
+
.optional(),
|
|
34
|
+
data: z
|
|
35
|
+
.object({
|
|
36
|
+
accept: z.array(z.string()).optional(),
|
|
37
|
+
fail: z.array(z.string()).optional(),
|
|
38
|
+
})
|
|
39
|
+
.optional(),
|
|
40
|
+
})
|
|
41
|
+
.optional();
|
|
42
|
+
const ProxyInnerRequestSchema = z
|
|
43
|
+
.object({
|
|
44
|
+
method: ProxyHttpMethod.describe("HTTP method"),
|
|
45
|
+
url: z.string().url().describe("Target URL. {ts} placeholder is replaced with current Unix timestamp."),
|
|
46
|
+
headers: z
|
|
47
|
+
.array(z.tuple([z.string(), z.string()]))
|
|
48
|
+
.optional()
|
|
49
|
+
.describe("Custom HTTP headers as [name, value] tuples"),
|
|
50
|
+
unblocker: z
|
|
51
|
+
.boolean()
|
|
52
|
+
.optional()
|
|
53
|
+
.describe("Inject realistic browser headers (User-Agent, etc.) — strongly recommended when going through proxies."),
|
|
54
|
+
data: z
|
|
55
|
+
.union([z.string(), z.record(z.string(), z.unknown())])
|
|
56
|
+
.optional()
|
|
57
|
+
.describe("Request body."),
|
|
58
|
+
timeout_ms: z.number().int().min(0).max(120_000).optional().describe("Per-attempt timeout in ms"),
|
|
59
|
+
connect_timeout_ms: z.number().int().min(0).max(120_000).optional(),
|
|
60
|
+
accept_timeout_ms: z.number().int().min(0).max(120_000).optional(),
|
|
61
|
+
server_response_timeout_ms: z.number().int().min(0).max(120_000).optional(),
|
|
62
|
+
dns_cache_timeout_sec: z.number().int().min(0).max(240).optional(),
|
|
63
|
+
followRedirects: z.number().int().min(0).max(20).optional(),
|
|
64
|
+
tryJsonData: z.boolean().optional(),
|
|
65
|
+
returnBuffer: z.boolean().optional(),
|
|
66
|
+
validate: ProxyValidateSchema,
|
|
67
|
+
})
|
|
68
|
+
.describe("The inner HTTP request to send through each proxy attempt. Validation rules here determine when a proxy is treated as failed and retried.");
|
|
69
|
+
const proxyInputShape = {
|
|
70
|
+
request: ProxyInnerRequestSchema,
|
|
71
|
+
maxTries: z
|
|
72
|
+
.number()
|
|
73
|
+
.int()
|
|
74
|
+
.min(1)
|
|
75
|
+
.max(90)
|
|
76
|
+
.optional()
|
|
77
|
+
.describe("Maximum proxy rotation attempts before giving up (default 5, max 90)"),
|
|
78
|
+
timeout_ms: z
|
|
79
|
+
.number()
|
|
80
|
+
.int()
|
|
81
|
+
.min(0)
|
|
82
|
+
.max(120_000)
|
|
83
|
+
.optional()
|
|
84
|
+
.describe("Overall timeout across all rotation attempts in ms (default 45000, max 120000)"),
|
|
85
|
+
ignoreProxies: z
|
|
86
|
+
.array(z.string())
|
|
87
|
+
.optional()
|
|
88
|
+
.describe("Encoded proxy IDs to exclude from rotation (e.g. ones that failed earlier in your workflow)"),
|
|
89
|
+
};
|
|
90
|
+
export function registerProxyTool(server) {
|
|
91
|
+
server.registerTool("foura_proxy", {
|
|
92
|
+
title: "FourA — HTTP request via rotating proxies",
|
|
93
|
+
description: "Route an HTTP request through FourA's proxy pool with automatic retry across multiple proxies. " +
|
|
94
|
+
"Per-host proxy rating picks proxies most likely to succeed for the target. Use when foura_single " +
|
|
95
|
+
"returns 403, captcha, or geo-blocked content. Typical latency 1–5s. The response includes the " +
|
|
96
|
+
"encoded proxy ID that succeeded ('proxy' field) and the total number of attempts.",
|
|
97
|
+
inputSchema: proxyInputShape,
|
|
98
|
+
}, async (input) => {
|
|
99
|
+
try {
|
|
100
|
+
await assertPublicTarget(input.request.url);
|
|
101
|
+
}
|
|
102
|
+
catch (e) {
|
|
103
|
+
if (e instanceof SsrfBlockedError) {
|
|
104
|
+
return { isError: true, content: [{ type: "text", text: e.message }] };
|
|
105
|
+
}
|
|
106
|
+
throw e;
|
|
107
|
+
}
|
|
108
|
+
const res = await request(PROXY_API_URL, {
|
|
109
|
+
method: "POST",
|
|
110
|
+
headers: {
|
|
111
|
+
"X-API-Key": getApiKey(),
|
|
112
|
+
"Content-Type": "application/json",
|
|
113
|
+
"User-Agent": "foura-mcp/0.1.1 (proxy)",
|
|
114
|
+
},
|
|
115
|
+
body: JSON.stringify(input),
|
|
116
|
+
});
|
|
117
|
+
const text = await res.body.text();
|
|
118
|
+
let parsed;
|
|
119
|
+
try {
|
|
120
|
+
parsed = JSON.parse(text);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return {
|
|
124
|
+
isError: true,
|
|
125
|
+
content: [
|
|
126
|
+
{
|
|
127
|
+
type: "text",
|
|
128
|
+
text: `FourA proxy — non-JSON response (${res.statusCode}): ${text.slice(0, 200)}`,
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
134
|
+
const e = parsed;
|
|
135
|
+
const hint = e.retryAfter !== undefined ? ` Retry after ${e.retryAfter}s.` : "";
|
|
136
|
+
return {
|
|
137
|
+
isError: true,
|
|
138
|
+
content: [
|
|
139
|
+
{
|
|
140
|
+
type: "text",
|
|
141
|
+
text: `FourA proxy error (${res.statusCode}): ${e.error ?? "Unknown"}.${hint}\n\n${JSON.stringify(parsed, null, 2)}`,
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
const parsedObj = parsed;
|
|
147
|
+
const data = parsedObj.data;
|
|
148
|
+
let bodyStr = null;
|
|
149
|
+
if (typeof data === "string")
|
|
150
|
+
bodyStr = data;
|
|
151
|
+
else if (data && typeof data === "object")
|
|
152
|
+
bodyStr = JSON.stringify(data);
|
|
153
|
+
if (bodyStr && Buffer.byteLength(bodyStr, "utf8") >= THRESHOLD_BYTES) {
|
|
154
|
+
const ct = extractContentTypeFromHeaderInfo(parsedObj.headers) ?? "text/plain";
|
|
155
|
+
const stored = await storePayload(bodyStr, ct, "response-body");
|
|
156
|
+
const preview = bodyStr.slice(0, 500);
|
|
157
|
+
const trimmed = { ...parsedObj, data: `<offloaded ${stored.size} bytes — see resource ${stored.uri}>` };
|
|
158
|
+
return {
|
|
159
|
+
content: [
|
|
160
|
+
{ type: "text", text: JSON.stringify(trimmed, null, 2) },
|
|
161
|
+
{ type: "text", text: `Body preview (${(stored.size / 1024).toFixed(1)} KB total):\n${preview}` },
|
|
162
|
+
{ type: "resource_link", uri: stored.uri, name: stored.name, mimeType: stored.mimeType },
|
|
163
|
+
],
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
content: [{ type: "text", text: JSON.stringify(parsed, null, 2) }],
|
|
168
|
+
};
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
//# sourceMappingURL=proxy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proxy.js","sourceRoot":"","sources":["../../src/tools/proxy.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { request } from "undici";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { getApiKey } from "../auth.js";
|
|
4
|
+
import { assertPublicTarget, SsrfBlockedError } from "../safe-target.js";
|
|
5
|
+
import { storePayload, THRESHOLD_BYTES } from "../resources.js";
|
|
6
|
+
function extractContentTypeFromHeaderInfo(headers) {
|
|
7
|
+
if (!Array.isArray(headers))
|
|
8
|
+
return null;
|
|
9
|
+
for (const h of headers) {
|
|
10
|
+
if (h && typeof h === "object") {
|
|
11
|
+
const ct = h["content-type"];
|
|
12
|
+
if (typeof ct === "string")
|
|
13
|
+
return ct.split(";")[0]?.trim() ?? null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
const SINGLE_API_URL = (process.env.FOURA_API_BASE ?? "https://api.foura.ai/api") + "/single/";
|
|
19
|
+
const SingleHttpMethod = z.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
|
|
20
|
+
const SingleValidateSchema = z
|
|
21
|
+
.object({
|
|
22
|
+
status: z
|
|
23
|
+
.object({
|
|
24
|
+
accept: z.array(z.number().int()).optional().describe("HTTP status codes to treat as success"),
|
|
25
|
+
fail: z.array(z.number().int()).optional().describe("HTTP status codes to treat as failure"),
|
|
26
|
+
})
|
|
27
|
+
.optional(),
|
|
28
|
+
headers: z
|
|
29
|
+
.object({
|
|
30
|
+
accept: z.record(z.string(), z.string()).optional(),
|
|
31
|
+
fail: z.record(z.string(), z.string()).optional(),
|
|
32
|
+
})
|
|
33
|
+
.optional(),
|
|
34
|
+
data: z
|
|
35
|
+
.object({
|
|
36
|
+
accept: z.array(z.string()).optional().describe("Substrings the response body must contain"),
|
|
37
|
+
fail: z.array(z.string()).optional().describe("Substrings the response body must NOT contain"),
|
|
38
|
+
})
|
|
39
|
+
.optional(),
|
|
40
|
+
})
|
|
41
|
+
.optional();
|
|
42
|
+
const singleInputShape = {
|
|
43
|
+
method: SingleHttpMethod.describe("HTTP method"),
|
|
44
|
+
url: z.string().url().describe("Target URL. Use {ts} anywhere in the URL to insert current Unix timestamp (cache-bust)."),
|
|
45
|
+
headers: z
|
|
46
|
+
.array(z.tuple([z.string(), z.string()]))
|
|
47
|
+
.optional()
|
|
48
|
+
.describe("Custom HTTP headers as [name, value] tuples"),
|
|
49
|
+
unblocker: z
|
|
50
|
+
.boolean()
|
|
51
|
+
.optional()
|
|
52
|
+
.describe("Inject realistic browser headers (User-Agent, Sec-Ch-Ua, Accept-Encoding, …) and make the request look like it's coming from a real browser at the wire level. Recommended for sites with anti-bot protection."),
|
|
53
|
+
data: z
|
|
54
|
+
.union([z.string(), z.record(z.string(), z.unknown())])
|
|
55
|
+
.optional()
|
|
56
|
+
.describe("Request body. Strings sent as-is; objects auto-serialized to JSON."),
|
|
57
|
+
proxy: z
|
|
58
|
+
.string()
|
|
59
|
+
.optional()
|
|
60
|
+
.describe("Optional proxy URL (http://, socks4://, socks5://). For automatic proxy rotation use foura_proxy instead."),
|
|
61
|
+
timeout_ms: z.number().int().min(0).max(120_000).optional().describe("Overall request timeout in ms (max 120000, default 15000)"),
|
|
62
|
+
connect_timeout_ms: z.number().int().min(0).max(120_000).optional(),
|
|
63
|
+
accept_timeout_ms: z.number().int().min(0).max(120_000).optional(),
|
|
64
|
+
server_response_timeout_ms: z.number().int().min(0).max(120_000).optional(),
|
|
65
|
+
dns_cache_timeout_sec: z.number().int().min(0).max(240).optional(),
|
|
66
|
+
followRedirects: z
|
|
67
|
+
.number()
|
|
68
|
+
.int()
|
|
69
|
+
.min(0)
|
|
70
|
+
.max(20)
|
|
71
|
+
.optional()
|
|
72
|
+
.describe("Max number of redirects to follow (0-20). Omit to disable redirect following."),
|
|
73
|
+
tryJsonData: z.boolean().optional().describe("If true, attempt JSON.parse on response body; on success, data is an object."),
|
|
74
|
+
returnBuffer: z.boolean().optional().describe("Return raw bytes (base64) instead of decoded string. Use for binary responses (images, protobuf)."),
|
|
75
|
+
validate: SingleValidateSchema,
|
|
76
|
+
};
|
|
77
|
+
export function registerSingleTool(server) {
|
|
78
|
+
server.registerTool("foura_single", {
|
|
79
|
+
title: "FourA — single HTTP request",
|
|
80
|
+
description: "Send one HTTP request and return the response. Fastest of the three tools (typically 200ms–2s). " +
|
|
81
|
+
"Use for static pages, JSON APIs, server-rendered HTML. Set unblocker:true if the target has " +
|
|
82
|
+
"wire-level anti-bot protection. Switch to foura_proxy if you get blocked (403, captcha), or to " +
|
|
83
|
+
"foura_browser if the page needs JavaScript to render.",
|
|
84
|
+
inputSchema: singleInputShape,
|
|
85
|
+
}, async (input) => {
|
|
86
|
+
try {
|
|
87
|
+
await assertPublicTarget(input.url);
|
|
88
|
+
}
|
|
89
|
+
catch (e) {
|
|
90
|
+
if (e instanceof SsrfBlockedError) {
|
|
91
|
+
return { isError: true, content: [{ type: "text", text: e.message }] };
|
|
92
|
+
}
|
|
93
|
+
throw e;
|
|
94
|
+
}
|
|
95
|
+
const res = await request(SINGLE_API_URL, {
|
|
96
|
+
method: "POST",
|
|
97
|
+
headers: {
|
|
98
|
+
"X-API-Key": getApiKey(),
|
|
99
|
+
"Content-Type": "application/json",
|
|
100
|
+
"User-Agent": "foura-mcp/0.1.1 (single)",
|
|
101
|
+
},
|
|
102
|
+
body: JSON.stringify(input),
|
|
103
|
+
});
|
|
104
|
+
const text = await res.body.text();
|
|
105
|
+
let parsed;
|
|
106
|
+
try {
|
|
107
|
+
parsed = JSON.parse(text);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return {
|
|
111
|
+
isError: true,
|
|
112
|
+
content: [
|
|
113
|
+
{
|
|
114
|
+
type: "text",
|
|
115
|
+
text: `FourA single — non-JSON response (${res.statusCode}): ${text.slice(0, 200)}`,
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
121
|
+
const e = parsed;
|
|
122
|
+
const hint = e.retryAfter !== undefined ? ` Retry after ${e.retryAfter}s.` : "";
|
|
123
|
+
return {
|
|
124
|
+
isError: true,
|
|
125
|
+
content: [
|
|
126
|
+
{
|
|
127
|
+
type: "text",
|
|
128
|
+
text: `FourA single error (${res.statusCode}): ${e.error ?? "Unknown"}.${hint}\n\n${JSON.stringify(parsed, null, 2)}`,
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
const parsedObj = parsed;
|
|
134
|
+
const data = parsedObj.data;
|
|
135
|
+
let bodyStr = null;
|
|
136
|
+
if (typeof data === "string")
|
|
137
|
+
bodyStr = data;
|
|
138
|
+
else if (data && typeof data === "object")
|
|
139
|
+
bodyStr = JSON.stringify(data);
|
|
140
|
+
if (bodyStr && Buffer.byteLength(bodyStr, "utf8") >= THRESHOLD_BYTES) {
|
|
141
|
+
const ct = extractContentTypeFromHeaderInfo(parsedObj.headers) ?? "text/plain";
|
|
142
|
+
const stored = await storePayload(bodyStr, ct, "response-body");
|
|
143
|
+
const preview = bodyStr.slice(0, 500);
|
|
144
|
+
const trimmed = { ...parsedObj, data: `<offloaded ${stored.size} bytes — see resource ${stored.uri}>` };
|
|
145
|
+
return {
|
|
146
|
+
content: [
|
|
147
|
+
{ type: "text", text: JSON.stringify(trimmed, null, 2) },
|
|
148
|
+
{ type: "text", text: `Body preview (${(stored.size / 1024).toFixed(1)} KB total):\n${preview}` },
|
|
149
|
+
{ type: "resource_link", uri: stored.uri, name: stored.name, mimeType: stored.mimeType },
|
|
150
|
+
],
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
content: [{ type: "text", text: JSON.stringify(parsed, null, 2) }],
|
|
155
|
+
};
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
//# sourceMappingURL=single.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"single.js","sourceRoot":"","sources":["../../src/tools/single.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fouradata/mcp",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "MCP server for the FourA web scraping API — three tools for HTTP requests, rotating proxies, and full browser sessions, usable from any MCP client.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/server.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"foura-mcp": "bin/foura-mcp.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin",
|
|
12
|
+
"dist",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc",
|
|
17
|
+
"dev": "tsx src/stdio.ts",
|
|
18
|
+
"dev:http": "tsx src/http.ts",
|
|
19
|
+
"start:http": "node dist/http.js",
|
|
20
|
+
"test": "tsx --test tests/**/*.test.ts",
|
|
21
|
+
"smoke": "node tests/smoke.mjs",
|
|
22
|
+
"inspector": "npx -y @modelcontextprotocol/inspector tsx src/stdio.ts",
|
|
23
|
+
"lint": "tsc --noEmit",
|
|
24
|
+
"prepublishOnly": "npm run lint && npm run build"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
28
|
+
"express": "^5.2.1",
|
|
29
|
+
"undici": "^8.2.0",
|
|
30
|
+
"zod": "^4.4.3"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/express": "^5.0.6",
|
|
34
|
+
"@types/node": "^25.7.0",
|
|
35
|
+
"tsx": "^4.21.0",
|
|
36
|
+
"typescript": "^6.0.3"
|
|
37
|
+
},
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=20.0.0"
|
|
40
|
+
},
|
|
41
|
+
"keywords": [
|
|
42
|
+
"mcp",
|
|
43
|
+
"model-context-protocol",
|
|
44
|
+
"foura",
|
|
45
|
+
"web-scraping",
|
|
46
|
+
"scraping-api",
|
|
47
|
+
"rotating-proxies",
|
|
48
|
+
"anti-bot",
|
|
49
|
+
"browser-automation"
|
|
50
|
+
],
|
|
51
|
+
"license": "MIT"
|
|
52
|
+
}
|