busybee-mcp 0.2.1__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 BusyBee Data
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,202 @@
1
+ Metadata-Version: 2.4
2
+ Name: busybee-mcp
3
+ Version: 0.2.1
4
+ Summary: BusyBee Data MCP server — public social media data for AI agents (TikTok, Instagram, YouTube, X, Douyin, Xiaohongshu).
5
+ Author: BusyBee Data
6
+ License: MIT
7
+ Project-URL: Homepage, https://lucydomi.com
8
+ Keywords: mcp,social-media,tiktok,instagram,youtube,creator-discovery
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: mcp>=2.0.0
13
+ Provides-Extra: http
14
+ Requires-Dist: uvicorn>=0.30; extra == "http"
15
+ Dynamic: license-file
16
+
17
+ # BusyBee MCP
18
+
19
+ **Public social media data for AI agents.** Ten business-level tools, one schema — TikTok, Instagram, YouTube, X (Twitter), Douyin and Xiaohongshu.
20
+
21
+ Ask your agent *"who are the fastest-growing coffee creators on TikTok right now, and what do their comment sections complain about?"* — it answers with real data, in one conversation.
22
+
23
+ ```
24
+ Endpoint https://lucydomi.com/mcp
25
+ Protocols MCP Streamable HTTP · MCP SSE (legacy) · REST/JSON · stdio
26
+ Tools 10
27
+ Billing pay per call; every response returns cost_usd + balance_usd
28
+ ```
29
+
30
+ ---
31
+
32
+ ## Works with every kind of agent
33
+
34
+ Most "social data for AI" products ship one transport and hope your client speaks it. This server speaks four, on the same host, with the same 10 tools behind all of them.
35
+
36
+ | Your agent | What it supports | How to connect |
37
+ |---|---|---|
38
+ | **Claude Code** | Streamable HTTP | `claude mcp add busybee --transport http https://lucydomi.com/mcp --header "Authorization: Bearer sk-..."` |
39
+ | **Cursor / Cline / Windsurf / VS Code** | Streamable HTTP | add to `mcp.json` (see below) |
40
+ | **Claude Desktop** | stdio | `uvx --from git+https://github.com/danfeistar/busybee-mcp busybee-mcp` with `BUSYBEE_API_KEY` (not on PyPI yet) |
41
+ | **Older MCP clients (2024-11 era)** | SSE only | `https://lucydomi.com/sse` + `https://lucydomi.com/messages/` |
42
+ | **Dify / n8n / Coze / FastGPT** | OpenAPI / plain HTTP | import `https://lucydomi.com/mcp/openapi.json` (3.1), or `?spec=3.0` for older importers |
43
+ | **LangChain / LlamaIndex / custom code** | tools JSON or HTTP | `https://lucydomi.com/mcp/tools?format=openai`, or call REST directly |
44
+ | **GPT Actions / Zapier / Make** | OpenAPI 3.x | same spec URL, bearer auth |
45
+ | **Anything that can only GET a URL** | query string | `https://lucydomi.com/mcp/v1/search_creators?platform=tiktok&keyword=coffee&api_key=sk-...` |
46
+
47
+ Discovery document (machine-readable, tells an agent what's available): `https://lucydomi.com/mcp/info`
48
+
49
+ ---
50
+
51
+ ## The 10 tools
52
+
53
+ | Tool | What it answers |
54
+ |---|---|
55
+ | `list_platforms` | Which platforms exist, which tools each supports, which key group you need |
56
+ | `search_creators` | "Find me mid-size coffee creators on TikTok" |
57
+ | `search_posts` | "What is being posted about cold brew this week?" |
58
+ | `get_creator` | "How big is this account, what's their bio?" |
59
+ | `list_creator_posts` | "What has this account posted recently?" |
60
+ | `get_post` | "Give me the details of this one video" |
61
+ | `get_comments` | "What are people saying under this post?" |
62
+ | `get_trending` | "What's trending on Douyin right now?" |
63
+ | `get_comment_insights` | "Summarize the comment section: sentiment, questions, complaints" |
64
+ | `check_balance` | "How much credit do I have left?" |
65
+
66
+ `get_comment_insights` is the one people keep open: it samples comments, then returns sentiment split, question ratio, high-frequency phrases and top-liked comments — so the agent answers *"what do users actually want"* instead of dumping fifty raw rows.
67
+
68
+ ---
69
+
70
+ ## One schema, every platform
71
+
72
+ ```json
73
+ {
74
+ "platform": "tiktok",
75
+ "id": "7690199760430566669",
76
+ "url": "https://www.tiktok.com/@creator/video/7690199760430566669",
77
+ "author": { "id": "6948174171005158405", "handle": "goldenbrown.coffee", "name": "Golden Brown Coffee" },
78
+ "text": "Spanish latte, but make it iced",
79
+ "created_at": "2026-10-18T09:12:00Z",
80
+ "stats": { "likes": 41200, "comments": 812, "shares": 2331, "views": 918000, "collects": 5120 },
81
+ "cover": "https://..."
82
+ }
83
+ ```
84
+
85
+ Every response also carries `cost_usd` and `balance_usd`, so the agent can budget itself and you can see what a task actually cost.
86
+
87
+ ---
88
+
89
+ ## Setup per client
90
+
91
+ Need a key first: register at [lucydomi.com](https://lucydomi.com) → *Console → API Keys → New key* (`sk-…`). Overseas and China platforms use different key groups.
92
+
93
+ ### Claude Code
94
+ ```bash
95
+ claude mcp add busybee --transport http https://lucydomi.com/mcp \
96
+ --header "Authorization: Bearer sk-YOUR_KEY"
97
+ ```
98
+
99
+ ### Cursor / Cline / Windsurf / VS Code (`~/.cursor/mcp.json`)
100
+ ```json
101
+ {
102
+ "mcpServers": {
103
+ "busybee": {
104
+ "url": "https://lucydomi.com/mcp",
105
+ "headers": { "Authorization": "Bearer sk-YOUR_KEY" }
106
+ }
107
+ }
108
+ }
109
+ ```
110
+
111
+ ### Claude Desktop (local stdio)
112
+ ```json
113
+ {
114
+ "mcpServers": {
115
+ "busybee": {
116
+ "command": "uvx",
117
+ "args": ["--from", "git+https://github.com/danfeistar/busybee-mcp", "busybee-mcp"],
118
+ "args": ["busybee-mcp"],
119
+ "env": { "BUSYBEE_API_KEY": "sk-YOUR_KEY" }
120
+ }
121
+ }
122
+ }
123
+ ```
124
+
125
+ ### Legacy SSE clients
126
+ ```
127
+ SSE URL https://lucydomi.com/sse
128
+ Messages URL https://lucydomi.com/messages/?session_id=<from the SSE event>
129
+ Header Authorization: Bearer sk-YOUR_KEY
130
+ ```
131
+
132
+ ### Dify / n8n / Coze / GPT Actions
133
+ Import `https://lucydomi.com/mcp/openapi.json`, set auth to Bearer `sk-YOUR_KEY`. If the importer rejects 3.1, use `https://lucydomi.com/mcp/openapi.json?spec=3.0`.
134
+
135
+ ### LangChain / custom Python (no MCP needed)
136
+ ```python
137
+ import json, urllib.request
138
+ req = urllib.request.Request(
139
+ "https://lucydomi.com/mcp/v1/search_creators",
140
+ data=json.dumps({"platform": "tiktok", "keyword": "coffee", "limit": 3}).encode(),
141
+ headers={"Content-Type": "application/json", "Authorization": "Bearer sk-YOUR_KEY"},
142
+ )
143
+ print(json.load(urllib.request.urlopen(req)))
144
+ ```
145
+ Tool schemas for function calling: `GET https://lucydomi.com/mcp/tools?format=openai`
146
+
147
+ ### Local package
148
+ ```bash
149
+ pip install busybee-mcp
150
+ BUSYBEE_API_KEY=sk-YOUR_KEY busybee-mcp # stdio
151
+ BUSYBEE_API_KEY=sk-YOUR_KEY busybee-mcp http # serve all transports on :3013
152
+ ```
153
+
154
+ ---
155
+
156
+ ## Design decisions worth knowing
157
+
158
+ - **Ten tools, not a thousand.** Call a business intent, not an endpoint. The more tools a model sees, the more often it picks wrong and burns money.
159
+ - **Normalized fields.** `author / text / created_at / stats` mean the same thing on every platform.
160
+ - **One implementation behind every protocol.** MCP, SSE and REST share the same dispatch layer, so behavior can't drift between clients.
161
+ - **Pay per call, priced per endpoint.** Cheap calls cost fractions of a cent. Price is visible in your console.
162
+ - **Cost transparency inside the result.** `cost_usd` + `balance_usd` on every response.
163
+ - **Failures are not billed.**
164
+ - **Clean errors.** No stack traces, no third-party vendor names, no upstream documentation links leaking into your agent's context.
165
+ - **CORS enabled**, so browser-based agents and web widgets can call it directly.
166
+
167
+ ## Errors
168
+
169
+ | `error` | Meaning | What to do |
170
+ |---|---|---|
171
+ | `no_api_key` | No key supplied | Add `Authorization: Bearer sk-...`, `X-API-Key`, or `?api_key=` |
172
+ | `unauthorized` | Key invalid or deleted | Issue a new key in the console |
173
+ | `insufficient_balance` | Out of credit | Top up |
174
+ | `forbidden` | Key group doesn't cover that platform | Create a key in the right group |
175
+ | `rate_limited` | Too many requests | Back off and retry |
176
+ | `unsupported_tool` | That platform doesn't expose the tool | Call `list_platforms` to see the matrix |
177
+ | `upstream_error` | Temporary provider failure | Retry once; not billed |
178
+
179
+ ## Scope and compliance
180
+
181
+ BusyBee serves **public** social media data only, and only to users **outside mainland China**. Do not use it to profile private individuals, scrape private accounts, manipulate engagement, or train models on personal data without a lawful basis. See the [data policy](https://lucydomi.com/data-policy).
182
+
183
+ ## License
184
+
185
+ MIT — see [LICENSE](LICENSE). This repository is the client-side MCP server; the hosted service is metered separately.
186
+
187
+ ---
188
+
189
+ ## 中文速览
190
+
191
+ **BusyBee MCP** 把 TikTok、Instagram、YouTube、X、抖音、小红书六个平台的公开数据,压成 **10 个业务级工具 + 一套统一字段**,**同一台服务器支持四种接入方式**:
192
+
193
+ | 你的 AI 工具 | 接入地址 |
194
+ |---|---|
195
+ | Claude Code / Cursor / Cline(MCP 新版) | `https://lucydomi.com/mcp` |
196
+ | 只认旧版 SSE 的客户端 | `https://lucydomi.com/sse` |
197
+ | Dify / n8n / Coze / GPT Actions(OpenAPI) | `https://lucydomi.com/mcp/openapi.json` |
198
+ | 任何能发 HTTP 的程序 | `https://lucydomi.com/mcp/v1/<工具名>` |
199
+
200
+ - 密钥:注册后在控制台建 `sk-` 开头的串
201
+ - 计费:按次,每次返回都带本次花费与余额;失败不扣费
202
+ - 报错是干净中文/英文,不暴露任何上游信息
@@ -0,0 +1,186 @@
1
+ # BusyBee MCP
2
+
3
+ **Public social media data for AI agents.** Ten business-level tools, one schema — TikTok, Instagram, YouTube, X (Twitter), Douyin and Xiaohongshu.
4
+
5
+ Ask your agent *"who are the fastest-growing coffee creators on TikTok right now, and what do their comment sections complain about?"* — it answers with real data, in one conversation.
6
+
7
+ ```
8
+ Endpoint https://lucydomi.com/mcp
9
+ Protocols MCP Streamable HTTP · MCP SSE (legacy) · REST/JSON · stdio
10
+ Tools 10
11
+ Billing pay per call; every response returns cost_usd + balance_usd
12
+ ```
13
+
14
+ ---
15
+
16
+ ## Works with every kind of agent
17
+
18
+ Most "social data for AI" products ship one transport and hope your client speaks it. This server speaks four, on the same host, with the same 10 tools behind all of them.
19
+
20
+ | Your agent | What it supports | How to connect |
21
+ |---|---|---|
22
+ | **Claude Code** | Streamable HTTP | `claude mcp add busybee --transport http https://lucydomi.com/mcp --header "Authorization: Bearer sk-..."` |
23
+ | **Cursor / Cline / Windsurf / VS Code** | Streamable HTTP | add to `mcp.json` (see below) |
24
+ | **Claude Desktop** | stdio | `uvx --from git+https://github.com/danfeistar/busybee-mcp busybee-mcp` with `BUSYBEE_API_KEY` (not on PyPI yet) |
25
+ | **Older MCP clients (2024-11 era)** | SSE only | `https://lucydomi.com/sse` + `https://lucydomi.com/messages/` |
26
+ | **Dify / n8n / Coze / FastGPT** | OpenAPI / plain HTTP | import `https://lucydomi.com/mcp/openapi.json` (3.1), or `?spec=3.0` for older importers |
27
+ | **LangChain / LlamaIndex / custom code** | tools JSON or HTTP | `https://lucydomi.com/mcp/tools?format=openai`, or call REST directly |
28
+ | **GPT Actions / Zapier / Make** | OpenAPI 3.x | same spec URL, bearer auth |
29
+ | **Anything that can only GET a URL** | query string | `https://lucydomi.com/mcp/v1/search_creators?platform=tiktok&keyword=coffee&api_key=sk-...` |
30
+
31
+ Discovery document (machine-readable, tells an agent what's available): `https://lucydomi.com/mcp/info`
32
+
33
+ ---
34
+
35
+ ## The 10 tools
36
+
37
+ | Tool | What it answers |
38
+ |---|---|
39
+ | `list_platforms` | Which platforms exist, which tools each supports, which key group you need |
40
+ | `search_creators` | "Find me mid-size coffee creators on TikTok" |
41
+ | `search_posts` | "What is being posted about cold brew this week?" |
42
+ | `get_creator` | "How big is this account, what's their bio?" |
43
+ | `list_creator_posts` | "What has this account posted recently?" |
44
+ | `get_post` | "Give me the details of this one video" |
45
+ | `get_comments` | "What are people saying under this post?" |
46
+ | `get_trending` | "What's trending on Douyin right now?" |
47
+ | `get_comment_insights` | "Summarize the comment section: sentiment, questions, complaints" |
48
+ | `check_balance` | "How much credit do I have left?" |
49
+
50
+ `get_comment_insights` is the one people keep open: it samples comments, then returns sentiment split, question ratio, high-frequency phrases and top-liked comments — so the agent answers *"what do users actually want"* instead of dumping fifty raw rows.
51
+
52
+ ---
53
+
54
+ ## One schema, every platform
55
+
56
+ ```json
57
+ {
58
+ "platform": "tiktok",
59
+ "id": "7690199760430566669",
60
+ "url": "https://www.tiktok.com/@creator/video/7690199760430566669",
61
+ "author": { "id": "6948174171005158405", "handle": "goldenbrown.coffee", "name": "Golden Brown Coffee" },
62
+ "text": "Spanish latte, but make it iced",
63
+ "created_at": "2026-10-18T09:12:00Z",
64
+ "stats": { "likes": 41200, "comments": 812, "shares": 2331, "views": 918000, "collects": 5120 },
65
+ "cover": "https://..."
66
+ }
67
+ ```
68
+
69
+ Every response also carries `cost_usd` and `balance_usd`, so the agent can budget itself and you can see what a task actually cost.
70
+
71
+ ---
72
+
73
+ ## Setup per client
74
+
75
+ Need a key first: register at [lucydomi.com](https://lucydomi.com) → *Console → API Keys → New key* (`sk-…`). Overseas and China platforms use different key groups.
76
+
77
+ ### Claude Code
78
+ ```bash
79
+ claude mcp add busybee --transport http https://lucydomi.com/mcp \
80
+ --header "Authorization: Bearer sk-YOUR_KEY"
81
+ ```
82
+
83
+ ### Cursor / Cline / Windsurf / VS Code (`~/.cursor/mcp.json`)
84
+ ```json
85
+ {
86
+ "mcpServers": {
87
+ "busybee": {
88
+ "url": "https://lucydomi.com/mcp",
89
+ "headers": { "Authorization": "Bearer sk-YOUR_KEY" }
90
+ }
91
+ }
92
+ }
93
+ ```
94
+
95
+ ### Claude Desktop (local stdio)
96
+ ```json
97
+ {
98
+ "mcpServers": {
99
+ "busybee": {
100
+ "command": "uvx",
101
+ "args": ["--from", "git+https://github.com/danfeistar/busybee-mcp", "busybee-mcp"],
102
+ "args": ["busybee-mcp"],
103
+ "env": { "BUSYBEE_API_KEY": "sk-YOUR_KEY" }
104
+ }
105
+ }
106
+ }
107
+ ```
108
+
109
+ ### Legacy SSE clients
110
+ ```
111
+ SSE URL https://lucydomi.com/sse
112
+ Messages URL https://lucydomi.com/messages/?session_id=<from the SSE event>
113
+ Header Authorization: Bearer sk-YOUR_KEY
114
+ ```
115
+
116
+ ### Dify / n8n / Coze / GPT Actions
117
+ Import `https://lucydomi.com/mcp/openapi.json`, set auth to Bearer `sk-YOUR_KEY`. If the importer rejects 3.1, use `https://lucydomi.com/mcp/openapi.json?spec=3.0`.
118
+
119
+ ### LangChain / custom Python (no MCP needed)
120
+ ```python
121
+ import json, urllib.request
122
+ req = urllib.request.Request(
123
+ "https://lucydomi.com/mcp/v1/search_creators",
124
+ data=json.dumps({"platform": "tiktok", "keyword": "coffee", "limit": 3}).encode(),
125
+ headers={"Content-Type": "application/json", "Authorization": "Bearer sk-YOUR_KEY"},
126
+ )
127
+ print(json.load(urllib.request.urlopen(req)))
128
+ ```
129
+ Tool schemas for function calling: `GET https://lucydomi.com/mcp/tools?format=openai`
130
+
131
+ ### Local package
132
+ ```bash
133
+ pip install busybee-mcp
134
+ BUSYBEE_API_KEY=sk-YOUR_KEY busybee-mcp # stdio
135
+ BUSYBEE_API_KEY=sk-YOUR_KEY busybee-mcp http # serve all transports on :3013
136
+ ```
137
+
138
+ ---
139
+
140
+ ## Design decisions worth knowing
141
+
142
+ - **Ten tools, not a thousand.** Call a business intent, not an endpoint. The more tools a model sees, the more often it picks wrong and burns money.
143
+ - **Normalized fields.** `author / text / created_at / stats` mean the same thing on every platform.
144
+ - **One implementation behind every protocol.** MCP, SSE and REST share the same dispatch layer, so behavior can't drift between clients.
145
+ - **Pay per call, priced per endpoint.** Cheap calls cost fractions of a cent. Price is visible in your console.
146
+ - **Cost transparency inside the result.** `cost_usd` + `balance_usd` on every response.
147
+ - **Failures are not billed.**
148
+ - **Clean errors.** No stack traces, no third-party vendor names, no upstream documentation links leaking into your agent's context.
149
+ - **CORS enabled**, so browser-based agents and web widgets can call it directly.
150
+
151
+ ## Errors
152
+
153
+ | `error` | Meaning | What to do |
154
+ |---|---|---|
155
+ | `no_api_key` | No key supplied | Add `Authorization: Bearer sk-...`, `X-API-Key`, or `?api_key=` |
156
+ | `unauthorized` | Key invalid or deleted | Issue a new key in the console |
157
+ | `insufficient_balance` | Out of credit | Top up |
158
+ | `forbidden` | Key group doesn't cover that platform | Create a key in the right group |
159
+ | `rate_limited` | Too many requests | Back off and retry |
160
+ | `unsupported_tool` | That platform doesn't expose the tool | Call `list_platforms` to see the matrix |
161
+ | `upstream_error` | Temporary provider failure | Retry once; not billed |
162
+
163
+ ## Scope and compliance
164
+
165
+ BusyBee serves **public** social media data only, and only to users **outside mainland China**. Do not use it to profile private individuals, scrape private accounts, manipulate engagement, or train models on personal data without a lawful basis. See the [data policy](https://lucydomi.com/data-policy).
166
+
167
+ ## License
168
+
169
+ MIT — see [LICENSE](LICENSE). This repository is the client-side MCP server; the hosted service is metered separately.
170
+
171
+ ---
172
+
173
+ ## 中文速览
174
+
175
+ **BusyBee MCP** 把 TikTok、Instagram、YouTube、X、抖音、小红书六个平台的公开数据,压成 **10 个业务级工具 + 一套统一字段**,**同一台服务器支持四种接入方式**:
176
+
177
+ | 你的 AI 工具 | 接入地址 |
178
+ |---|---|
179
+ | Claude Code / Cursor / Cline(MCP 新版) | `https://lucydomi.com/mcp` |
180
+ | 只认旧版 SSE 的客户端 | `https://lucydomi.com/sse` |
181
+ | Dify / n8n / Coze / GPT Actions(OpenAPI) | `https://lucydomi.com/mcp/openapi.json` |
182
+ | 任何能发 HTTP 的程序 | `https://lucydomi.com/mcp/v1/<工具名>` |
183
+
184
+ - 密钥:注册后在控制台建 `sk-` 开头的串
185
+ - 计费:按次,每次返回都带本次花费与余额;失败不扣费
186
+ - 报错是干净中文/英文,不暴露任何上游信息
@@ -0,0 +1,2 @@
1
+ """BusyBee MCP —— 给 AI Agent 用的社媒公开数据工具箱。"""
2
+ __version__ = "0.2.1"
@@ -0,0 +1,20 @@
1
+ """命令行入口:默认 stdio;`busybee-mcp http` 起远程服务。"""
2
+ from __future__ import annotations
3
+
4
+ import sys
5
+
6
+
7
+ def main() -> None:
8
+ mode = sys.argv[1] if len(sys.argv) > 1 else "stdio"
9
+ if mode in ("http", "serve", "streamable-http"):
10
+ from .http_app import main as http_main
11
+
12
+ http_main()
13
+ return
14
+ from .server import serve
15
+
16
+ serve("stdio")
17
+
18
+
19
+ if __name__ == "__main__":
20
+ main()
@@ -0,0 +1,138 @@
1
+ """HTTP 客户端 —— 只在进程内持有 API Key,不打印、不落盘。"""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import os
6
+ import time
7
+ import urllib.error
8
+ import urllib.parse
9
+ import urllib.request
10
+ from typing import Any
11
+
12
+ DEFAULT_BASE = "https://lucydomi.com"
13
+ SIGNUP_URL = "https://lucydomi.com"
14
+
15
+
16
+ class BusyBeeError(Exception):
17
+ """带中文说明的业务异常,直接回给 AI/用户看。"""
18
+
19
+ def __init__(self, message: str, code: str = "error", hint: str | None = None):
20
+ super().__init__(message)
21
+ self.message = message
22
+ self.code = code
23
+ self.hint = hint
24
+
25
+ def as_dict(self) -> dict:
26
+ d = {"ok": False, "error": self.code, "message": self.message}
27
+ if self.hint:
28
+ d["hint"] = self.hint
29
+ return d
30
+
31
+
32
+ class Client:
33
+ def __init__(self, api_key: str | None = None, base_url: str | None = None, timeout: int | None = None):
34
+ self.api_key = api_key or os.environ.get("BUSYBEE_API_KEY") or os.environ.get("XMF_API_KEY")
35
+ self.base_url = (base_url or os.environ.get("BUSYBEE_BASE_URL") or DEFAULT_BASE).rstrip("/")
36
+ self.timeout = int(timeout or os.environ.get("BUSYBEE_TIMEOUT") or 90)
37
+ self._bal_cache: tuple[float, float] | None = None
38
+
39
+ # ------------------------------------------------------------ 基础请求
40
+ def _request(self, path: str, params: dict | None = None, method: str = "GET",
41
+ body: dict | None = None, auth: bool = True) -> dict:
42
+ if auth and not self.api_key:
43
+ raise BusyBeeError(
44
+ "还没有配置 API Key。免费领取地址:%s" % SIGNUP_URL,
45
+ code="no_api_key",
46
+ hint="注册后到「控制台 → API 密钥 → 新建密钥」复制 sk- 开头的串,"
47
+ "并在客户端里配置环境变量 BUSYBEE_API_KEY。",
48
+ )
49
+ url = self.base_url + path
50
+ if params:
51
+ clean = {k: v for k, v in params.items() if v not in (None, "")}
52
+ if clean:
53
+ url += "?" + urllib.parse.urlencode(clean)
54
+ data = None
55
+ headers = {"Accept": "application/json", "User-Agent": "busybee-mcp/0.1"}
56
+ if auth:
57
+ headers["Authorization"] = "Bearer " + str(self.api_key)
58
+ if body is not None:
59
+ data = json.dumps(body).encode()
60
+ headers["Content-Type"] = "application/json"
61
+ req = urllib.request.Request(url, data=data, headers=headers,
62
+ method="POST" if body is not None else method)
63
+ try:
64
+ with urllib.request.urlopen(req, timeout=self.timeout) as r:
65
+ raw = r.read()
66
+ except urllib.error.HTTPError as e:
67
+ raw = e.read()
68
+ raise self._http_error(e.code, raw) from None
69
+ except urllib.error.URLError as e:
70
+ raise BusyBeeError(
71
+ "连不上服务(%s)。请检查网络后重试。" % getattr(e, "reason", e),
72
+ code="network_error") from None
73
+ try:
74
+ return json.loads(raw)
75
+ except ValueError:
76
+ raise BusyBeeError("服务返回了无法解析的内容,请稍后重试。", code="bad_response") from None
77
+
78
+ @staticmethod
79
+ def _http_error(status: int, raw: bytes) -> BusyBeeError:
80
+ msg, hint = None, None
81
+ try:
82
+ j = json.loads(raw)
83
+ err = j.get("error") if isinstance(j, dict) else None
84
+ if isinstance(err, dict):
85
+ msg, hint = err, (err.get("hint") or err.get("groups_cn"))
86
+ elif isinstance(j, dict):
87
+ msg = j.get("message") or j.get("detail")
88
+ if isinstance(msg, dict):
89
+ hint = hint or msg.get("hint")
90
+ msg = msg.get("message_zh") or msg.get("message") or msg.get("msg")
91
+ if msg is not None and not isinstance(msg, str):
92
+ msg = None
93
+ except ValueError:
94
+ pass
95
+ if status == 401:
96
+ return BusyBeeError(msg or "API Key 无效或未登录。", code="unauthorized",
97
+ hint=hint or "确认 Key 是 sk- 开头且未删除;免费领取:%s" % SIGNUP_URL)
98
+ if status == 402:
99
+ return BusyBeeError(msg or "账户余额不足。", code="insufficient_balance",
100
+ hint="到 %s 充值后重试。" % SIGNUP_URL)
101
+ if status == 403:
102
+ return BusyBeeError(msg or "当前 API Key 无权调用该平台。", code="forbidden", hint=hint)
103
+ if status == 429:
104
+ return BusyBeeError(msg or "请求太频繁,触发了限流。", code="rate_limited",
105
+ hint="降低并发或稍后重试。")
106
+ if status >= 500:
107
+ return BusyBeeError(msg or "上游服务暂时不可用(%d)。" % status, code="upstream_error",
108
+ hint="这通常是临时故障,重试一次通常可恢复。")
109
+ return BusyBeeError(msg or "请求失败(HTTP %d)。" % status, code="http_%d" % status, hint=hint)
110
+
111
+ # ------------------------------------------------------------ 业务封装
112
+ def call_endpoint(self, path: str, params: dict | None = None, method: str = "GET",
113
+ body: dict | None = None) -> dict:
114
+ return self._request(path, params=params, method=method, body=body)
115
+
116
+ def me(self) -> dict:
117
+ """账户概览:用户名、余额、分组。"""
118
+ j = self._request("/api/gw/me")
119
+ return j.get("data", j) if isinstance(j, dict) else {}
120
+
121
+ def balance_usd(self, fresh: bool = False) -> float | None:
122
+ now = time.time()
123
+ if not fresh and self._bal_cache and now - self._bal_cache[0] < 30:
124
+ return self._bal_cache[1]
125
+ try:
126
+ data = self.me()
127
+ except BusyBeeError:
128
+ return None
129
+ bal = data.get("quota_exact_usd")
130
+ if bal is None:
131
+ bal = data.get("quota_usd")
132
+ if isinstance(bal, (int, float)):
133
+ self._bal_cache = (now, float(bal))
134
+ return float(bal)
135
+ return None
136
+
137
+ def pricing(self) -> dict:
138
+ return self._request("/api/gw/pricing")