captchaai-mcp 1.0.0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 CaptchaAI (captchaai-developers)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,152 @@
1
+ Metadata-Version: 2.4
2
+ Name: captchaai-mcp
3
+ Version: 1.0.0
4
+ Summary: Official MCP server for CaptchaAI — expose token-mode CAPTCHA solving to AI agents.
5
+ Keywords: mcp,mcp-server,model-context-protocol,fastmcp,ai-agents,llm,captcha,captchaai,captcha-solver,captcha-solving,captcha-bypass,recaptcha,recaptcha-v2,recaptcha-v3,recaptcha-enterprise,cloudflare-turnstile,turnstile-solver,cloudflare-challenge,geetest,captchafox,friendly-captcha,lemin,automation,web-scraping
6
+ Author: captchaai-developers
7
+ Author-email: captchaai-developers <dev@captchaai.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Internet :: WWW/HTTP
19
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Requires-Dist: captchaai>=1.0.3
22
+ Requires-Dist: fastmcp>=4.1,<5
23
+ Requires-Python: >=3.10
24
+ Project-URL: Homepage, https://captchaai.com/
25
+ Project-URL: Documentation, https://docs.captchaai.com/
26
+ Project-URL: Repository, https://github.com/CaptchaAI/captchaai-mcp
27
+ Project-URL: Issues, https://github.com/CaptchaAI/captchaai-mcp/issues
28
+ Project-URL: Changelog, https://github.com/CaptchaAI/captchaai-mcp/releases
29
+ Description-Content-Type: text/markdown
30
+
31
+ # CaptchaAI MCP Server
32
+
33
+ Official Model Context Protocol (MCP) server for [CaptchaAI](https://captchaai.com/), built with [FastMCP](https://gofastmcp.com/) and backed by the official [`captchaai`](https://github.com/CaptchaAI/captchaai-python) Python SDK.
34
+
35
+ Exposes **token-mode** (pure API, no browser required) tools so AI assistants and automation agents can inspect account capacity, check supported CAPTCHA types, and solve challenges.
36
+
37
+ - **Website:** https://captchaai.com/
38
+ - **API Documentation:** https://docs.captchaai.com/
39
+ - **MCP Client Integration Guide:** [docs/mcp-integration.md](docs/mcp-integration.md) (Claude Code, OpenAI Codex, Cursor, Claude Desktop, VS Code, Windsurf, Antigravity)
40
+ - **Repository:** https://github.com/CaptchaAI/captchaai-mcp
41
+
42
+ ## Tools
43
+
44
+ | Tool | Description | API Key Needed |
45
+ |---|---|---|
46
+ | `get_supported_captchas` | List the 8 token-mode CAPTCHA types with required/optional arguments and proxy/UA notes | No |
47
+ | `get_threads` | Get account thread usage (`threads`, `working_threads`, `free_threads`) | Yes |
48
+ | `get_balance` | Get account balance string (`balance`) | Yes |
49
+ | `solve_captcha` | Solve a CAPTCHA in token mode and return `task_id`, `solution`, and `user_agent` | Yes |
50
+
51
+ ## Supported `captcha_type` Values
52
+
53
+ - `recaptcha_v2` (supports `invisible=True` and `enterprise=True`)
54
+ - `recaptcha_v3` (requires `action`; supports `enterprise=True` and `min_score`)
55
+ - `turnstile`
56
+ - `cloudflare_challenge` (requires `proxy` + `proxytype` (`"HTTP"` or `"HTTPS"`); returns `user_agent`)
57
+ - `geetest` (requires `gt` + `challenge`; returns `{challenge, validate, seccode}`)
58
+ - `captchafox` (requires `proxy` + `proxytype` (`"HTTP"` or `"HTTPS"`); returns `user_agent`)
59
+ - `friendly_captcha`
60
+ - `lemin` (requires `captcha_id` + `div_id`; returns `{answer, challenge_uuid}`)
61
+
62
+ ## Installation
63
+
64
+ Install directly from PyPI with `pip` or run via `uvx`:
65
+
66
+ ```bash
67
+ pip install captchaai-mcp
68
+ ```
69
+
70
+ Or run from a local clone:
71
+
72
+ ```bash
73
+ git clone https://github.com/CaptchaAI/captchaai-mcp.git
74
+ cd captchaai-mcp
75
+ uv sync
76
+ ```
77
+
78
+ Set your API key:
79
+
80
+ ```bash
81
+ export CAPTCHAAI_API_KEY="YOUR_32_CHAR_API_KEY"
82
+ ```
83
+
84
+ ## Running
85
+
86
+ ```bash
87
+ # STDIO (default — for Claude Desktop, Cursor, Windsurf, etc.)
88
+ uvx captchaai-mcp
89
+ # or from source:
90
+ uv run captchaai-mcp
91
+
92
+ # Streamable HTTP (serves at http://127.0.0.1:8000/mcp)
93
+ uvx captchaai-mcp --transport streamable-http --host 127.0.0.1 --port 8000
94
+ ```
95
+
96
+ > [!NOTE]
97
+ > Prefer `CAPTCHAAI_API_KEY` in the environment over `--api-key` on multi-user hosts so the key is not visible in process listings. When using `--transport streamable-http`, bind to loopback or place it behind an authenticating TLS reverse proxy.
98
+
99
+ ### MCP Client Configuration
100
+
101
+ See the full **[MCP Client Integration Guide](docs/mcp-integration.md)** for step-by-step setup with **Claude Code**, **OpenAI Codex CLI**, **Cursor**, **Claude Desktop**, **VS Code**, **Windsurf**, and **Antigravity**.
102
+
103
+ Using `uvx` (recommended):
104
+
105
+ ```json
106
+ {
107
+ "mcpServers": {
108
+ "captchaai": {
109
+ "command": "uvx",
110
+ "args": ["captchaai-mcp"],
111
+ "env": {
112
+ "CAPTCHAAI_API_KEY": "YOUR_32_CHAR_API_KEY"
113
+ }
114
+ }
115
+ }
116
+ }
117
+ ```
118
+
119
+ Or from a local checkout:
120
+
121
+ ```json
122
+ {
123
+ "mcpServers": {
124
+ "captchaai": {
125
+ "command": "uv",
126
+ "args": [
127
+ "run",
128
+ "--directory",
129
+ "/path/to/captchaai-mcp",
130
+ "captchaai-mcp"
131
+ ],
132
+ "env": {
133
+ "CAPTCHAAI_API_KEY": "YOUR_32_CHAR_API_KEY"
134
+ }
135
+ }
136
+ }
137
+ }
138
+ ```
139
+
140
+ ## Development & Tests
141
+
142
+ ```bash
143
+ # Run MCP server tests
144
+ uv run pytest -q
145
+
146
+ # Inspect the MCP server manifest
147
+ uv run fastmcp inspect src/captchaai_mcp/server.py:create_server
148
+ ```
149
+
150
+ ## License
151
+
152
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,122 @@
1
+ # CaptchaAI MCP Server
2
+
3
+ Official Model Context Protocol (MCP) server for [CaptchaAI](https://captchaai.com/), built with [FastMCP](https://gofastmcp.com/) and backed by the official [`captchaai`](https://github.com/CaptchaAI/captchaai-python) Python SDK.
4
+
5
+ Exposes **token-mode** (pure API, no browser required) tools so AI assistants and automation agents can inspect account capacity, check supported CAPTCHA types, and solve challenges.
6
+
7
+ - **Website:** https://captchaai.com/
8
+ - **API Documentation:** https://docs.captchaai.com/
9
+ - **MCP Client Integration Guide:** [docs/mcp-integration.md](docs/mcp-integration.md) (Claude Code, OpenAI Codex, Cursor, Claude Desktop, VS Code, Windsurf, Antigravity)
10
+ - **Repository:** https://github.com/CaptchaAI/captchaai-mcp
11
+
12
+ ## Tools
13
+
14
+ | Tool | Description | API Key Needed |
15
+ |---|---|---|
16
+ | `get_supported_captchas` | List the 8 token-mode CAPTCHA types with required/optional arguments and proxy/UA notes | No |
17
+ | `get_threads` | Get account thread usage (`threads`, `working_threads`, `free_threads`) | Yes |
18
+ | `get_balance` | Get account balance string (`balance`) | Yes |
19
+ | `solve_captcha` | Solve a CAPTCHA in token mode and return `task_id`, `solution`, and `user_agent` | Yes |
20
+
21
+ ## Supported `captcha_type` Values
22
+
23
+ - `recaptcha_v2` (supports `invisible=True` and `enterprise=True`)
24
+ - `recaptcha_v3` (requires `action`; supports `enterprise=True` and `min_score`)
25
+ - `turnstile`
26
+ - `cloudflare_challenge` (requires `proxy` + `proxytype` (`"HTTP"` or `"HTTPS"`); returns `user_agent`)
27
+ - `geetest` (requires `gt` + `challenge`; returns `{challenge, validate, seccode}`)
28
+ - `captchafox` (requires `proxy` + `proxytype` (`"HTTP"` or `"HTTPS"`); returns `user_agent`)
29
+ - `friendly_captcha`
30
+ - `lemin` (requires `captcha_id` + `div_id`; returns `{answer, challenge_uuid}`)
31
+
32
+ ## Installation
33
+
34
+ Install directly from PyPI with `pip` or run via `uvx`:
35
+
36
+ ```bash
37
+ pip install captchaai-mcp
38
+ ```
39
+
40
+ Or run from a local clone:
41
+
42
+ ```bash
43
+ git clone https://github.com/CaptchaAI/captchaai-mcp.git
44
+ cd captchaai-mcp
45
+ uv sync
46
+ ```
47
+
48
+ Set your API key:
49
+
50
+ ```bash
51
+ export CAPTCHAAI_API_KEY="YOUR_32_CHAR_API_KEY"
52
+ ```
53
+
54
+ ## Running
55
+
56
+ ```bash
57
+ # STDIO (default — for Claude Desktop, Cursor, Windsurf, etc.)
58
+ uvx captchaai-mcp
59
+ # or from source:
60
+ uv run captchaai-mcp
61
+
62
+ # Streamable HTTP (serves at http://127.0.0.1:8000/mcp)
63
+ uvx captchaai-mcp --transport streamable-http --host 127.0.0.1 --port 8000
64
+ ```
65
+
66
+ > [!NOTE]
67
+ > Prefer `CAPTCHAAI_API_KEY` in the environment over `--api-key` on multi-user hosts so the key is not visible in process listings. When using `--transport streamable-http`, bind to loopback or place it behind an authenticating TLS reverse proxy.
68
+
69
+ ### MCP Client Configuration
70
+
71
+ See the full **[MCP Client Integration Guide](docs/mcp-integration.md)** for step-by-step setup with **Claude Code**, **OpenAI Codex CLI**, **Cursor**, **Claude Desktop**, **VS Code**, **Windsurf**, and **Antigravity**.
72
+
73
+ Using `uvx` (recommended):
74
+
75
+ ```json
76
+ {
77
+ "mcpServers": {
78
+ "captchaai": {
79
+ "command": "uvx",
80
+ "args": ["captchaai-mcp"],
81
+ "env": {
82
+ "CAPTCHAAI_API_KEY": "YOUR_32_CHAR_API_KEY"
83
+ }
84
+ }
85
+ }
86
+ }
87
+ ```
88
+
89
+ Or from a local checkout:
90
+
91
+ ```json
92
+ {
93
+ "mcpServers": {
94
+ "captchaai": {
95
+ "command": "uv",
96
+ "args": [
97
+ "run",
98
+ "--directory",
99
+ "/path/to/captchaai-mcp",
100
+ "captchaai-mcp"
101
+ ],
102
+ "env": {
103
+ "CAPTCHAAI_API_KEY": "YOUR_32_CHAR_API_KEY"
104
+ }
105
+ }
106
+ }
107
+ }
108
+ ```
109
+
110
+ ## Development & Tests
111
+
112
+ ```bash
113
+ # Run MCP server tests
114
+ uv run pytest -q
115
+
116
+ # Inspect the MCP server manifest
117
+ uv run fastmcp inspect src/captchaai_mcp/server.py:create_server
118
+ ```
119
+
120
+ ## License
121
+
122
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,80 @@
1
+ [project]
2
+ name = "captchaai-mcp"
3
+ version = "1.0.0"
4
+ description = "Official MCP server for CaptchaAI — expose token-mode CAPTCHA solving to AI agents."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ requires-python = ">=3.10"
9
+ dependencies = [
10
+ "captchaai>=1.0.3",
11
+ "fastmcp>=4.1,<5",
12
+ ]
13
+ keywords = [
14
+ "mcp",
15
+ "mcp-server",
16
+ "model-context-protocol",
17
+ "fastmcp",
18
+ "ai-agents",
19
+ "llm",
20
+ "captcha",
21
+ "captchaai",
22
+ "captcha-solver",
23
+ "captcha-solving",
24
+ "captcha-bypass",
25
+ "recaptcha",
26
+ "recaptcha-v2",
27
+ "recaptcha-v3",
28
+ "recaptcha-enterprise",
29
+ "cloudflare-turnstile",
30
+ "turnstile-solver",
31
+ "cloudflare-challenge",
32
+ "geetest",
33
+ "captchafox",
34
+ "friendly-captcha",
35
+ "lemin",
36
+ "automation",
37
+ "web-scraping",
38
+ ]
39
+ classifiers = [
40
+ "Development Status :: 5 - Production/Stable",
41
+ "Intended Audience :: Developers",
42
+ "Operating System :: OS Independent",
43
+ "Programming Language :: Python :: 3",
44
+ "Programming Language :: Python :: 3.10",
45
+ "Programming Language :: Python :: 3.11",
46
+ "Programming Language :: Python :: 3.12",
47
+ "Programming Language :: Python :: 3.13",
48
+ "Topic :: Internet :: WWW/HTTP",
49
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
50
+ "Topic :: Software Development :: Libraries :: Python Modules",
51
+ ]
52
+
53
+ [[project.authors]]
54
+ name = "captchaai-developers"
55
+ email = "dev@captchaai.com"
56
+
57
+ [project.urls]
58
+ Homepage = "https://captchaai.com/"
59
+ Documentation = "https://docs.captchaai.com/"
60
+ Repository = "https://github.com/CaptchaAI/captchaai-mcp"
61
+ Issues = "https://github.com/CaptchaAI/captchaai-mcp/issues"
62
+ Changelog = "https://github.com/CaptchaAI/captchaai-mcp/releases"
63
+
64
+ [project.scripts]
65
+ captchaai-mcp = "captchaai_mcp.__main__:main"
66
+
67
+ [build-system]
68
+ requires = ["uv_build>=0.13.7,<0.14.0"]
69
+ build-backend = "uv_build"
70
+
71
+ [dependency-groups]
72
+ dev = [
73
+ "pytest>=9.2.0",
74
+ "pytest-asyncio>=1.4.1.post1",
75
+ ]
76
+
77
+ [tool.pytest.ini_options]
78
+ asyncio_mode = "auto"
79
+ asyncio_default_fixture_loop_scope = "function"
80
+ testpaths = ["tests"]
@@ -0,0 +1,79 @@
1
+ [project]
2
+ name = "captchaai-mcp"
3
+ version = "1.0.0"
4
+ description = "Official MCP server for CaptchaAI — expose token-mode CAPTCHA solving to AI agents."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ authors = [
9
+ { name = "captchaai-developers", email = "dev@captchaai.com" }
10
+ ]
11
+ requires-python = ">=3.10"
12
+ dependencies = [
13
+ "captchaai>=1.0.3",
14
+ "fastmcp>=4.1,<5",
15
+ ]
16
+ keywords = [
17
+ "mcp",
18
+ "mcp-server",
19
+ "model-context-protocol",
20
+ "fastmcp",
21
+ "ai-agents",
22
+ "llm",
23
+ "captcha",
24
+ "captchaai",
25
+ "captcha-solver",
26
+ "captcha-solving",
27
+ "captcha-bypass",
28
+ "recaptcha",
29
+ "recaptcha-v2",
30
+ "recaptcha-v3",
31
+ "recaptcha-enterprise",
32
+ "cloudflare-turnstile",
33
+ "turnstile-solver",
34
+ "cloudflare-challenge",
35
+ "geetest",
36
+ "captchafox",
37
+ "friendly-captcha",
38
+ "lemin",
39
+ "automation",
40
+ "web-scraping",
41
+ ]
42
+ classifiers = [
43
+ "Development Status :: 5 - Production/Stable",
44
+ "Intended Audience :: Developers",
45
+ "Operating System :: OS Independent",
46
+ "Programming Language :: Python :: 3",
47
+ "Programming Language :: Python :: 3.10",
48
+ "Programming Language :: Python :: 3.11",
49
+ "Programming Language :: Python :: 3.12",
50
+ "Programming Language :: Python :: 3.13",
51
+ "Topic :: Internet :: WWW/HTTP",
52
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
53
+ "Topic :: Software Development :: Libraries :: Python Modules",
54
+ ]
55
+
56
+ [project.urls]
57
+ Homepage = "https://captchaai.com/"
58
+ Documentation = "https://docs.captchaai.com/"
59
+ Repository = "https://github.com/CaptchaAI/captchaai-mcp"
60
+ Issues = "https://github.com/CaptchaAI/captchaai-mcp/issues"
61
+ Changelog = "https://github.com/CaptchaAI/captchaai-mcp/releases"
62
+
63
+ [project.scripts]
64
+ captchaai-mcp = "captchaai_mcp.__main__:main"
65
+
66
+ [build-system]
67
+ requires = ["uv_build>=0.13.7,<0.14.0"]
68
+ build-backend = "uv_build"
69
+
70
+ [dependency-groups]
71
+ dev = [
72
+ "pytest>=9.2.0",
73
+ "pytest-asyncio>=1.4.1.post1",
74
+ ]
75
+
76
+ [tool.pytest.ini_options]
77
+ asyncio_mode = "auto"
78
+ asyncio_default_fixture_loop_scope = "function"
79
+ testpaths = ["tests"]
@@ -0,0 +1,9 @@
1
+ """CaptchaAI MCP server package."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .server import create_server
6
+
7
+ __version__ = "1.0.0"
8
+
9
+ __all__ = ["__version__", "create_server"]
@@ -0,0 +1,53 @@
1
+ """CLI entry point for `python -m captchaai_mcp` and the `captchaai-mcp` script."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+
7
+ from captchaai_mcp.server import create_server
8
+
9
+
10
+ def main() -> None:
11
+ """Parse CLI arguments and run the CaptchaAI MCP server."""
12
+ parser = argparse.ArgumentParser(
13
+ description="CaptchaAI MCP Server — expose token-mode captcha solving to AI agents.",
14
+ )
15
+ parser.add_argument(
16
+ "--transport",
17
+ choices=["stdio", "streamable-http"],
18
+ default="stdio",
19
+ help="Transport protocol: stdio (default) or streamable-http.",
20
+ )
21
+ parser.add_argument(
22
+ "--host",
23
+ default="127.0.0.1",
24
+ help="Host to bind for streamable-http transport (default: 127.0.0.1).",
25
+ )
26
+ parser.add_argument(
27
+ "--port",
28
+ type=int,
29
+ default=8000,
30
+ help="Port to bind for streamable-http transport (default: 8000).",
31
+ )
32
+ parser.add_argument(
33
+ "--api-key",
34
+ default=None,
35
+ help="CaptchaAI API key. Falls back to CAPTCHAAI_API_KEY env var.",
36
+ )
37
+ parser.add_argument(
38
+ "--name",
39
+ default="captchaai",
40
+ help="Server name advertised to MCP clients (default: captchaai).",
41
+ )
42
+
43
+ args = parser.parse_args()
44
+ server = create_server(api_key=args.api_key, name=args.name)
45
+
46
+ if args.transport == "stdio":
47
+ server.run()
48
+ else:
49
+ server.run(transport="http", host=args.host, port=args.port)
50
+
51
+
52
+ if __name__ == "__main__":
53
+ main()
@@ -0,0 +1,180 @@
1
+ """Catalog of supported token-mode CAPTCHA types and SDK call dispatch."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any
7
+
8
+ from captchaai import SUPPORTED_PROXY_TYPES, supported_types
9
+ from fastmcp.exceptions import ToolError
10
+
11
+ _COMMON_ARGS = ("cookies", "user_agent", "proxy", "proxytype")
12
+ _PROXY_ONLY_ARGS = ("proxy", "proxytype")
13
+ _ARG_TO_SDK_KWARG = {
14
+ "website_url": "url",
15
+ "website_key": "sitekey",
16
+ }
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class PublicType:
21
+ """Definition of one public token-mode CAPTCHA type exposed by MCP."""
22
+
23
+ sdk_method: str
24
+ description: str
25
+ registry_names: tuple[str, ...]
26
+ required: tuple[str, ...]
27
+ optional: tuple[str, ...]
28
+ variants: dict[str, str]
29
+
30
+
31
+ CATALOG: dict[str, PublicType] = {
32
+ "recaptcha_v2": PublicType(
33
+ sdk_method="recaptcha_v2",
34
+ description="Google reCAPTCHA v2 checkbox, invisible, or Enterprise.",
35
+ registry_names=("recaptcha_v2", "recaptcha_v2_invisible", "recaptcha_v2_enterprise"),
36
+ required=("website_url", "website_key"),
37
+ optional=("invisible", "enterprise", "action", *_COMMON_ARGS),
38
+ variants={
39
+ "invisible": "Invisible reCAPTCHA v2 widget",
40
+ "enterprise": "reCAPTCHA v2 Enterprise (returns user_agent to reuse)",
41
+ },
42
+ ),
43
+ "recaptcha_v3": PublicType(
44
+ sdk_method="recaptcha_v3",
45
+ description="Google reCAPTCHA v3 score-based challenge (standard or Enterprise).",
46
+ registry_names=("recaptcha_v3", "recaptcha_v3_enterprise"),
47
+ required=("website_url", "website_key", "action"),
48
+ optional=("enterprise", "min_score", *_COMMON_ARGS),
49
+ variants={
50
+ "enterprise": "reCAPTCHA v3 Enterprise (returns user_agent to reuse)",
51
+ },
52
+ ),
53
+ "turnstile": PublicType(
54
+ sdk_method="turnstile",
55
+ description="Cloudflare Turnstile widget.",
56
+ registry_names=("turnstile",),
57
+ required=("website_url", "website_key"),
58
+ optional=_COMMON_ARGS,
59
+ variants={},
60
+ ),
61
+ "cloudflare_challenge": PublicType(
62
+ sdk_method="cloudflare_challenge",
63
+ description="Cloudflare interstitial challenge page (returns cf_clearance + user_agent).",
64
+ registry_names=("cloudflare_challenge",),
65
+ required=("website_url", "proxy", "proxytype"),
66
+ optional=("cookies", "user_agent"),
67
+ variants={},
68
+ ),
69
+ "geetest": PublicType(
70
+ sdk_method="geetest",
71
+ description="GeeTest v3 slide/puzzle challenge (returns dict with challenge/validate/seccode).",
72
+ registry_names=("geetest",),
73
+ required=("website_url", "gt", "challenge"),
74
+ optional=_COMMON_ARGS,
75
+ variants={},
76
+ ),
77
+ "captchafox": PublicType(
78
+ sdk_method="captchafox",
79
+ description="CaptchaFox slider challenge (proxy required; returns user_agent to reuse).",
80
+ registry_names=("captchafox",),
81
+ required=("website_url", "website_key", "proxy", "proxytype"),
82
+ optional=("cookies", "user_agent"),
83
+ variants={},
84
+ ),
85
+ "friendly_captcha": PublicType(
86
+ sdk_method="friendly_captcha",
87
+ description="Friendly Captcha proof-of-work challenge.",
88
+ registry_names=("friendly_captcha",),
89
+ required=("website_url", "website_key"),
90
+ optional=("version", *_PROXY_ONLY_ARGS),
91
+ variants={},
92
+ ),
93
+ "lemin": PublicType(
94
+ sdk_method="lemin",
95
+ description="Lemin puzzle challenge (returns dict with answer and challenge_uuid).",
96
+ registry_names=("lemin",),
97
+ required=("website_url", "captcha_id", "div_id"),
98
+ optional=("api_server", *_PROXY_ONLY_ARGS),
99
+ variants={},
100
+ ),
101
+ }
102
+
103
+
104
+ def _is_missing(value: Any) -> bool:
105
+ return value is None or (isinstance(value, str) and not value.strip())
106
+
107
+
108
+ def build_call(captcha_type: str, args: dict[str, Any]) -> tuple[str, dict[str, Any]]:
109
+ """Validate MCP arguments and build `(sdk_method_name, sdk_kwargs)`.
110
+
111
+ Raises ToolError before any network call if `captcha_type` is unknown,
112
+ required arguments are missing, proxy/proxytype are mismatched, or arguments
113
+ not supported by the chosen type are supplied.
114
+ """
115
+ spec = CATALOG.get(captcha_type)
116
+ if spec is None:
117
+ supported = ", ".join(CATALOG)
118
+ raise ToolError(f"Unknown captcha_type {captcha_type!r}. Supported: {supported}.")
119
+
120
+ allowed = set(spec.required) | set(spec.optional)
121
+
122
+ # Reject inapplicable arguments rather than silently dropping them, while
123
+ # skipping default False on boolean flags (SDK defaults to False as well).
124
+ provided: dict[str, Any] = {}
125
+ for key, value in args.items():
126
+ if value is None:
127
+ continue
128
+ if key in ("invisible", "enterprise") and value is False:
129
+ continue
130
+ provided[key] = value
131
+
132
+ inapplicable = sorted(key for key in provided if key not in allowed)
133
+ if inapplicable:
134
+ raise ToolError(
135
+ f"Arguments not supported for {captcha_type!r}: {', '.join(inapplicable)}. "
136
+ "Call get_supported_captchas() to see valid parameters."
137
+ )
138
+
139
+ missing = [key for key in spec.required if _is_missing(provided.get(key))]
140
+ if missing:
141
+ raise ToolError(
142
+ f"Missing required arguments for {captcha_type!r}: {', '.join(missing)}. "
143
+ "Call get_supported_captchas() for parameter details."
144
+ )
145
+
146
+ has_proxy = not _is_missing(provided.get("proxy"))
147
+ has_proxytype = not _is_missing(provided.get("proxytype"))
148
+ if has_proxy and not has_proxytype:
149
+ raise ToolError("proxytype is required when proxy is set (HTTP or HTTPS).")
150
+ if has_proxytype and not has_proxy:
151
+ raise ToolError("proxy is required when proxytype is set (format: 'user:pass@host:port').")
152
+ if has_proxytype:
153
+ normalized_proxytype = str(provided["proxytype"]).strip().upper()
154
+ if normalized_proxytype not in SUPPORTED_PROXY_TYPES:
155
+ supported = ", ".join(SUPPORTED_PROXY_TYPES)
156
+ raise ToolError(
157
+ f"Unsupported proxytype {provided['proxytype']!r}. Supported values: {supported}."
158
+ )
159
+ provided["proxytype"] = normalized_proxytype
160
+
161
+ sdk_kwargs = {_ARG_TO_SDK_KWARG.get(key, key): value for key, value in provided.items()}
162
+ return spec.sdk_method, sdk_kwargs
163
+
164
+
165
+ def describe_captchas() -> dict[str, dict[str, Any]]:
166
+ """Return per-type metadata combining MCP parameters with live SDK registry facts."""
167
+ sdk_info = supported_types()
168
+ out: dict[str, dict[str, Any]] = {}
169
+ for name, spec in CATALOG.items():
170
+ entries = [sdk_info[reg] for reg in spec.registry_names if reg in sdk_info]
171
+ out[name] = {
172
+ "description": spec.description,
173
+ "required_args": list(spec.required),
174
+ "optional_args": list(spec.optional),
175
+ "variants": dict(spec.variants),
176
+ "requires_proxy": any(item["requires_proxy"] for item in entries),
177
+ "returns_user_agent": any(item["returns_user_agent"] for item in entries),
178
+ "supported_proxy_types": list(SUPPORTED_PROXY_TYPES),
179
+ }
180
+ return out
@@ -0,0 +1,38 @@
1
+ """Map CaptchaAI SDK exceptions to structured FastMCP ToolError instances."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from captchaai import (
6
+ APIError,
7
+ CaptchaAIError,
8
+ InvalidKeyError,
9
+ NetworkError,
10
+ NoThreadsError,
11
+ ProxyError,
12
+ ThreadLimitError,
13
+ TimeoutError,
14
+ UnsolvableError,
15
+ ValidationError,
16
+ )
17
+ from fastmcp.exceptions import ToolError
18
+
19
+ _HINTS: dict[type[CaptchaAIError], str] = {
20
+ InvalidKeyError: "Check CAPTCHAAI_API_KEY or --api-key.",
21
+ ValidationError: "Fix the arguments; see get_supported_captchas().",
22
+ ThreadLimitError: "All threads busy; check get_threads() and retry later.",
23
+ NoThreadsError: "Account has no threads; check your plan at captchaai.com.",
24
+ UnsolvableError: "Workers could not solve it; verify website_key/website_url and retry.",
25
+ ProxyError: "Proxy failed; check proxy format 'user:pass@host:port' and proxytype.",
26
+ TimeoutError: "Solve took too long; retry.",
27
+ NetworkError: "Could not reach CaptchaAI; retry.",
28
+ APIError: "Unexpected API response.",
29
+ }
30
+
31
+
32
+ def to_tool_error(exc: CaptchaAIError) -> ToolError:
33
+ """Convert a CaptchaAI SDK exception into an agent-readable ToolError."""
34
+ hint = next((text for cls, text in _HINTS.items() if isinstance(exc, cls)), "")
35
+ message = f"{type(exc).__name__}: {exc}".rstrip(".")
36
+ if hint:
37
+ message = f"{message}. {hint}"
38
+ return ToolError(message)
@@ -0,0 +1,193 @@
1
+ """FastMCP server definition, lazy client holder, and token-mode MCP tools."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import os
7
+ from collections.abc import Awaitable, Callable
8
+ from typing import Any, Literal, TypeVar
9
+
10
+ from captchaai import AsyncCaptchaAI, CaptchaAIError
11
+ from fastmcp import Context, FastMCP
12
+ from fastmcp.exceptions import ToolError
13
+ from fastmcp.server.lifespan import lifespan
14
+
15
+ from captchaai_mcp.catalog import build_call, describe_captchas
16
+ from captchaai_mcp.errors import to_tool_error
17
+
18
+ _T = TypeVar("_T")
19
+
20
+ CaptchaTypeLiteral = Literal[
21
+ "recaptcha_v2",
22
+ "recaptcha_v3",
23
+ "turnstile",
24
+ "cloudflare_challenge",
25
+ "geetest",
26
+ "captchafox",
27
+ "friendly_captcha",
28
+ "lemin",
29
+ ]
30
+ ProxyTypeLiteral = Literal["HTTP", "HTTPS"]
31
+
32
+
33
+ class ClientHolder:
34
+ """Lazily builds and caches one AsyncCaptchaAI instance for the server lifetime."""
35
+
36
+ def __init__(self, api_key: str | None) -> None:
37
+ self._api_key = api_key
38
+ self._client: AsyncCaptchaAI | None = None
39
+ self._lock = asyncio.Lock()
40
+
41
+ async def get(self) -> AsyncCaptchaAI:
42
+ """Return the cached AsyncCaptchaAI client, creating it on first use."""
43
+ async with self._lock:
44
+ if self._client is None:
45
+ if not self._api_key:
46
+ raise ToolError("CAPTCHAAI_API_KEY is not set (or pass --api-key).")
47
+ try:
48
+ self._client = await AsyncCaptchaAI.create(api_key=self._api_key)
49
+ except CaptchaAIError as exc:
50
+ raise to_tool_error(exc) from exc
51
+ return self._client
52
+
53
+ async def aclose(self) -> None:
54
+ """Close the cached client if it was created."""
55
+ if self._client is not None:
56
+ client = self._client
57
+ self._client = None
58
+ await client.aclose()
59
+
60
+
61
+ async def _call(ctx: Context, fn: Callable[[AsyncCaptchaAI], Awaitable[_T]]) -> _T:
62
+ """Retrieve the shared SDK client, run `fn`, and map SDK errors to ToolError."""
63
+ holder: ClientHolder = ctx.lifespan_context["holder"]
64
+ client = await holder.get()
65
+ try:
66
+ return await fn(client)
67
+ except CaptchaAIError as exc:
68
+ raise to_tool_error(exc) from exc
69
+
70
+
71
+ def create_server(api_key: str | None = None, name: str = "captchaai") -> FastMCP:
72
+ """Create and configure the CaptchaAI FastMCP server."""
73
+ import captchaai_mcp
74
+
75
+ resolved_key = api_key if api_key is not None else os.environ.get("CAPTCHAAI_API_KEY")
76
+ holder = ClientHolder(resolved_key)
77
+
78
+ @lifespan
79
+ async def app_lifespan(server: FastMCP):
80
+ try:
81
+ yield {"holder": holder}
82
+ finally:
83
+ await holder.aclose()
84
+
85
+ mcp = FastMCP(
86
+ name,
87
+ version=captchaai_mcp.__version__,
88
+ lifespan=app_lifespan,
89
+ mask_error_details=True,
90
+ instructions=(
91
+ "CaptchaAI token-mode captcha solving. "
92
+ "Call get_supported_captchas() first to inspect required and optional parameters per type."
93
+ ),
94
+ )
95
+
96
+ @mcp.tool(annotations={"readOnlyHint": True, "openWorldHint": True})
97
+ async def get_threads(ctx: Context) -> dict[str, int]:
98
+ """Get CaptchaAI thread usage: total threads, active working_threads, and free_threads."""
99
+ info = await _call(ctx, lambda client: client.threads_info())
100
+ return {
101
+ "threads": info["threads"],
102
+ "working_threads": info["working_threads"],
103
+ "free_threads": info["threads"] - info["working_threads"],
104
+ }
105
+
106
+ @mcp.tool(annotations={"readOnlyHint": True, "openWorldHint": True})
107
+ async def get_balance(ctx: Context) -> dict[str, str]:
108
+ """Get the CaptchaAI account balance as returned by the API."""
109
+ balance = await _call(ctx, lambda client: client.get_balance())
110
+ return {"balance": balance}
111
+
112
+ @mcp.tool(annotations={"readOnlyHint": True, "idempotentHint": True, "openWorldHint": False})
113
+ def get_supported_captchas() -> dict[str, dict[str, Any]]:
114
+ """List all supported token-mode CAPTCHA types with required/optional arguments and capabilities.
115
+
116
+ Works without an API key.
117
+ """
118
+ return {"captchas": describe_captchas()}
119
+
120
+ @mcp.tool(annotations={"readOnlyHint": False, "openWorldHint": True})
121
+ async def solve_captcha(
122
+ ctx: Context,
123
+ captcha_type: CaptchaTypeLiteral,
124
+ website_url: str,
125
+ website_key: str | None = None,
126
+ action: str | None = None,
127
+ min_score: float | None = None,
128
+ invisible: bool = False,
129
+ enterprise: bool = False,
130
+ gt: str | None = None,
131
+ challenge: str | None = None,
132
+ captcha_id: str | None = None,
133
+ div_id: str | None = None,
134
+ api_server: str | None = None,
135
+ version: str | None = None,
136
+ cookies: str | None = None,
137
+ user_agent: str | None = None,
138
+ proxy: str | None = None,
139
+ proxytype: ProxyTypeLiteral | None = None,
140
+ ) -> dict[str, Any]:
141
+ """Solve a CAPTCHA in token mode (no browser) and return the solved token or payload.
142
+
143
+ Blocks until solved (typically 10-80s). Call get_supported_captchas() first to check
144
+ which arguments are required for the chosen captcha_type. When `user_agent` is returned
145
+ (Enterprise, Cloudflare Challenge, CaptchaFox), reuse it when submitting the token.
146
+
147
+ Args:
148
+ captcha_type: CAPTCHA challenge type (e.g. "recaptcha_v2", "recaptcha_v3", "turnstile").
149
+ website_url: Full URL of the page containing the CAPTCHA.
150
+ website_key: Site key (data-sitekey) for reCAPTCHA, Turnstile, CaptchaFox, or Friendly Captcha.
151
+ action: Action name (required for recaptcha_v3; optional `sa=` for Enterprise v2).
152
+ min_score: Minimum score threshold (0.0-1.0) for recaptcha_v3.
153
+ invisible: Enable invisible mode (recaptcha_v2 only).
154
+ enterprise: Enable Enterprise mode (recaptcha_v2 or recaptcha_v3).
155
+ gt: GeeTest public `gt` parameter (required for geetest).
156
+ challenge: GeeTest `challenge` parameter (required for geetest).
157
+ captcha_id: Lemin captcha identifier (required for lemin).
158
+ div_id: Lemin target container element ID (required for lemin).
159
+ api_server: Optional Lemin API server URL.
160
+ version: Optional Friendly Captcha version string.
161
+ cookies: Optional cookies forwarded to the solver ("KEY1:Val1;KEY2:Val2;").
162
+ user_agent: Optional User-Agent string forwarded to the solver.
163
+ proxy: Proxy string in "user:pass@host:port" format (required for cloudflare_challenge and captchafox).
164
+ proxytype: Proxy protocol when proxy is set ("HTTP" or "HTTPS").
165
+ """
166
+ args: dict[str, Any] = {
167
+ "website_url": website_url,
168
+ "website_key": website_key,
169
+ "action": action,
170
+ "min_score": min_score,
171
+ "invisible": invisible,
172
+ "enterprise": enterprise,
173
+ "gt": gt,
174
+ "challenge": challenge,
175
+ "captcha_id": captcha_id,
176
+ "div_id": div_id,
177
+ "api_server": api_server,
178
+ "version": version,
179
+ "cookies": cookies,
180
+ "user_agent": user_agent,
181
+ "proxy": proxy,
182
+ "proxytype": proxytype,
183
+ }
184
+ method_name, sdk_kwargs = build_call(captcha_type, args)
185
+ result = await _call(ctx, lambda client: getattr(client, method_name)(**sdk_kwargs))
186
+ return {
187
+ "captcha_type": captcha_type,
188
+ "task_id": result.task_id,
189
+ "solution": result.solution,
190
+ "user_agent": result.user_agent,
191
+ }
192
+
193
+ return mcp