kwork-mcp 0.1.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,11 @@
1
+ # Required: one of (LOGIN+PASSWORD) or TOKEN
2
+ KWORK_LOGIN=your_login
3
+ KWORK_PASSWORD=your_password
4
+ # KWORK_PHONE_LAST=1234 # Last 4 digits for 2FA (if enabled)
5
+ # KWORK_TOKEN=existing_token # Skip login, use this token directly
6
+
7
+ # Optional
8
+ # KWORK_PROXY_URL=socks5://host:port
9
+ # KWORK_RPS_LIMIT=2
10
+ # KWORK_BURST_LIMIT=5
11
+ # KWORK_TOKEN_FILE=~/.kwork_token
@@ -0,0 +1,13 @@
1
+ .env
2
+ .venv/
3
+ __pycache__/
4
+ *.pyc
5
+ .kwork_token
6
+ *.egg-info/
7
+ dist/
8
+ .ruff_cache/
9
+ .pytest_cache/
10
+ .mypy_cache/
11
+ uv.lock
12
+ CLAUDE.md
13
+ .superpowers/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 simonether
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,213 @@
1
+ Metadata-Version: 2.4
2
+ Name: kwork-mcp
3
+ Version: 0.1.0
4
+ Summary: MCP server for managing Kwork freelance marketplace
5
+ Project-URL: Homepage, https://github.com/simonether/kwork-mcp
6
+ Project-URL: Repository, https://github.com/simonether/kwork-mcp
7
+ Project-URL: Issues, https://github.com/simonether/kwork-mcp/issues
8
+ Author-email: Simon Sudarushkin <simonsudarushkin@gmail.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: claude,freelance,kwork,mcp,mcp-server
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Software Development :: Libraries
18
+ Classifier: Typing :: Typed
19
+ Requires-Python: >=3.12
20
+ Requires-Dist: kwork>=0.2.0
21
+ Requires-Dist: loguru>=0.7
22
+ Requires-Dist: mcp>=1.20
23
+ Requires-Dist: pydantic-settings>=2.7
24
+ Description-Content-Type: text/markdown
25
+
26
+ <p align="center">
27
+ <img src="assets/banner.svg" alt="kwork-mcp" width="100%">
28
+ </p>
29
+
30
+ [![Python 3.12+](https://img.shields.io/badge/python-3.12+-3776AB?logo=python&logoColor=white)](https://www.python.org/downloads/)
31
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
32
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
33
+ [![MCP](https://img.shields.io/badge/MCP-compatible-8A2BE2)](https://modelcontextprotocol.io)
34
+
35
+ MCP server that exposes 25 tools for the [Kwork](https://kwork.ru) freelance marketplace — browse projects, submit offers, manage orders, send messages, and more.
36
+
37
+ Built with [FastMCP](https://github.com/jlowin/fastmcp) (via [MCP Python SDK](https://github.com/modelcontextprotocol/python-sdk)) and [pykwork](https://github.com/kesha1225/pykwork).
38
+
39
+ ## 🚀 Setup
40
+
41
+ ### Requirements
42
+
43
+ - Python 3.12+
44
+ - [uv](https://docs.astral.sh/uv/)
45
+ - Kwork account (login/password or API token)
46
+
47
+ ### Install
48
+
49
+ ```bash
50
+ git clone https://github.com/simonether/kwork-mcp.git
51
+ cd kwork-mcp
52
+ uv sync
53
+ ```
54
+
55
+ ### Configure
56
+
57
+ Copy `.env.example` to `.env` and fill in your credentials:
58
+
59
+ ```bash
60
+ cp .env.example .env
61
+ ```
62
+
63
+ | Variable | Required | Default | Description |
64
+ |---|---|---|---|
65
+ | `KWORK_LOGIN` | yes* | — | Kwork login |
66
+ | `KWORK_PASSWORD` | yes* | — | Kwork password |
67
+ | `KWORK_TOKEN` | yes* | — | Auth token (skips login) |
68
+ | `KWORK_PHONE_LAST` | no | — | Last 4 digits of phone (2FA) |
69
+ | `KWORK_PROXY_URL` | no | — | SOCKS5 proxy |
70
+ | `KWORK_RPS_LIMIT` | no | `2` | Requests per second |
71
+ | `KWORK_BURST_LIMIT` | no | `5` | Burst limit |
72
+ | `KWORK_TOKEN_FILE` | no | `~/.kwork_token` | Token persistence path |
73
+
74
+ \*Either `KWORK_TOKEN` or both `KWORK_LOGIN` + `KWORK_PASSWORD`.
75
+
76
+ Auth priority: `KWORK_TOKEN` env → saved token file → fresh login.
77
+
78
+ ## 💡 Usage
79
+
80
+ ### Claude Desktop
81
+
82
+ Add to `claude_desktop_config.json`:
83
+
84
+ ```json
85
+ {
86
+ "mcpServers": {
87
+ "kwork": {
88
+ "command": "uv",
89
+ "args": ["run", "--directory", "/path/to/kwork-mcp", "kwork-mcp"],
90
+ "env": {
91
+ "KWORK_LOGIN": "your_login",
92
+ "KWORK_PASSWORD": "your_password"
93
+ }
94
+ }
95
+ }
96
+ }
97
+ ```
98
+
99
+ ### Claude Code
100
+
101
+ ```bash
102
+ claude mcp add kwork -- uv run --directory /path/to/kwork-mcp kwork-mcp
103
+ ```
104
+
105
+ ### stdio
106
+
107
+ ```bash
108
+ uv run kwork-mcp
109
+ ```
110
+
111
+ ## 🛠 Tools
112
+
113
+ <details>
114
+ <summary><strong>Profile</strong> (3 tools)</summary>
115
+
116
+ | Tool | Description |
117
+ |---|---|
118
+ | `get_me` | Current user profile, rating, balance |
119
+ | `get_connects` | Connect count for exchange offers |
120
+ | `get_user_info` | Public user info by ID |
121
+
122
+ </details>
123
+
124
+ <details>
125
+ <summary><strong>Projects</strong> (4 tools)</summary>
126
+
127
+ | Tool | Description |
128
+ |---|---|
129
+ | `list_projects` | Browse exchange projects with filters |
130
+ | `get_project` | Project details by ID |
131
+ | `search_projects` | Search by text query |
132
+ | `get_exchange_info` | Exchange marketplace stats |
133
+
134
+ </details>
135
+
136
+ <details>
137
+ <summary><strong>Offers</strong> (4 tools)</summary>
138
+
139
+ | Tool | Description |
140
+ |---|---|
141
+ | `list_my_offers` | Your exchange offers |
142
+ | `get_offer` | Offer details by ID |
143
+ | `submit_offer` | Submit offer to a project (costs 1 connect) |
144
+ | `delete_offer` | Delete an offer |
145
+
146
+ </details>
147
+
148
+ <details>
149
+ <summary><strong>Orders</strong> (3 tools)</summary>
150
+
151
+ | Tool | Description |
152
+ |---|---|
153
+ | `list_worker_orders` | Seller orders (all statuses) |
154
+ | `get_order_details` | Order details by ID |
155
+ | `send_order_for_approval` | Submit work for buyer review |
156
+
157
+ </details>
158
+
159
+ <details>
160
+ <summary><strong>Dialogs</strong> (4 tools)</summary>
161
+
162
+ | Tool | Description |
163
+ |---|---|
164
+ | `list_dialogs` | Conversations with latest messages |
165
+ | `get_dialog` | Messages by username |
166
+ | `send_message` | Send direct message |
167
+ | `mark_dialog_read` | Mark as read |
168
+
169
+ </details>
170
+
171
+ <details>
172
+ <summary><strong>Kworks</strong> (4 tools)</summary>
173
+
174
+ | Tool | Description |
175
+ |---|---|
176
+ | `list_my_kworks` | Your services grouped by status |
177
+ | `get_kwork_details` | Kwork details by ID |
178
+ | `start_kwork` | Activate a paused kwork |
179
+ | `pause_kwork` | Pause an active kwork |
180
+
181
+ </details>
182
+
183
+ <details>
184
+ <summary><strong>Categories</strong> (2 tools)</summary>
185
+
186
+ | Tool | Description |
187
+ |---|---|
188
+ | `list_categories` | Full category tree |
189
+ | `get_favorite_categories` | User's favorite categories |
190
+
191
+ </details>
192
+
193
+ <details>
194
+ <summary><strong>Notifications</strong> (1 tool)</summary>
195
+
196
+ | Tool | Description |
197
+ |---|---|
198
+ | `list_notifications` | User notifications |
199
+
200
+ </details>
201
+
202
+ ## 🧑‍💻 Development
203
+
204
+ ```bash
205
+ uv sync --dev
206
+ uv run python -m pytest tests/ -x -v
207
+ uv run ruff check .
208
+ uv run ruff format --check .
209
+ ```
210
+
211
+ ## License
212
+
213
+ [MIT](LICENSE)
@@ -0,0 +1,188 @@
1
+ <p align="center">
2
+ <img src="assets/banner.svg" alt="kwork-mcp" width="100%">
3
+ </p>
4
+
5
+ [![Python 3.12+](https://img.shields.io/badge/python-3.12+-3776AB?logo=python&logoColor=white)](https://www.python.org/downloads/)
6
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
7
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
8
+ [![MCP](https://img.shields.io/badge/MCP-compatible-8A2BE2)](https://modelcontextprotocol.io)
9
+
10
+ MCP server that exposes 25 tools for the [Kwork](https://kwork.ru) freelance marketplace — browse projects, submit offers, manage orders, send messages, and more.
11
+
12
+ Built with [FastMCP](https://github.com/jlowin/fastmcp) (via [MCP Python SDK](https://github.com/modelcontextprotocol/python-sdk)) and [pykwork](https://github.com/kesha1225/pykwork).
13
+
14
+ ## 🚀 Setup
15
+
16
+ ### Requirements
17
+
18
+ - Python 3.12+
19
+ - [uv](https://docs.astral.sh/uv/)
20
+ - Kwork account (login/password or API token)
21
+
22
+ ### Install
23
+
24
+ ```bash
25
+ git clone https://github.com/simonether/kwork-mcp.git
26
+ cd kwork-mcp
27
+ uv sync
28
+ ```
29
+
30
+ ### Configure
31
+
32
+ Copy `.env.example` to `.env` and fill in your credentials:
33
+
34
+ ```bash
35
+ cp .env.example .env
36
+ ```
37
+
38
+ | Variable | Required | Default | Description |
39
+ |---|---|---|---|
40
+ | `KWORK_LOGIN` | yes* | — | Kwork login |
41
+ | `KWORK_PASSWORD` | yes* | — | Kwork password |
42
+ | `KWORK_TOKEN` | yes* | — | Auth token (skips login) |
43
+ | `KWORK_PHONE_LAST` | no | — | Last 4 digits of phone (2FA) |
44
+ | `KWORK_PROXY_URL` | no | — | SOCKS5 proxy |
45
+ | `KWORK_RPS_LIMIT` | no | `2` | Requests per second |
46
+ | `KWORK_BURST_LIMIT` | no | `5` | Burst limit |
47
+ | `KWORK_TOKEN_FILE` | no | `~/.kwork_token` | Token persistence path |
48
+
49
+ \*Either `KWORK_TOKEN` or both `KWORK_LOGIN` + `KWORK_PASSWORD`.
50
+
51
+ Auth priority: `KWORK_TOKEN` env → saved token file → fresh login.
52
+
53
+ ## 💡 Usage
54
+
55
+ ### Claude Desktop
56
+
57
+ Add to `claude_desktop_config.json`:
58
+
59
+ ```json
60
+ {
61
+ "mcpServers": {
62
+ "kwork": {
63
+ "command": "uv",
64
+ "args": ["run", "--directory", "/path/to/kwork-mcp", "kwork-mcp"],
65
+ "env": {
66
+ "KWORK_LOGIN": "your_login",
67
+ "KWORK_PASSWORD": "your_password"
68
+ }
69
+ }
70
+ }
71
+ }
72
+ ```
73
+
74
+ ### Claude Code
75
+
76
+ ```bash
77
+ claude mcp add kwork -- uv run --directory /path/to/kwork-mcp kwork-mcp
78
+ ```
79
+
80
+ ### stdio
81
+
82
+ ```bash
83
+ uv run kwork-mcp
84
+ ```
85
+
86
+ ## 🛠 Tools
87
+
88
+ <details>
89
+ <summary><strong>Profile</strong> (3 tools)</summary>
90
+
91
+ | Tool | Description |
92
+ |---|---|
93
+ | `get_me` | Current user profile, rating, balance |
94
+ | `get_connects` | Connect count for exchange offers |
95
+ | `get_user_info` | Public user info by ID |
96
+
97
+ </details>
98
+
99
+ <details>
100
+ <summary><strong>Projects</strong> (4 tools)</summary>
101
+
102
+ | Tool | Description |
103
+ |---|---|
104
+ | `list_projects` | Browse exchange projects with filters |
105
+ | `get_project` | Project details by ID |
106
+ | `search_projects` | Search by text query |
107
+ | `get_exchange_info` | Exchange marketplace stats |
108
+
109
+ </details>
110
+
111
+ <details>
112
+ <summary><strong>Offers</strong> (4 tools)</summary>
113
+
114
+ | Tool | Description |
115
+ |---|---|
116
+ | `list_my_offers` | Your exchange offers |
117
+ | `get_offer` | Offer details by ID |
118
+ | `submit_offer` | Submit offer to a project (costs 1 connect) |
119
+ | `delete_offer` | Delete an offer |
120
+
121
+ </details>
122
+
123
+ <details>
124
+ <summary><strong>Orders</strong> (3 tools)</summary>
125
+
126
+ | Tool | Description |
127
+ |---|---|
128
+ | `list_worker_orders` | Seller orders (all statuses) |
129
+ | `get_order_details` | Order details by ID |
130
+ | `send_order_for_approval` | Submit work for buyer review |
131
+
132
+ </details>
133
+
134
+ <details>
135
+ <summary><strong>Dialogs</strong> (4 tools)</summary>
136
+
137
+ | Tool | Description |
138
+ |---|---|
139
+ | `list_dialogs` | Conversations with latest messages |
140
+ | `get_dialog` | Messages by username |
141
+ | `send_message` | Send direct message |
142
+ | `mark_dialog_read` | Mark as read |
143
+
144
+ </details>
145
+
146
+ <details>
147
+ <summary><strong>Kworks</strong> (4 tools)</summary>
148
+
149
+ | Tool | Description |
150
+ |---|---|
151
+ | `list_my_kworks` | Your services grouped by status |
152
+ | `get_kwork_details` | Kwork details by ID |
153
+ | `start_kwork` | Activate a paused kwork |
154
+ | `pause_kwork` | Pause an active kwork |
155
+
156
+ </details>
157
+
158
+ <details>
159
+ <summary><strong>Categories</strong> (2 tools)</summary>
160
+
161
+ | Tool | Description |
162
+ |---|---|
163
+ | `list_categories` | Full category tree |
164
+ | `get_favorite_categories` | User's favorite categories |
165
+
166
+ </details>
167
+
168
+ <details>
169
+ <summary><strong>Notifications</strong> (1 tool)</summary>
170
+
171
+ | Tool | Description |
172
+ |---|---|
173
+ | `list_notifications` | User notifications |
174
+
175
+ </details>
176
+
177
+ ## 🧑‍💻 Development
178
+
179
+ ```bash
180
+ uv sync --dev
181
+ uv run python -m pytest tests/ -x -v
182
+ uv run ruff check .
183
+ uv run ruff format --check .
184
+ ```
185
+
186
+ ## License
187
+
188
+ [MIT](LICENSE)
@@ -0,0 +1,56 @@
1
+ <svg viewBox="0 0 800 240" xmlns="http://www.w3.org/2000/svg">
2
+ <defs>
3
+ <linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
4
+ <stop offset="0%" style="stop-color:#0c0c0c"/>
5
+ <stop offset="50%" style="stop-color:#100d16"/>
6
+ <stop offset="100%" style="stop-color:#161022"/>
7
+ </linearGradient>
8
+ <linearGradient id="ac" x1="0%" y1="0%" x2="100%" y2="0%">
9
+ <stop offset="0%" style="stop-color:#e040fb"/>
10
+ <stop offset="100%" style="stop-color:#536dfe"/>
11
+ </linearGradient>
12
+ <radialGradient id="gl" cx="65%" cy="50%">
13
+ <stop offset="0%" style="stop-color:rgba(224,64,251,0.07)"/>
14
+ <stop offset="100%" style="stop-color:transparent"/>
15
+ </radialGradient>
16
+ <filter id="txtGlow" x="-20%" y="-20%" width="140%" height="140%">
17
+ <feGaussianBlur stdDeviation="8" result="blur"/>
18
+ <feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
19
+ </filter>
20
+ </defs>
21
+
22
+ <rect width="800" height="240" fill="url(#bg)"/>
23
+ <rect width="800" height="240" fill="url(#gl)"/>
24
+
25
+ <!-- Sparse constellation -->
26
+ <line x1="520" y1="45" x2="620" y2="75" stroke="rgba(224,64,251,0.12)" stroke-width="0.5"/>
27
+ <line x1="620" y1="75" x2="580" y2="150" stroke="rgba(83,109,254,0.1)" stroke-width="0.5"/>
28
+ <line x1="580" y1="150" x2="520" y2="45" stroke="rgba(124,77,255,0.08)" stroke-width="0.5"/>
29
+ <line x1="620" y1="75" x2="720" y2="55" stroke="rgba(224,64,251,0.1)" stroke-width="0.5"/>
30
+ <line x1="720" y1="55" x2="740" y2="140" stroke="rgba(83,109,254,0.09)" stroke-width="0.5"/>
31
+ <line x1="740" y1="140" x2="620" y2="75" stroke="rgba(224,64,251,0.07)" stroke-width="0.5"/>
32
+ <line x1="580" y1="150" x2="680" y2="190" stroke="rgba(83,109,254,0.07)" stroke-width="0.5"/>
33
+ <line x1="680" y1="190" x2="740" y2="140" stroke="rgba(224,64,251,0.06)" stroke-width="0.5"/>
34
+
35
+ <!-- Vertices -->
36
+ <circle cx="520" cy="45" r="2.5" fill="rgba(224,64,251,0.5)"/>
37
+ <circle cx="620" cy="75" r="3.5" fill="rgba(224,64,251,0.6)"/>
38
+ <circle cx="580" cy="150" r="2.5" fill="rgba(83,109,254,0.45)"/>
39
+ <circle cx="720" cy="55" r="2" fill="rgba(124,77,255,0.4)"/>
40
+ <circle cx="740" cy="140" r="3" fill="rgba(83,109,254,0.5)"/>
41
+ <circle cx="680" cy="190" r="2" fill="rgba(224,64,251,0.35)"/>
42
+ <circle cx="480" cy="200" r="1.5" fill="rgba(124,77,255,0.2)"/>
43
+ <circle cx="770" cy="25" r="1" fill="rgba(224,64,251,0.15)"/>
44
+
45
+ <!-- Title glow layer -->
46
+ <text x="55" y="100" font-family="'IBM Plex Mono', 'Source Code Pro', monospace" font-size="44" font-weight="600" fill="rgba(224,64,251,0.18)" filter="url(#txtGlow)">kwork-mcp</text>
47
+ <!-- Title sharp layer -->
48
+ <text x="55" y="100" font-family="'IBM Plex Mono', 'Source Code Pro', monospace" font-size="44" font-weight="600" fill="url(#ac)">kwork-mcp</text>
49
+
50
+ <!-- Tagline -->
51
+ <text x="57" y="135" font-family="'Outfit', 'Sora', sans-serif" font-size="13.5" fill="rgba(255,255,255,0.38)" letter-spacing="1">MCP server · 25 tools · Kwork freelance marketplace</text>
52
+
53
+ <!-- Accent dot + line -->
54
+ <circle cx="59" cy="155" r="2.5" fill="rgba(224,64,251,0.4)"/>
55
+ <rect x="68" y="153.5" width="80" height="1" fill="url(#ac)" rx="0.5" opacity="0.3"/>
56
+ </svg>
@@ -0,0 +1,61 @@
1
+ [project]
2
+ name = "kwork-mcp"
3
+ version = "0.1.0"
4
+ description = "MCP server for managing Kwork freelance marketplace"
5
+ license = "MIT"
6
+ readme = "README.md"
7
+ requires-python = ">=3.12"
8
+ authors = [
9
+ { name = "Simon Sudarushkin", email = "simonsudarushkin@gmail.com" },
10
+ ]
11
+ keywords = ["mcp", "kwork", "freelance", "claude", "mcp-server"]
12
+ classifiers = [
13
+ "Development Status :: 4 - Beta",
14
+ "License :: OSI Approved :: MIT License",
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3.12",
17
+ "Programming Language :: Python :: 3.13",
18
+ "Topic :: Software Development :: Libraries",
19
+ "Typing :: Typed",
20
+ ]
21
+ dependencies = [
22
+ "mcp>=1.20",
23
+ "kwork>=0.2.0",
24
+ "pydantic-settings>=2.7",
25
+ "loguru>=0.7",
26
+ ]
27
+
28
+ [dependency-groups]
29
+ dev = [
30
+ "pytest>=8",
31
+ "pytest-asyncio>=0.25",
32
+ "ruff>=0.9",
33
+ ]
34
+
35
+ [project.scripts]
36
+ kwork-mcp = "kwork_mcp:main"
37
+
38
+ [project.urls]
39
+ Homepage = "https://github.com/simonether/kwork-mcp"
40
+ Repository = "https://github.com/simonether/kwork-mcp"
41
+ Issues = "https://github.com/simonether/kwork-mcp/issues"
42
+
43
+ [build-system]
44
+ requires = ["hatchling"]
45
+ build-backend = "hatchling.build"
46
+
47
+ [tool.hatch.build.targets.wheel]
48
+ packages = ["src/kwork_mcp"]
49
+
50
+ [tool.ruff]
51
+ target-version = "py312"
52
+ line-length = 120
53
+ src = ["src", "tests"]
54
+
55
+ [tool.ruff.lint]
56
+ select = ["E", "W", "F", "I", "N", "UP", "B", "SIM", "T20", "RUF", "ASYNC"]
57
+ ignore = ["RUF001", "RUF002", "E501"]
58
+
59
+ [tool.pytest.ini_options]
60
+ asyncio_mode = "auto"
61
+ testpaths = ["tests"]
@@ -0,0 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+
5
+ from loguru import logger
6
+
7
+
8
+ def main() -> None:
9
+ logger.remove()
10
+ logger.add(sys.stderr, level="INFO", format="{time:HH:mm:ss} | {level} | {message}")
11
+
12
+ from kwork_mcp.server import create_server
13
+
14
+ server = create_server()
15
+ server.run(transport="stdio")
@@ -0,0 +1,3 @@
1
+ from kwork_mcp import main
2
+
3
+ main()
@@ -0,0 +1,27 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from pydantic import SecretStr
6
+ from pydantic_settings import BaseSettings, SettingsConfigDict
7
+
8
+
9
+ class KworkConfig(BaseSettings):
10
+ model_config = SettingsConfigDict(
11
+ env_prefix="KWORK_",
12
+ env_file=".env",
13
+ extra="ignore",
14
+ )
15
+
16
+ login: str = ""
17
+ password: SecretStr = SecretStr("")
18
+ phone_last: str | None = None
19
+ token: SecretStr | None = None
20
+
21
+ proxy_url: str | None = None
22
+ timeout: int = 30
23
+
24
+ rps_limit: int = 2
25
+ burst_limit: int = 5
26
+
27
+ token_file: Path = Path.home() / ".kwork_token"
@@ -0,0 +1,49 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import AsyncIterator
4
+ from contextlib import asynccontextmanager
5
+
6
+ from kwork.exceptions import KworkException, KworkHTTPException
7
+ from loguru import logger
8
+ from mcp.server.fastmcp.exceptions import ToolError
9
+
10
+
11
+ def _get_error_code(exc: KworkException) -> int | None:
12
+ """Extract Kwork API error_code from KworkHTTPException.response_json."""
13
+ if isinstance(exc, KworkHTTPException) and exc.response_json:
14
+ return exc.response_json.get("error_code")
15
+ return None
16
+
17
+
18
+ @asynccontextmanager
19
+ async def api_guard(operation: str = "Kwork API") -> AsyncIterator[None]:
20
+ """Context manager that translates pykwork exceptions into ToolError."""
21
+ try:
22
+ yield
23
+ except ToolError:
24
+ raise
25
+ except KworkHTTPException as exc:
26
+ code = _get_error_code(exc)
27
+ if code == 118 or "captcha" in str(exc).lower():
28
+ raise ToolError(
29
+ "Kwork требует капчу. Откройте kwork.ru в браузере, "
30
+ "решите капчу, затем передайте свежий токен через KWORK_TOKEN."
31
+ ) from exc
32
+ if exc.status == 401:
33
+ raise ToolError(
34
+ "Сессия истекла, повторная авторизация не удалась. Проверьте KWORK_LOGIN и KWORK_PASSWORD."
35
+ ) from exc
36
+ if exc.status == 403 or code == 403:
37
+ raise ToolError(
38
+ "IP заблокирован Kwork. Попробуйте позже или используйте прокси (KWORK_PROXY_URL)."
39
+ ) from exc
40
+ logger.error("{op} HTTP error: status={status} {exc}", op=operation, status=exc.status, exc=exc)
41
+ raise ToolError(f"Ошибка {operation}: {exc}") from exc
42
+ except KworkException as exc:
43
+ logger.error("{op} error: {exc}", op=operation, exc=exc)
44
+ raise ToolError(f"Ошибка {operation}: {exc}") from exc
45
+ except TimeoutError as exc:
46
+ raise ToolError("Kwork API недоступен (таймаут). Попробуйте позже.") from exc
47
+ except Exception as exc:
48
+ logger.exception("Unexpected error in {op}", op=operation)
49
+ raise ToolError(f"Непредвиденная ошибка {operation}: {exc}") from exc