capskip-mcp 1.0.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/CHANGELOG.md ADDED
@@ -0,0 +1,28 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here.
4
+
5
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ Nothing yet. Add entries here as changes land; they move into a version
11
+ section at release time.
12
+
13
+ ## [1.0.0] - 2026-08-09
14
+
15
+ ### Added
16
+
17
+ - `capskip_status` — check that the CapSkip desktop app is running and reachable.
18
+ - `capskip_solve_image_captcha` — read the text from a distorted-text captcha.
19
+ - `capskip_solve_recaptcha` — reCAPTCHA v2 and v3, including invisible and Enterprise.
20
+ - `capskip_solve_turnstile` — Cloudflare Turnstile widgets and challenge pages,
21
+ returning the User-Agent the token must be submitted with.
22
+ - `capskip_solve_geetest` — GeeTest v3 sliders, returning `geetest_challenge`,
23
+ `geetest_validate`, and `geetest_seccode`.
24
+ - Progress notifications during long solves, so MCP clients do not time out.
25
+ - Configuration via `CAPSKIP_*` environment variables or CLI flags.
26
+
27
+ [Unreleased]: https://github.com/capskip/capskip-mcp/compare/v1.0.0...HEAD
28
+ [1.0.0]: https://github.com/capskip/capskip-mcp/releases/tag/v1.0.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 CapSkip
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,255 @@
1
+ # CapSkip MCP Server — Unlimited Captcha Solver for AI Agents
2
+
3
+ [![Node.js 18+](https://img.shields.io/badge/node-18%2B-brightgreen.svg)](https://nodejs.org/)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
5
+ [![Tests](https://github.com/capskip/capskip-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/capskip/capskip-mcp/actions/workflows/ci.yml)
6
+ [![npm](https://img.shields.io/npm/v/capskip-mcp.svg)](https://www.npmjs.com/package/capskip-mcp)
7
+
8
+ **A captcha solver [MCP](https://modelcontextprotocol.io) server that lets AI agents solve reCAPTCHA, Cloudflare Turnstile, GeeTest and image captchas instead of stalling on them.**
9
+
10
+ Works with Claude Desktop, Claude Code, Cursor, VS Code, and any Model Context Protocol client. Powered by [CapSkip](https://capskip.com) — a **local captcha solver** that runs on your own machine, licensed once rather than billed per solve.
11
+
12
+ ```bash
13
+ npx -y capskip-mcp
14
+ ```
15
+
16
+ ---
17
+
18
+ ## What this solves
19
+
20
+ An AI agent driving a browser hits a captcha and stops. This server gives it five tools so it can read the sitekey, solve the challenge, and carry on — without a human stepping in and without a per-solve API bill.
21
+
22
+ CapSkip runs as a desktop app exposing a captcha-solving HTTP API on `127.0.0.1:8080`. `capskip-mcp` is a thin translation layer over that API: the fifth official CapSkip client, alongside the [Python](https://github.com/capskip/capskip-python), [Node.js](https://github.com/capskip/capskip-node), [PHP](https://github.com/capskip/capskip-php) and [.NET](https://github.com/capskip/capskip-dotnet) SDKs.
23
+
24
+ ## Supported captcha types
25
+
26
+ | Captcha | Tool | Notes |
27
+ |---|---|---|
28
+ | **reCAPTCHA v2 solver** (checkbox) | `capskip_solve_recaptcha` | Returns a `g-recaptcha-response` token |
29
+ | **reCAPTCHA v2 invisible solver** | `capskip_solve_recaptcha` | Pass `invisible: true` |
30
+ | **reCAPTCHA Enterprise solver** | `capskip_solve_recaptcha` | Pass `enterprise: true`, works with v2 and v3 |
31
+ | **reCAPTCHA v3 solver** | `capskip_solve_recaptcha` | Pass `version: "v3"` and the page's `action` |
32
+ | **Cloudflare Turnstile solver** | `capskip_solve_turnstile` | Widget and interstitial challenge pages |
33
+ | **GeeTest v3 solver** | `capskip_solve_geetest` | Slide puzzle; returns challenge/validate/seccode |
34
+ | **Image captcha solver** (text/OCR) | `capskip_solve_image_captcha` | File path, URL, data URI, or base64 |
35
+
36
+ **Not supported: hCaptcha and FunCaptcha/Arkose.** There is no tool for them and `capskip_solve_recaptcha` will not work on one. hCaptcha is the easiest to misidentify since it also carries a `data-sitekey` — check for `class="h-captcha"` or a `js.hcaptcha.com` script first.
37
+
38
+ Try them against live widgets on the [captcha demo pages](https://capskip.com/captcha-demo/).
39
+
40
+ ---
41
+
42
+ ## Quick start (5 minutes)
43
+
44
+ ### 1. Install the CapSkip captcha solver
45
+
46
+ Download and run the CapSkip desktop app from [capskip.com](https://capskip.com). Leave it running in the background.
47
+
48
+ In CapSkip settings, note the **API port** (default `8080`) and **API key** (optional — if key validation is disabled, any string works).
49
+
50
+ ### 2. Add capskip-mcp to your MCP client
51
+
52
+ No install step — `npx` fetches and runs it on demand.
53
+
54
+ ```json
55
+ {
56
+ "mcpServers": {
57
+ "capskip": {
58
+ "command": "npx",
59
+ "args": ["-y", "capskip-mcp"],
60
+ "env": {
61
+ "CAPSKIP_HOST": "127.0.0.1",
62
+ "CAPSKIP_PORT": "8080",
63
+ "CAPSKIP_API_KEY": "capskip"
64
+ }
65
+ }
66
+ }
67
+ }
68
+ ```
69
+
70
+ | MCP client | Config file | Example |
71
+ |---|---|---|
72
+ | Claude Desktop | `claude_desktop_config.json` | [examples/claude-desktop.json](examples/claude-desktop.json) |
73
+ | Claude Code | `claude mcp add` (CLI) | [examples/claude-code.md](examples/claude-code.md) |
74
+ | Cursor | `.cursor/mcp.json` | [examples/cursor.json](examples/cursor.json) |
75
+ | VS Code | `.vscode/mcp.json` | [examples/vscode.json](examples/vscode.json) |
76
+
77
+ ### 3. Restart your client
78
+
79
+ It should list five tools, all prefixed `capskip_`.
80
+
81
+ ### 4. Ask your agent to solve a captcha
82
+
83
+ > "Call capskip_status to confirm CapSkip is running, then solve the reCAPTCHA on this page and submit the form."
84
+
85
+ The agent reads the sitekey off the page, calls `capskip_solve_recaptcha`, and places the returned token in the page's `g-recaptcha-response` field.
86
+
87
+ ---
88
+
89
+ ## Why a local captcha solver
90
+
91
+ Cloud captcha APIs bill per solve, so an agent that retries is an agent that costs money, and every page URL and sitekey you solve leaves your network.
92
+
93
+ CapSkip runs on your machine:
94
+
95
+ - **Unlimited captcha solving** — licensed once, no per-solve fees, no credit balance to top up
96
+ - **Local by default** — the solver talks to `127.0.0.1`; nothing is proxied through a third-party queue
97
+ - **No rate limit per key** — throughput is bounded by your machine, not a vendor's plan tier
98
+ - **Fast** — image captchas return in well under a second; typical reCAPTCHA v2 solves land in 30–45s
99
+
100
+ ### Using an existing 2captcha or Anti-Captcha integration?
101
+
102
+ CapSkip exposes the familiar `in.php` / `res.php` endpoints, so it works as a **2captcha API alternative** — point your existing client at `127.0.0.1:8080` and keep your code. See the [migration notes](https://capskip.com/2captcha-api-alternative/). This MCP server is the equivalent for AI agents rather than scripts.
103
+
104
+ ---
105
+
106
+ ## Tools
107
+
108
+ | Tool | Purpose | Required arguments |
109
+ |---|---|---|
110
+ | `capskip_status` | Check whether the CapSkip desktop app is running and reachable | none |
111
+ | `capskip_solve_image_captcha` | Read the text out of a distorted-text captcha image | `image` |
112
+ | `capskip_solve_recaptcha` | Solve reCAPTCHA v2 or v3, including invisible and Enterprise | `sitekey`, `url` |
113
+ | `capskip_solve_turnstile` | Solve a Cloudflare Turnstile widget or challenge page | `sitekey`, `url` |
114
+ | `capskip_solve_geetest` | Solve a GeeTest v3 slide-puzzle captcha | `gt`, `challenge`, `url` |
115
+
116
+ > **There is no `min_score` parameter on `capskip_solve_recaptcha`.** reCAPTCHA v3 scores are assigned by Google from signals no solver has access to — local or cloud, none can raise a score after the fact. A `min_score` option would promise control that does not exist, so it is deliberately left out. Passing it anyway is rejected as an unrecognized key, not silently ignored.
117
+
118
+ Full parameter tables and worked examples: [API Reference](docs/API_REFERENCE.md).
119
+
120
+ | Guide | Description |
121
+ |---|---|
122
+ | [Tutorial](docs/TUTORIAL.md) | Every captcha type — how to recognize it, what to read off the page, what call to make, where the answer goes |
123
+ | [Getting Started](docs/GETTING_STARTED.md) | Full setup: CapSkip app, client config, first solve |
124
+ | [API Reference](docs/API_REFERENCE.md) | Every tool, parameter, and return shape |
125
+ | [Troubleshooting](docs/TROUBLESHOOTING.md) | Connection errors, timeouts, rejected tokens |
126
+
127
+ ---
128
+
129
+ ## Browser automation: Playwright, Puppeteer and Selenium
130
+
131
+ This server solves the captcha and hands back a token; your agent's existing browser tooling does the driving. The pattern is the same whichever you use:
132
+
133
+ 1. Read the sitekey from the page (`data-sitekey`, or the widget's config object).
134
+ 2. Call the matching `capskip_solve_*` tool with that sitekey and the page URL.
135
+ 3. Write the token into the response field and submit.
136
+
137
+ ```js
138
+ // The agent does this via its browser tool after capskip_solve_recaptcha returns
139
+ document.querySelector('#g-recaptcha-response').value = TOKEN;
140
+ ```
141
+
142
+ For non-agent scripts, use the language SDKs directly — see the [Playwright](https://capskip.com/playwright-captcha-solver/), [Puppeteer](https://capskip.com/puppeteer-captcha-solver/) and [Selenium](https://capskip.com/selenium-captcha-solver/) guides.
143
+
144
+ ---
145
+
146
+ ## Configuration
147
+
148
+ | Variable | Default | Meaning |
149
+ |---|---|---|
150
+ | `CAPSKIP_API_KEY` | `capskip` | Any string when key validation is off |
151
+ | `CAPSKIP_HOST` | `127.0.0.1` | CapSkip host |
152
+ | `CAPSKIP_PORT` | `8080` | API port from CapSkip settings |
153
+ | `CAPSKIP_TIMEOUT` | `120` | Default `timeout` for `capskip_solve_image_captcha`, seconds |
154
+ | `CAPSKIP_RECAPTCHA_TIMEOUT` | `300` | Default `timeout` for the reCAPTCHA / Turnstile / GeeTest tools, seconds |
155
+ | `CAPSKIP_POLLING_INTERVAL` | `5` | Max seconds between polls |
156
+
157
+ CLI flags override environment variables, which override the defaults:
158
+
159
+ ```
160
+ capskip-mcp --api-key <key> --host <host> --port <port> --timeout <seconds> \
161
+ --recaptcha-timeout <seconds> --polling-interval <seconds>
162
+ ```
163
+
164
+ An invalid value (non-numeric port, port outside 1–65535, a negative or out-of-range timeout, an unknown flag) fails at startup naming the offending flag or variable, rather than surfacing later as a confusing solve failure.
165
+
166
+ ---
167
+
168
+ ## What you get back
169
+
170
+ Every solve tool returns a human-readable text block and `structuredContent` matching its declared output schema:
171
+
172
+ ```json
173
+ {
174
+ "captchaId": "12345",
175
+ "code": "03AGdBq26f...",
176
+ "solveSeconds": 11.8
177
+ }
178
+ ```
179
+
180
+ - **`capskip_solve_turnstile`** adds `userAgent`. Submit the token with this exact User-Agent — Cloudflare rejects a token replayed under a different one.
181
+ - **`capskip_solve_geetest`** adds `challenge`, `validate` and `seccode`, to post back exactly as the site's own front-end would; `code` keeps the raw JSON string CapSkip returns.
182
+
183
+ Long solves emit MCP progress notifications, so a 45-second reCAPTCHA does not trip your client's tool-call timeout.
184
+
185
+ ---
186
+
187
+ ## Errors
188
+
189
+ Every tool call returns `isError: true` with readable text on failure — never a stack trace or a bare protocol error — so the model can read the message and correct course.
190
+
191
+ | Cause | Message |
192
+ |---|---|
193
+ | CapSkip unreachable | `CapSkip is not reachable at <host>:<port>. Confirm the CapSkip desktop app is running and that its API port matches this setting (override with CAPSKIP_HOST / CAPSKIP_PORT).` |
194
+ | Another process holds the port | `Something is listening on <host>:<port> but it did not answer as CapSkip (HTTP <code>). Check the API port in CapSkip settings, and that nothing else has taken that port — override with CAPSKIP_HOST / CAPSKIP_PORT.` |
195
+ | Wrong API key | `CapSkip rejected the API key. Set CAPSKIP_API_KEY to the key shown in CapSkip settings, or disable key validation there.` |
196
+ | `ERROR_CAPTCHA_UNSOLVABLE` | `CapSkip could not solve this captcha. Fetch a fresh sitekey or challenge from the page and try again.` |
197
+ | `ERROR_GOOGLEKEY` | `CapSkip rejected the sitekey. Re-read data-sitekey from the page and retry.` |
198
+ | `ERROR_PAGEURL` | `CapSkip rejected the url. Pass the full page URL, including its scheme.` |
199
+ | `ERROR_INVALID_IMAGE` | `CapSkip could not read that image. Check the file is a valid, uncorrupted PNG or JPEG.` |
200
+ | `ERROR_BAD_PARAMETERS` | `CapSkip rejected the parameters for this captcha type. Re-check the values supplied.` |
201
+ | Solve exceeded `timeout` | `The solve did not finish within <elapsed>s. Raise the timeout parameter, or check CapSkip's own queue.` — plus the captcha id when one was assigned |
202
+ | Unknown or misspelled parameter | Rejected before the call reaches CapSkip, naming the key, e.g. `Unrecognized key: "min_score"` |
203
+
204
+ See [Troubleshooting](docs/TROUBLESHOOTING.md) for fixes.
205
+
206
+ ---
207
+
208
+ ## FAQ
209
+
210
+ ### Can AI agents solve captchas?
211
+
212
+ Not on their own — a model cannot produce a valid reCAPTCHA or Turnstile token. It needs a solver. This MCP server connects your agent to CapSkip so it can request a real token and continue the task.
213
+
214
+ ### Does this work with Claude, Cursor and VS Code?
215
+
216
+ Yes. It is a standard MCP stdio server, so it works with any Model Context Protocol client, including Claude Desktop, Claude Code, Cursor and VS Code. Config examples for each are in [examples/](examples/).
217
+
218
+ ### Is there a free captcha solver here?
219
+
220
+ The MCP server is MIT-licensed and free. It requires the CapSkip desktop app, which is licensed once and then solves without per-solve charges — unlike cloud APIs that bill per captcha.
221
+
222
+ ### Which captchas can it solve?
223
+
224
+ reCAPTCHA v2 (checkbox and invisible), reCAPTCHA v3, reCAPTCHA Enterprise, Cloudflare Turnstile, GeeTest v3, and image/text captchas. hCaptcha and FunCaptcha/Arkose are not supported.
225
+
226
+ ### Why does my reCAPTCHA v3 token get a low score?
227
+
228
+ Google assigns v3 scores from signals such as IP reputation and browsing history. A solver returns a valid token, but cannot raise the score. If a site enforces a high threshold, solve from a cleaner IP — a proxy is supported on the reCAPTCHA, Turnstile and GeeTest tools.
229
+
230
+ ### Does it need my captcha to be on a public page?
231
+
232
+ Yes for widget captchas — CapSkip loads the page URL you pass. Image captchas need only the image, which can be a local file.
233
+
234
+ ### Can I use it with Playwright or Puppeteer?
235
+
236
+ Yes. The agent drives the browser; this server supplies the token. See the [browser automation section](#browser-automation-playwright-puppeteer-and-selenium).
237
+
238
+ ---
239
+
240
+ ## Requirements
241
+
242
+ - Node.js 18 or newer
243
+ - The CapSkip desktop app installed and running ([download](https://capskip.com))
244
+
245
+ ## Links
246
+
247
+ - [CapSkip — unlimited captcha solver](https://capskip.com)
248
+ - [Captcha demo pages](https://capskip.com/captcha-demo/) — live reCAPTCHA, Turnstile, GeeTest and image widgets
249
+ - [HTTP API docs](https://capskip.com/api-docs/)
250
+ - SDKs: [Python](https://github.com/capskip/capskip-python) · [Node.js](https://github.com/capskip/capskip-node) · [PHP](https://github.com/capskip/capskip-php) · [.NET](https://github.com/capskip/capskip-dotnet)
251
+ - [Report an issue](https://github.com/capskip/capskip-mcp/issues)
252
+
253
+ ## License
254
+
255
+ MIT — see [LICENSE](LICENSE).
package/SECURITY.md ADDED
@@ -0,0 +1,35 @@
1
+ # Security Policy
2
+
3
+ ## Supported versions
4
+
5
+ | Version | Supported |
6
+ |---|---|
7
+ | 1.0.x | Yes |
8
+
9
+ ## Reporting a vulnerability
10
+
11
+ If you discover a security vulnerability in the CapSkip MCP server, please report it responsibly.
12
+
13
+ **Do not** open a public GitHub issue for security vulnerabilities.
14
+
15
+ Instead, email **support@capskip.com** with:
16
+
17
+ - Description of the vulnerability
18
+ - Steps to reproduce
19
+ - Potential impact
20
+ - Suggested fix (if any)
21
+
22
+ We aim to acknowledge reports within 48 hours and provide a status update within 7 days.
23
+
24
+ ## Scope
25
+
26
+ This policy covers the `capskip-mcp` npm package in this repository.
27
+
28
+ The CapSkip desktop application itself is maintained separately — report app-level issues to CapSkip support.
29
+
30
+ ## Best practices for running this server
31
+
32
+ - Configure `CAPSKIP_API_KEY` through your MCP client's `env` block, not by hard-coding it into a committed config file
33
+ - Client configuration files (`claude_desktop_config.json`, `.mcp.json`, `.vscode/mcp.json`) often live in a repository — keep API keys and proxy credentials out of them
34
+ - CapSkip runs locally — ensure your firewall rules match your security requirements
35
+ - When using proxies, avoid logging credentials in application logs
package/dist/config.js ADDED
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ConfigError = void 0;
4
+ exports.resolveConfig = resolveConfig;
5
+ /** Thrown at startup when a setting is missing or malformed. */
6
+ class ConfigError extends Error {
7
+ constructor(message) {
8
+ super(message);
9
+ this.name = 'ConfigError';
10
+ }
11
+ }
12
+ exports.ConfigError = ConfigError;
13
+ // CLI flag -> [config key, env var name]. The env name is carried so an
14
+ // invalid value can be reported under whichever spelling the user actually
15
+ // supplied.
16
+ const NUMERIC = {
17
+ '--port': ['port', 'CAPSKIP_PORT'],
18
+ '--timeout': ['defaultTimeout', 'CAPSKIP_TIMEOUT'],
19
+ '--recaptcha-timeout': ['recaptchaTimeout', 'CAPSKIP_RECAPTCHA_TIMEOUT'],
20
+ '--polling-interval': ['pollingInterval', 'CAPSKIP_POLLING_INTERVAL'],
21
+ };
22
+ const STRING = {
23
+ '--api-key': ['apiKey', 'CAPSKIP_API_KEY'],
24
+ '--host': ['host', 'CAPSKIP_HOST'],
25
+ };
26
+ function parseArgv(argv) {
27
+ const known = new Set([...Object.keys(NUMERIC), ...Object.keys(STRING)]);
28
+ const out = {};
29
+ for (let i = 0; i < argv.length; i += 1) {
30
+ const arg = argv[i];
31
+ if (!arg.startsWith('--')) {
32
+ throw new ConfigError(`Unexpected argument '${arg}'.`);
33
+ }
34
+ const eq = arg.indexOf('=');
35
+ const flag = eq === -1 ? arg : arg.slice(0, eq);
36
+ if (!known.has(flag)) {
37
+ throw new ConfigError(`Unknown option '${flag}'. Supported: ${[...known].sort().join(', ')}.`);
38
+ }
39
+ if (eq !== -1) {
40
+ out[flag] = arg.slice(eq + 1);
41
+ continue;
42
+ }
43
+ const value = argv[i + 1];
44
+ if (value === undefined || value.startsWith('--')) {
45
+ throw new ConfigError(`Option '${flag}' requires a value.`);
46
+ }
47
+ out[flag] = value;
48
+ i += 1;
49
+ }
50
+ return out;
51
+ }
52
+ function readNumber(raw, label, min, max) {
53
+ const value = Number(raw);
54
+ if (!Number.isInteger(value)) {
55
+ throw new ConfigError(`${label} must be a whole number, got '${raw}'.`);
56
+ }
57
+ if (value < min || value > max) {
58
+ throw new ConfigError(`${label} must be between ${min} and ${max}, got ${value}.`);
59
+ }
60
+ return value;
61
+ }
62
+ /** CLI flags beat environment variables, which beat defaults. */
63
+ function resolveConfig(argv, env) {
64
+ const flags = parseArgv(argv);
65
+ const config = {
66
+ apiKey: 'capskip',
67
+ host: '127.0.0.1',
68
+ port: 8080,
69
+ defaultTimeout: 120,
70
+ recaptchaTimeout: 300,
71
+ pollingInterval: 5,
72
+ };
73
+ for (const [flag, [key, envName]] of Object.entries(STRING)) {
74
+ const raw = flags[flag] ?? env[envName];
75
+ if (raw !== undefined && raw !== '') {
76
+ config[key] = raw;
77
+ }
78
+ }
79
+ const bounds = {
80
+ port: [1, 65535],
81
+ defaultTimeout: [1, 3600],
82
+ recaptchaTimeout: [1, 3600],
83
+ pollingInterval: [1, 60],
84
+ };
85
+ for (const [flag, [key, envName]] of Object.entries(NUMERIC)) {
86
+ const fromFlag = flags[flag];
87
+ const raw = fromFlag ?? env[envName];
88
+ if (raw === undefined || raw === '') {
89
+ continue;
90
+ }
91
+ const label = fromFlag !== undefined ? flag : envName;
92
+ const [min, max] = bounds[key];
93
+ config[key] = readNumber(raw, label, min, max);
94
+ }
95
+ return config;
96
+ }
97
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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","sourcesContent":["/** Resolved CapSkip connection and polling settings. */\nexport interface CapSkipConfig {\n apiKey: string;\n host: string;\n port: number;\n defaultTimeout: number;\n recaptchaTimeout: number;\n pollingInterval: number;\n}\n\n/** Thrown at startup when a setting is missing or malformed. */\nexport class ConfigError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'ConfigError';\n }\n}\n\n// CLI flag -> [config key, env var name]. The env name is carried so an\n// invalid value can be reported under whichever spelling the user actually\n// supplied.\nconst NUMERIC = {\n '--port': ['port', 'CAPSKIP_PORT'],\n '--timeout': ['defaultTimeout', 'CAPSKIP_TIMEOUT'],\n '--recaptcha-timeout': ['recaptchaTimeout', 'CAPSKIP_RECAPTCHA_TIMEOUT'],\n '--polling-interval': ['pollingInterval', 'CAPSKIP_POLLING_INTERVAL'],\n} as const;\n\nconst STRING = {\n '--api-key': ['apiKey', 'CAPSKIP_API_KEY'],\n '--host': ['host', 'CAPSKIP_HOST'],\n} as const;\n\nfunction parseArgv(argv: string[]): Record<string, string> {\n const known = new Set([...Object.keys(NUMERIC), ...Object.keys(STRING)]);\n const out: Record<string, string> = {};\n\n for (let i = 0; i < argv.length; i += 1) {\n const arg = argv[i];\n if (!arg.startsWith('--')) {\n throw new ConfigError(`Unexpected argument '${arg}'.`);\n }\n\n const eq = arg.indexOf('=');\n const flag = eq === -1 ? arg : arg.slice(0, eq);\n\n if (!known.has(flag)) {\n throw new ConfigError(\n `Unknown option '${flag}'. Supported: ${[...known].sort().join(', ')}.`,\n );\n }\n\n if (eq !== -1) {\n out[flag] = arg.slice(eq + 1);\n continue;\n }\n\n const value = argv[i + 1];\n if (value === undefined || value.startsWith('--')) {\n throw new ConfigError(`Option '${flag}' requires a value.`);\n }\n out[flag] = value;\n i += 1;\n }\n\n return out;\n}\n\nfunction readNumber(raw: string, label: string, min: number, max: number): number {\n const value = Number(raw);\n if (!Number.isInteger(value)) {\n throw new ConfigError(`${label} must be a whole number, got '${raw}'.`);\n }\n if (value < min || value > max) {\n throw new ConfigError(`${label} must be between ${min} and ${max}, got ${value}.`);\n }\n return value;\n}\n\n/** CLI flags beat environment variables, which beat defaults. */\nexport function resolveConfig(argv: string[], env: NodeJS.ProcessEnv): CapSkipConfig {\n const flags = parseArgv(argv);\n\n const config: CapSkipConfig = {\n apiKey: 'capskip',\n host: '127.0.0.1',\n port: 8080,\n defaultTimeout: 120,\n recaptchaTimeout: 300,\n pollingInterval: 5,\n };\n\n for (const [flag, [key, envName]] of Object.entries(STRING)) {\n const raw = flags[flag] ?? env[envName];\n if (raw !== undefined && raw !== '') {\n config[key] = raw;\n }\n }\n\n const bounds: Record<string, [number, number]> = {\n port: [1, 65535],\n defaultTimeout: [1, 3600],\n recaptchaTimeout: [1, 3600],\n pollingInterval: [1, 60],\n };\n\n for (const [flag, [key, envName]] of Object.entries(NUMERIC)) {\n const fromFlag = flags[flag];\n const raw = fromFlag ?? env[envName];\n if (raw === undefined || raw === '') {\n continue;\n }\n const label = fromFlag !== undefined ? flag : envName;\n const [min, max] = bounds[key];\n config[key] = readNumber(raw, label, min, max);\n }\n\n return config;\n}\n"]}
package/dist/errors.js ADDED
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toolError = toolError;
4
+ exports.mapError = mapError;
5
+ const capskip_1 = require("capskip");
6
+ /** Build a tool result the model can read and act on. */
7
+ function toolError(message) {
8
+ return { content: [{ type: 'text', text: message }], isError: true };
9
+ }
10
+ function messageOf(err) {
11
+ if (err instanceof Error) {
12
+ return err.message;
13
+ }
14
+ return String(err);
15
+ }
16
+ // CapSkip surfaces API failures as an error code inside the exception message.
17
+ const API_CODES = [
18
+ [
19
+ /ERROR_(KEY_DOES_NOT_EXIST|WRONG_USER_KEY)/,
20
+ () => 'CapSkip rejected the API key. Set CAPSKIP_API_KEY to the key shown in '
21
+ + 'CapSkip settings, or disable key validation there.',
22
+ ],
23
+ [
24
+ /ERROR_CAPTCHA_UNSOLVABLE/,
25
+ () => 'CapSkip could not solve this captcha. Fetch a fresh sitekey or challenge '
26
+ + 'from the page and try again.',
27
+ ],
28
+ [
29
+ /ERROR_GOOGLEKEY/,
30
+ () => 'CapSkip rejected the sitekey. Re-read data-sitekey from the page and retry.',
31
+ ],
32
+ [
33
+ /ERROR_PAGEURL/,
34
+ () => 'CapSkip rejected the url. Pass the full page URL, including its scheme.',
35
+ ],
36
+ [
37
+ /ERROR_INVALID_IMAGE/,
38
+ () => 'CapSkip could not read that image. Check the file is a valid, uncorrupted PNG or JPEG.',
39
+ ],
40
+ [
41
+ /ERROR_BAD_PARAMETERS/,
42
+ () => 'CapSkip rejected the parameters for this captcha type. Re-check the values supplied.',
43
+ ],
44
+ ];
45
+ function unreachable(ctx) {
46
+ return (`CapSkip is not reachable at ${ctx.host}:${ctx.port}. Confirm the CapSkip `
47
+ + 'desktop app is running and that its API port matches this setting '
48
+ + '(override with CAPSKIP_HOST / CAPSKIP_PORT).');
49
+ }
50
+ // The SDK reports a non-200 from in.php/res.php as `bad response: <code>`.
51
+ // Something *is* listening on that port, so "not reachable" would send the model
52
+ // looking for a process that is already running. This mirrors the wording
53
+ // capskip_status uses for the same situation.
54
+ const BAD_RESPONSE = /bad response: (\d{3})/;
55
+ /**
56
+ * A connection-level failure, whether the SDK wrapped it or Node raised it raw.
57
+ * One function so both call sites cannot drift apart.
58
+ */
59
+ function networkFailure(message, ctx) {
60
+ const badResponse = BAD_RESPONSE.exec(message);
61
+ if (badResponse) {
62
+ return toolError(`Something is listening on ${ctx.host}:${ctx.port} but it did not answer as `
63
+ + `CapSkip (HTTP ${badResponse[1]}). Check the API port in CapSkip settings, `
64
+ + 'and that nothing else has taken that port — override with CAPSKIP_HOST / '
65
+ + 'CAPSKIP_PORT.');
66
+ }
67
+ return toolError(`${unreachable(ctx)} (underlying error: ${message})`);
68
+ }
69
+ /** Translate anything thrown during a solve into an actionable tool error. */
70
+ function mapError(err, ctx) {
71
+ const message = messageOf(err);
72
+ if (err instanceof capskip_1.TimeoutException) {
73
+ const elapsed = ctx.elapsedSeconds !== undefined
74
+ ? `${ctx.elapsedSeconds.toFixed(1)}s`
75
+ : 'the configured timeout';
76
+ const id = ctx.captchaId
77
+ ? ` CapSkip captcha id ${ctx.captchaId} may still complete — it can be read later `
78
+ + 'via res.php or any CapSkip SDK.'
79
+ : '';
80
+ return toolError(`The solve did not finish within ${elapsed}. Raise the timeout parameter, or `
81
+ + `check CapSkip's own queue.${id}`);
82
+ }
83
+ if (err instanceof capskip_1.ApiException) {
84
+ for (const [pattern, build] of API_CODES) {
85
+ if (pattern.test(message)) {
86
+ return toolError(build(ctx));
87
+ }
88
+ }
89
+ return toolError(`CapSkip returned an error: ${message}`);
90
+ }
91
+ if (err instanceof capskip_1.NetworkException) {
92
+ return networkFailure(message, ctx);
93
+ }
94
+ if (err instanceof capskip_1.ValidationException) {
95
+ return toolError(message);
96
+ }
97
+ // Connection failures can surface as plain Node errors before the SDK wraps them.
98
+ if (/ECONNREFUSED|ECONNRESET|EHOSTUNREACH|ETIMEDOUT|ENOTFOUND/.test(message)) {
99
+ return networkFailure(message, ctx);
100
+ }
101
+ return toolError(`Unexpected failure while solving: ${message}`);
102
+ }
103
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"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","sourcesContent":["import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';\nimport {\n ApiException,\n NetworkException,\n TimeoutException,\n ValidationException,\n} from 'capskip';\n\nexport interface ErrorContext {\n host: string;\n port: number;\n captchaId?: string;\n elapsedSeconds?: number;\n}\n\n/** Build a tool result the model can read and act on. */\nexport function toolError(message: string): CallToolResult {\n return { content: [{ type: 'text', text: message }], isError: true };\n}\n\nfunction messageOf(err: unknown): string {\n if (err instanceof Error) {\n return err.message;\n }\n return String(err);\n}\n\n// CapSkip surfaces API failures as an error code inside the exception message.\nconst API_CODES: Array<[RegExp, (ctx: ErrorContext) => string]> = [\n [\n /ERROR_(KEY_DOES_NOT_EXIST|WRONG_USER_KEY)/,\n () =>\n 'CapSkip rejected the API key. Set CAPSKIP_API_KEY to the key shown in '\n + 'CapSkip settings, or disable key validation there.',\n ],\n [\n /ERROR_CAPTCHA_UNSOLVABLE/,\n () =>\n 'CapSkip could not solve this captcha. Fetch a fresh sitekey or challenge '\n + 'from the page and try again.',\n ],\n [\n /ERROR_GOOGLEKEY/,\n () => 'CapSkip rejected the sitekey. Re-read data-sitekey from the page and retry.',\n ],\n [\n /ERROR_PAGEURL/,\n () => 'CapSkip rejected the url. Pass the full page URL, including its scheme.',\n ],\n [\n /ERROR_INVALID_IMAGE/,\n () => 'CapSkip could not read that image. Check the file is a valid, uncorrupted PNG or JPEG.',\n ],\n [\n /ERROR_BAD_PARAMETERS/,\n () => 'CapSkip rejected the parameters for this captcha type. Re-check the values supplied.',\n ],\n];\n\nfunction unreachable(ctx: ErrorContext): string {\n return (\n `CapSkip is not reachable at ${ctx.host}:${ctx.port}. Confirm the CapSkip `\n + 'desktop app is running and that its API port matches this setting '\n + '(override with CAPSKIP_HOST / CAPSKIP_PORT).'\n );\n}\n\n// The SDK reports a non-200 from in.php/res.php as `bad response: <code>`.\n// Something *is* listening on that port, so \"not reachable\" would send the model\n// looking for a process that is already running. This mirrors the wording\n// capskip_status uses for the same situation.\nconst BAD_RESPONSE = /bad response: (\\d{3})/;\n\n/**\n * A connection-level failure, whether the SDK wrapped it or Node raised it raw.\n * One function so both call sites cannot drift apart.\n */\nfunction networkFailure(message: string, ctx: ErrorContext): CallToolResult {\n const badResponse = BAD_RESPONSE.exec(message);\n if (badResponse) {\n return toolError(\n `Something is listening on ${ctx.host}:${ctx.port} but it did not answer as `\n + `CapSkip (HTTP ${badResponse[1]}). Check the API port in CapSkip settings, `\n + 'and that nothing else has taken that port — override with CAPSKIP_HOST / '\n + 'CAPSKIP_PORT.',\n );\n }\n return toolError(`${unreachable(ctx)} (underlying error: ${message})`);\n}\n\n/** Translate anything thrown during a solve into an actionable tool error. */\nexport function mapError(err: unknown, ctx: ErrorContext): CallToolResult {\n const message = messageOf(err);\n\n if (err instanceof TimeoutException) {\n const elapsed = ctx.elapsedSeconds !== undefined\n ? `${ctx.elapsedSeconds.toFixed(1)}s`\n : 'the configured timeout';\n const id = ctx.captchaId\n ? ` CapSkip captcha id ${ctx.captchaId} may still complete — it can be read later `\n + 'via res.php or any CapSkip SDK.'\n : '';\n return toolError(\n `The solve did not finish within ${elapsed}. Raise the timeout parameter, or `\n + `check CapSkip's own queue.${id}`,\n );\n }\n\n if (err instanceof ApiException) {\n for (const [pattern, build] of API_CODES) {\n if (pattern.test(message)) {\n return toolError(build(ctx));\n }\n }\n return toolError(`CapSkip returned an error: ${message}`);\n }\n\n if (err instanceof NetworkException) {\n return networkFailure(message, ctx);\n }\n\n if (err instanceof ValidationException) {\n return toolError(message);\n }\n\n // Connection failures can surface as plain Node errors before the SDK wraps them.\n if (/ECONNREFUSED|ECONNRESET|EHOSTUNREACH|ETIMEDOUT|ENOTFOUND/.test(message)) {\n return networkFailure(message, ctx);\n }\n\n return toolError(`Unexpected failure while solving: ${message}`);\n}\n"]}
package/dist/index.js ADDED
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
5
+ const config_js_1 = require("./config.js");
6
+ const server_js_1 = require("./server.js");
7
+ async function main() {
8
+ let config;
9
+ try {
10
+ config = (0, config_js_1.resolveConfig)(process.argv.slice(2), process.env);
11
+ }
12
+ catch (err) {
13
+ if (err instanceof config_js_1.ConfigError) {
14
+ process.stderr.write(`capskip-mcp: ${err.message}\n`);
15
+ process.exit(1);
16
+ }
17
+ throw err;
18
+ }
19
+ const server = (0, server_js_1.createServer)(config);
20
+ const transport = new stdio_js_1.StdioServerTransport();
21
+ await server.connect(transport);
22
+ // stdout carries protocol traffic only; anything else corrupts the stream.
23
+ process.stderr.write(`capskip-mcp ready (CapSkip at ${config.host}:${config.port})\n`);
24
+ }
25
+ main().catch((err) => {
26
+ const message = err instanceof Error ? err.message : String(err);
27
+ process.stderr.write(`capskip-mcp: fatal: ${message}\n`);
28
+ process.exit(1);
29
+ });
30
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAEA,wEAAiF;AAEjF,2CAAyD;AACzD,2CAA2C;AAE3C,KAAK,UAAU,IAAI;IACjB,IAAI,MAAM,CAAC;IACX,IAAI,CAAC;QACH,MAAM,GAAG,IAAA,yBAAa,EAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC;IAC7D,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,GAAG,YAAY,uBAAW,EAAE,CAAC;YAC/B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,gBAAgB,GAAG,CAAC,OAAO,IAAI,CAAC,CAAC;YACtD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;QACD,MAAM,GAAG,CAAC;IACZ,CAAC;IAED,MAAM,MAAM,GAAG,IAAA,wBAAY,EAAC,MAAM,CAAC,CAAC;IACpC,MAAM,SAAS,GAAG,IAAI,+BAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAEhC,2EAA2E;IAC3E,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,iCAAiC,MAAM,CAAC,IAAI,IAAI,MAAM,CAAC,IAAI,KAAK,CACjE,CAAC;AACJ,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;IAC5B,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACjE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,uBAAuB,OAAO,IAAI,CAAC,CAAC;IACzD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC","sourcesContent":["#!/usr/bin/env node\n\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\n\nimport { ConfigError, resolveConfig } from './config.js';\nimport { createServer } from './server.js';\n\nasync function main(): Promise<void> {\n let config;\n try {\n config = resolveConfig(process.argv.slice(2), process.env);\n } catch (err) {\n if (err instanceof ConfigError) {\n process.stderr.write(`capskip-mcp: ${err.message}\\n`);\n process.exit(1);\n }\n throw err;\n }\n\n const server = createServer(config);\n const transport = new StdioServerTransport();\n await server.connect(transport);\n\n // stdout carries protocol traffic only; anything else corrupts the stream.\n process.stderr.write(\n `capskip-mcp ready (CapSkip at ${config.host}:${config.port})\\n`,\n );\n}\n\nmain().catch((err: unknown) => {\n const message = err instanceof Error ? err.message : String(err);\n process.stderr.write(`capskip-mcp: fatal: ${message}\\n`);\n process.exit(1);\n});\n"]}