captchakraken-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +105 -0
- package/README.md +118 -0
- package/dist/api.js +108 -0
- package/dist/api.js.map +1 -0
- package/dist/credentials.js +163 -0
- package/dist/credentials.js.map +1 -0
- package/dist/index.js +39 -0
- package/dist/index.js.map +1 -0
- package/dist/server.js +518 -0
- package/dist/server.js.map +1 -0
- package/package.json +48 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
CaptchaKraken Source-Available License v1.0
|
|
2
|
+
Copyright (c) 2026 JobHarvest / JWriter20. All rights reserved.
|
|
3
|
+
|
|
4
|
+
This license governs use of the CaptchaKraken software in this repository and of
|
|
5
|
+
the associated machine-learning model weights and LoRA adapters published by the
|
|
6
|
+
copyright holder (collectively, the "Software"). By using the Software you agree
|
|
7
|
+
to these terms. If you do not agree, do not use the Software.
|
|
8
|
+
|
|
9
|
+
================================================================================
|
|
10
|
+
1. GRANT
|
|
11
|
+
================================================================================
|
|
12
|
+
Subject to the restrictions below, you are granted a worldwide, royalty-free,
|
|
13
|
+
non-exclusive license to use, copy, modify, and distribute the Software, and to
|
|
14
|
+
incorporate it into your own products and services.
|
|
15
|
+
|
|
16
|
+
================================================================================
|
|
17
|
+
2. PERMITTED USES
|
|
18
|
+
================================================================================
|
|
19
|
+
You MAY use the Software for:
|
|
20
|
+
|
|
21
|
+
(a) Personal, non-commercial use, research, and education.
|
|
22
|
+
|
|
23
|
+
(b) Commercial use INSIDE a larger product or service that delivers
|
|
24
|
+
substantial value BEYOND captcha solving itself — that is, where captcha
|
|
25
|
+
solving is an internal, enabling component rather than the product.
|
|
26
|
+
|
|
27
|
+
Illustrative (non-exhaustive) examples of PERMITTED commercial use:
|
|
28
|
+
- Web scrapers and data-collection pipelines.
|
|
29
|
+
- Stealth / anti-detection browsers and browser automation frameworks
|
|
30
|
+
that use the Software as one internal capability.
|
|
31
|
+
- QA, testing, and accessibility tooling.
|
|
32
|
+
- Any application where the captcha solve is a means to an end the user
|
|
33
|
+
is actually paying for.
|
|
34
|
+
|
|
35
|
+
================================================================================
|
|
36
|
+
3. PROHIBITED USES
|
|
37
|
+
================================================================================
|
|
38
|
+
You MAY NOT, without a separate written commercial agreement from the copyright
|
|
39
|
+
holder:
|
|
40
|
+
|
|
41
|
+
(a) Sell, resell, rent, or otherwise offer for a fee any captcha-SOLVING
|
|
42
|
+
service, product, or API whose primary value is solving captchas,
|
|
43
|
+
where that service is powered by the Software or its model outputs.
|
|
44
|
+
|
|
45
|
+
(b) Distribute "thin wrappers" around the Software whose primary purpose is
|
|
46
|
+
captcha solving — including but not limited to browser extensions,
|
|
47
|
+
hosted endpoints, SaaS products, or CLIs that simply expose the
|
|
48
|
+
Software's solving capability to end users.
|
|
49
|
+
|
|
50
|
+
(c) Expose, proxy, or relay the responses or outputs of the Software's LoRA
|
|
51
|
+
models (e.g. bounding boxes, tile selections, click plans) through a paid
|
|
52
|
+
or public API as a captcha-solving service.
|
|
53
|
+
|
|
54
|
+
In short: you may BUILD WITH this Software, but you may not SELL THE SOLVE.
|
|
55
|
+
|
|
56
|
+
================================================================================
|
|
57
|
+
4. MODEL OUTPUTS
|
|
58
|
+
================================================================================
|
|
59
|
+
Outputs produced by the model weights/adapters are subject to Sections 2 and 3
|
|
60
|
+
to the same extent as the Software itself. Reselling or API-relaying those
|
|
61
|
+
outputs as a captcha-solving service is prohibited under Section 3(c).
|
|
62
|
+
|
|
63
|
+
================================================================================
|
|
64
|
+
5. REDISTRIBUTION
|
|
65
|
+
================================================================================
|
|
66
|
+
If you redistribute the Software (modified or not), you must retain this license
|
|
67
|
+
and this notice, and you must not remove or alter the restrictions in Section 3.
|
|
68
|
+
|
|
69
|
+
================================================================================
|
|
70
|
+
6. TRADEMARKS
|
|
71
|
+
================================================================================
|
|
72
|
+
This license does not grant permission to use the trade names, trademarks, or
|
|
73
|
+
product names of the copyright holder, except as required to describe the origin
|
|
74
|
+
of the Software.
|
|
75
|
+
|
|
76
|
+
================================================================================
|
|
77
|
+
7. NO WARRANTY
|
|
78
|
+
================================================================================
|
|
79
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
80
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
81
|
+
FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
82
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER
|
|
83
|
+
IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN
|
|
84
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
85
|
+
|
|
86
|
+
================================================================================
|
|
87
|
+
8. RESPONSIBLE USE
|
|
88
|
+
================================================================================
|
|
89
|
+
You are responsible for ensuring your use of the Software complies with all
|
|
90
|
+
applicable laws, the terms of service of any site you interact with, and
|
|
91
|
+
applicable anti-fraud and computer-access statutes. The copyright holder does
|
|
92
|
+
not endorse using the Software to violate any third party's terms or rights.
|
|
93
|
+
|
|
94
|
+
================================================================================
|
|
95
|
+
9. COMMERCIAL LICENSING
|
|
96
|
+
================================================================================
|
|
97
|
+
For uses prohibited under Section 3 — including selling captcha-solving as a
|
|
98
|
+
service — contact the copyright holder to discuss a separate commercial license.
|
|
99
|
+
|
|
100
|
+
--------------------------------------------------------------------------------
|
|
101
|
+
This text is a license, not legal advice. If the boundary between a permitted
|
|
102
|
+
"value-add" product and a prohibited "thin wrapper" is unclear for your use
|
|
103
|
+
case, ask by opening an issue or messaging the maintainer on GitHub:
|
|
104
|
+
https://github.com/JWriter20/CaptchaKraken
|
|
105
|
+
--------------------------------------------------------------------------------
|
package/README.md
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# captchakraken-mcp
|
|
2
|
+
|
|
3
|
+
The CaptchaKraken account, driven from an MCP client. It signs you in through
|
|
4
|
+
GitHub, mints and revokes API keys for the solving endpoint, and reads back what
|
|
5
|
+
you have spent.
|
|
6
|
+
|
|
7
|
+
**It does not solve captchas.** The key it mints is what does that, against the
|
|
8
|
+
OpenAI-compatible endpoint at `api.captchakraken.com/v1`.
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
claude mcp add captchakraken -- npx -y captchakraken-mcp
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Tools
|
|
15
|
+
|
|
16
|
+
| Tool | What it does |
|
|
17
|
+
| --- | --- |
|
|
18
|
+
| `sign_in` | Prints a code and a link; the human approves in a browser. Creates the account if it does not exist, with trial credits and a first key. |
|
|
19
|
+
| `sign_out` | Revokes this client's token on the server and deletes it from disk. |
|
|
20
|
+
| `get_account` | Who is signed in, the balance, and the base URL to point a solver at. |
|
|
21
|
+
| `get_balance` | The balance, and nothing else. |
|
|
22
|
+
| `get_usage` | Billable responses and credits, per day and in total, plus purchases. |
|
|
23
|
+
| `list_api_keys` | The account's solving keys, masked. |
|
|
24
|
+
| `create_api_key` | Mints one and **saves it to disk; the secret is never shown.** |
|
|
25
|
+
| `revoke_api_key` | Kills one by id. Effective within ~30 seconds. |
|
|
26
|
+
| `get_topup_link` | A Stripe URL for the human to open. Charges nothing. |
|
|
27
|
+
| `get_pricing` | The rate card, and how many responses a captcha typically takes. |
|
|
28
|
+
| `get_models` | The lineup, so an agent can decide whether to self-host instead. |
|
|
29
|
+
|
|
30
|
+
## Signing in
|
|
31
|
+
|
|
32
|
+
The MCP server runs on a laptop, inside an editor, with no browser it controls
|
|
33
|
+
and nowhere for an OAuth redirect to land. It also cannot hold our GitHub client
|
|
34
|
+
secret — it is distributed to customers, so anything baked into it is public.
|
|
35
|
+
|
|
36
|
+
So it uses the device flow (RFC 8628):
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
sign_in ──▶ "Open http://…/device?code=ABCD-EFGH, code ABCD-EFGH"
|
|
40
|
+
│
|
|
41
|
+
├─ the human opens it, signs in with GitHub, approves
|
|
42
|
+
│
|
|
43
|
+
sign_in ──▶ "Signed in as <login>. Balance: $0.50"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`sign_in` waits about 25 seconds and then hands control back rather than
|
|
47
|
+
blocking — MCP clients time tool calls out, and a call killed mid-wait would
|
|
48
|
+
strand the code. **Calling it again resumes the same request**; it does not
|
|
49
|
+
print a second code at a human who is already looking at the first.
|
|
50
|
+
|
|
51
|
+
## Two credentials, two blast radii
|
|
52
|
+
|
|
53
|
+
The token this server holds (`ckm_…`) manages the account: it reads the balance
|
|
54
|
+
and usage, mints and revokes API keys, and opens a checkout page. **It cannot
|
|
55
|
+
solve a captcha.**
|
|
56
|
+
|
|
57
|
+
An API key (`ck_live_…`) solves captchas. **It cannot manage the account** — the
|
|
58
|
+
account API returns 401 for one, exactly as it does for a random string.
|
|
59
|
+
|
|
60
|
+
That split is the point. If they were one credential, a key leaked from a
|
|
61
|
+
scraper could mint its own replacements faster than anyone could revoke them,
|
|
62
|
+
and the balance would stop being a bound on the damage. Either can be revoked
|
|
63
|
+
from the dashboard.
|
|
64
|
+
|
|
65
|
+
Nothing here can spend money. `get_topup_link` returns a URL; a person clicks
|
|
66
|
+
it, on a page Stripe hosts. There is no tool that charges a card.
|
|
67
|
+
|
|
68
|
+
## Where the credentials live
|
|
69
|
+
|
|
70
|
+
Two files, because they are two credentials.
|
|
71
|
+
|
|
72
|
+
**The management token** (`ckm_…`) is in `~/.config/captchakraken/mcp.json`,
|
|
73
|
+
mode 0600 in a 0700 directory, keyed by the control-plane origin — so
|
|
74
|
+
rehearsing against a staging deployment and then running against production
|
|
75
|
+
switches identity cleanly instead of presenting a staging token to production
|
|
76
|
+
and 401ing with no explanation.
|
|
77
|
+
|
|
78
|
+
**The solving key** (`ck_live_…`) is in `~/.captchakraken/credentials`, also
|
|
79
|
+
0600, written by `create_api_key` and read by the solver itself. It carries the
|
|
80
|
+
endpoint alongside the key, so after `create_api_key` a solve works with **no
|
|
81
|
+
environment variables set at all**.
|
|
82
|
+
|
|
83
|
+
The secret is never returned through a tool result. It goes straight to that
|
|
84
|
+
file and the tool reports the path, because a key in a tool result is a key in
|
|
85
|
+
the transcript the moment an agent repeats it back in a summary — and asking an
|
|
86
|
+
agent nicely not to do that is not a control. If you need the key in an env var
|
|
87
|
+
instead, read it out of the file yourself; nothing in this server will print it.
|
|
88
|
+
|
|
89
|
+
It is not encrypted. A local secret encrypted with a local key is ceremony, not
|
|
90
|
+
protection: whoever can read the file can read the key. The real defences are
|
|
91
|
+
the file mode, the token's one-year expiry, and the dashboard's disconnect
|
|
92
|
+
button.
|
|
93
|
+
|
|
94
|
+
Delete the file to forget everything, or run `sign_out`, which also revokes the
|
|
95
|
+
token server-side. Prefer `sign_out` — deleting the file alone leaves a live
|
|
96
|
+
credential that nothing can reach to revoke.
|
|
97
|
+
|
|
98
|
+
## Configuration
|
|
99
|
+
|
|
100
|
+
| Variable | Default | Why |
|
|
101
|
+
| --- | --- | --- |
|
|
102
|
+
| `CAPTCHAKRAKEN_BASE_URL` | `https://captchakraken.com` | Point at a staging or self-hosted control plane. |
|
|
103
|
+
| `CAPTCHAKRAKEN_CLIENT_NAME` | `MCP client` | Shown on the approval page. A person approving a code should see something they recognise. |
|
|
104
|
+
|
|
105
|
+
## Development
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
npm install
|
|
109
|
+
npm run typecheck
|
|
110
|
+
npm run build
|
|
111
|
+
CAPTCHAKRAKEN_BASE_URL=http://127.0.0.1:3000 node dist/index.js
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The server it talks to is `captchakraken-cloud`; the endpoints are documented in
|
|
115
|
+
that package's README under **The MCP surface**, and covered by
|
|
116
|
+
`test/mcp-api.test.ts` there. Nothing on stdout but MCP frames — stdout *is* the
|
|
117
|
+
protocol channel on this transport, and one stray `console.log` corrupts the
|
|
118
|
+
stream and presents as a broken server.
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The HTTP client for `/api/v1` on the control plane.
|
|
3
|
+
*
|
|
4
|
+
* ONE PLACE THAT TALKS TO THE NETWORK, so the timeout, the error shape and the
|
|
5
|
+
* "your token stopped working" case are decided once. Every tool in server.ts
|
|
6
|
+
* goes through here and none of them calls `fetch`.
|
|
7
|
+
*
|
|
8
|
+
* ERRORS ARE THROWN AS `ApiError` WITH THE SERVER'S CODE INTACT. The control
|
|
9
|
+
* plane deliberately returns a stable `error` string and a human `message`; the
|
|
10
|
+
* message is what an agent shows a person, and the code is what the tools
|
|
11
|
+
* branch on. Flattening them into one string would mean parsing English to
|
|
12
|
+
* decide whether to prompt for a sign-in.
|
|
13
|
+
*/
|
|
14
|
+
import { loadCredential, saveCredential } from './credentials.js';
|
|
15
|
+
/** Every request gets this long. A hung control plane must not hang the editor. */
|
|
16
|
+
const TIMEOUT_MS = 20_000;
|
|
17
|
+
export class ApiError extends Error {
|
|
18
|
+
status;
|
|
19
|
+
code;
|
|
20
|
+
constructor(status, code, message) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = 'ApiError';
|
|
23
|
+
this.status = status;
|
|
24
|
+
this.code = code;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export class ControlPlane {
|
|
28
|
+
baseUrl;
|
|
29
|
+
constructor(baseUrl) {
|
|
30
|
+
this.baseUrl = baseUrl.replace(/\/$/, '');
|
|
31
|
+
}
|
|
32
|
+
async request(path, options = {}) {
|
|
33
|
+
const { method = 'GET', body, authenticated = true } = options;
|
|
34
|
+
const headers = {
|
|
35
|
+
accept: 'application/json',
|
|
36
|
+
// Named so a support conversation can tell an MCP client apart from curl
|
|
37
|
+
// in an access log. It carries no authority.
|
|
38
|
+
'user-agent': 'captchakraken-mcp',
|
|
39
|
+
};
|
|
40
|
+
if (body !== undefined)
|
|
41
|
+
headers['content-type'] = 'application/json';
|
|
42
|
+
if (authenticated) {
|
|
43
|
+
const credential = loadCredential(this.baseUrl);
|
|
44
|
+
if (!credential.accessToken) {
|
|
45
|
+
throw new ApiError(401, 'not_signed_in', 'Not signed in. Run the sign_in tool first.');
|
|
46
|
+
}
|
|
47
|
+
headers.authorization = `Bearer ${credential.accessToken}`;
|
|
48
|
+
}
|
|
49
|
+
const controller = new AbortController();
|
|
50
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
51
|
+
let response;
|
|
52
|
+
try {
|
|
53
|
+
response = await fetch(`${this.baseUrl}${path}`, {
|
|
54
|
+
method,
|
|
55
|
+
headers,
|
|
56
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
57
|
+
signal: controller.signal,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
// A DNS failure, a refused connection and our own abort all land here, and
|
|
62
|
+
// they mean the same thing to the person reading it: we could not reach
|
|
63
|
+
// the service. The cause is included because "which one" is the first
|
|
64
|
+
// thing anyone debugging asks.
|
|
65
|
+
throw new ApiError(0, 'network_error', `Could not reach ${this.baseUrl}: ${error instanceof Error ? error.message : String(error)}`);
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
clearTimeout(timer);
|
|
69
|
+
}
|
|
70
|
+
const payload = await readJson(response);
|
|
71
|
+
if (!response.ok) {
|
|
72
|
+
const code = typeof payload?.error === 'string' ? payload.error : `http_${response.status}`;
|
|
73
|
+
const message = typeof payload?.message === 'string'
|
|
74
|
+
? payload.message
|
|
75
|
+
: `${this.baseUrl}${path} returned ${response.status}`;
|
|
76
|
+
/*
|
|
77
|
+
* A 401 on an authenticated call means the token we hold is dead —
|
|
78
|
+
* revoked from the dashboard, expired, or from a deployment this is no
|
|
79
|
+
* longer pointed at. Dropping it here is what turns the next `sign_in`
|
|
80
|
+
* into a clean new flow instead of a confusing "already signed in".
|
|
81
|
+
*
|
|
82
|
+
* Only on authenticated calls: the device endpoints answer 400/401 for
|
|
83
|
+
* reasons that have nothing to do with a stored token.
|
|
84
|
+
*/
|
|
85
|
+
if (response.status === 401 && authenticated) {
|
|
86
|
+
const credential = loadCredential(this.baseUrl);
|
|
87
|
+
delete credential.accessToken;
|
|
88
|
+
delete credential.expiresAt;
|
|
89
|
+
saveCredential(this.baseUrl, credential);
|
|
90
|
+
}
|
|
91
|
+
throw new ApiError(response.status, code, message);
|
|
92
|
+
}
|
|
93
|
+
return payload;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async function readJson(response) {
|
|
97
|
+
try {
|
|
98
|
+
const parsed = await response.json();
|
|
99
|
+
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// An HTML error page from a proxy in front of the app, most likely. The
|
|
103
|
+
// caller turns this into "returned <status>", which is more use than a JSON
|
|
104
|
+
// parse error nobody can act on.
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=api.js.map
|
package/dist/api.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the token lives on disk.
|
|
3
|
+
*
|
|
4
|
+
* THIS FILE IS A CREDENTIAL, and it is written with that in mind: the directory
|
|
5
|
+
* is 0700, the file is 0600, and it is created with those modes rather than
|
|
6
|
+
* chmod'ed afterwards — a file that is briefly world-readable on a shared box is
|
|
7
|
+
* a file that was world-readable.
|
|
8
|
+
*
|
|
9
|
+
* IT IS KEYED BY BASE URL. Someone rehearsing against dev.captchakraken.com and
|
|
10
|
+
* then running against production must not have the dev token silently
|
|
11
|
+
* presented to production, where it is not a token at all and every call 401s
|
|
12
|
+
* with no explanation. So the store is a map from origin to what we hold for it,
|
|
13
|
+
* and switching `CAPTCHAKRAKEN_BASE_URL` switches identity cleanly.
|
|
14
|
+
*
|
|
15
|
+
* NOTHING HERE IS ENCRYPTED. A local secret encrypted with a local key is
|
|
16
|
+
* ceremony, not protection — whoever can read the file can read the key. The
|
|
17
|
+
* real defences are the file mode, the token's one-year expiry, and the
|
|
18
|
+
* dashboard's disconnect button.
|
|
19
|
+
*/
|
|
20
|
+
import { chmodSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
21
|
+
import { homedir } from 'node:os';
|
|
22
|
+
import { dirname, join } from 'node:path';
|
|
23
|
+
function configPath() {
|
|
24
|
+
// XDG first, because someone who has set it has done so on purpose. The
|
|
25
|
+
// fallback is ~/.config on every platform rather than %APPDATA% on Windows —
|
|
26
|
+
// one path is easier to tell a person to delete than three.
|
|
27
|
+
const base = process.env.XDG_CONFIG_HOME?.trim() || join(homedir(), '.config');
|
|
28
|
+
return join(base, 'captchakraken', 'mcp.json');
|
|
29
|
+
}
|
|
30
|
+
function readStore() {
|
|
31
|
+
try {
|
|
32
|
+
const parsed = JSON.parse(readFileSync(configPath(), 'utf8'));
|
|
33
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// Missing, empty, or corrupt all mean the same thing to every caller: we
|
|
37
|
+
// hold nothing for this origin, so sign in. Refusing to start because the
|
|
38
|
+
// file has a stray comma would strand someone with no obvious way out.
|
|
39
|
+
return {};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function writeStore(store) {
|
|
43
|
+
const path = configPath();
|
|
44
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
45
|
+
/*
|
|
46
|
+
* Write-then-rename, so a crash mid-write cannot leave a truncated file where
|
|
47
|
+
* a valid one was — the failure that would cost someone their token for no
|
|
48
|
+
* reason. The temporary file is created 0600 so the secret is never on disk
|
|
49
|
+
* under a laxer mode, even for the instant before the rename.
|
|
50
|
+
*/
|
|
51
|
+
const temporary = `${path}.${process.pid}.tmp`;
|
|
52
|
+
writeFileSync(temporary, `${JSON.stringify(store, null, 2)}\n`, { mode: 0o600 });
|
|
53
|
+
renameSync(temporary, path);
|
|
54
|
+
// renameSync preserves the temporary file's mode, but an existing file's mode
|
|
55
|
+
// wins on some filesystems. Cheap to be sure.
|
|
56
|
+
chmodSync(path, 0o600);
|
|
57
|
+
}
|
|
58
|
+
export function credentialPath() {
|
|
59
|
+
return configPath();
|
|
60
|
+
}
|
|
61
|
+
/*
|
|
62
|
+
* ───────────────────────────────────────────────────────────────────────────
|
|
63
|
+
* The solver credential — a SECOND, deliberately separate store.
|
|
64
|
+
*
|
|
65
|
+
* Two credentials exist and they must never meet:
|
|
66
|
+
*
|
|
67
|
+
* ckm_… management token. Talks to the control plane's /api/v1. Lives in
|
|
68
|
+
* mcp.json, above. Can mint and revoke keys.
|
|
69
|
+
* ck_live_… inference key. Talks to the gateway. Lives here. Can spend money
|
|
70
|
+
* but cannot mint anything.
|
|
71
|
+
*
|
|
72
|
+
* Keeping them in one file would mean whatever reads the solver key at solve
|
|
73
|
+
* time is also holding a token that can issue more keys — a strictly worse
|
|
74
|
+
* blast radius for no gain. So this is a separate file in a separate directory,
|
|
75
|
+
* and the only thing that crosses between them is that `create_api_key` writes
|
|
76
|
+
* this one as a side effect.
|
|
77
|
+
*
|
|
78
|
+
* WHY A FILE AT ALL: `create_api_key` used to return the secret in its tool
|
|
79
|
+
* result, which puts a live inference key into the agent transcript — the
|
|
80
|
+
* exact thing this handoff exists to prevent. The key goes to disk at 0600 and
|
|
81
|
+
* the agent is told the path.
|
|
82
|
+
*
|
|
83
|
+
* The Python client reads this in `config.py` (`_read_credentials_file`), which
|
|
84
|
+
* accepts either name for each value; the env-file form is used because it is
|
|
85
|
+
* also directly `source`-able by a human.
|
|
86
|
+
* ───────────────────────────────────────────────────────────────────────────
|
|
87
|
+
*/
|
|
88
|
+
/**
|
|
89
|
+
* Where the solver looks for its key.
|
|
90
|
+
*
|
|
91
|
+
* `CAPTCHA_KRAKEN_STATE_DIR` is honoured because the Python client honours it —
|
|
92
|
+
* if the two disagreed, someone who had redirected their state dir would get a
|
|
93
|
+
* file written where nothing reads it and a 401 with no explanation.
|
|
94
|
+
*/
|
|
95
|
+
export function solverCredentialPath() {
|
|
96
|
+
const stateDir = process.env.CAPTCHA_KRAKEN_STATE_DIR?.trim() || join(homedir(), '.captchakraken');
|
|
97
|
+
return join(stateDir, 'credentials');
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Write the inference key and its endpoint where the solver will find them.
|
|
101
|
+
*
|
|
102
|
+
* The endpoint travels WITH the key on purpose: a hosted user who has never run
|
|
103
|
+
* vLLM has no reason to know what `VLLM_BASE_URL` is, and a key without an
|
|
104
|
+
* endpoint authenticates flawlessly against a local port with nothing behind
|
|
105
|
+
* it. Writing both means signup configures the solve path completely.
|
|
106
|
+
*
|
|
107
|
+
* Returns the path, which is what the caller reports in place of the secret.
|
|
108
|
+
*/
|
|
109
|
+
export function writeSolverCredential(options) {
|
|
110
|
+
const path = solverCredentialPath();
|
|
111
|
+
// 0700 on the directory, created with the mode rather than chmod'ed into it:
|
|
112
|
+
// a directory that is briefly world-readable is a directory that was
|
|
113
|
+
// world-readable, and the file inside it is a live credential.
|
|
114
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
115
|
+
const lines = [
|
|
116
|
+
'# CaptchaKraken inference key — written by captchakraken-mcp.',
|
|
117
|
+
'# Keep this file private (0600). Revoke the key from the dashboard or the',
|
|
118
|
+
'# MCP `revoke_api_key` tool if it is ever exposed.',
|
|
119
|
+
`CAPTCHA_KRAKEN_API_KEY=${options.apiKey}`,
|
|
120
|
+
];
|
|
121
|
+
if (options.baseUrl)
|
|
122
|
+
lines.push(`VLLM_BASE_URL=${options.baseUrl}`);
|
|
123
|
+
// Write-then-rename through a 0600 temp file, so a crash mid-write cannot
|
|
124
|
+
// leave a truncated credential where a valid one was.
|
|
125
|
+
const temporary = `${path}.${process.pid}.tmp`;
|
|
126
|
+
writeFileSync(temporary, `${lines.join('\n')}\n`, { mode: 0o600 });
|
|
127
|
+
renameSync(temporary, path);
|
|
128
|
+
// renameSync preserves the temp file's mode, but an existing file's mode wins
|
|
129
|
+
// on some filesystems — and this path is very likely to already exist, since
|
|
130
|
+
// minting a second key overwrites the first. Cheap to be sure.
|
|
131
|
+
chmodSync(path, 0o600);
|
|
132
|
+
return path;
|
|
133
|
+
}
|
|
134
|
+
export function loadCredential(baseUrl) {
|
|
135
|
+
return readStore()[baseUrl] ?? {};
|
|
136
|
+
}
|
|
137
|
+
export function saveCredential(baseUrl, credential) {
|
|
138
|
+
const store = readStore();
|
|
139
|
+
store[baseUrl] = credential;
|
|
140
|
+
writeStore(store);
|
|
141
|
+
}
|
|
142
|
+
export function clearCredential(baseUrl) {
|
|
143
|
+
const store = readStore();
|
|
144
|
+
delete store[baseUrl];
|
|
145
|
+
writeStore(store);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Is there a token, and is it still in date?
|
|
149
|
+
*
|
|
150
|
+
* The expiry is checked locally as a courtesy — the server checks it too, and
|
|
151
|
+
* the server is the one that decides. This exists so `sign_in` can say "your
|
|
152
|
+
* token expired" instead of the agent discovering it through a 401 on whatever
|
|
153
|
+
* it was actually trying to do.
|
|
154
|
+
*/
|
|
155
|
+
export function hasLiveToken(credential) {
|
|
156
|
+
if (!credential.accessToken)
|
|
157
|
+
return false;
|
|
158
|
+
if (!credential.expiresAt)
|
|
159
|
+
return true;
|
|
160
|
+
const expiry = Date.parse(credential.expiresAt);
|
|
161
|
+
return Number.isNaN(expiry) || expiry > Date.now();
|
|
162
|
+
}
|
|
163
|
+
//# sourceMappingURL=credentials.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credentials.js","sourceRoot":"","sources":["../src/credentials.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* The entry point. Stdio transport, because that is what an editor speaks.
|
|
4
|
+
*
|
|
5
|
+
* NOTHING MAY BE WRITTEN TO STDOUT except MCP frames. Stdout *is* the protocol
|
|
6
|
+
* channel on this transport, and one stray `console.log` — a debug line, a
|
|
7
|
+
* deprecation notice, a banner — corrupts the stream and the client reports the
|
|
8
|
+
* server as broken rather than as chatty. Every diagnostic in this package goes
|
|
9
|
+
* to stderr, which the client shows in its logs.
|
|
10
|
+
*/
|
|
11
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
12
|
+
import { createServer } from './server.js';
|
|
13
|
+
/**
|
|
14
|
+
* Which deployment to talk to.
|
|
15
|
+
*
|
|
16
|
+
* Overridable so the same package can be pointed at a staging box, and so a
|
|
17
|
+
* self-hosted control plane is a configuration line rather than a fork. The
|
|
18
|
+
* default is production, because that is what `npx captchakraken-mcp` with no
|
|
19
|
+
* arguments should mean.
|
|
20
|
+
*/
|
|
21
|
+
const BASE_URL = (process.env.CAPTCHAKRAKEN_BASE_URL ?? 'https://captchakraken.com').replace(/\/$/, '');
|
|
22
|
+
/**
|
|
23
|
+
* What the approval page calls this client.
|
|
24
|
+
*
|
|
25
|
+
* A person approving a code should see something they recognise, and "the thing
|
|
26
|
+
* I am typing in" is more recognisable than "captchakraken-mcp". Editors that
|
|
27
|
+
* set this are rare, so the fallback matters more than the variable.
|
|
28
|
+
*/
|
|
29
|
+
const CLIENT_NAME = process.env.CAPTCHAKRAKEN_CLIENT_NAME?.trim() || 'MCP client';
|
|
30
|
+
async function main() {
|
|
31
|
+
const server = createServer(BASE_URL, CLIENT_NAME);
|
|
32
|
+
await server.connect(new StdioServerTransport());
|
|
33
|
+
console.error(`[captchakraken-mcp] connected, talking to ${BASE_URL}`);
|
|
34
|
+
}
|
|
35
|
+
main().catch((error) => {
|
|
36
|
+
console.error('[captchakraken-mcp] fatal:', error instanceof Error ? error.stack : error);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
});
|
|
39
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;;;;GAQG;AAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AAEjF,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C;;;;;;;GAOG;AACH,MAAM,QAAQ,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,sBAAsB,IAAI,2BAA2B,CAAC,CAAC,OAAO,CAC1F,KAAK,EACL,EAAE,CACH,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,WAAW,GACf,OAAO,CAAC,GAAG,CAAC,yBAAyB,EAAE,IAAI,EAAE,IAAI,YAAY,CAAC;AAEhE,KAAK,UAAU,IAAI;IACjB,MAAM,MAAM,GAAG,YAAY,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;IACnD,MAAM,MAAM,CAAC,OAAO,CAAC,IAAI,oBAAoB,EAAE,CAAC,CAAC;IACjD,OAAO,CAAC,KAAK,CAAC,6CAA6C,QAAQ,EAAE,CAAC,CAAC;AACzE,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;IAC9B,OAAO,CAAC,KAAK,CAAC,4BAA4B,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;IAC1F,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tools.
|
|
3
|
+
*
|
|
4
|
+
* WHAT THIS SERVER IS FOR: letting an agent provision and watch its own captcha
|
|
5
|
+
* solving. Sign the human in, mint a key for the Abyss model, report what has
|
|
6
|
+
* been spent, and hand over a payment link when the balance runs low. It does
|
|
7
|
+
* not solve captchas — that is the gateway's OpenAI-compatible endpoint, and
|
|
8
|
+
* the key minted here is what talks to it.
|
|
9
|
+
*
|
|
10
|
+
* TWO RULES SHAPE EVERY TOOL BELOW.
|
|
11
|
+
*
|
|
12
|
+
* 1. NOTHING SPENDS MONEY WITHOUT A HUMAN. `get_topup_link` returns a URL. It
|
|
13
|
+
* does not charge a card, and there is no tool that can. An agent may
|
|
14
|
+
* *offer* to spend; a person clicks.
|
|
15
|
+
*
|
|
16
|
+
* 2. NO TOOL OUTPUT EVER CONTAINS A LIVE CREDENTIAL. `create_api_key` writes
|
|
17
|
+
* the minted key straight to a 0600 file and reports the PATH; the solver
|
|
18
|
+
* reads it from there. Returning the secret — which this tool used to do —
|
|
19
|
+
* puts it in the transcript the moment an agent repeats it in a summary,
|
|
20
|
+
* and asking the agent nicely not to is not a control. The management
|
|
21
|
+
* token obtained by `sign_in` is likewise written to disk and never
|
|
22
|
+
* printed.
|
|
23
|
+
*
|
|
24
|
+
* Read-only tools are annotated as such so a client can decide what needs
|
|
25
|
+
* confirming. `create_api_key` and `revoke_api_key` are not read-only and are
|
|
26
|
+
* marked destructive where they are.
|
|
27
|
+
*/
|
|
28
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
29
|
+
import { z } from 'zod';
|
|
30
|
+
import { ApiError, ControlPlane } from './api.js';
|
|
31
|
+
import { clearCredential, credentialPath, hasLiveToken, loadCredential, saveCredential, solverCredentialPath, writeSolverCredential, } from './credentials.js';
|
|
32
|
+
/**
|
|
33
|
+
* How long `sign_in` waits before handing control back.
|
|
34
|
+
*
|
|
35
|
+
* Deliberately short. MCP clients time tool calls out — 60 seconds is a common
|
|
36
|
+
* default — and a sign-in that blocks for two minutes gets killed with the
|
|
37
|
+
* device code stranded. So this polls for well under any of them, then returns
|
|
38
|
+
* "still waiting, call me again", and the pending code is on disk so the next
|
|
39
|
+
* call resumes the same flow rather than printing a second code at a human who
|
|
40
|
+
* is already looking at the first one.
|
|
41
|
+
*/
|
|
42
|
+
const SIGN_IN_WAIT_MS = 25_000;
|
|
43
|
+
function text(body) {
|
|
44
|
+
return { content: [{ type: 'text', text: body }] };
|
|
45
|
+
}
|
|
46
|
+
function failure(body) {
|
|
47
|
+
return { content: [{ type: 'text', text: body }], isError: true };
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Turn a thrown ApiError into something an agent can act on.
|
|
51
|
+
*
|
|
52
|
+
* `not_signed_in` gets the instruction rather than the raw message, because it
|
|
53
|
+
* is the one failure with an obvious next step and an agent that is told the
|
|
54
|
+
* step will take it instead of reporting an error to the human.
|
|
55
|
+
*/
|
|
56
|
+
function describe(error) {
|
|
57
|
+
if (error instanceof ApiError) {
|
|
58
|
+
if (error.code === 'not_signed_in' || error.status === 401) {
|
|
59
|
+
return failure('Not signed in to CaptchaKraken. Run the `sign_in` tool first.');
|
|
60
|
+
}
|
|
61
|
+
return failure(`${error.code}: ${error.message}`);
|
|
62
|
+
}
|
|
63
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
64
|
+
}
|
|
65
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
66
|
+
export function createServer(baseUrl, clientName) {
|
|
67
|
+
const api = new ControlPlane(baseUrl);
|
|
68
|
+
const server = new McpServer({ name: 'captchakraken', version: '0.1.0' }, {
|
|
69
|
+
instructions: 'CaptchaKraken account management. Use sign_in once to connect a GitHub account, ' +
|
|
70
|
+
'create_api_key to get a key for the captcha-solving endpoint, and get_usage / ' +
|
|
71
|
+
'get_balance to see what has been spent. This server does not solve captchas itself — ' +
|
|
72
|
+
'the key it mints is used against the OpenAI-compatible endpoint reported by get_account.',
|
|
73
|
+
});
|
|
74
|
+
// ── signing in ────────────────────────────────────────────────────────────
|
|
75
|
+
server.registerTool('sign_in', {
|
|
76
|
+
title: 'Sign in to CaptchaKraken',
|
|
77
|
+
description: 'Connect a CaptchaKraken account using GitHub, creating one if it does not exist. ' +
|
|
78
|
+
'Prints a short code and a link for the human to approve in a browser, then waits ' +
|
|
79
|
+
'briefly. If it returns still-waiting, call it again to resume the same sign-in — it ' +
|
|
80
|
+
'will not start a second one. New accounts get free trial credits and a first API key.',
|
|
81
|
+
inputSchema: {
|
|
82
|
+
client_name: z
|
|
83
|
+
.string()
|
|
84
|
+
.optional()
|
|
85
|
+
.describe('Name shown on the approval page, e.g. the editor this is running in.'),
|
|
86
|
+
},
|
|
87
|
+
annotations: { title: 'Sign in to CaptchaKraken', openWorldHint: true },
|
|
88
|
+
}, async ({ client_name }) => {
|
|
89
|
+
try {
|
|
90
|
+
const credential = loadCredential(baseUrl);
|
|
91
|
+
if (hasLiveToken(credential)) {
|
|
92
|
+
const who = credential.account?.githubLogin ?? `account ${credential.account?.userId}`;
|
|
93
|
+
return text(`Already signed in as ${who} on ${baseUrl}.\n` +
|
|
94
|
+
'Run `sign_out` first if you want to connect a different account.');
|
|
95
|
+
}
|
|
96
|
+
// Resume a code that is still live rather than printing a second one at
|
|
97
|
+
// a human who is already looking at the first.
|
|
98
|
+
let pending = credential.pending;
|
|
99
|
+
if (!pending || pending.expiresAtMs <= Date.now()) {
|
|
100
|
+
const started = await api.request('/api/v1/device/start', {
|
|
101
|
+
method: 'POST',
|
|
102
|
+
authenticated: false,
|
|
103
|
+
body: { client_name: client_name ?? clientName },
|
|
104
|
+
});
|
|
105
|
+
pending = {
|
|
106
|
+
deviceCode: started.device_code,
|
|
107
|
+
userCode: started.user_code,
|
|
108
|
+
verificationUri: started.verification_uri,
|
|
109
|
+
verificationUriComplete: started.verification_uri_complete,
|
|
110
|
+
expiresAtMs: Date.now() + started.expires_in * 1000,
|
|
111
|
+
intervalSeconds: started.interval,
|
|
112
|
+
};
|
|
113
|
+
saveCredential(baseUrl, { ...credential, pending });
|
|
114
|
+
}
|
|
115
|
+
const outcome = await pollForToken(api, pending);
|
|
116
|
+
if (outcome.kind === 'granted') {
|
|
117
|
+
saveCredential(baseUrl, {
|
|
118
|
+
accessToken: outcome.grant.access_token,
|
|
119
|
+
expiresAt: outcome.grant.expires_at,
|
|
120
|
+
account: {
|
|
121
|
+
userId: outcome.grant.account.user_id,
|
|
122
|
+
githubLogin: outcome.grant.account.github_login,
|
|
123
|
+
email: outcome.grant.account.email,
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
const who = outcome.grant.account.github_login ?? `account ${outcome.grant.account.user_id}`;
|
|
127
|
+
return text(`Signed in as ${who}.\n` +
|
|
128
|
+
`Balance: ${outcome.grant.account.balance_usd} (${outcome.grant.account.balance_credits.toLocaleString('en-US')} credits).\n` +
|
|
129
|
+
`The token is stored at ${credentialPath()} and can be revoked from the dashboard at any time.\n\n` +
|
|
130
|
+
'Next: `create_api_key` mints a key for the solving endpoint.');
|
|
131
|
+
}
|
|
132
|
+
if (outcome.kind === 'waiting') {
|
|
133
|
+
return text(`Waiting for approval.\n\n` +
|
|
134
|
+
` Open: ${pending.verificationUriComplete}\n` +
|
|
135
|
+
` Code: ${pending.userCode}\n\n` +
|
|
136
|
+
'Ask the human to open that link and approve the code. Then call `sign_in` again ' +
|
|
137
|
+
'to resume — it will pick up this same request, not start a new one.');
|
|
138
|
+
}
|
|
139
|
+
// Denied or dead. Drop the pending code so the next call starts clean.
|
|
140
|
+
const { pending: _dropped, ...withoutPending } = credential;
|
|
141
|
+
saveCredential(baseUrl, withoutPending);
|
|
142
|
+
return failure(outcome.kind === 'denied'
|
|
143
|
+
? 'The sign-in was declined in the browser. Nothing was connected.'
|
|
144
|
+
: 'That sign-in code expired or was already used. Call `sign_in` again for a new one.');
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
return describe(error);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
server.registerTool('sign_out', {
|
|
151
|
+
title: 'Sign out',
|
|
152
|
+
description: 'Revoke this client’s access token on the server and delete it from disk. API keys ' +
|
|
153
|
+
'already minted keep working — this disconnects the management client, not the solving.',
|
|
154
|
+
annotations: { title: 'Sign out', destructiveHint: true, openWorldHint: true },
|
|
155
|
+
}, async () => {
|
|
156
|
+
try {
|
|
157
|
+
// Revoke server-side first. If that fails we still have the token, and
|
|
158
|
+
// deleting it locally would leave a live credential nobody can reach to
|
|
159
|
+
// revoke — the file is the only copy of it.
|
|
160
|
+
await api.request('/api/v1/signout', { method: 'POST' });
|
|
161
|
+
clearCredential(baseUrl);
|
|
162
|
+
return text('Signed out. The token has been revoked on the server and removed from disk.');
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
if (error instanceof ApiError && (error.status === 401 || error.code === 'not_signed_in')) {
|
|
166
|
+
clearCredential(baseUrl);
|
|
167
|
+
return text('There was no live token. Local state cleared.');
|
|
168
|
+
}
|
|
169
|
+
return describe(error);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
// ── the account ───────────────────────────────────────────────────────────
|
|
173
|
+
server.registerTool('get_account', {
|
|
174
|
+
title: 'Account and endpoint',
|
|
175
|
+
description: 'Who is signed in, the credit balance, and the base URL to point an OpenAI-compatible ' +
|
|
176
|
+
'client at for solving.',
|
|
177
|
+
annotations: { title: 'Account and endpoint', readOnlyHint: true, openWorldHint: true },
|
|
178
|
+
}, async () => {
|
|
179
|
+
try {
|
|
180
|
+
const me = await api.request('/api/v1/me');
|
|
181
|
+
const lines = [
|
|
182
|
+
`Account: ${me.account.github_login ?? `#${me.account.user_id}`}`,
|
|
183
|
+
`Email: ${me.account.email ?? '(none on file)'}`,
|
|
184
|
+
`Balance: ${me.account.balance_usd} (${me.account.balance_credits.toLocaleString('en-US')} credits)`,
|
|
185
|
+
`Endpoint: ${me.endpoint.base_url}`,
|
|
186
|
+
`Dashboard: ${me.endpoint.dashboard_url}`,
|
|
187
|
+
];
|
|
188
|
+
if (me.account.low_balance) {
|
|
189
|
+
lines.push('', 'Balance is low. `get_topup_link` returns a payment link for the human.');
|
|
190
|
+
}
|
|
191
|
+
if (me.endpoint.requires_dev_header) {
|
|
192
|
+
lines.push('', 'This is a development deployment: its gateway also requires the shared X-CK-Dev-Auth header in front of the key.');
|
|
193
|
+
}
|
|
194
|
+
return text(lines.join('\n'));
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
return describe(error);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
server.registerTool('get_balance', {
|
|
201
|
+
title: 'Credit balance',
|
|
202
|
+
description: 'The remaining credit balance, in credits and in dollars. Nothing else.',
|
|
203
|
+
annotations: { title: 'Credit balance', readOnlyHint: true, openWorldHint: true },
|
|
204
|
+
}, async () => {
|
|
205
|
+
try {
|
|
206
|
+
const me = await api.request('/api/v1/me');
|
|
207
|
+
return text(`${me.account.balance_usd} (${me.account.balance_credits.toLocaleString('en-US')} credits)` +
|
|
208
|
+
(me.account.low_balance ? ' — low' : ''));
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
return describe(error);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
server.registerTool('get_usage', {
|
|
215
|
+
title: 'Usage and spending',
|
|
216
|
+
description: 'Billable inference responses and credits spent, per day and in total, plus recent ' +
|
|
217
|
+
'purchases. The server keeps a rolling 30-day window; `days` narrows what is reported.',
|
|
218
|
+
inputSchema: {
|
|
219
|
+
days: z
|
|
220
|
+
.number()
|
|
221
|
+
.int()
|
|
222
|
+
.min(1)
|
|
223
|
+
.max(30)
|
|
224
|
+
.optional()
|
|
225
|
+
.describe('How many of the last 30 days to report. Defaults to all 30.'),
|
|
226
|
+
},
|
|
227
|
+
annotations: { title: 'Usage and spending', readOnlyHint: true, openWorldHint: true },
|
|
228
|
+
}, async ({ days }) => {
|
|
229
|
+
try {
|
|
230
|
+
const usage = await api.request('/api/v1/usage');
|
|
231
|
+
// Sliced here rather than asked for: the window is fixed server-side
|
|
232
|
+
// because that is what makes the read cheap. See the note on the route.
|
|
233
|
+
const window = days ?? usage.window_days;
|
|
234
|
+
const daily = usage.daily.slice(-window);
|
|
235
|
+
const credits = daily.reduce((sum, day) => sum + day.credits, 0);
|
|
236
|
+
const rounds = daily.reduce((sum, day) => sum + day.billable_rounds, 0);
|
|
237
|
+
const lines = [
|
|
238
|
+
`Last ${window} day${window === 1 ? '' : 's'}:`,
|
|
239
|
+
` ${rounds.toLocaleString('en-US')} billable responses`,
|
|
240
|
+
` ${credits.toLocaleString('en-US')} credits spent`,
|
|
241
|
+
`Balance now: ${usage.balance_usd} (${usage.balance_credits.toLocaleString('en-US')} credits)`,
|
|
242
|
+
];
|
|
243
|
+
if (usage.totals.waived_rounds > 0) {
|
|
244
|
+
lines.push('', `${usage.totals.waived_rounds} response(s) in the full window were served free — our own retries and the per-attempt cap. Those are not billing errors.`);
|
|
245
|
+
}
|
|
246
|
+
if (usage.totals.by_client.length > 1) {
|
|
247
|
+
lines.push('', 'By integration (full window):');
|
|
248
|
+
for (const row of usage.totals.by_client) {
|
|
249
|
+
lines.push(` ${row.client ?? 'direct'}: ${row.billable_rounds.toLocaleString('en-US')} rounds, ${row.credits.toLocaleString('en-US')} credits`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
const spending = daily.filter((day) => day.credits > 0);
|
|
253
|
+
if (spending.length > 0) {
|
|
254
|
+
lines.push('', 'Per day:');
|
|
255
|
+
for (const day of spending) {
|
|
256
|
+
lines.push(` ${day.day} ${day.credits.toLocaleString('en-US')} credits`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (usage.ledger.length > 0) {
|
|
260
|
+
lines.push('', 'Purchases and grants:');
|
|
261
|
+
for (const entry of usage.ledger.slice(0, 10)) {
|
|
262
|
+
const paid = entry.usd_amount_cents === null ? '' : ` ($${(entry.usd_amount_cents / 100).toFixed(2)})`;
|
|
263
|
+
lines.push(` ${entry.at.slice(0, 10)} ${entry.kind} ${entry.delta_credits > 0 ? '+' : ''}${entry.delta_credits.toLocaleString('en-US')}${paid}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return text(lines.join('\n'));
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
return describe(error);
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
// ── keys ──────────────────────────────────────────────────────────────────
|
|
273
|
+
server.registerTool('list_api_keys', {
|
|
274
|
+
title: 'List API keys',
|
|
275
|
+
description: 'The account’s solving keys, masked. The secret half is not stored anywhere and cannot ' +
|
|
276
|
+
'be listed — only minted.',
|
|
277
|
+
annotations: { title: 'List API keys', readOnlyHint: true, openWorldHint: true },
|
|
278
|
+
}, async () => {
|
|
279
|
+
try {
|
|
280
|
+
const listing = await api.request('/api/v1/keys');
|
|
281
|
+
if (listing.keys.length === 0) {
|
|
282
|
+
return text('No keys yet. `create_api_key` mints one.');
|
|
283
|
+
}
|
|
284
|
+
const rows = listing.keys.map((key) => {
|
|
285
|
+
const state = key.revoked ? 'revoked' : 'live';
|
|
286
|
+
const used = key.last_used_at ? `last used ${key.last_used_at.slice(0, 10)}` : 'never used';
|
|
287
|
+
return ` #${key.id} ${key.masked} ${key.name ?? '(unnamed)'} [${state}, ${used}]`;
|
|
288
|
+
});
|
|
289
|
+
const live = listing.keys.filter((key) => !key.revoked).length;
|
|
290
|
+
return text([`${live} live of ${listing.max_active_keys} allowed:`, ...rows].join('\n'));
|
|
291
|
+
}
|
|
292
|
+
catch (error) {
|
|
293
|
+
return describe(error);
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
server.registerTool('create_api_key', {
|
|
297
|
+
title: 'Create an API key',
|
|
298
|
+
description: 'Mint a key for the captcha-solving endpoint (the Abyss model) and save it to disk where ' +
|
|
299
|
+
'the solver will find it. THE SECRET IS NEVER RETURNED — it is written straight to a ' +
|
|
300
|
+
'0600 file and only the path is reported back, so it cannot leak through this ' +
|
|
301
|
+
'conversation. After this runs, a solve works with no environment variables set.',
|
|
302
|
+
inputSchema: {
|
|
303
|
+
name: z
|
|
304
|
+
.string()
|
|
305
|
+
.max(60)
|
|
306
|
+
.optional()
|
|
307
|
+
.describe('A label so this key is identifiable in the dashboard, e.g. "scraper-prod".'),
|
|
308
|
+
},
|
|
309
|
+
annotations: { title: 'Create an API key', readOnlyHint: false, openWorldHint: true },
|
|
310
|
+
}, async ({ name }) => {
|
|
311
|
+
try {
|
|
312
|
+
const created = await api.request('/api/v1/keys', {
|
|
313
|
+
method: 'POST',
|
|
314
|
+
body: { name: name ?? null },
|
|
315
|
+
});
|
|
316
|
+
// The secret goes to disk and NOT into this tool's result. Everything
|
|
317
|
+
// returned below is safe to repeat: an id, a masked form, and a path.
|
|
318
|
+
//
|
|
319
|
+
// The write is the last thing that can fail, and if it does the key
|
|
320
|
+
// already exists server-side — so say so plainly rather than pretending
|
|
321
|
+
// nothing happened, and point at revoke. Silently swallowing this would
|
|
322
|
+
// strand a live key that the user does not know they own.
|
|
323
|
+
let credentialFile;
|
|
324
|
+
try {
|
|
325
|
+
credentialFile = writeSolverCredential({
|
|
326
|
+
apiKey: created.api_key,
|
|
327
|
+
baseUrl: created.base_url,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
catch (writeError) {
|
|
331
|
+
const reason = writeError instanceof Error ? writeError.message : String(writeError);
|
|
332
|
+
return text([
|
|
333
|
+
`Key #${created.id} was created (${created.masked}), but saving it to ` +
|
|
334
|
+
`${solverCredentialPath()} failed: ${reason}`,
|
|
335
|
+
'',
|
|
336
|
+
'The secret is NOT being printed here — it would end up in this transcript.',
|
|
337
|
+
'The key exists server-side and cannot be retrieved again, so either fix the',
|
|
338
|
+
`permissions on that path and mint a new key, or revoke key #${created.id} with`,
|
|
339
|
+
'revoke_api_key so nothing untracked is left live.',
|
|
340
|
+
].join('\n'));
|
|
341
|
+
}
|
|
342
|
+
return text([
|
|
343
|
+
`Key #${created.id} created${created.name ? ` (${created.name})` : ''}, shown as ${created.masked} in the dashboard.`,
|
|
344
|
+
'',
|
|
345
|
+
`Saved to ${credentialFile} (0600), pointing at ${created.base_url}.`,
|
|
346
|
+
'',
|
|
347
|
+
'The secret itself is deliberately not shown — it went straight to that file so it',
|
|
348
|
+
'never enters this conversation, and only a SHA-256 is stored server-side. Solving',
|
|
349
|
+
'now works with no environment variables set at all.',
|
|
350
|
+
].join('\n'));
|
|
351
|
+
}
|
|
352
|
+
catch (error) {
|
|
353
|
+
return describe(error);
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
server.registerTool('revoke_api_key', {
|
|
357
|
+
title: 'Revoke an API key',
|
|
358
|
+
description: 'Kill a key by its id. It stops working within about 30 seconds — the gateway’s refresh ' +
|
|
359
|
+
'interval. This cannot be undone; mint a new key instead of trying to restore one.',
|
|
360
|
+
inputSchema: {
|
|
361
|
+
id: z.number().int().positive().describe('The key id from list_api_keys.'),
|
|
362
|
+
},
|
|
363
|
+
annotations: { title: 'Revoke an API key', readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
364
|
+
}, async ({ id }) => {
|
|
365
|
+
try {
|
|
366
|
+
await api.request(`/api/v1/keys/${id}/revoke`, { method: 'POST' });
|
|
367
|
+
return text(`Key #${id} revoked. It stops working within 30 seconds.`);
|
|
368
|
+
}
|
|
369
|
+
catch (error) {
|
|
370
|
+
return describe(error);
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
// ── money ─────────────────────────────────────────────────────────────────
|
|
374
|
+
server.registerTool('get_topup_link', {
|
|
375
|
+
title: 'Get a top-up link',
|
|
376
|
+
description: 'A URL the human opens to add credits. THIS DOES NOT CHARGE ANYTHING — it returns a ' +
|
|
377
|
+
'link; a person completes the payment on Stripe. Omit `usd` to let them choose the ' +
|
|
378
|
+
'amount, which is the right default when the agent has not been told what to spend.',
|
|
379
|
+
inputSchema: {
|
|
380
|
+
usd: z
|
|
381
|
+
.number()
|
|
382
|
+
.positive()
|
|
383
|
+
.optional()
|
|
384
|
+
.describe('A published pack amount. Omit to return the chooser page instead.'),
|
|
385
|
+
},
|
|
386
|
+
annotations: { title: 'Get a top-up link', readOnlyHint: false, openWorldHint: true },
|
|
387
|
+
}, async ({ usd }) => {
|
|
388
|
+
try {
|
|
389
|
+
const result = await api.request('/api/v1/billing/checkout', {
|
|
390
|
+
method: 'POST',
|
|
391
|
+
body: usd === undefined ? {} : { usd },
|
|
392
|
+
});
|
|
393
|
+
if (result.kind === 'checkout') {
|
|
394
|
+
return text(`Stripe checkout for $${result.usd} (${result.credits?.toLocaleString('en-US')} credits):\n\n ${result.url}\n\n` +
|
|
395
|
+
'Nothing has been charged. The human completes the payment on that page, and the credits land within seconds of it succeeding.');
|
|
396
|
+
}
|
|
397
|
+
const packs = (result.packs ?? [])
|
|
398
|
+
.map((pack) => ` $${pack.usd} → ${pack.credits.toLocaleString('en-US')} credits`)
|
|
399
|
+
.join('\n');
|
|
400
|
+
return text(`Top-up page:\n\n ${result.url}\n\nAvailable packs:\n${packs}\n\nNothing has been charged.`);
|
|
401
|
+
}
|
|
402
|
+
catch (error) {
|
|
403
|
+
return describe(error);
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
server.registerTool('get_pricing', {
|
|
407
|
+
title: 'Pricing',
|
|
408
|
+
description: 'The rate card: dollars per 1,000 inference responses for images and for video, how many ' +
|
|
409
|
+
'responses a captcha typically takes, and the per-captcha billing ceiling. Use this to ' +
|
|
410
|
+
'estimate a job before running it.',
|
|
411
|
+
annotations: { title: 'Pricing', readOnlyHint: true, openWorldHint: true },
|
|
412
|
+
}, async () => {
|
|
413
|
+
try {
|
|
414
|
+
const pricing = await api.request('/api/v1/pricing', { authenticated: false });
|
|
415
|
+
const lines = [
|
|
416
|
+
`Billed per inference response. $1.00 = ${pricing.credits_per_usd.toLocaleString('en-US')} credits.`,
|
|
417
|
+
'',
|
|
418
|
+
];
|
|
419
|
+
for (const row of pricing.classes) {
|
|
420
|
+
lines.push(`${row.label}: $${row.usd_per_1000_responses.toFixed(2)} per 1,000 (${row.credits_per_response} credits each)`, ` ${row.covers}`,
|
|
421
|
+
// The number that turns a rate into a cost. Without it an agent
|
|
422
|
+
// estimating a job would quietly assume one response per captcha.
|
|
423
|
+
` Typically ${row.typical_responses_per_captcha} response(s) per captcha`);
|
|
424
|
+
}
|
|
425
|
+
lines.push('', `Free (never reach the model): ${pricing.free_challenges.join(', ')}`,
|
|
426
|
+
// The ceiling an agent should actually plan against: one captcha can
|
|
427
|
+
// never cost more than this, however badly it goes.
|
|
428
|
+
`One captcha costs at most ${pricing.max_billable_responses_per_session} billable responses ` +
|
|
429
|
+
`($${((pricing.classes.find((row) => row.puzzle_class === 'image')?.usd_per_1000_responses ?? 0) * pricing.max_billable_responses_per_session).toFixed(2)} per 1,000 for images). ` +
|
|
430
|
+
`Responses past that are served free, and the attempt is abandoned after ${pricing.max_responses_per_session}.`, `Packs: ${pricing.credit_packs.map((pack) => `$${pack.usd}`).join(', ')}`);
|
|
431
|
+
return text(lines.join('\n'));
|
|
432
|
+
}
|
|
433
|
+
catch (error) {
|
|
434
|
+
return describe(error);
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
server.registerTool('get_models', {
|
|
438
|
+
title: 'Models',
|
|
439
|
+
description: 'The model lineup: which weights can be self-hosted, and which one the hosted endpoint ' +
|
|
440
|
+
'runs. Useful for deciding whether to pay per solve or run it yourself.',
|
|
441
|
+
annotations: { title: 'Models', readOnlyHint: true, openWorldHint: true },
|
|
442
|
+
}, async () => {
|
|
443
|
+
try {
|
|
444
|
+
const listing = await api.request('/api/v1/models', { authenticated: false });
|
|
445
|
+
const lines = [`Hosted endpoint: ${listing.base_url}`, ''];
|
|
446
|
+
for (const model of listing.models) {
|
|
447
|
+
lines.push(`${model.name} — ${model.zone}`);
|
|
448
|
+
lines.push(` ${model.tagline}`);
|
|
449
|
+
lines.push(model.hosted
|
|
450
|
+
? ' Hosted only; not downloadable.'
|
|
451
|
+
: model.published
|
|
452
|
+
? ` Weights: ${model.hugging_face_id} (~${model.weights_gb} GB, needs ${model.min_vram})`
|
|
453
|
+
: ` Weights: ${model.hugging_face_id} — reserved, not uploaded yet`);
|
|
454
|
+
if (model.accuracy !== null) {
|
|
455
|
+
lines.push(` Measured: ${(model.accuracy * 100).toFixed(1)}% exact match`);
|
|
456
|
+
}
|
|
457
|
+
if (model.video)
|
|
458
|
+
lines.push(' Handles video challenges.');
|
|
459
|
+
lines.push('');
|
|
460
|
+
}
|
|
461
|
+
return text(lines.join('\n').trimEnd());
|
|
462
|
+
}
|
|
463
|
+
catch (error) {
|
|
464
|
+
return describe(error);
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
return server;
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Poll until the token arrives or the budget runs out.
|
|
471
|
+
*
|
|
472
|
+
* `slow_down` doubles the interval, permanently for this attempt, which is what
|
|
473
|
+
* RFC 8628 §3.5 asks for. It should not happen — we honour the interval the
|
|
474
|
+
* server gave us — but a clock that jumps or a retried call can produce it, and
|
|
475
|
+
* an implementation that ignores it is one that hammers an unauthenticated
|
|
476
|
+
* endpoint whenever it does.
|
|
477
|
+
*/
|
|
478
|
+
async function pollForToken(api, pending) {
|
|
479
|
+
const deadline = Date.now() + SIGN_IN_WAIT_MS;
|
|
480
|
+
let interval = Math.max(1, pending.intervalSeconds) * 1000;
|
|
481
|
+
while (Date.now() < deadline) {
|
|
482
|
+
if (pending.expiresAtMs <= Date.now())
|
|
483
|
+
return { kind: 'dead' };
|
|
484
|
+
try {
|
|
485
|
+
const grant = await api.request('/api/v1/device/token', {
|
|
486
|
+
method: 'POST',
|
|
487
|
+
authenticated: false,
|
|
488
|
+
body: { device_code: pending.deviceCode },
|
|
489
|
+
});
|
|
490
|
+
return { kind: 'granted', grant };
|
|
491
|
+
}
|
|
492
|
+
catch (error) {
|
|
493
|
+
if (!(error instanceof ApiError))
|
|
494
|
+
throw error;
|
|
495
|
+
switch (error.code) {
|
|
496
|
+
case 'authorization_pending':
|
|
497
|
+
break;
|
|
498
|
+
case 'slow_down':
|
|
499
|
+
interval *= 2;
|
|
500
|
+
break;
|
|
501
|
+
case 'access_denied':
|
|
502
|
+
return { kind: 'denied' };
|
|
503
|
+
case 'expired_token':
|
|
504
|
+
return { kind: 'dead' };
|
|
505
|
+
default:
|
|
506
|
+
throw error;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
// Do not overshoot the deadline sleeping: returning a moment early with
|
|
510
|
+
// "call me again" is better than being killed by the client's timeout.
|
|
511
|
+
const remaining = deadline - Date.now();
|
|
512
|
+
if (remaining <= 0)
|
|
513
|
+
break;
|
|
514
|
+
await sleep(Math.min(interval, remaining));
|
|
515
|
+
}
|
|
516
|
+
return { kind: 'waiting' };
|
|
517
|
+
}
|
|
518
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "captchakraken-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for CaptchaKraken: sign in with GitHub, mint API keys for the Abyss model, and read your usage and spending.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"author": "Jake Writer",
|
|
7
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
8
|
+
"bin": {
|
|
9
|
+
"captchakraken-mcp": "dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=20"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/JWriter20/CaptchaKraken.git",
|
|
22
|
+
"directory": "mcp"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"mcp",
|
|
26
|
+
"model-context-protocol",
|
|
27
|
+
"captcha",
|
|
28
|
+
"captchakraken"
|
|
29
|
+
],
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://github.com/JWriter20/CaptchaKraken/issues"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://github.com/JWriter20/CaptchaKraken#readme",
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsc -p tsconfig.json",
|
|
36
|
+
"typecheck": "tsc --noEmit",
|
|
37
|
+
"prepublishOnly": "npm run build",
|
|
38
|
+
"start": "node dist/index.js"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
42
|
+
"zod": "^3.25.76"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/node": "^22.20.1",
|
|
46
|
+
"typescript": "^5.9.3"
|
|
47
|
+
}
|
|
48
|
+
}
|