capskip-mcp 1.0.0 → 1.2.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 CHANGED
@@ -10,6 +10,75 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
10
10
  Nothing yet. Add entries here as changes land; they move into a version
11
11
  section at release time.
12
12
 
13
+ ## [1.2.0] - 2026-09-22
14
+
15
+ ### Added
16
+
17
+ - **`capskip_solve_capy`** — solve a Capy Puzzle captcha. Unlike every other
18
+ solve tool, the answer is **not a token**: the result carries `captchakey`,
19
+ `challengekey` and `answer`, which all go into the target form's
20
+ `capy_captchakey`, `capy_challengekey` and `capy_answer` fields. The text reply
21
+ names all three, since an agent acting on `code` alone would post a JSON blob
22
+ into a field expecting a drag path. Optional `api_server` and `proxy`.
23
+ - **`capskip_solve_captchafox`** — solve a CaptchaFox challenge. Returns `token`
24
+ for the form's `cf-captcha-response` field, plus `userAgent` when the solve
25
+ reported one: the UA the browser actually minted the token under, which is not
26
+ one the caller sent. Optional `api_server` selects the widget source (the MAM
27
+ package returns a `MAM_` prefixed token) and `proxy`.
28
+ - **`capskip_solve_friendly_captcha`** — solve a Friendly Captcha proof-of-work
29
+ challenge. Takes `version` (`v1`/`v2`) or `module_script` / `nomodule_script`
30
+ so CapSkip can read the version off the build the site loads, plus optional
31
+ `api_server` for the data-residency tenant and `proxy`. The text reply names
32
+ the form field for the version that was actually solved —
33
+ `frc-captcha-solution` on v1, `frc-captcha-response` on v2 — because mixing
34
+ those up is the single most common way a working integration breaks.
35
+ - Output schemas `capyOutput`, `captchaFoxOutput` and `friendlyCaptchaOutput`.
36
+
37
+ ### Changed
38
+
39
+ - Requires `capskip` (the Node SDK) `^1.3.0`, which is where the three new solve
40
+ methods live.
41
+ - The server's instructions now list Capy, CaptchaFox and Friendly Captcha among
42
+ the types it can solve, so a client choosing a tool from the listing sees them.
43
+
44
+ ### Notes
45
+
46
+ - CaptchaFox and Friendly Captcha use `CAPSKIP_RECAPTCHA_TIMEOUT`: the first is a
47
+ real browser solve, and the second has its difficulty set per request by the
48
+ service (and always solves in a browser on v2). Capy uses
49
+ `CAPSKIP_TIMEOUT` — it is one HTTP fetch plus pixel math, though it still takes
50
+ about two seconds because Capy refuses answers that arrive faster than a human
51
+ could have produced them.
52
+
53
+ ## [1.1.0] - 2026-09-12
54
+
55
+ ### Added
56
+
57
+ - **`capskip_solve_altcha`** — solve an ALTCHA proof-of-work challenge. Takes
58
+ `url` plus one of `challenge_url` (the endpoint the `<altcha-widget>` fetches
59
+ from, which CapSkip fetches for you) or `challenge_json` (the challenge
60
+ document itself, solved locally with no network request). Returns `token`, the
61
+ payload to submit verbatim in the site's form field named `altcha`, and
62
+ `number`, the counter that solved it.
63
+ - Calling it with neither challenge parameter returns an error naming what is
64
+ missing, and where to find it, rather than spending a round trip to learn
65
+ CapSkip's `ERROR_BAD_PARAMETERS`.
66
+ - `number` is reported for both ALTCHA generations — the legacy scheme and
67
+ proof-of-work v2 (PBKDF2/SHA), whose token carries no top-level `number` at
68
+ all. Verified against live v1 and v2 deployments.
69
+
70
+ ### Changed
71
+
72
+ - Requires `capskip` >= 1.2.0, which is where `altcha()` was added.
73
+ - The server instructions now list ALTCHA among the solvable types.
74
+
75
+ ### Notes
76
+
77
+ - ALTCHA uses `CAPSKIP_DEFAULT_TIMEOUT`, not `CAPSKIP_RECAPTCHA_TIMEOUT` — it is
78
+ CPU proof-of-work measured in milliseconds, not a browser solve.
79
+ - A `proxy` passed to this tool applies only to the `challenge_url` fetch; an
80
+ inline `challenge_json` never touches the network.
81
+
13
82
  ## [1.0.0] - 2026-08-09
14
83
 
15
84
  ### Added
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
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
6
  [![npm](https://img.shields.io/npm/v/capskip-mcp.svg)](https://www.npmjs.com/package/capskip-mcp)
7
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.**
8
+ **A captcha solver [MCP](https://modelcontextprotocol.io) server that lets AI agents solve reCAPTCHA, Cloudflare Turnstile, GeeTest, ALTCHA, Capy Puzzle, CaptchaFox, Friendly Captcha and image captchas instead of stalling on them.**
9
9
 
10
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
11
 
@@ -17,7 +17,7 @@ npx -y capskip-mcp
17
17
 
18
18
  ## What this solves
19
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.
20
+ An AI agent driving a browser hits a captcha and stops. This server gives it nine 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
21
 
22
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
23
 
@@ -31,6 +31,10 @@ CapSkip runs as a desktop app exposing a captcha-solving HTTP API on `127.0.0.1:
31
31
  | **reCAPTCHA v3 solver** | `capskip_solve_recaptcha` | Pass `version: "v3"` and the page's `action` |
32
32
  | **Cloudflare Turnstile solver** | `capskip_solve_turnstile` | Widget and interstitial challenge pages |
33
33
  | **GeeTest v3 solver** | `capskip_solve_geetest` | Slide puzzle; returns challenge/validate/seccode |
34
+ | **ALTCHA solver** | `capskip_solve_altcha` | Proof-of-work; returns the token for the `altcha` field |
35
+ | **Capy Puzzle solver** | `capskip_solve_capy` | Slide puzzle; returns three form values, not a token |
36
+ | **CaptchaFox solver** | `capskip_solve_captchafox` | Returns the token for the `cf-captcha-response` field |
37
+ | **Friendly Captcha solver** | `capskip_solve_friendly_captcha` | Proof-of-work; say which version (v1 or v2) |
34
38
  | **Image captcha solver** (text/OCR) | `capskip_solve_image_captcha` | File path, URL, data URI, or base64 |
35
39
 
36
40
  **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.
@@ -76,7 +80,7 @@ No install step — `npx` fetches and runs it on demand.
76
80
 
77
81
  ### 3. Restart your client
78
82
 
79
- It should list five tools, all prefixed `capskip_`.
83
+ It should list nine tools, all prefixed `capskip_`.
80
84
 
81
85
  ### 4. Ask your agent to solve a captcha
82
86
 
@@ -112,6 +116,10 @@ CapSkip exposes the familiar `in.php` / `res.php` endpoints, so it works as a **
112
116
  | `capskip_solve_recaptcha` | Solve reCAPTCHA v2 or v3, including invisible and Enterprise | `sitekey`, `url` |
113
117
  | `capskip_solve_turnstile` | Solve a Cloudflare Turnstile widget or challenge page | `sitekey`, `url` |
114
118
  | `capskip_solve_geetest` | Solve a GeeTest v3 slide-puzzle captcha | `gt`, `challenge`, `url` |
119
+ | `capskip_solve_altcha` | Solve an ALTCHA proof-of-work challenge | `url`, and one of `challenge_url` / `challenge_json` |
120
+ | `capskip_solve_capy` | Solve a Capy Puzzle captcha | `sitekey`, `url` |
121
+ | `capskip_solve_captchafox` | Solve a CaptchaFox challenge | `sitekey`, `url` |
122
+ | `capskip_solve_friendly_captcha` | Solve a Friendly Captcha proof-of-work challenge | `sitekey`, `url` |
115
123
 
116
124
  > **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
125
 
@@ -151,7 +159,16 @@ For non-agent scripts, use the language SDKs directly — see the [Playwright](h
151
159
  | `CAPSKIP_HOST` | `127.0.0.1` | CapSkip host |
152
160
  | `CAPSKIP_PORT` | `8080` | API port from CapSkip settings |
153
161
  | `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 |
162
+ | `CAPSKIP_RECAPTCHA_TIMEOUT` | `300` | Default `timeout` for the reCAPTCHA / Turnstile / GeeTest / CaptchaFox / Friendly Captcha tools, seconds |
163
+
164
+ ALTCHA and Capy use `CAPSKIP_DEFAULT_TIMEOUT`, not the reCAPTCHA one — neither
165
+ is a browser solve. ALTCHA is CPU proof-of-work measured in milliseconds; a Capy
166
+ solve is one HTTP fetch plus pixel math, held back to roughly two seconds because
167
+ Capy refuses answers that arrive faster than a human could have produced them.
168
+
169
+ Friendly Captcha is proof-of-work too but takes the longer timeout: the service
170
+ sets the difficulty per request, and v2 always solves in a browser.
171
+
155
172
  | `CAPSKIP_POLLING_INTERVAL` | `5` | Max seconds between polls |
156
173
 
157
174
  CLI flags override environment variables, which override the defaults:
@@ -179,6 +196,10 @@ Every solve tool returns a human-readable text block and `structuredContent` mat
179
196
 
180
197
  - **`capskip_solve_turnstile`** adds `userAgent`. Submit the token with this exact User-Agent — Cloudflare rejects a token replayed under a different one.
181
198
  - **`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.
199
+ - **`capskip_solve_altcha`** adds `token`, the base64 payload to submit verbatim in the site's form field named `altcha`, and `number`, the counter that solved the challenge; `code` holds the same string as `token`.
200
+ - **`capskip_solve_capy`** adds `captchakey`, `challengekey` and `answer` — **not** a token. All three go into the target form, in fields named `capy_captchakey`, `capy_challengekey` and `capy_answer`; `code` keeps the raw answer as JSON. The `answer` is the drag path the widget would have recorded, so submit it verbatim, and promptly: the challenge key is single-use and short-lived.
201
+ - **`capskip_solve_captchafox`** adds `token`, for the form field named `cf-captcha-response`, and `userAgent` when the solve reported one — the UA the browser minted the token under, not any you sent. Submit under that one.
202
+ - **`capskip_solve_friendly_captcha`** adds `token`. The field it goes into differs by version: `frc-captcha-solution` on v1, `frc-captcha-response` on v2. The text reply names the right one for the version that was solved.
182
203
 
183
204
  Long solves emit MCP progress notifications, so a 45-second reCAPTCHA does not trip your client's tool-call timeout.
184
205
 
@@ -221,11 +242,11 @@ The MCP server is MIT-licensed and free. It requires the CapSkip desktop app, wh
221
242
 
222
243
  ### Which captchas can it solve?
223
244
 
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.
245
+ reCAPTCHA v2 (checkbox and invisible), reCAPTCHA v3, reCAPTCHA Enterprise, Cloudflare Turnstile, GeeTest v3, ALTCHA, Capy Puzzle, CaptchaFox, Friendly Captcha, and image/text captchas. hCaptcha and FunCaptcha/Arkose are not supported.
225
246
 
226
247
  ### Why does my reCAPTCHA v3 token get a low score?
227
248
 
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.
249
+ 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 every solve tool except the image one (for ALTCHA it applies only to the challenge fetch, and for Capy only to the puzzle-image fetch).
229
250
 
230
251
  ### Does it need my captcha to be on a public page?
231
252
 
package/dist/index.js CHANGED
File without changes
package/dist/schemas.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.statusOutput = exports.geetestOutput = exports.turnstileOutput = exports.baseSolveOutput = exports.pageUrlSchema = exports.timeoutSchema = exports.proxySchema = void 0;
3
+ exports.statusOutput = exports.friendlyCaptchaOutput = exports.captchaFoxOutput = exports.capyOutput = exports.altchaOutput = exports.geetestOutput = exports.turnstileOutput = exports.baseSolveOutput = exports.pageUrlSchema = exports.timeoutSchema = exports.proxySchema = void 0;
4
4
  const zod_1 = require("zod");
5
5
  /**
6
6
  * CapSkip maps only these four proxy schemes and answers ERROR_BAD_PARAMETERS
@@ -42,6 +42,38 @@ exports.geetestOutput = exports.baseSolveOutput.extend({
42
42
  validate: zod_1.z.string().optional(),
43
43
  seccode: zod_1.z.string().optional(),
44
44
  });
45
+ exports.altchaOutput = exports.baseSolveOutput.extend({
46
+ /** The base64 payload to submit in the site's `altcha` form field, verbatim. */
47
+ token: zod_1.z.string(),
48
+ /** The counter that satisfied the challenge. Absent if the payload did not decode. */
49
+ number: zod_1.z.number().optional(),
50
+ });
51
+ exports.capyOutput = exports.baseSolveOutput.extend({
52
+ /** The value for the target form's `capy_captchakey` field. */
53
+ captchakey: zod_1.z.string().optional(),
54
+ /** The value for the target form's `capy_challengekey` field. Single-use. */
55
+ challengekey: zod_1.z.string().optional(),
56
+ /** The value for the target form's `capy_answer` field — the drag path, verbatim. */
57
+ answer: zod_1.z.string().optional(),
58
+ /** Empty for a puzzle solve; present only for shape compatibility. */
59
+ respKey: zod_1.z.string().optional(),
60
+ });
61
+ exports.captchaFoxOutput = exports.baseSolveOutput.extend({
62
+ /** The token to submit in the site's `cf-captcha-response` field, verbatim. */
63
+ token: zod_1.z.string(),
64
+ /**
65
+ * The User-Agent the token was minted under — the browser's own, not one the
66
+ * caller sent. Absent when the solve captured none rather than guessed.
67
+ */
68
+ userAgent: zod_1.z.string().optional(),
69
+ });
70
+ exports.friendlyCaptchaOutput = exports.baseSolveOutput.extend({
71
+ /**
72
+ * The token to submit verbatim — in `frc-captcha-solution` on v1, or
73
+ * `frc-captcha-response` on v2. The field names differ between versions.
74
+ */
75
+ token: zod_1.z.string(),
76
+ });
45
77
  exports.statusOutput = zod_1.z.object({
46
78
  reachable: zod_1.z.boolean(),
47
79
  host: zod_1.z.string(),
@@ -1 +1 @@
1
- {"version":3,"file":"schemas.js","sourceRoot":"","sources":["../src/schemas.ts"],"names":[],"mappings":";;;AAAA,6BAAwB;AAExB;;;GAGG;AACU,QAAA,WAAW,GAAG,OAAC,CAAC,MAAM,CAAC;IAClC,IAAI,EAAE,OAAC;SACJ,IAAI,CAAC,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,SAAS,CAAC,CAAC;SAC5C,QAAQ,CAAC,iDAAiD,CAAC;IAC9D,GAAG,EAAE,OAAC;SACH,MAAM,EAAE;SACR,GAAG,CAAC,CAAC,CAAC;SACN,QAAQ,CAAC,6DAA6D,CAAC;CAC3E,CAAC,CAAC;AAIH;;;GAGG;AACU,QAAA,aAAa,GAAG,OAAC;KAC3B,MAAM,EAAE;KACR,GAAG,EAAE;KACL,GAAG,CAAC,CAAC,CAAC;KACN,GAAG,CAAC,GAAG,CAAC;KACR,QAAQ,EAAE;KACV,QAAQ,CAAC,gDAAgD,CAAC,CAAC;AAEjD,QAAA,aAAa,GAAG,OAAC;KAC3B,GAAG,EAAE;KACL,QAAQ,CAAC,gEAAgE,CAAC,CAAC;AAEjE,QAAA,eAAe,GAAG,OAAC,CAAC,MAAM,CAAC;IACtC,SAAS,EAAE,OAAC,CAAC,MAAM,EAAE;IACrB,IAAI,EAAE,OAAC,CAAC,MAAM,EAAE;IAChB,YAAY,EAAE,OAAC,CAAC,MAAM,EAAE;CACzB,CAAC,CAAC;AAEU,QAAA,eAAe,GAAG,uBAAe,CAAC,MAAM,CAAC;IACpD,SAAS,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CACjC,CAAC,CAAC;AAEU,QAAA,aAAa,GAAG,uBAAe,CAAC,MAAM,CAAC;IAClD,SAAS,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAChC,QAAQ,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC/B,OAAO,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CAC/B,CAAC,CAAC;AAEU,QAAA,YAAY,GAAG,OAAC,CAAC,MAAM,CAAC;IACnC,SAAS,EAAE,OAAC,CAAC,OAAO,EAAE;IACtB,IAAI,EAAE,OAAC,CAAC,MAAM,EAAE;IAChB,IAAI,EAAE,OAAC,CAAC,MAAM,EAAE;IAChB,SAAS,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAChC,MAAM,EAAE,OAAC,CAAC,MAAM,EAAE;CACnB,CAAC,CAAC","sourcesContent":["import { z } from 'zod';\n\n/**\n * CapSkip maps only these four proxy schemes and answers ERROR_BAD_PARAMETERS\n * for anything else, SOCKS4 included.\n */\nexport const proxySchema = z.object({\n type: z\n .enum(['HTTP', 'HTTPS', 'SOCKS5', 'SOCKS5H'])\n .describe('Proxy scheme. CapSkip supports only these four.'),\n uri: z\n .string()\n .min(1)\n .describe('Proxy address as `host:port` or `login:password@host:port`.'),\n});\n\nexport type ProxyInput = z.infer<typeof proxySchema>;\n\n/**\n * Bounded at 600s. A larger value is rejected rather than clamped so the caller\n * learns the limit instead of silently getting a shorter wait.\n */\nexport const timeoutSchema = z\n .number()\n .int()\n .min(1)\n .max(600)\n .optional()\n .describe('Seconds to wait before giving up. Maximum 600.');\n\nexport const pageUrlSchema = z\n .url()\n .describe('Full URL of the page the captcha appears on, including scheme.');\n\nexport const baseSolveOutput = z.object({\n captchaId: z.string(),\n code: z.string(),\n solveSeconds: z.number(),\n});\n\nexport const turnstileOutput = baseSolveOutput.extend({\n userAgent: z.string().optional(),\n});\n\nexport const geetestOutput = baseSolveOutput.extend({\n challenge: z.string().optional(),\n validate: z.string().optional(),\n seccode: z.string().optional(),\n});\n\nexport const statusOutput = z.object({\n reachable: z.boolean(),\n host: z.string(),\n port: z.number(),\n latencyMs: z.number().optional(),\n detail: z.string(),\n});\n"]}
1
+ {"version":3,"file":"schemas.js","sourceRoot":"","sources":["../src/schemas.ts"],"names":[],"mappings":";;;AAAA,6BAAwB;AAExB;;;GAGG;AACU,QAAA,WAAW,GAAG,OAAC,CAAC,MAAM,CAAC;IAClC,IAAI,EAAE,OAAC;SACJ,IAAI,CAAC,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,SAAS,CAAC,CAAC;SAC5C,QAAQ,CAAC,iDAAiD,CAAC;IAC9D,GAAG,EAAE,OAAC;SACH,MAAM,EAAE;SACR,GAAG,CAAC,CAAC,CAAC;SACN,QAAQ,CAAC,6DAA6D,CAAC;CAC3E,CAAC,CAAC;AAIH;;;GAGG;AACU,QAAA,aAAa,GAAG,OAAC;KAC3B,MAAM,EAAE;KACR,GAAG,EAAE;KACL,GAAG,CAAC,CAAC,CAAC;KACN,GAAG,CAAC,GAAG,CAAC;KACR,QAAQ,EAAE;KACV,QAAQ,CAAC,gDAAgD,CAAC,CAAC;AAEjD,QAAA,aAAa,GAAG,OAAC;KAC3B,GAAG,EAAE;KACL,QAAQ,CAAC,gEAAgE,CAAC,CAAC;AAEjE,QAAA,eAAe,GAAG,OAAC,CAAC,MAAM,CAAC;IACtC,SAAS,EAAE,OAAC,CAAC,MAAM,EAAE;IACrB,IAAI,EAAE,OAAC,CAAC,MAAM,EAAE;IAChB,YAAY,EAAE,OAAC,CAAC,MAAM,EAAE;CACzB,CAAC,CAAC;AAEU,QAAA,eAAe,GAAG,uBAAe,CAAC,MAAM,CAAC;IACpD,SAAS,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CACjC,CAAC,CAAC;AAEU,QAAA,aAAa,GAAG,uBAAe,CAAC,MAAM,CAAC;IAClD,SAAS,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAChC,QAAQ,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC/B,OAAO,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CAC/B,CAAC,CAAC;AAEU,QAAA,YAAY,GAAG,uBAAe,CAAC,MAAM,CAAC;IACjD,gFAAgF;IAChF,KAAK,EAAE,OAAC,CAAC,MAAM,EAAE;IACjB,sFAAsF;IACtF,MAAM,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CAC9B,CAAC,CAAC;AAEU,QAAA,UAAU,GAAG,uBAAe,CAAC,MAAM,CAAC;IAC/C,+DAA+D;IAC/D,UAAU,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACjC,6EAA6E;IAC7E,YAAY,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACnC,qFAAqF;IACrF,MAAM,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC7B,sEAAsE;IACtE,OAAO,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CAC/B,CAAC,CAAC;AAEU,QAAA,gBAAgB,GAAG,uBAAe,CAAC,MAAM,CAAC;IACrD,+EAA+E;IAC/E,KAAK,EAAE,OAAC,CAAC,MAAM,EAAE;IACjB;;;OAGG;IACH,SAAS,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CACjC,CAAC,CAAC;AAEU,QAAA,qBAAqB,GAAG,uBAAe,CAAC,MAAM,CAAC;IAC1D;;;OAGG;IACH,KAAK,EAAE,OAAC,CAAC,MAAM,EAAE;CAClB,CAAC,CAAC;AAEU,QAAA,YAAY,GAAG,OAAC,CAAC,MAAM,CAAC;IACnC,SAAS,EAAE,OAAC,CAAC,OAAO,EAAE;IACtB,IAAI,EAAE,OAAC,CAAC,MAAM,EAAE;IAChB,IAAI,EAAE,OAAC,CAAC,MAAM,EAAE;IAChB,SAAS,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAChC,MAAM,EAAE,OAAC,CAAC,MAAM,EAAE;CACnB,CAAC,CAAC","sourcesContent":["import { z } from 'zod';\n\n/**\n * CapSkip maps only these four proxy schemes and answers ERROR_BAD_PARAMETERS\n * for anything else, SOCKS4 included.\n */\nexport const proxySchema = z.object({\n type: z\n .enum(['HTTP', 'HTTPS', 'SOCKS5', 'SOCKS5H'])\n .describe('Proxy scheme. CapSkip supports only these four.'),\n uri: z\n .string()\n .min(1)\n .describe('Proxy address as `host:port` or `login:password@host:port`.'),\n});\n\nexport type ProxyInput = z.infer<typeof proxySchema>;\n\n/**\n * Bounded at 600s. A larger value is rejected rather than clamped so the caller\n * learns the limit instead of silently getting a shorter wait.\n */\nexport const timeoutSchema = z\n .number()\n .int()\n .min(1)\n .max(600)\n .optional()\n .describe('Seconds to wait before giving up. Maximum 600.');\n\nexport const pageUrlSchema = z\n .url()\n .describe('Full URL of the page the captcha appears on, including scheme.');\n\nexport const baseSolveOutput = z.object({\n captchaId: z.string(),\n code: z.string(),\n solveSeconds: z.number(),\n});\n\nexport const turnstileOutput = baseSolveOutput.extend({\n userAgent: z.string().optional(),\n});\n\nexport const geetestOutput = baseSolveOutput.extend({\n challenge: z.string().optional(),\n validate: z.string().optional(),\n seccode: z.string().optional(),\n});\n\nexport const altchaOutput = baseSolveOutput.extend({\n /** The base64 payload to submit in the site's `altcha` form field, verbatim. */\n token: z.string(),\n /** The counter that satisfied the challenge. Absent if the payload did not decode. */\n number: z.number().optional(),\n});\n\nexport const capyOutput = baseSolveOutput.extend({\n /** The value for the target form's `capy_captchakey` field. */\n captchakey: z.string().optional(),\n /** The value for the target form's `capy_challengekey` field. Single-use. */\n challengekey: z.string().optional(),\n /** The value for the target form's `capy_answer` field — the drag path, verbatim. */\n answer: z.string().optional(),\n /** Empty for a puzzle solve; present only for shape compatibility. */\n respKey: z.string().optional(),\n});\n\nexport const captchaFoxOutput = baseSolveOutput.extend({\n /** The token to submit in the site's `cf-captcha-response` field, verbatim. */\n token: z.string(),\n /**\n * The User-Agent the token was minted under — the browser's own, not one the\n * caller sent. Absent when the solve captured none rather than guessed.\n */\n userAgent: z.string().optional(),\n});\n\nexport const friendlyCaptchaOutput = baseSolveOutput.extend({\n /**\n * The token to submit verbatim — in `frc-captcha-solution` on v1, or\n * `frc-captcha-response` on v2. The field names differ between versions.\n */\n token: z.string(),\n});\n\nexport const statusOutput = z.object({\n reachable: z.boolean(),\n host: z.string(),\n port: z.number(),\n latencyMs: z.number().optional(),\n detail: z.string(),\n});\n"]}
package/dist/server.js CHANGED
@@ -4,13 +4,17 @@ exports.SERVER_VERSION = exports.SERVER_NAME = void 0;
4
4
  exports.createServer = createServer;
5
5
  const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
6
6
  const solver_js_1 = require("./solver.js");
7
+ const altcha_js_1 = require("./tools/altcha.js");
8
+ const captchafox_js_1 = require("./tools/captchafox.js");
9
+ const capy_js_1 = require("./tools/capy.js");
10
+ const friendlycaptcha_js_1 = require("./tools/friendlycaptcha.js");
7
11
  const geetest_js_1 = require("./tools/geetest.js");
8
12
  const image_js_1 = require("./tools/image.js");
9
13
  const recaptcha_js_1 = require("./tools/recaptcha.js");
10
14
  const status_js_1 = require("./tools/status.js");
11
15
  const turnstile_js_1 = require("./tools/turnstile.js");
12
16
  exports.SERVER_NAME = 'capskip';
13
- exports.SERVER_VERSION = '1.0.0';
17
+ exports.SERVER_VERSION = '1.2.0';
14
18
  /** Build a fully-registered MCP server. Does not connect a transport. */
15
19
  function createServer(config) {
16
20
  const server = new mcp_js_1.McpServer({ name: exports.SERVER_NAME, version: exports.SERVER_VERSION }, {
@@ -18,7 +22,8 @@ function createServer(config) {
18
22
  + 'tools when browser automation is blocked by a captcha: read the sitekey '
19
23
  + 'from the page, call the matching tool, then place the returned token in '
20
24
  + "the page's response field. CapSkip solves image captchas, reCAPTCHA v2 "
21
- + 'and v3, Cloudflare Turnstile, and GeeTest v3 it cannot solve hCaptcha '
25
+ + 'and v3, Cloudflare Turnstile, GeeTest v3, ALTCHA, Capy Puzzle, CaptchaFox '
26
+ + 'and Friendly Captcha — it cannot solve hCaptcha '
22
27
  + 'or FunCaptcha/Arkose, so do not call these tools for those. An hCaptcha '
23
28
  + 'widget renders as <div class="h-captcha" data-sitekey="…">, which is '
24
29
  + 'easy to mistake for reCAPTCHA when scanning the DOM for data-sitekey. '
@@ -31,6 +36,10 @@ function createServer(config) {
31
36
  (0, recaptcha_js_1.registerRecaptchaTool)(server, ctx);
32
37
  (0, turnstile_js_1.registerTurnstileTool)(server, ctx);
33
38
  (0, geetest_js_1.registerGeetestTool)(server, ctx);
39
+ (0, altcha_js_1.registerAltchaTool)(server, ctx);
40
+ (0, capy_js_1.registerCapyTool)(server, ctx);
41
+ (0, captchafox_js_1.registerCaptchaFoxTool)(server, ctx);
42
+ (0, friendlycaptcha_js_1.registerFriendlyCaptchaTool)(server, ctx);
34
43
  return server;
35
44
  }
36
45
  //# sourceMappingURL=server.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":";;;AAcA,oCA2BC;AAzCD,oEAAoE;AAGpE,2CAA4C;AAC5C,mDAAyD;AACzD,+CAAqD;AACrD,uDAA6D;AAC7D,iDAAuD;AACvD,uDAA6D;AAEhD,QAAA,WAAW,GAAG,SAAS,CAAC;AACxB,QAAA,cAAc,GAAG,OAAO,CAAC;AAEtC,yEAAyE;AACzE,SAAgB,YAAY,CAAC,MAAqB;IAChD,MAAM,MAAM,GAAG,IAAI,kBAAS,CAC1B,EAAE,IAAI,EAAE,mBAAW,EAAE,OAAO,EAAE,sBAAc,EAAE,EAC9C;QACE,YAAY,EACV,0EAA0E;cACxE,0EAA0E;cAC1E,0EAA0E;cAC1E,yEAAyE;cACzE,0EAA0E;cAC1E,0EAA0E;cAC1E,uEAAuE;cACvE,wEAAwE;cACxE,yEAAyE;cACzE,UAAU;KACf,CACF,CAAC;IAEF,MAAM,GAAG,GAAG,IAAA,yBAAa,EAAC,MAAM,CAAC,CAAC;IAElC,IAAA,8BAAkB,EAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAChC,IAAA,4BAAiB,EAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC/B,IAAA,oCAAqB,EAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACnC,IAAA,oCAAqB,EAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACnC,IAAA,gCAAmB,EAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAEjC,OAAO,MAAM,CAAC;AAChB,CAAC","sourcesContent":["import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\n\nimport type { CapSkipConfig } from './config.js';\nimport { createContext } from './solver.js';\nimport { registerGeetestTool } from './tools/geetest.js';\nimport { registerImageTool } from './tools/image.js';\nimport { registerRecaptchaTool } from './tools/recaptcha.js';\nimport { registerStatusTool } from './tools/status.js';\nimport { registerTurnstileTool } from './tools/turnstile.js';\n\nexport const SERVER_NAME = 'capskip';\nexport const SERVER_VERSION = '1.0.0';\n\n/** Build a fully-registered MCP server. Does not connect a transport. */\nexport function createServer(config: CapSkipConfig): McpServer {\n const server = new McpServer(\n { name: SERVER_NAME, version: SERVER_VERSION },\n {\n instructions:\n 'Solve captchas through a locally-running CapSkip desktop app. Use these '\n + 'tools when browser automation is blocked by a captcha: read the sitekey '\n + 'from the page, call the matching tool, then place the returned token in '\n + \"the page's response field. CapSkip solves image captchas, reCAPTCHA v2 \"\n + 'and v3, Cloudflare Turnstile, and GeeTest v3 — it cannot solve hCaptcha '\n + 'or FunCaptcha/Arkose, so do not call these tools for those. An hCaptcha '\n + 'widget renders as <div class=\"h-captcha\" data-sitekey=\"…\">, which is '\n + 'easy to mistake for reCAPTCHA when scanning the DOM for data-sitekey. '\n + 'If a solve fails unexpectedly, call capskip_status to check CapSkip is '\n + 'running.',\n },\n );\n\n const ctx = createContext(config);\n\n registerStatusTool(server, ctx);\n registerImageTool(server, ctx);\n registerRecaptchaTool(server, ctx);\n registerTurnstileTool(server, ctx);\n registerGeetestTool(server, ctx);\n\n return server;\n}\n"]}
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":";;;AAkBA,oCAgCC;AAlDD,oEAAoE;AAGpE,2CAA4C;AAC5C,iDAAuD;AACvD,yDAA+D;AAC/D,6CAAmD;AACnD,mEAAyE;AACzE,mDAAyD;AACzD,+CAAqD;AACrD,uDAA6D;AAC7D,iDAAuD;AACvD,uDAA6D;AAEhD,QAAA,WAAW,GAAG,SAAS,CAAC;AACxB,QAAA,cAAc,GAAG,OAAO,CAAC;AAEtC,yEAAyE;AACzE,SAAgB,YAAY,CAAC,MAAqB;IAChD,MAAM,MAAM,GAAG,IAAI,kBAAS,CAC1B,EAAE,IAAI,EAAE,mBAAW,EAAE,OAAO,EAAE,sBAAc,EAAE,EAC9C;QACE,YAAY,EACV,0EAA0E;cACxE,0EAA0E;cAC1E,0EAA0E;cAC1E,yEAAyE;cACzE,4EAA4E;cAC5E,kDAAkD;cAClD,0EAA0E;cAC1E,uEAAuE;cACvE,wEAAwE;cACxE,yEAAyE;cACzE,UAAU;KACf,CACF,CAAC;IAEF,MAAM,GAAG,GAAG,IAAA,yBAAa,EAAC,MAAM,CAAC,CAAC;IAElC,IAAA,8BAAkB,EAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAChC,IAAA,4BAAiB,EAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC/B,IAAA,oCAAqB,EAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACnC,IAAA,oCAAqB,EAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACnC,IAAA,gCAAmB,EAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACjC,IAAA,8BAAkB,EAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAChC,IAAA,0BAAgB,EAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC9B,IAAA,sCAAsB,EAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACpC,IAAA,gDAA2B,EAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAEzC,OAAO,MAAM,CAAC;AAChB,CAAC","sourcesContent":["import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\n\nimport type { CapSkipConfig } from './config.js';\nimport { createContext } from './solver.js';\nimport { registerAltchaTool } from './tools/altcha.js';\nimport { registerCaptchaFoxTool } from './tools/captchafox.js';\nimport { registerCapyTool } from './tools/capy.js';\nimport { registerFriendlyCaptchaTool } from './tools/friendlycaptcha.js';\nimport { registerGeetestTool } from './tools/geetest.js';\nimport { registerImageTool } from './tools/image.js';\nimport { registerRecaptchaTool } from './tools/recaptcha.js';\nimport { registerStatusTool } from './tools/status.js';\nimport { registerTurnstileTool } from './tools/turnstile.js';\n\nexport const SERVER_NAME = 'capskip';\nexport const SERVER_VERSION = '1.2.0';\n\n/** Build a fully-registered MCP server. Does not connect a transport. */\nexport function createServer(config: CapSkipConfig): McpServer {\n const server = new McpServer(\n { name: SERVER_NAME, version: SERVER_VERSION },\n {\n instructions:\n 'Solve captchas through a locally-running CapSkip desktop app. Use these '\n + 'tools when browser automation is blocked by a captcha: read the sitekey '\n + 'from the page, call the matching tool, then place the returned token in '\n + \"the page's response field. CapSkip solves image captchas, reCAPTCHA v2 \"\n + 'and v3, Cloudflare Turnstile, GeeTest v3, ALTCHA, Capy Puzzle, CaptchaFox '\n + 'and Friendly Captcha — it cannot solve hCaptcha '\n + 'or FunCaptcha/Arkose, so do not call these tools for those. An hCaptcha '\n + 'widget renders as <div class=\"h-captcha\" data-sitekey=\"…\">, which is '\n + 'easy to mistake for reCAPTCHA when scanning the DOM for data-sitekey. '\n + 'If a solve fails unexpectedly, call capskip_status to check CapSkip is '\n + 'running.',\n },\n );\n\n const ctx = createContext(config);\n\n registerStatusTool(server, ctx);\n registerImageTool(server, ctx);\n registerRecaptchaTool(server, ctx);\n registerTurnstileTool(server, ctx);\n registerGeetestTool(server, ctx);\n registerAltchaTool(server, ctx);\n registerCapyTool(server, ctx);\n registerCaptchaFoxTool(server, ctx);\n registerFriendlyCaptchaTool(server, ctx);\n\n return server;\n}\n"]}
@@ -0,0 +1,100 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerAltchaTool = registerAltchaTool;
4
+ const zod_1 = require("zod");
5
+ const schemas_js_1 = require("../schemas.js");
6
+ const solve_js_1 = require("../solve.js");
7
+ const inputSchema = zod_1.z.strictObject({
8
+ url: schemas_js_1.pageUrlSchema,
9
+ challenge_url: zod_1.z
10
+ .string()
11
+ .min(1)
12
+ .optional()
13
+ .describe('The endpoint the ALTCHA widget fetches its challenge from, e.g. '
14
+ + "'https://site.com/altcha/challenge'. CapSkip fetches it for you. Pass "
15
+ + 'this or challenge_json.'),
16
+ challenge_json: zod_1.z
17
+ .string()
18
+ .min(1)
19
+ .optional()
20
+ .describe('The challenge document itself, as a JSON string, when you already have '
21
+ + 'it. Solved locally with no network request. Pass this or challenge_url.'),
22
+ proxy: schemas_js_1.proxySchema
23
+ .optional()
24
+ .describe('Fetch the challenge through this proxy. Used ONLY for the challenge_url '
25
+ + 'fetch — an inline challenge_json never touches the network.'),
26
+ timeout: schemas_js_1.timeoutSchema,
27
+ });
28
+ function registerAltchaTool(server, ctx) {
29
+ server.registerTool('capskip_solve_altcha', {
30
+ title: 'Solve ALTCHA',
31
+ description: 'Solve an ALTCHA proof-of-work challenge. ALTCHA is not a recognition '
32
+ + 'captcha — there is nothing to read; the client brute-forces a number '
33
+ + 'that satisfies a challenge, so a solve is deterministic and takes '
34
+ + 'milliseconds. Give it either challenge_url (the endpoint the '
35
+ + '<altcha-widget> fetches from, which CapSkip will fetch) or '
36
+ + 'challenge_json (the challenge document itself). Returns a token to put '
37
+ + "in the page's form field named `altcha`, verbatim. IMPORTANT: "
38
+ + 'challenges expire quickly — some sites inside two minutes — so read '
39
+ + 'the challenge immediately before calling and submit the token '
40
+ + 'promptly. An expired challenge is rejected with a bare "verification '
41
+ + 'failed" that looks exactly like a wrong answer.',
42
+ inputSchema,
43
+ outputSchema: schemas_js_1.altchaOutput,
44
+ // destructiveHint defaults to *true* whenever readOnlyHint is false, so
45
+ // omitting it would advertise this tool as potentially destructive and cost
46
+ // it auto-approval in clients that read the hint. Solving a captcha
47
+ // destroys nothing. idempotentHint is deliberately left at its false
48
+ // default: each call consumes a challenge that expires.
49
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
50
+ }, async (args, extra) => {
51
+ // CapSkip answers ERROR_BAD_PARAMETERS when neither is sent. zod cannot
52
+ // express "at least one of" on a strictObject without reshaping the schema
53
+ // into a union, which renders poorly in tool listings — so check here and
54
+ // name what is missing.
55
+ if (args.challenge_url === undefined && args.challenge_json === undefined) {
56
+ return {
57
+ isError: true,
58
+ content: [{
59
+ type: 'text',
60
+ text: 'ALTCHA needs a challenge. Pass challenge_url (the endpoint the '
61
+ + '<altcha-widget> fetches from — look in DevTools → Network for a '
62
+ + 'request like /altcha/challenge) or challenge_json (that '
63
+ + "endpoint's JSON response). Widget v1/v2 name it in a "
64
+ + 'challengeurl="…" attribute; v3+ uses challenge="…" for both a '
65
+ + 'URL and inline data.',
66
+ }],
67
+ };
68
+ }
69
+ const timeout = args.timeout ?? ctx.config.defaultTimeout;
70
+ const options = {};
71
+ if (args.challenge_url !== undefined)
72
+ options.challenge_url = args.challenge_url;
73
+ if (args.challenge_json !== undefined)
74
+ options.challenge_json = args.challenge_json;
75
+ if (args.proxy !== undefined)
76
+ options.proxy = args.proxy;
77
+ return (0, solve_js_1.runSolve)(ctx, extra, { label: 'Solving ALTCHA', timeoutSeconds: timeout }, (client) => client.altcha(args.url, options), (result, seconds) => {
78
+ const token = result.token ?? String(result.code ?? '');
79
+ const number = result.number;
80
+ const structured = {
81
+ captchaId: String(result.captchaId ?? ''),
82
+ code: String(result.code ?? ''),
83
+ solveSeconds: Number(seconds.toFixed(2)),
84
+ token,
85
+ ...(number !== undefined ? { number } : {}),
86
+ };
87
+ return {
88
+ structured,
89
+ text: `Solved ALTCHA in ${structured.solveSeconds}s.\n`
90
+ + 'Submit this verbatim in the form field the widget uses, named '
91
+ + '`altcha` — do not re-encode, trim or re-order it, or the '
92
+ + "server's signature check fails:\n"
93
+ + `altcha: ${token}\n`
94
+ + (number !== undefined ? `(counter ${number}; ` : '(')
95
+ + `CapSkip captcha id ${structured.captchaId})`,
96
+ };
97
+ });
98
+ });
99
+ }
100
+ //# sourceMappingURL=altcha.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"altcha.js","sourceRoot":"","sources":["../../src/tools/altcha.ts"],"names":[],"mappings":"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","sourcesContent":["import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { z } from 'zod';\n\nimport { altchaOutput, pageUrlSchema, proxySchema, timeoutSchema } from '../schemas.js';\nimport { runSolve } from '../solve.js';\nimport type { ToolContext } from '../solver.js';\n\nconst inputSchema = z.strictObject({\n url: pageUrlSchema,\n challenge_url: z\n .string()\n .min(1)\n .optional()\n .describe(\n 'The endpoint the ALTCHA widget fetches its challenge from, e.g. '\n + \"'https://site.com/altcha/challenge'. CapSkip fetches it for you. Pass \"\n + 'this or challenge_json.',\n ),\n challenge_json: z\n .string()\n .min(1)\n .optional()\n .describe(\n 'The challenge document itself, as a JSON string, when you already have '\n + 'it. Solved locally with no network request. Pass this or challenge_url.',\n ),\n proxy: proxySchema\n .optional()\n .describe(\n 'Fetch the challenge through this proxy. Used ONLY for the challenge_url '\n + 'fetch — an inline challenge_json never touches the network.',\n ),\n timeout: timeoutSchema,\n});\n\nexport function registerAltchaTool(server: McpServer, ctx: ToolContext): void {\n server.registerTool(\n 'capskip_solve_altcha',\n {\n title: 'Solve ALTCHA',\n description:\n 'Solve an ALTCHA proof-of-work challenge. ALTCHA is not a recognition '\n + 'captcha — there is nothing to read; the client brute-forces a number '\n + 'that satisfies a challenge, so a solve is deterministic and takes '\n + 'milliseconds. Give it either challenge_url (the endpoint the '\n + '<altcha-widget> fetches from, which CapSkip will fetch) or '\n + 'challenge_json (the challenge document itself). Returns a token to put '\n + \"in the page's form field named `altcha`, verbatim. IMPORTANT: \"\n + 'challenges expire quickly — some sites inside two minutes — so read '\n + 'the challenge immediately before calling and submit the token '\n + 'promptly. An expired challenge is rejected with a bare \"verification '\n + 'failed\" that looks exactly like a wrong answer.',\n inputSchema,\n outputSchema: altchaOutput,\n // destructiveHint defaults to *true* whenever readOnlyHint is false, so\n // omitting it would advertise this tool as potentially destructive and cost\n // it auto-approval in clients that read the hint. Solving a captcha\n // destroys nothing. idempotentHint is deliberately left at its false\n // default: each call consumes a challenge that expires.\n annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },\n },\n async (args, extra) => {\n // CapSkip answers ERROR_BAD_PARAMETERS when neither is sent. zod cannot\n // express \"at least one of\" on a strictObject without reshaping the schema\n // into a union, which renders poorly in tool listings — so check here and\n // name what is missing.\n if (args.challenge_url === undefined && args.challenge_json === undefined) {\n return {\n isError: true,\n content: [{\n type: 'text' as const,\n text:\n 'ALTCHA needs a challenge. Pass challenge_url (the endpoint the '\n + '<altcha-widget> fetches from — look in DevTools → Network for a '\n + 'request like /altcha/challenge) or challenge_json (that '\n + \"endpoint's JSON response). Widget v1/v2 name it in a \"\n + 'challengeurl=\"…\" attribute; v3+ uses challenge=\"…\" for both a '\n + 'URL and inline data.',\n }],\n };\n }\n\n const timeout = args.timeout ?? ctx.config.defaultTimeout;\n\n const options: Record<string, unknown> = {};\n if (args.challenge_url !== undefined) options.challenge_url = args.challenge_url;\n if (args.challenge_json !== undefined) options.challenge_json = args.challenge_json;\n if (args.proxy !== undefined) options.proxy = args.proxy;\n\n return runSolve(\n ctx,\n extra,\n { label: 'Solving ALTCHA', timeoutSeconds: timeout },\n (client) => client.altcha(args.url, options as never),\n (result, seconds) => {\n const token = (result.token as string | undefined) ?? String(result.code ?? '');\n const number = result.number as number | undefined;\n\n const structured = {\n captchaId: String(result.captchaId ?? ''),\n code: String(result.code ?? ''),\n solveSeconds: Number(seconds.toFixed(2)),\n token,\n ...(number !== undefined ? { number } : {}),\n };\n\n return {\n structured,\n text:\n `Solved ALTCHA in ${structured.solveSeconds}s.\\n`\n + 'Submit this verbatim in the form field the widget uses, named '\n + '`altcha` — do not re-encode, trim or re-order it, or the '\n + \"server's signature check fails:\\n\"\n + `altcha: ${token}\\n`\n + (number !== undefined ? `(counter ${number}; ` : '(')\n + `CapSkip captcha id ${structured.captchaId})`,\n };\n },\n );\n },\n );\n}\n"]}
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerCaptchaFoxTool = registerCaptchaFoxTool;
4
+ const zod_1 = require("zod");
5
+ const schemas_js_1 = require("../schemas.js");
6
+ const solve_js_1 = require("../solve.js");
7
+ const inputSchema = zod_1.z.strictObject({
8
+ sitekey: zod_1.z
9
+ .string()
10
+ .min(1)
11
+ .describe("The public key the widget renders with, conventionally prefixed 'sk_'. "
12
+ + "Read it from data-sitekey on the widget container, from the "
13
+ + 'captchafox.render(...) call, or — if the page builds the widget at '
14
+ + 'runtime — from the path segment after /captcha/ in the request to '
15
+ + 'api.captchafox.com.'),
16
+ url: schemas_js_1.pageUrlSchema,
17
+ api_server: zod_1.z
18
+ .string()
19
+ .min(1)
20
+ .optional()
21
+ .describe('The widget entry point the target page loads. Defaults to '
22
+ + "'https://cdn.captchafox.com/'. Send the MAM package path instead when "
23
+ + 'the page loads that build — it returns a MAM_ prefixed token, and the '
24
+ + 'two are not interchangeable.'),
25
+ proxy: schemas_js_1.proxySchema
26
+ .optional()
27
+ .describe('Solve from this network. CaptchaFox scores the address a widget runs on '
28
+ + 'as well as the browser, so repeated solves from one address drift '
29
+ + 'toward interactive challenges and then refusals.'),
30
+ timeout: schemas_js_1.timeoutSchema,
31
+ });
32
+ function registerCaptchaFoxTool(server, ctx) {
33
+ server.registerTool('capskip_solve_captchafox', {
34
+ title: 'Solve CaptchaFox',
35
+ description: 'Solve a CaptchaFox challenge. CaptchaFox scores the browser itself '
36
+ + 'rather than asking the visitor to read anything, so most solves draw '
37
+ + 'no puzzle at all. Returns a token to put in the form field named '
38
+ + "'cf-captcha-response', verbatim — it is verified server side against "
39
+ + 'the session that produced it, so any edit invalidates it. IMPORTANT: '
40
+ + 'the url must be the page the widget actually runs on. CaptchaFox '
41
+ + 'checks it against the domains the key is registered for and refuses a '
42
+ + 'mismatch permanently, not intermittently — so a sitekey that fails '
43
+ + 'immediately and consistently usually means the wrong page URL, not a '
44
+ + 'bad key. The result also carries the User-Agent the token was minted '
45
+ + 'under; submit under that one, not your own.',
46
+ inputSchema,
47
+ outputSchema: schemas_js_1.captchaFoxOutput,
48
+ // destructiveHint defaults to *true* whenever readOnlyHint is false, so
49
+ // omitting it would advertise this tool as potentially destructive and cost
50
+ // it auto-approval in clients that read the hint. Solving a captcha
51
+ // destroys nothing. idempotentHint is deliberately left at its false
52
+ // default: each call mints a new, short-lived token.
53
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
54
+ }, async (args, extra) => {
55
+ // A real browser session, like reCAPTCHA and GeeTest, and longer again when
56
+ // an interactive challenge is drawn — so it takes the browser timeout.
57
+ const timeout = args.timeout ?? ctx.config.recaptchaTimeout;
58
+ const options = {};
59
+ if (args.api_server !== undefined)
60
+ options.api_server = args.api_server;
61
+ if (args.proxy !== undefined)
62
+ options.proxy = args.proxy;
63
+ return (0, solve_js_1.runSolve)(ctx, extra, { label: 'Solving CaptchaFox', timeoutSeconds: timeout }, (client) => client.captchafox(args.sitekey, args.url, options), (result, seconds) => {
64
+ const token = result.token ?? String(result.code ?? '');
65
+ const userAgent = result.userAgent;
66
+ const structured = {
67
+ captchaId: String(result.captchaId ?? ''),
68
+ code: String(result.code ?? ''),
69
+ solveSeconds: Number(seconds.toFixed(2)),
70
+ token,
71
+ ...(userAgent !== undefined ? { userAgent } : {}),
72
+ };
73
+ return {
74
+ structured,
75
+ text: `Solved CaptchaFox in ${structured.solveSeconds}s.\n`
76
+ + 'Submit this verbatim in the form field the widget uses, named '
77
+ + '`cf-captcha-response` — treat it as opaque, since it is '
78
+ + 'verified against the session that produced it:\n'
79
+ + `cf-captcha-response: ${token}\n`
80
+ + (userAgent !== undefined
81
+ ? `Submit it under this User-Agent, which is the one that minted `
82
+ + `it (not any you sent): ${userAgent}\n`
83
+ : '')
84
+ + `Tokens are short lived — submit promptly. (CapSkip captcha id ${structured.captchaId})`,
85
+ };
86
+ });
87
+ });
88
+ }
89
+ //# sourceMappingURL=captchafox.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"captchafox.js","sourceRoot":"","sources":["../../src/tools/captchafox.ts"],"names":[],"mappings":"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","sourcesContent":["import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { z } from 'zod';\n\nimport { captchaFoxOutput, pageUrlSchema, proxySchema, timeoutSchema } from '../schemas.js';\nimport { runSolve } from '../solve.js';\nimport type { ToolContext } from '../solver.js';\n\nconst inputSchema = z.strictObject({\n sitekey: z\n .string()\n .min(1)\n .describe(\n \"The public key the widget renders with, conventionally prefixed 'sk_'. \"\n + \"Read it from data-sitekey on the widget container, from the \"\n + 'captchafox.render(...) call, or — if the page builds the widget at '\n + 'runtime — from the path segment after /captcha/ in the request to '\n + 'api.captchafox.com.',\n ),\n url: pageUrlSchema,\n api_server: z\n .string()\n .min(1)\n .optional()\n .describe(\n 'The widget entry point the target page loads. Defaults to '\n + \"'https://cdn.captchafox.com/'. Send the MAM package path instead when \"\n + 'the page loads that build — it returns a MAM_ prefixed token, and the '\n + 'two are not interchangeable.',\n ),\n proxy: proxySchema\n .optional()\n .describe(\n 'Solve from this network. CaptchaFox scores the address a widget runs on '\n + 'as well as the browser, so repeated solves from one address drift '\n + 'toward interactive challenges and then refusals.',\n ),\n timeout: timeoutSchema,\n});\n\nexport function registerCaptchaFoxTool(server: McpServer, ctx: ToolContext): void {\n server.registerTool(\n 'capskip_solve_captchafox',\n {\n title: 'Solve CaptchaFox',\n description:\n 'Solve a CaptchaFox challenge. CaptchaFox scores the browser itself '\n + 'rather than asking the visitor to read anything, so most solves draw '\n + 'no puzzle at all. Returns a token to put in the form field named '\n + \"'cf-captcha-response', verbatim — it is verified server side against \"\n + 'the session that produced it, so any edit invalidates it. IMPORTANT: '\n + 'the url must be the page the widget actually runs on. CaptchaFox '\n + 'checks it against the domains the key is registered for and refuses a '\n + 'mismatch permanently, not intermittently — so a sitekey that fails '\n + 'immediately and consistently usually means the wrong page URL, not a '\n + 'bad key. The result also carries the User-Agent the token was minted '\n + 'under; submit under that one, not your own.',\n inputSchema,\n outputSchema: captchaFoxOutput,\n // destructiveHint defaults to *true* whenever readOnlyHint is false, so\n // omitting it would advertise this tool as potentially destructive and cost\n // it auto-approval in clients that read the hint. Solving a captcha\n // destroys nothing. idempotentHint is deliberately left at its false\n // default: each call mints a new, short-lived token.\n annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },\n },\n async (args, extra) => {\n // A real browser session, like reCAPTCHA and GeeTest, and longer again when\n // an interactive challenge is drawn — so it takes the browser timeout.\n const timeout = args.timeout ?? ctx.config.recaptchaTimeout;\n\n const options: Record<string, unknown> = {};\n if (args.api_server !== undefined) options.api_server = args.api_server;\n if (args.proxy !== undefined) options.proxy = args.proxy;\n\n return runSolve(\n ctx,\n extra,\n { label: 'Solving CaptchaFox', timeoutSeconds: timeout },\n (client) => client.captchafox(args.sitekey, args.url, options as never),\n (result, seconds) => {\n const token = (result.token as string | undefined) ?? String(result.code ?? '');\n const userAgent = result.userAgent as string | undefined;\n\n const structured = {\n captchaId: String(result.captchaId ?? ''),\n code: String(result.code ?? ''),\n solveSeconds: Number(seconds.toFixed(2)),\n token,\n ...(userAgent !== undefined ? { userAgent } : {}),\n };\n\n return {\n structured,\n text:\n `Solved CaptchaFox in ${structured.solveSeconds}s.\\n`\n + 'Submit this verbatim in the form field the widget uses, named '\n + '`cf-captcha-response` — treat it as opaque, since it is '\n + 'verified against the session that produced it:\\n'\n + `cf-captcha-response: ${token}\\n`\n + (userAgent !== undefined\n ? `Submit it under this User-Agent, which is the one that minted `\n + `it (not any you sent): ${userAgent}\\n`\n : '')\n + `Tokens are short lived — submit promptly. (CapSkip captcha id ${structured.captchaId})`,\n };\n },\n );\n },\n );\n}\n"]}
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerCapyTool = registerCapyTool;
4
+ const zod_1 = require("zod");
5
+ const schemas_js_1 = require("../schemas.js");
6
+ const solve_js_1 = require("../solve.js");
7
+ const inputSchema = zod_1.z.strictObject({
8
+ sitekey: zod_1.z
9
+ .string()
10
+ .min(1)
11
+ .describe("The site's public Capy key, conventionally prefixed 'PUZZLE_'. Read it "
12
+ + "from 'capy_captchakey' in the page source, or from the widget script "
13
+ + "URL: <script src='https://jp.api.capy.me/puzzle/get_js/?k=PUZZLE_XXXX'>."),
14
+ url: schemas_js_1.pageUrlSchema,
15
+ api_server: zod_1.z
16
+ .string()
17
+ .min(1)
18
+ .optional()
19
+ .describe('The root of the Capy API the key lives behind, taken from the widget '
20
+ + "script URL. Defaults to 'https://jp.api.capy.me'. Note that "
21
+ + "'api.capy.me' no longer resolves, although several solver services "
22
+ + 'still document it.'),
23
+ proxy: schemas_js_1.proxySchema
24
+ .optional()
25
+ .describe('Draw the puzzle image through this proxy. That fetch is the only request '
26
+ + 'a Capy solve makes.'),
27
+ timeout: schemas_js_1.timeoutSchema,
28
+ });
29
+ function registerCapyTool(server, ctx) {
30
+ server.registerTool('capskip_solve_capy', {
31
+ title: 'Solve Capy Puzzle',
32
+ description: 'Solve a Capy Puzzle captcha — a slide puzzle where a piece is dragged '
33
+ + 'into a hole cut out of a photograph. IMPORTANT: unlike every other '
34
+ + 'captcha tool here, the answer is NOT a single token. It is three '
35
+ + 'values — captchakey, challengekey and answer — which all go into the '
36
+ + "target form, in fields named 'capy_captchakey', 'capy_challengekey' "
37
+ + "and 'capy_answer'. Submit the answer verbatim: it is the drag path "
38
+ + 'the widget would have recorded, so trimming or re-encoding it '
39
+ + 'invalidates the solve. The challenge key is single-use and '
40
+ + 'short-lived, so submit promptly rather than caching the three values. '
41
+ + 'A solve takes about two seconds — Capy refuses answers that arrive '
42
+ + 'faster than a human could have produced them, so CapSkip holds the '
43
+ + 'result back deliberately.',
44
+ inputSchema,
45
+ outputSchema: schemas_js_1.capyOutput,
46
+ // destructiveHint defaults to *true* whenever readOnlyHint is false, so
47
+ // omitting it would advertise this tool as potentially destructive and cost
48
+ // it auto-approval in clients that read the hint. Solving a captcha
49
+ // destroys nothing. idempotentHint is deliberately left at its false
50
+ // default: each call draws a fresh puzzle bound to a new challenge key.
51
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
52
+ }, async (args, extra) => {
53
+ // Capy is one HTTP fetch plus pixel math, not a browser solve, so it takes
54
+ // the default timeout rather than the longer browser one.
55
+ const timeout = args.timeout ?? ctx.config.defaultTimeout;
56
+ const options = {};
57
+ if (args.api_server !== undefined)
58
+ options.api_server = args.api_server;
59
+ if (args.proxy !== undefined)
60
+ options.proxy = args.proxy;
61
+ return (0, solve_js_1.runSolve)(ctx, extra, { label: 'Solving Capy Puzzle', timeoutSeconds: timeout }, (client) => client.capy(args.sitekey, args.url, options), (result, seconds) => {
62
+ const captchakey = result.captchakey;
63
+ const challengekey = result.challengekey;
64
+ const answer = result.answer;
65
+ const respKey = result.respKey;
66
+ const structured = {
67
+ captchaId: String(result.captchaId ?? ''),
68
+ // Capy's `code` is the raw answer object, which the Node SDK leaves
69
+ // as-is. Stringify it so the declared string output holds the same
70
+ // JSON a plain-text poll would have delivered.
71
+ code: typeof result.code === 'string'
72
+ ? result.code
73
+ : JSON.stringify(result.code ?? ''),
74
+ solveSeconds: Number(seconds.toFixed(2)),
75
+ ...(captchakey !== undefined ? { captchakey } : {}),
76
+ ...(challengekey !== undefined ? { challengekey } : {}),
77
+ ...(answer !== undefined ? { answer } : {}),
78
+ ...(respKey !== undefined ? { respKey } : {}),
79
+ };
80
+ return {
81
+ structured,
82
+ text: `Solved Capy Puzzle in ${structured.solveSeconds}s.\n`
83
+ + 'This is NOT a token — put all three values in the form, '
84
+ + 'verbatim, and submit promptly (the challenge key is '
85
+ + 'single-use and short-lived):\n'
86
+ + `capy_captchakey: ${captchakey ?? '(missing)'}\n`
87
+ + `capy_challengekey: ${challengekey ?? '(missing)'}\n`
88
+ + `capy_answer: ${answer ?? '(missing)'}\n`
89
+ + `(CapSkip captcha id ${structured.captchaId})`,
90
+ };
91
+ });
92
+ });
93
+ }
94
+ //# sourceMappingURL=capy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capy.js","sourceRoot":"","sources":["../../src/tools/capy.ts"],"names":[],"mappings":"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","sourcesContent":["import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { z } from 'zod';\n\nimport { capyOutput, pageUrlSchema, proxySchema, timeoutSchema } from '../schemas.js';\nimport { runSolve } from '../solve.js';\nimport type { ToolContext } from '../solver.js';\n\nconst inputSchema = z.strictObject({\n sitekey: z\n .string()\n .min(1)\n .describe(\n \"The site's public Capy key, conventionally prefixed 'PUZZLE_'. Read it \"\n + \"from 'capy_captchakey' in the page source, or from the widget script \"\n + \"URL: <script src='https://jp.api.capy.me/puzzle/get_js/?k=PUZZLE_XXXX'>.\",\n ),\n url: pageUrlSchema,\n api_server: z\n .string()\n .min(1)\n .optional()\n .describe(\n 'The root of the Capy API the key lives behind, taken from the widget '\n + \"script URL. Defaults to 'https://jp.api.capy.me'. Note that \"\n + \"'api.capy.me' no longer resolves, although several solver services \"\n + 'still document it.',\n ),\n proxy: proxySchema\n .optional()\n .describe(\n 'Draw the puzzle image through this proxy. That fetch is the only request '\n + 'a Capy solve makes.',\n ),\n timeout: timeoutSchema,\n});\n\nexport function registerCapyTool(server: McpServer, ctx: ToolContext): void {\n server.registerTool(\n 'capskip_solve_capy',\n {\n title: 'Solve Capy Puzzle',\n description:\n 'Solve a Capy Puzzle captcha — a slide puzzle where a piece is dragged '\n + 'into a hole cut out of a photograph. IMPORTANT: unlike every other '\n + 'captcha tool here, the answer is NOT a single token. It is three '\n + 'values — captchakey, challengekey and answer — which all go into the '\n + \"target form, in fields named 'capy_captchakey', 'capy_challengekey' \"\n + \"and 'capy_answer'. Submit the answer verbatim: it is the drag path \"\n + 'the widget would have recorded, so trimming or re-encoding it '\n + 'invalidates the solve. The challenge key is single-use and '\n + 'short-lived, so submit promptly rather than caching the three values. '\n + 'A solve takes about two seconds — Capy refuses answers that arrive '\n + 'faster than a human could have produced them, so CapSkip holds the '\n + 'result back deliberately.',\n inputSchema,\n outputSchema: capyOutput,\n // destructiveHint defaults to *true* whenever readOnlyHint is false, so\n // omitting it would advertise this tool as potentially destructive and cost\n // it auto-approval in clients that read the hint. Solving a captcha\n // destroys nothing. idempotentHint is deliberately left at its false\n // default: each call draws a fresh puzzle bound to a new challenge key.\n annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },\n },\n async (args, extra) => {\n // Capy is one HTTP fetch plus pixel math, not a browser solve, so it takes\n // the default timeout rather than the longer browser one.\n const timeout = args.timeout ?? ctx.config.defaultTimeout;\n\n const options: Record<string, unknown> = {};\n if (args.api_server !== undefined) options.api_server = args.api_server;\n if (args.proxy !== undefined) options.proxy = args.proxy;\n\n return runSolve(\n ctx,\n extra,\n { label: 'Solving Capy Puzzle', timeoutSeconds: timeout },\n (client) => client.capy(args.sitekey, args.url, options as never),\n (result, seconds) => {\n const captchakey = result.captchakey as string | undefined;\n const challengekey = result.challengekey as string | undefined;\n const answer = result.answer as string | undefined;\n const respKey = result.respKey as string | undefined;\n\n const structured = {\n captchaId: String(result.captchaId ?? ''),\n // Capy's `code` is the raw answer object, which the Node SDK leaves\n // as-is. Stringify it so the declared string output holds the same\n // JSON a plain-text poll would have delivered.\n code: typeof result.code === 'string'\n ? result.code\n : JSON.stringify(result.code ?? ''),\n solveSeconds: Number(seconds.toFixed(2)),\n ...(captchakey !== undefined ? { captchakey } : {}),\n ...(challengekey !== undefined ? { challengekey } : {}),\n ...(answer !== undefined ? { answer } : {}),\n ...(respKey !== undefined ? { respKey } : {}),\n };\n\n return {\n structured,\n text:\n `Solved Capy Puzzle in ${structured.solveSeconds}s.\\n`\n + 'This is NOT a token — put all three values in the form, '\n + 'verbatim, and submit promptly (the challenge key is '\n + 'single-use and short-lived):\\n'\n + `capy_captchakey: ${captchakey ?? '(missing)'}\\n`\n + `capy_challengekey: ${challengekey ?? '(missing)'}\\n`\n + `capy_answer: ${answer ?? '(missing)'}\\n`\n + `(CapSkip captcha id ${structured.captchaId})`,\n };\n },\n );\n },\n );\n}\n"]}
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerFriendlyCaptchaTool = registerFriendlyCaptchaTool;
4
+ const zod_1 = require("zod");
5
+ const schemas_js_1 = require("../schemas.js");
6
+ const solve_js_1 = require("../solve.js");
7
+ const inputSchema = zod_1.z.strictObject({
8
+ sitekey: zod_1.z
9
+ .string()
10
+ .min(1)
11
+ .describe("The data-sitekey attribute of the widget element — the one carrying "
12
+ + "class='frc-captcha'."),
13
+ url: schemas_js_1.pageUrlSchema,
14
+ version: zod_1.z
15
+ .enum(['v1', 'v2'])
16
+ .optional()
17
+ .describe('The protocol version the site uses. Two entirely different protocols '
18
+ + 'ship under this one name and a sitekey does not say which, so send '
19
+ + 'this or module_script. Defaults to v1 when neither is given.'),
20
+ module_script: zod_1.z
21
+ .string()
22
+ .min(1)
23
+ .optional()
24
+ .describe("The src of the widget script tag carrying type='module'. The most "
25
+ + 'reliable version signal there is, because it is the build the site '
26
+ + "actually loads: 'widget.module.min.js' means v1, 'site.min.js' v2."),
27
+ nomodule_script: zod_1.z
28
+ .string()
29
+ .min(1)
30
+ .optional()
31
+ .describe("The src of the widget script tag carrying 'nomodule'. Read for the same reason."),
32
+ api_server: zod_1.z
33
+ .string()
34
+ .min(1)
35
+ .optional()
36
+ .describe("The data-residency tenant the sitekey belongs to: 'global' (the "
37
+ + "default), 'eu', or a full URL. Both tenants mint a token for the same "
38
+ + "sitekey, so the wrong one is only caught by the site's own check."),
39
+ proxy: schemas_js_1.proxySchema
40
+ .optional()
41
+ .describe('Solve from this network. Worth setting sooner here than almost anywhere '
42
+ + 'else: the service raises the difficulty for addresses it has already '
43
+ + 'seen a lot of.'),
44
+ timeout: schemas_js_1.timeoutSchema,
45
+ });
46
+ function registerFriendlyCaptchaTool(server, ctx) {
47
+ server.registerTool('capskip_solve_friendly_captcha', {
48
+ title: 'Solve Friendly Captcha',
49
+ description: 'Solve a Friendly Captcha proof-of-work challenge. Nothing is shown on '
50
+ + 'screen — the widget does arithmetic instead of asking the visitor to '
51
+ + 'do anything. IMPORTANT: two entirely different protocols ship under '
52
+ + 'this one name, sharing a brand and a sitekey namespace and nothing '
53
+ + 'else, and a sitekey does not tell you which a site uses. Send version '
54
+ + '(v1 or v2), or send module_script and let CapSkip read the version off '
55
+ + 'the build the site loads. Solving the wrong one returns a well-formed '
56
+ + 'token the site rejects, with nothing to indicate the version was the '
57
+ + 'problem. The form field also differs by version: v1 uses '
58
+ + "'frc-captcha-solution', v2 uses 'frc-captcha-response'. Solve time is "
59
+ + 'not a constant — the service sets the difficulty per request — so '
60
+ + 'budget for that rather than assuming a fixed duration.',
61
+ inputSchema,
62
+ outputSchema: schemas_js_1.friendlyCaptchaOutput,
63
+ // destructiveHint defaults to *true* whenever readOnlyHint is false, so
64
+ // omitting it would advertise this tool as potentially destructive and cost
65
+ // it auto-approval in clients that read the hint. Solving a captcha
66
+ // destroys nothing. idempotentHint is deliberately left at its false
67
+ // default: each call mints a new token against fresh work.
68
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
69
+ }, async (args, extra) => {
70
+ // Proof-of-work, but not the millisecond kind ALTCHA does: the difficulty
71
+ // is set per request, and v2 always solves in a browser. Both make solve
72
+ // time variable enough to want the longer timeout.
73
+ const timeout = args.timeout ?? ctx.config.recaptchaTimeout;
74
+ const options = {};
75
+ if (args.version !== undefined)
76
+ options.version = args.version;
77
+ if (args.module_script !== undefined)
78
+ options.module_script = args.module_script;
79
+ if (args.nomodule_script !== undefined)
80
+ options.nomodule_script = args.nomodule_script;
81
+ if (args.api_server !== undefined)
82
+ options.api_server = args.api_server;
83
+ if (args.proxy !== undefined)
84
+ options.proxy = args.proxy;
85
+ return (0, solve_js_1.runSolve)(ctx, extra, { label: 'Solving Friendly Captcha', timeoutSeconds: timeout }, (client) => client.friendlyCaptcha(args.sitekey, args.url, options), (result, seconds) => {
86
+ const token = result.token ?? String(result.code ?? '');
87
+ const structured = {
88
+ captchaId: String(result.captchaId ?? ''),
89
+ code: String(result.code ?? ''),
90
+ solveSeconds: Number(seconds.toFixed(2)),
91
+ token,
92
+ };
93
+ // The field the token goes into differs per version, and getting it
94
+ // wrong is the single most common way a working v1 integration breaks
95
+ // on a v2 site — so name the right one rather than leaving it implied.
96
+ // A v2 token is a single opaque string beginning AQQA.; a v1 token is
97
+ // four dot-separated parts. Fall back to the token's own shape when no
98
+ // explicit version was sent.
99
+ const isV2 = args.version === 'v2'
100
+ || (args.version === undefined && token.startsWith('AQQA.'));
101
+ const field = isV2 ? 'frc-captcha-response' : 'frc-captcha-solution';
102
+ return {
103
+ structured,
104
+ text: `Solved Friendly Captcha in ${structured.solveSeconds}s.\n`
105
+ + `Submit this verbatim in the form field named \`${field}\` `
106
+ + `(${isV2 ? 'v2' : 'v1'}) — do not trim or re-encode it:\n`
107
+ + `${field}: ${token}\n`
108
+ + `(${token.length} characters; CapSkip captcha id ${structured.captchaId})`,
109
+ };
110
+ });
111
+ });
112
+ }
113
+ //# sourceMappingURL=friendlycaptcha.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"friendlycaptcha.js","sourceRoot":"","sources":["../../src/tools/friendlycaptcha.ts"],"names":[],"mappings":"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","sourcesContent":["import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { z } from 'zod';\n\nimport {\n friendlyCaptchaOutput, pageUrlSchema, proxySchema, timeoutSchema,\n} from '../schemas.js';\nimport { runSolve } from '../solve.js';\nimport type { ToolContext } from '../solver.js';\n\nconst inputSchema = z.strictObject({\n sitekey: z\n .string()\n .min(1)\n .describe(\n \"The data-sitekey attribute of the widget element — the one carrying \"\n + \"class='frc-captcha'.\",\n ),\n url: pageUrlSchema,\n version: z\n .enum(['v1', 'v2'])\n .optional()\n .describe(\n 'The protocol version the site uses. Two entirely different protocols '\n + 'ship under this one name and a sitekey does not say which, so send '\n + 'this or module_script. Defaults to v1 when neither is given.',\n ),\n module_script: z\n .string()\n .min(1)\n .optional()\n .describe(\n \"The src of the widget script tag carrying type='module'. The most \"\n + 'reliable version signal there is, because it is the build the site '\n + \"actually loads: 'widget.module.min.js' means v1, 'site.min.js' v2.\",\n ),\n nomodule_script: z\n .string()\n .min(1)\n .optional()\n .describe(\"The src of the widget script tag carrying 'nomodule'. Read for the same reason.\"),\n api_server: z\n .string()\n .min(1)\n .optional()\n .describe(\n \"The data-residency tenant the sitekey belongs to: 'global' (the \"\n + \"default), 'eu', or a full URL. Both tenants mint a token for the same \"\n + \"sitekey, so the wrong one is only caught by the site's own check.\",\n ),\n proxy: proxySchema\n .optional()\n .describe(\n 'Solve from this network. Worth setting sooner here than almost anywhere '\n + 'else: the service raises the difficulty for addresses it has already '\n + 'seen a lot of.',\n ),\n timeout: timeoutSchema,\n});\n\nexport function registerFriendlyCaptchaTool(server: McpServer, ctx: ToolContext): void {\n server.registerTool(\n 'capskip_solve_friendly_captcha',\n {\n title: 'Solve Friendly Captcha',\n description:\n 'Solve a Friendly Captcha proof-of-work challenge. Nothing is shown on '\n + 'screen — the widget does arithmetic instead of asking the visitor to '\n + 'do anything. IMPORTANT: two entirely different protocols ship under '\n + 'this one name, sharing a brand and a sitekey namespace and nothing '\n + 'else, and a sitekey does not tell you which a site uses. Send version '\n + '(v1 or v2), or send module_script and let CapSkip read the version off '\n + 'the build the site loads. Solving the wrong one returns a well-formed '\n + 'token the site rejects, with nothing to indicate the version was the '\n + 'problem. The form field also differs by version: v1 uses '\n + \"'frc-captcha-solution', v2 uses 'frc-captcha-response'. Solve time is \"\n + 'not a constant — the service sets the difficulty per request — so '\n + 'budget for that rather than assuming a fixed duration.',\n inputSchema,\n outputSchema: friendlyCaptchaOutput,\n // destructiveHint defaults to *true* whenever readOnlyHint is false, so\n // omitting it would advertise this tool as potentially destructive and cost\n // it auto-approval in clients that read the hint. Solving a captcha\n // destroys nothing. idempotentHint is deliberately left at its false\n // default: each call mints a new token against fresh work.\n annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },\n },\n async (args, extra) => {\n // Proof-of-work, but not the millisecond kind ALTCHA does: the difficulty\n // is set per request, and v2 always solves in a browser. Both make solve\n // time variable enough to want the longer timeout.\n const timeout = args.timeout ?? ctx.config.recaptchaTimeout;\n\n const options: Record<string, unknown> = {};\n if (args.version !== undefined) options.version = args.version;\n if (args.module_script !== undefined) options.module_script = args.module_script;\n if (args.nomodule_script !== undefined) options.nomodule_script = args.nomodule_script;\n if (args.api_server !== undefined) options.api_server = args.api_server;\n if (args.proxy !== undefined) options.proxy = args.proxy;\n\n return runSolve(\n ctx,\n extra,\n { label: 'Solving Friendly Captcha', timeoutSeconds: timeout },\n (client) => client.friendlyCaptcha(args.sitekey, args.url, options as never),\n (result, seconds) => {\n const token = (result.token as string | undefined) ?? String(result.code ?? '');\n\n const structured = {\n captchaId: String(result.captchaId ?? ''),\n code: String(result.code ?? ''),\n solveSeconds: Number(seconds.toFixed(2)),\n token,\n };\n\n // The field the token goes into differs per version, and getting it\n // wrong is the single most common way a working v1 integration breaks\n // on a v2 site — so name the right one rather than leaving it implied.\n // A v2 token is a single opaque string beginning AQQA.; a v1 token is\n // four dot-separated parts. Fall back to the token's own shape when no\n // explicit version was sent.\n const isV2 = args.version === 'v2'\n || (args.version === undefined && token.startsWith('AQQA.'));\n const field = isV2 ? 'frc-captcha-response' : 'frc-captcha-solution';\n\n return {\n structured,\n text:\n `Solved Friendly Captcha in ${structured.solveSeconds}s.\\n`\n + `Submit this verbatim in the form field named \\`${field}\\` `\n + `(${isV2 ? 'v2' : 'v1'}) — do not trim or re-encode it:\\n`\n + `${field}: ${token}\\n`\n + `(${token.length} characters; CapSkip captcha id ${structured.captchaId})`,\n };\n },\n );\n },\n );\n}\n"]}
@@ -1,6 +1,6 @@
1
1
  # CapSkip MCP Server — API Reference
2
2
 
3
- Complete reference for all five tools `capskip-mcp` registers. Parameter descriptions below are copied verbatim from each tool's schema in `src/tools/` so this document cannot drift from what a client actually sees when it lists tools.
3
+ Complete reference for all nine tools `capskip-mcp` registers. Parameter descriptions below are copied verbatim from each tool's schema in `src/tools/` so this document cannot drift from what a client actually sees when it lists tools.
4
4
 
5
5
  Every solve tool declares an `outputSchema` and returns MCP `structuredContent` matching it, plus a human-readable text block for clients that do not render structured output. Every tool schema rejects unknown keys — a misspelled or unsupported parameter is rejected by name rather than silently ignored.
6
6
 
@@ -15,6 +15,10 @@ Every solve tool declares an `outputSchema` and returns MCP `structuredContent`
15
15
  | `capskip_solve_recaptcha` | Solve a Google reCAPTCHA v2 or v3 widget | Yes |
16
16
  | `capskip_solve_turnstile` | Solve a Cloudflare Turnstile widget or challenge page | Yes |
17
17
  | `capskip_solve_geetest` | Solve a GeeTest v3 slide-puzzle captcha | Yes |
18
+ | `capskip_solve_altcha` | Solve an ALTCHA proof-of-work challenge | Yes (challenge fetch only) |
19
+ | `capskip_solve_capy` | Solve a Capy Puzzle captcha | Yes (puzzle-image fetch only) |
20
+ | `capskip_solve_captchafox` | Solve a CaptchaFox challenge | Yes |
21
+ | `capskip_solve_friendly_captcha` | Solve a Friendly Captcha proof-of-work challenge | Yes |
18
22
 
19
23
  ---
20
24
 
@@ -153,7 +157,7 @@ Solve a Google reCAPTCHA v2 or v3 widget, including invisible and Enterprise var
153
157
  | `proxy` | object | No | — | "Solve through this proxy so the token is issued against its IP." |
154
158
  | `timeout` | integer | No | `CAPSKIP_RECAPTCHA_TIMEOUT` (300) | "Seconds to wait before giving up. Maximum 600." |
155
159
 
156
- `proxy` shape (shared with `capskip_solve_turnstile` and `capskip_solve_geetest`):
160
+ `proxy` shape (shared with every solve tool except `capskip_solve_image_captcha`):
157
161
 
158
162
  | Field | Type | Required | Description |
159
163
  |---|---|---|---|
@@ -321,6 +325,281 @@ Text output: `Solved GeeTest in 14.2s.\nPost these back exactly as the site's ow
321
325
 
322
326
  ---
323
327
 
328
+ ## `capskip_solve_altcha`
329
+
330
+ Solve an ALTCHA proof-of-work challenge. ALTCHA is not a recognition captcha — there is nothing to read; the client brute-forces a number that satisfies a challenge, so a solve is deterministic and takes milliseconds. Give it either `challenge_url` (the endpoint the `<altcha-widget>` fetches from, which CapSkip will fetch) or `challenge_json` (the challenge document itself). Returns a token to put in the page's form field named `altcha`, verbatim. IMPORTANT: challenges expire quickly — some sites inside two minutes — so read the challenge immediately before calling and submit the token promptly. An expired challenge is rejected with a bare "verification failed" that looks exactly like a wrong answer.
331
+
332
+ ### Parameters
333
+
334
+ | Name | Type | Required | Default | Description |
335
+ |---|---|---|---|---|
336
+ | `url` | string (URL) | Yes | — | "Full URL of the page the captcha appears on, including scheme." |
337
+ | `challenge_url` | string | One of the two | — | "The endpoint the ALTCHA widget fetches its challenge from. CapSkip fetches it for you." |
338
+ | `challenge_json` | string | One of the two | — | "The challenge document itself, as a JSON string, when you already have it. Solved locally with no network request." |
339
+ | `proxy` | object | No | — | "Fetch the challenge through this proxy. Used ONLY for the challenge_url fetch." |
340
+ | `timeout` | integer | No | `CAPSKIP_DEFAULT_TIMEOUT` (120) | "Seconds to wait before giving up. Maximum 600." |
341
+
342
+ Passing neither `challenge_url` nor `challenge_json` returns an error naming what is missing, rather than spending a round trip to learn CapSkip's `ERROR_BAD_PARAMETERS`. Passing both is allowed — the inline document wins.
343
+
344
+ `proxy` has the same `{ type, uri }` shape documented under `capskip_solve_recaptcha` above.
345
+
346
+ ### Finding the challenge
347
+
348
+ Open DevTools → Network on the target page and look for the request the `<altcha-widget>` makes for its challenge (often something like `/altcha/challenge`). The request URL is `challenge_url`; its JSON response is `challenge_json`. The widget attribute naming that endpoint depends on the widget version: v1/v2 use `challengeurl="…"`, while v3+ uses `challenge="…"` for both a URL and inline data.
349
+
350
+ ### Output shape
351
+
352
+ | Field | Type | Always present | Description |
353
+ |---|---|---|---|
354
+ | `captchaId` | string | yes | CapSkip's internal id for this solve |
355
+ | `code` | string | yes | The base64 token — the same string as `token` |
356
+ | `solveSeconds` | number | yes | Wall-clock time the solve took |
357
+ | `token` | string | yes | The payload to submit in the `altcha` form field |
358
+ | `number` | number | when the server reports one | The counter that satisfied the challenge. Present for both the legacy and proof-of-work v2 schemes, whose tokens carry it differently. |
359
+
360
+ ### Example
361
+
362
+ Request:
363
+
364
+ ```json
365
+ {
366
+ "url": "https://example.com/signup",
367
+ "challenge_url": "https://example.com/captcha/api/altcha/challenge"
368
+ }
369
+ ```
370
+
371
+ Response (`structuredContent`):
372
+
373
+ ```json
374
+ {
375
+ "captchaId": "10433",
376
+ "code": "eyJhbGdvcml0aG0iOiJTSEEtMjU2Iiwi...",
377
+ "solveSeconds": 0.27,
378
+ "token": "eyJhbGdvcml0aG0iOiJTSEEtMjU2Iiwi...",
379
+ "number": 9661
380
+ }
381
+ ```
382
+
383
+ Text output: `Solved ALTCHA in 0.27s.\nSubmit this verbatim in the form field the widget uses, named \`altcha\` — do not re-encode, trim or re-order it, or the server's signature check fails:\naltcha: eyJhbGdvcml0aG0iOiJTSEEtMjU2Iiwi...\n(counter 9661; CapSkip captcha id 10433)`
384
+
385
+ ### Unsupported algorithms
386
+
387
+ CapSkip solves the legacy scheme (SHA-1/256/384/512) and PoW v2 with PBKDF2 or SHA. **Argon2id and scrypt are refused**, not attempted: the solve returns `ERROR_CAPTCHA_UNSOLVABLE` and is never retried. ALTCHA recommends PBKDF2 as the default, so this affects a minority of sites.
388
+
389
+ ---
390
+
391
+ ## `capskip_solve_capy`
392
+
393
+ Solve a Capy Puzzle captcha — a slide puzzle where a piece is dragged into a hole cut out of a photograph. IMPORTANT: unlike every other captcha tool here, the answer is NOT a single token. It is three values — `captchakey`, `challengekey` and `answer` — which all go into the target form, in fields named `capy_captchakey`, `capy_challengekey` and `capy_answer`. Submit the answer verbatim: it is the drag path the widget would have recorded, so trimming or re-encoding it invalidates the solve. The challenge key is single-use and short-lived, so submit promptly rather than caching the three values.
394
+
395
+ ### Parameters
396
+
397
+ | Name | Type | Required | Default | Description |
398
+ |---|---|---|---|---|
399
+ | `sitekey` | string | Yes | — | "The site's public Capy key, conventionally prefixed 'PUZZLE_'." |
400
+ | `url` | string (URL) | Yes | — | "Full URL of the page the captcha appears on, including scheme." |
401
+ | `api_server` | string | No | `https://jp.api.capy.me` | "The root of the Capy API the key lives behind, taken from the widget script URL." |
402
+ | `proxy` | object | No | — | "Draw the puzzle image through this proxy. That fetch is the only request a Capy solve makes." |
403
+ | `timeout` | integer | No | `CAPSKIP_TIMEOUT` (120) | "Seconds to wait before giving up. Maximum 600." |
404
+
405
+ `proxy` has the same `{ type, uri }` shape documented under `capskip_solve_recaptcha` above.
406
+
407
+ > `api.capy.me` no longer resolves, although several solver services still document it. The live host is `jp.api.capy.me`; if a site's widget points somewhere else, pass that as `api_server`.
408
+
409
+ ### Finding the captcha key
410
+
411
+ It is in the page source as `capy_captchakey`, or in the widget script URL: `<script src="https://jp.api.capy.me/puzzle/get_js/?k=PUZZLE_XXXX">`.
412
+
413
+ ### Output shape
414
+
415
+ | Field | Type | Always present | Description |
416
+ |---|---|---|---|
417
+ | `captchaId` | string | yes | CapSkip's internal id for this solve |
418
+ | `code` | string | yes | The raw answer object, as a JSON string |
419
+ | `solveSeconds` | number | yes | Wall-clock time the solve took |
420
+ | `captchakey` | string | when the answer parsed | The value for the form's `capy_captchakey` field |
421
+ | `challengekey` | string | when the answer parsed | The value for the form's `capy_challengekey` field. Single-use. |
422
+ | `answer` | string | when the answer parsed | The value for the form's `capy_answer` field — the drag path |
423
+ | `respKey` | string | when the answer parsed | Empty for a puzzle solve; present only for shape compatibility |
424
+
425
+ ### Example
426
+
427
+ Request:
428
+
429
+ ```json
430
+ {
431
+ "sitekey": "PUZZLE_Abc1dEFghIJKLM2no34P56q7rStu8v",
432
+ "url": "https://example.com/login"
433
+ }
434
+ ```
435
+
436
+ Response (`structuredContent`):
437
+
438
+ ```json
439
+ {
440
+ "captchaId": "10433",
441
+ "code": "{\"captchakey\":\"PUZZLE_Abc1dEFghIJKLM2no34P56q7rStu8v\",\"challengekey\":\"BalY2gJaI8uA2SGVOZhqBQ3V0CYSNNGP\",\"answer\":\"0xax8ex0xax84x...\",\"respKey\":\"\"}",
442
+ "solveSeconds": 2.14,
443
+ "captchakey": "PUZZLE_Abc1dEFghIJKLM2no34P56q7rStu8v",
444
+ "challengekey": "BalY2gJaI8uA2SGVOZhqBQ3V0CYSNNGP",
445
+ "answer": "0xax8ex0xax84x0xkx7qx0x18x76x...",
446
+ "respKey": ""
447
+ }
448
+ ```
449
+
450
+ ### Why a solve takes about two seconds
451
+
452
+ Capy grades the wall-clock gap between issuing the puzzle and verifying the answer, and refuses anything superhuman with `CAPTCHA verification failed` — the same message a wrong answer gets. CapSkip therefore holds every result until two seconds have elapsed since it drew the puzzle. The detection itself takes about a tenth of a second; the rest is a deliberate wait. Nothing to configure.
453
+
454
+ ---
455
+
456
+ ## `capskip_solve_captchafox`
457
+
458
+ Solve a CaptchaFox challenge. CaptchaFox scores the browser itself rather than asking the visitor to read anything, so most solves draw no puzzle at all. Returns a token to put in the form field named `cf-captcha-response`, verbatim — it is verified server side against the session that produced it, so any edit invalidates it.
459
+
460
+ ### Parameters
461
+
462
+ | Name | Type | Required | Default | Description |
463
+ |---|---|---|---|---|
464
+ | `sitekey` | string | Yes | — | "The public key the widget renders with, conventionally prefixed 'sk_'." |
465
+ | `url` | string (URL) | Yes | — | "Full URL of the page the captcha appears on, including scheme." |
466
+ | `api_server` | string | No | `https://cdn.captchafox.com/` | "The widget entry point the target page loads." |
467
+ | `proxy` | object | No | — | "Solve from this network." |
468
+ | `timeout` | integer | No | `CAPSKIP_RECAPTCHA_TIMEOUT` (300) | "Seconds to wait before giving up. Maximum 600." |
469
+
470
+ ### The page URL has to match the key
471
+
472
+ CaptchaFox keys are registered against a list of allowed domains and the service checks the host before it issues anything. A key that is correct but used on a page outside that list is refused permanently, not intermittently — so a sitekey that fails immediately and consistently usually means the wrong page URL, not a bad key. CapSkip reports that case rather than retrying it, because retrying cannot help.
473
+
474
+ ### Choosing the widget source
475
+
476
+ | `api_server` | Token | Default |
477
+ |---|---|---|
478
+ | `https://cdn.captchafox.com/` | Plain | Yes |
479
+ | `https://s.uicdn.com/mampkg/…` | `MAM_` prefixed | No |
480
+
481
+ Read the value from the `<script>` tag that loads the widget. Sending the wrong one still succeeds, but returns a token in a format the target site will not accept — a silent verification failure rather than an error.
482
+
483
+ ### Challenge types
484
+
485
+ | Challenge | Frequency | Supported |
486
+ |---|---|---|
487
+ | Invisible | Usually | Yes |
488
+ | Slide | Sometimes | Yes |
489
+ | Image select | Rarely | No |
490
+ | Audio | Rarely | No |
491
+
492
+ The two unsupported ones are uncommon and a retry usually draws a different challenge, so treat an unsolvable result as a signal to resubmit rather than a permanent failure of the key.
493
+
494
+ ### Output shape
495
+
496
+ | Field | Type | Always present | Description |
497
+ |---|---|---|---|
498
+ | `captchaId` | string | yes | CapSkip's internal id for this solve |
499
+ | `code` | string | yes | The token — the same string as `token` |
500
+ | `solveSeconds` | number | yes | Wall-clock time the solve took |
501
+ | `token` | string | yes | The value to submit in the `cf-captcha-response` field |
502
+ | `userAgent` | string | when the solve reported one | The UA the token was minted under — the browser's own, not one you sent |
503
+
504
+ ### Example
505
+
506
+ Request:
507
+
508
+ ```json
509
+ {
510
+ "sitekey": "sk_xtNxpk6fCdFbxh1_xJeGflSdCE9tn99G",
511
+ "url": "https://example.com/signup"
512
+ }
513
+ ```
514
+
515
+ Response (`structuredContent`):
516
+
517
+ ```json
518
+ {
519
+ "captchaId": "10434",
520
+ "code": "177f50c25b845601e5c779cdb51b040d...",
521
+ "solveSeconds": 6.31,
522
+ "token": "177f50c25b845601e5c779cdb51b040d...",
523
+ "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) ... Chrome/146.0.0.0 Safari/537.36"
524
+ }
525
+ ```
526
+
527
+ Submit the token under the reported `userAgent`, not your own: CapSkip solves in a real browser and the token is bound to that browser's identity.
528
+
529
+ ---
530
+
531
+ ## `capskip_solve_friendly_captcha`
532
+
533
+ Solve a Friendly Captcha proof-of-work challenge. Nothing is shown on screen — the widget does arithmetic instead of asking the visitor to do anything.
534
+
535
+ ### Parameters
536
+
537
+ | Name | Type | Required | Default | Description |
538
+ |---|---|---|---|---|
539
+ | `sitekey` | string | Yes | — | "The data-sitekey attribute of the widget element — the one carrying class='frc-captcha'." |
540
+ | `url` | string (URL) | Yes | — | "Full URL of the page the captcha appears on, including scheme." |
541
+ | `version` | `"v1"` \| `"v2"` | No | `v1` | "The protocol version the site uses. Send this or module_script." |
542
+ | `module_script` | string | No | — | "The src of the widget script tag carrying type='module'." |
543
+ | `nomodule_script` | string | No | — | "The src of the widget script tag carrying 'nomodule'." |
544
+ | `api_server` | string | No | `global` | "The data-residency tenant the sitekey belongs to: 'global', 'eu', or a full URL." |
545
+ | `proxy` | object | No | — | "Solve from this network." |
546
+ | `timeout` | integer | No | `CAPSKIP_RECAPTCHA_TIMEOUT` (300) | "Seconds to wait before giving up. Maximum 600." |
547
+
548
+ ### Version 1 and version 2
549
+
550
+ Two entirely different protocols ship under this one name, sharing a brand and a sitekey namespace and nothing else, and a sitekey does not tell you which a site uses. Both are live. Solve the wrong one and you get a well-formed token the target site rejects, with no indication that the version was the problem.
551
+
552
+ | `version` | Widget package | Script | Form field |
553
+ |---|---|---|---|
554
+ | `v1` | `friendly-challenge` | `widget.module.min.js` / `widget.min.js` | `frc-captcha-solution` |
555
+ | `v2` | `@friendlycaptcha/sdk` | `site.min.js` | `frc-captcha-response` |
556
+
557
+ CapSkip decides in this order, stopping at the first answer: the `version` parameter; then the script URL from `module_script` or `nomodule_script`, which is the most reliable signal because it is the build the site actually loads; then v1. The tool's text reply names the form field matching the version that was actually solved, so an agent does not have to work it out.
558
+
559
+ A `version` outside `v1`/`v2` is refused by the schema before the call reaches CapSkip.
560
+
561
+ ### Output shape
562
+
563
+ | Field | Type | Always present | Description |
564
+ |---|---|---|---|
565
+ | `captchaId` | string | yes | CapSkip's internal id for this solve |
566
+ | `code` | string | yes | The token — the same string as `token` |
567
+ | `solveSeconds` | number | yes | Wall-clock time the solve took |
568
+ | `token` | string | yes | The value to submit, in `frc-captcha-solution` (v1) or `frc-captcha-response` (v2) |
569
+
570
+ ### Example
571
+
572
+ Request:
573
+
574
+ ```json
575
+ {
576
+ "sitekey": "FCMGEMUD2M567T8G",
577
+ "url": "https://example.com/signup",
578
+ "version": "v2"
579
+ }
580
+ ```
581
+
582
+ Response (`structuredContent`):
583
+
584
+ ```json
585
+ {
586
+ "captchaId": "10435",
587
+ "code": "AQQA.vW7kd3CujKT8PaQgEcW18QaH...",
588
+ "solveSeconds": 18.4,
589
+ "token": "AQQA.vW7kd3CujKT8PaQgEcW18QaH..."
590
+ }
591
+ ```
592
+
593
+ ### Token sizes differ sharply between versions
594
+
595
+ A v1 token is four dot-separated parts and runs to a few hundred characters. A v2 token is a single opaque string beginning `AQQA.` and is roughly six kilobytes, so make sure whatever carries it — a hidden field, a database column, a proxied request — is sized for that.
596
+
597
+ ### Solve time is not a constant
598
+
599
+ The service decides how much work a request is worth at the moment it is made, and raises that figure for addresses it has already seen a lot of: the published range between a fresh address and a heavily used one is close to thirty times the work for the same token. Budget for that in your timeout rather than assuming a fixed duration, and use a proxy pool past casual volume.
600
+
601
+ ---
602
+
324
603
  ## Validation errors
325
604
 
326
605
  Every tool's schema is a strict object: any key it does not declare is rejected rather than ignored, and every required key must be present. These failures return `isError: true` before the call ever reaches CapSkip. Examples:
@@ -156,6 +156,28 @@ The timeout error also names the CapSkip captcha id when one was assigned, so a
156
156
 
157
157
  ---
158
158
 
159
+ ## ALTCHA is rejected even though the solve succeeded
160
+
161
+ **Symptom:** `capskip_solve_altcha` returns a token, but the site answers "verification failed".
162
+
163
+ **Cause:** The challenge expired before the token was submitted. A v1 challenge carries its expiry inside `salt` (`…?expires=1788669437`); a v2 challenge declares it as `parameters.expiresAt`. The windows are tighter than people expect — some sites issue challenges valid for about two minutes. An expired challenge produces a technically correct token that the site refuses with no explanation, which looks exactly like a wrong answer.
164
+
165
+ **Fix:** Read the challenge immediately before calling the tool, and submit the token straight away. Do not fetch a batch of challenges in advance, and do not hold a token while a user fills in a form.
166
+
167
+ Passing `challenge_url` rather than `challenge_json` helps: if the challenge expires while the job is queued, CapSkip fetches a fresh one automatically, the way the widget's own `refetchonexpire` does.
168
+
169
+ ---
170
+
171
+ ## ALTCHA returns ERROR_CAPTCHA_UNSOLVABLE immediately
172
+
173
+ **Symptom:** The solve fails in well under a second, with no retry.
174
+
175
+ **Cause:** Either the challenge uses an algorithm CapSkip does not implement — **Argon2id** or **scrypt**, both memory-hard KDFs with no implementation in the .NET runtime CapSkip ships — or the inline `challenge_json` had already expired or was malformed. None of those can succeed on a second attempt, so they are refused rather than retried.
176
+
177
+ **Fix:** Check the challenge document's `algorithm` field. CapSkip solves the legacy scheme (SHA-1/256/384/512) and PoW v2 with PBKDF2 or SHA; ALTCHA itself recommends PBKDF2 as the default, so an Argon2id site is unusual and worth reporting. If the algorithm is supported, fetch a fresh challenge — the one you sent had expired.
178
+
179
+ ---
180
+
159
181
  ## GeeTest always fails
160
182
 
161
183
  **Symptom:** `capskip_solve_geetest` fails or times out even though `gt` and `challenge` look correct.
@@ -190,7 +212,7 @@ If the site loads GeeTest from a non-default domain, pass it through as `api_ser
190
212
 
191
213
  **Symptom:** `capskip_solve_recaptcha` is called on a widget that has a `data-sitekey`, and every attempt fails, times out, or returns a token the site rejects.
192
214
 
193
- **Cause:** The captcha is not a reCAPTCHA. CapSkip supports exactly four types — image captchas, reCAPTCHA v2/v3, Cloudflare Turnstile, and GeeTest v3. It **cannot** solve hCaptcha or FunCaptcha/Arkose, and there is no tool for them.
215
+ **Cause:** The captcha is not a reCAPTCHA. CapSkip supports exactly five types — image captchas, reCAPTCHA v2/v3, Cloudflare Turnstile, GeeTest v3, and ALTCHA. It **cannot** solve hCaptcha or FunCaptcha/Arkose, and there is no tool for them.
194
216
 
195
217
  hCaptcha is easy to misidentify because it also carries a `data-sitekey`:
196
218
 
package/docs/TUTORIAL.md CHANGED
@@ -19,7 +19,8 @@ For each captcha type, four questions in order:
19
19
  6. [Turnstile widget](#6-turnstile-widget)
20
20
  7. [Turnstile challenge page](#7-turnstile-challenge-page)
21
21
  8. [GeeTest v3](#8-geetest-v3)
22
- 9. [Using a proxy](#9-using-a-proxy)
22
+ 9. [ALTCHA](#9-altcha)
23
+ 10. [Using a proxy](#10-using-a-proxy)
23
24
 
24
25
  ---
25
26
 
@@ -242,9 +243,47 @@ A three-field post-back: `challenge`, `validate`, and `seccode` from the respons
242
243
 
243
244
  ---
244
245
 
245
- ## 9. Using a proxy
246
+ ## 9. ALTCHA
246
247
 
247
- Solving through the same IP you will submit from generally improves acceptance for reCAPTCHA, Turnstile, and GeeTest. All three tools accept an optional `proxy` object:
248
+ ### How do I recognize this captcha on a page?
249
+
250
+ An `<altcha-widget>` element, or a checkbox-style control whose container mentions "altcha". Unlike the others there is no sitekey: the widget names a challenge source instead. Read the page source rather than assuming which attribute — v1/v2 widgets use `challengeurl="…"` (with `challengejson="…"` for an inline challenge), while v3+ uses a single `challenge="…"` that takes either a URL or the challenge data.
251
+
252
+ Some deployments generate the challenge in-page and fetch nothing at all. In that case there is no network request to watch — read the challenge out of the widget.
253
+
254
+ ### What do I pass to the tool?
255
+
256
+ `url`, plus **one** of:
257
+
258
+ - `challenge_url` — the endpoint the widget fetches from; CapSkip fetches it for you
259
+ - `challenge_json` — the challenge document itself, as a JSON string; solved locally with no network request
260
+
261
+ ```json
262
+ {
263
+ "url": "https://example.com/signup",
264
+ "challenge_url": "https://example.com/captcha/api/altcha/challenge"
265
+ }
266
+ ```
267
+
268
+ Find the endpoint in DevTools → Network: it is the request the widget makes for its challenge, often something like `/altcha/challenge`. Its JSON response is what `challenge_json` would carry.
269
+
270
+ > **Challenges expire, and the window is short** — some sites inside two minutes. Fetch one immediately before solving and submit the token promptly. An expired challenge is rejected with a bare "verification failed" that looks exactly like a wrong answer, which sends people hunting for a solver bug that is not there.
271
+
272
+ ### Where does the answer go?
273
+
274
+ A single field. Submit `token` under the name `altcha`, exactly as the widget would:
275
+
276
+ ```
277
+ email=someone%40example.com&altcha=eyJhbGdvcml0aG0iOiJTSEEtMjU2Iiwi...
278
+ ```
279
+
280
+ Do not re-encode, trim or re-order it: the token is base64 of a JSON document whose fields are covered by the server's HMAC signature, so any modification invalidates it. Some integrations read the payload from a JSON body field instead — check what the page's own submit sends and mirror it.
281
+
282
+ ---
283
+
284
+ ## 10. Using a proxy
285
+
286
+ Solving through the same IP you will submit from generally improves acceptance for reCAPTCHA, Turnstile, and GeeTest. All three tools accept an optional `proxy` object, as does `capskip_solve_altcha` — though there it applies only to the challenge fetch, never to the solve itself:
248
287
 
249
288
  ```json
250
289
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "capskip-mcp",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Captcha solver MCP server for AI agents: solve reCAPTCHA v2/v3, Cloudflare Turnstile, GeeTest and image captchas via CapSkip, a local unlimited captcha solver with no per-solve fees.",
5
5
  "main": "./dist/server.js",
6
6
  "exports": {
@@ -33,7 +33,7 @@
33
33
  "files": ["dist", "docs", "examples", "CHANGELOG.md", "SECURITY.md"],
34
34
  "dependencies": {
35
35
  "@modelcontextprotocol/sdk": "^1.30.0",
36
- "capskip": "^1.1.0",
36
+ "capskip": "^1.3.0",
37
37
  "zod": "^4.4.3"
38
38
  },
39
39
  "devDependencies": {