capskip-mcp 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/README.md +10 -3
- package/dist/index.js +0 -0
- package/dist/schemas.js +7 -1
- package/dist/schemas.js.map +1 -1
- package/dist/server.js +4 -2
- package/dist/server.js.map +1 -1
- package/dist/tools/altcha.js +100 -0
- package/dist/tools/altcha.js.map +1 -0
- package/docs/API_REFERENCE.md +65 -1
- package/docs/TROUBLESHOOTING.md +23 -1
- package/docs/TUTORIAL.md +42 -3
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,35 @@ 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.1.0] - 2026-09-12
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- **`capskip_solve_altcha`** — solve an ALTCHA proof-of-work challenge. Takes
|
|
18
|
+
`url` plus one of `challenge_url` (the endpoint the `<altcha-widget>` fetches
|
|
19
|
+
from, which CapSkip fetches for you) or `challenge_json` (the challenge
|
|
20
|
+
document itself, solved locally with no network request). Returns `token`, the
|
|
21
|
+
payload to submit verbatim in the site's form field named `altcha`, and
|
|
22
|
+
`number`, the counter that solved it.
|
|
23
|
+
- Calling it with neither challenge parameter returns an error naming what is
|
|
24
|
+
missing, and where to find it, rather than spending a round trip to learn
|
|
25
|
+
CapSkip's `ERROR_BAD_PARAMETERS`.
|
|
26
|
+
- `number` is reported for both ALTCHA generations — the legacy scheme and
|
|
27
|
+
proof-of-work v2 (PBKDF2/SHA), whose token carries no top-level `number` at
|
|
28
|
+
all. Verified against live v1 and v2 deployments.
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
|
|
32
|
+
- Requires `capskip` >= 1.2.0, which is where `altcha()` was added.
|
|
33
|
+
- The server instructions now list ALTCHA among the solvable types.
|
|
34
|
+
|
|
35
|
+
### Notes
|
|
36
|
+
|
|
37
|
+
- ALTCHA uses `CAPSKIP_DEFAULT_TIMEOUT`, not `CAPSKIP_RECAPTCHA_TIMEOUT` — it is
|
|
38
|
+
CPU proof-of-work measured in milliseconds, not a browser solve.
|
|
39
|
+
- A `proxy` passed to this tool applies only to the `challenge_url` fetch; an
|
|
40
|
+
inline `challenge_json` never touches the network.
|
|
41
|
+
|
|
13
42
|
## [1.0.0] - 2026-08-09
|
|
14
43
|
|
|
15
44
|
### Added
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://github.com/capskip/capskip-mcp/actions/workflows/ci.yml)
|
|
6
6
|
[](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 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
|
|
|
@@ -31,6 +31,7 @@ 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 |
|
|
34
35
|
| **Image captcha solver** (text/OCR) | `capskip_solve_image_captcha` | File path, URL, data URI, or base64 |
|
|
35
36
|
|
|
36
37
|
**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.
|
|
@@ -112,6 +113,7 @@ CapSkip exposes the familiar `in.php` / `res.php` endpoints, so it works as a **
|
|
|
112
113
|
| `capskip_solve_recaptcha` | Solve reCAPTCHA v2 or v3, including invisible and Enterprise | `sitekey`, `url` |
|
|
113
114
|
| `capskip_solve_turnstile` | Solve a Cloudflare Turnstile widget or challenge page | `sitekey`, `url` |
|
|
114
115
|
| `capskip_solve_geetest` | Solve a GeeTest v3 slide-puzzle captcha | `gt`, `challenge`, `url` |
|
|
116
|
+
| `capskip_solve_altcha` | Solve an ALTCHA proof-of-work challenge | `url`, and one of `challenge_url` / `challenge_json` |
|
|
115
117
|
|
|
116
118
|
> **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
119
|
|
|
@@ -152,6 +154,10 @@ For non-agent scripts, use the language SDKs directly — see the [Playwright](h
|
|
|
152
154
|
| `CAPSKIP_PORT` | `8080` | API port from CapSkip settings |
|
|
153
155
|
| `CAPSKIP_TIMEOUT` | `120` | Default `timeout` for `capskip_solve_image_captcha`, seconds |
|
|
154
156
|
| `CAPSKIP_RECAPTCHA_TIMEOUT` | `300` | Default `timeout` for the reCAPTCHA / Turnstile / GeeTest tools, seconds |
|
|
157
|
+
|
|
158
|
+
ALTCHA uses `CAPSKIP_DEFAULT_TIMEOUT`, not the reCAPTCHA one — it is CPU
|
|
159
|
+
proof-of-work measured in milliseconds, not a browser solve.
|
|
160
|
+
|
|
155
161
|
| `CAPSKIP_POLLING_INTERVAL` | `5` | Max seconds between polls |
|
|
156
162
|
|
|
157
163
|
CLI flags override environment variables, which override the defaults:
|
|
@@ -179,6 +185,7 @@ Every solve tool returns a human-readable text block and `structuredContent` mat
|
|
|
179
185
|
|
|
180
186
|
- **`capskip_solve_turnstile`** adds `userAgent`. Submit the token with this exact User-Agent — Cloudflare rejects a token replayed under a different one.
|
|
181
187
|
- **`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.
|
|
188
|
+
- **`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`.
|
|
182
189
|
|
|
183
190
|
Long solves emit MCP progress notifications, so a 45-second reCAPTCHA does not trip your client's tool-call timeout.
|
|
184
191
|
|
|
@@ -221,11 +228,11 @@ The MCP server is MIT-licensed and free. It requires the CapSkip desktop app, wh
|
|
|
221
228
|
|
|
222
229
|
### Which captchas can it solve?
|
|
223
230
|
|
|
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.
|
|
231
|
+
reCAPTCHA v2 (checkbox and invisible), reCAPTCHA v3, reCAPTCHA Enterprise, Cloudflare Turnstile, GeeTest v3, ALTCHA, and image/text captchas. hCaptcha and FunCaptcha/Arkose are not supported.
|
|
225
232
|
|
|
226
233
|
### Why does my reCAPTCHA v3 token get a low score?
|
|
227
234
|
|
|
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
|
|
235
|
+
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, GeeTest and ALTCHA tools (for ALTCHA it applies only to the challenge fetch).
|
|
229
236
|
|
|
230
237
|
### Does it need my captcha to be on a public page?
|
|
231
238
|
|
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.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,12 @@ 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
|
+
});
|
|
45
51
|
exports.statusOutput = zod_1.z.object({
|
|
46
52
|
reachable: zod_1.z.boolean(),
|
|
47
53
|
host: zod_1.z.string(),
|
package/dist/schemas.js.map
CHANGED
|
@@ -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,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 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,14 @@ 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");
|
|
7
8
|
const geetest_js_1 = require("./tools/geetest.js");
|
|
8
9
|
const image_js_1 = require("./tools/image.js");
|
|
9
10
|
const recaptcha_js_1 = require("./tools/recaptcha.js");
|
|
10
11
|
const status_js_1 = require("./tools/status.js");
|
|
11
12
|
const turnstile_js_1 = require("./tools/turnstile.js");
|
|
12
13
|
exports.SERVER_NAME = 'capskip';
|
|
13
|
-
exports.SERVER_VERSION = '1.
|
|
14
|
+
exports.SERVER_VERSION = '1.1.0';
|
|
14
15
|
/** Build a fully-registered MCP server. Does not connect a transport. */
|
|
15
16
|
function createServer(config) {
|
|
16
17
|
const server = new mcp_js_1.McpServer({ name: exports.SERVER_NAME, version: exports.SERVER_VERSION }, {
|
|
@@ -18,7 +19,7 @@ function createServer(config) {
|
|
|
18
19
|
+ 'tools when browser automation is blocked by a captcha: read the sitekey '
|
|
19
20
|
+ 'from the page, call the matching tool, then place the returned token in '
|
|
20
21
|
+ "the page's response field. CapSkip solves image captchas, reCAPTCHA v2 "
|
|
21
|
-
+ 'and v3, Cloudflare Turnstile,
|
|
22
|
+
+ 'and v3, Cloudflare Turnstile, GeeTest v3, and ALTCHA — it cannot solve hCaptcha '
|
|
22
23
|
+ 'or FunCaptcha/Arkose, so do not call these tools for those. An hCaptcha '
|
|
23
24
|
+ 'widget renders as <div class="h-captcha" data-sitekey="…">, which is '
|
|
24
25
|
+ 'easy to mistake for reCAPTCHA when scanning the DOM for data-sitekey. '
|
|
@@ -31,6 +32,7 @@ function createServer(config) {
|
|
|
31
32
|
(0, recaptcha_js_1.registerRecaptchaTool)(server, ctx);
|
|
32
33
|
(0, turnstile_js_1.registerTurnstileTool)(server, ctx);
|
|
33
34
|
(0, geetest_js_1.registerGeetestTool)(server, ctx);
|
|
35
|
+
(0, altcha_js_1.registerAltchaTool)(server, ctx);
|
|
34
36
|
return server;
|
|
35
37
|
}
|
|
36
38
|
//# sourceMappingURL=server.js.map
|
package/dist/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":";;;AAeA,oCA4BC;AA3CD,oEAAoE;AAGpE,2CAA4C;AAC5C,iDAAuD;AACvD,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,kFAAkF;cAClF,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;IAEhC,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 { 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.1.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, and ALTCHA — 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\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":";;AAmCA,gDAsFC;AAxHD,6BAAwB;AAExB,8CAAwF;AACxF,0CAAuC;AAGvC,MAAM,WAAW,GAAG,OAAC,CAAC,YAAY,CAAC;IACjC,GAAG,EAAE,0BAAa;IAClB,aAAa,EAAE,OAAC;SACb,MAAM,EAAE;SACR,GAAG,CAAC,CAAC,CAAC;SACN,QAAQ,EAAE;SACV,QAAQ,CACP,kEAAkE;UAChE,wEAAwE;UACxE,yBAAyB,CAC5B;IACH,cAAc,EAAE,OAAC;SACd,MAAM,EAAE;SACR,GAAG,CAAC,CAAC,CAAC;SACN,QAAQ,EAAE;SACV,QAAQ,CACP,yEAAyE;UACvE,yEAAyE,CAC5E;IACH,KAAK,EAAE,wBAAW;SACf,QAAQ,EAAE;SACV,QAAQ,CACP,0EAA0E;UACxE,6DAA6D,CAChE;IACH,OAAO,EAAE,0BAAa;CACvB,CAAC,CAAC;AAEH,SAAgB,kBAAkB,CAAC,MAAiB,EAAE,GAAgB;IACpE,MAAM,CAAC,YAAY,CACjB,sBAAsB,EACtB;QACE,KAAK,EAAE,cAAc;QACrB,WAAW,EACT,uEAAuE;cACrE,uEAAuE;cACvE,oEAAoE;cACpE,+DAA+D;cAC/D,6DAA6D;cAC7D,yEAAyE;cACzE,gEAAgE;cAChE,sEAAsE;cACtE,gEAAgE;cAChE,uEAAuE;cACvE,iDAAiD;QACrD,WAAW;QACX,YAAY,EAAE,yBAAY;QAC1B,wEAAwE;QACxE,4EAA4E;QAC5E,oEAAoE;QACpE,qEAAqE;QACrE,wDAAwD;QACxD,WAAW,EAAE,EAAE,YAAY,EAAE,KAAK,EAAE,eAAe,EAAE,KAAK,EAAE,aAAa,EAAE,IAAI,EAAE;KAClF,EACD,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE;QACpB,wEAAwE;QACxE,2EAA2E;QAC3E,0EAA0E;QAC1E,wBAAwB;QACxB,IAAI,IAAI,CAAC,aAAa,KAAK,SAAS,IAAI,IAAI,CAAC,cAAc,KAAK,SAAS,EAAE,CAAC;YAC1E,OAAO;gBACL,OAAO,EAAE,IAAI;gBACb,OAAO,EAAE,CAAC;wBACR,IAAI,EAAE,MAAe;wBACrB,IAAI,EACF,iEAAiE;8BAC/D,kEAAkE;8BAClE,0DAA0D;8BAC1D,uDAAuD;8BACvD,gEAAgE;8BAChE,sBAAsB;qBAC3B,CAAC;aACH,CAAC;QACJ,CAAC;QAED,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,GAAG,CAAC,MAAM,CAAC,cAAc,CAAC;QAE1D,MAAM,OAAO,GAA4B,EAAE,CAAC;QAC5C,IAAI,IAAI,CAAC,aAAa,KAAK,SAAS;YAAE,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,aAAa,CAAC;QACjF,IAAI,IAAI,CAAC,cAAc,KAAK,SAAS;YAAE,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,cAAc,CAAC;QACpF,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS;YAAE,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;QAEzD,OAAO,IAAA,mBAAQ,EACb,GAAG,EACH,KAAK,EACL,EAAE,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAE,OAAO,EAAE,EACpD,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,OAAgB,CAAC,EACrD,CAAC,MAAM,EAAE,OAAO,EAAE,EAAE;YAClB,MAAM,KAAK,GAAI,MAAM,CAAC,KAA4B,IAAI,MAAM,CAAC,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;YAChF,MAAM,MAAM,GAAG,MAAM,CAAC,MAA4B,CAAC;YAEnD,MAAM,UAAU,GAAG;gBACjB,SAAS,EAAE,MAAM,CAAC,MAAM,CAAC,SAAS,IAAI,EAAE,CAAC;gBACzC,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;gBAC/B,YAAY,EAAE,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;gBACxC,KAAK;gBACL,GAAG,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAC5C,CAAC;YAEF,OAAO;gBACL,UAAU;gBACV,IAAI,EACF,oBAAoB,UAAU,CAAC,YAAY,MAAM;sBAC/C,gEAAgE;sBAChE,2DAA2D;sBAC3D,mCAAmC;sBACnC,WAAW,KAAK,IAAI;sBACpB,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,YAAY,MAAM,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC;sBACrD,sBAAsB,UAAU,CAAC,SAAS,GAAG;aAClD,CAAC;QACJ,CAAC,CACF,CAAC;IACJ,CAAC,CACF,CAAC;AACJ,CAAC","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"]}
|
package/docs/API_REFERENCE.md
CHANGED
|
@@ -15,6 +15,7 @@ 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) |
|
|
18
19
|
|
|
19
20
|
---
|
|
20
21
|
|
|
@@ -153,7 +154,7 @@ Solve a Google reCAPTCHA v2 or v3 widget, including invisible and Enterprise var
|
|
|
153
154
|
| `proxy` | object | No | — | "Solve through this proxy so the token is issued against its IP." |
|
|
154
155
|
| `timeout` | integer | No | `CAPSKIP_RECAPTCHA_TIMEOUT` (300) | "Seconds to wait before giving up. Maximum 600." |
|
|
155
156
|
|
|
156
|
-
`proxy` shape (shared with `capskip_solve_turnstile` and `
|
|
157
|
+
`proxy` shape (shared with `capskip_solve_turnstile`, `capskip_solve_geetest` and `capskip_solve_altcha`):
|
|
157
158
|
|
|
158
159
|
| Field | Type | Required | Description |
|
|
159
160
|
|---|---|---|---|
|
|
@@ -321,6 +322,69 @@ Text output: `Solved GeeTest in 14.2s.\nPost these back exactly as the site's ow
|
|
|
321
322
|
|
|
322
323
|
---
|
|
323
324
|
|
|
325
|
+
## `capskip_solve_altcha`
|
|
326
|
+
|
|
327
|
+
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.
|
|
328
|
+
|
|
329
|
+
### Parameters
|
|
330
|
+
|
|
331
|
+
| Name | Type | Required | Default | Description |
|
|
332
|
+
|---|---|---|---|---|
|
|
333
|
+
| `url` | string (URL) | Yes | — | "Full URL of the page the captcha appears on, including scheme." |
|
|
334
|
+
| `challenge_url` | string | One of the two | — | "The endpoint the ALTCHA widget fetches its challenge from. CapSkip fetches it for you." |
|
|
335
|
+
| `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." |
|
|
336
|
+
| `proxy` | object | No | — | "Fetch the challenge through this proxy. Used ONLY for the challenge_url fetch." |
|
|
337
|
+
| `timeout` | integer | No | `CAPSKIP_DEFAULT_TIMEOUT` (120) | "Seconds to wait before giving up. Maximum 600." |
|
|
338
|
+
|
|
339
|
+
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.
|
|
340
|
+
|
|
341
|
+
`proxy` has the same `{ type, uri }` shape documented under `capskip_solve_recaptcha` above.
|
|
342
|
+
|
|
343
|
+
### Finding the challenge
|
|
344
|
+
|
|
345
|
+
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.
|
|
346
|
+
|
|
347
|
+
### Output shape
|
|
348
|
+
|
|
349
|
+
| Field | Type | Always present | Description |
|
|
350
|
+
|---|---|---|---|
|
|
351
|
+
| `captchaId` | string | yes | CapSkip's internal id for this solve |
|
|
352
|
+
| `code` | string | yes | The base64 token — the same string as `token` |
|
|
353
|
+
| `solveSeconds` | number | yes | Wall-clock time the solve took |
|
|
354
|
+
| `token` | string | yes | The payload to submit in the `altcha` form field |
|
|
355
|
+
| `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. |
|
|
356
|
+
|
|
357
|
+
### Example
|
|
358
|
+
|
|
359
|
+
Request:
|
|
360
|
+
|
|
361
|
+
```json
|
|
362
|
+
{
|
|
363
|
+
"url": "https://example.com/signup",
|
|
364
|
+
"challenge_url": "https://example.com/captcha/api/altcha/challenge"
|
|
365
|
+
}
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
Response (`structuredContent`):
|
|
369
|
+
|
|
370
|
+
```json
|
|
371
|
+
{
|
|
372
|
+
"captchaId": "10433",
|
|
373
|
+
"code": "eyJhbGdvcml0aG0iOiJTSEEtMjU2Iiwi...",
|
|
374
|
+
"solveSeconds": 0.27,
|
|
375
|
+
"token": "eyJhbGdvcml0aG0iOiJTSEEtMjU2Iiwi...",
|
|
376
|
+
"number": 9661
|
|
377
|
+
}
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
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)`
|
|
381
|
+
|
|
382
|
+
### Unsupported algorithms
|
|
383
|
+
|
|
384
|
+
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.
|
|
385
|
+
|
|
386
|
+
---
|
|
387
|
+
|
|
324
388
|
## Validation errors
|
|
325
389
|
|
|
326
390
|
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:
|
package/docs/TROUBLESHOOTING.md
CHANGED
|
@@ -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
|
|
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. [
|
|
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.
|
|
246
|
+
## 9. ALTCHA
|
|
246
247
|
|
|
247
|
-
|
|
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.
|
|
3
|
+
"version": "1.1.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.
|
|
36
|
+
"capskip": "^1.2.0",
|
|
37
37
|
"zod": "^4.4.3"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|