redfox-mcp-core 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.
- redfox_mcp_core-0.1.0/.gitignore +6 -0
- redfox_mcp_core-0.1.0/PKG-INFO +32 -0
- redfox_mcp_core-0.1.0/README.md +17 -0
- redfox_mcp_core-0.1.0/pyproject.toml +29 -0
- redfox_mcp_core-0.1.0/src/redfox_mcp_core/__init__.py +28 -0
- redfox_mcp_core-0.1.0/src/redfox_mcp_core/runtime.py +155 -0
- redfox_mcp_core-0.1.0/src/redfox_mcp_core/server.py +36 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: redfox-mcp-core
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Shared runtime for RedFoxHub MCP servers: client management, multi-tenant auth, async task polling, stdio/HTTP serving
|
|
5
|
+
Project-URL: Homepage, https://github.com/redfox-data/redfox-mcp
|
|
6
|
+
Project-URL: Website, https://redfox.hk/apis/?source=mcp
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Keywords: mcp,redfox
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Requires-Dist: cryptography<49; platform_machine == 'x86_64' and sys_platform == 'darwin'
|
|
11
|
+
Requires-Dist: fastmcp>=2.0
|
|
12
|
+
Requires-Dist: httpx>=0.24
|
|
13
|
+
Requires-Dist: redfox-python-sdk
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# redfox-mcp-core
|
|
17
|
+
|
|
18
|
+
RedFoxHub 各平台 MCP server 的共享基础包,不直接面向终端用户。
|
|
19
|
+
|
|
20
|
+
提供能力:
|
|
21
|
+
|
|
22
|
+
- RedFoxClient 管理:stdio 模式全局单例;HTTP 模式按请求头 API Key 的多租户缓存(LRU,上限 1000)
|
|
23
|
+
- 统一异常封装:SDK 异常转为结构化结果(auth_failed / rate_limited / api_error),agent 可直接读取出错引导
|
|
24
|
+
- 异步任务辅助:提交后自动轮询至完成,超时返回 taskId 供 result 工具补查
|
|
25
|
+
- server 工厂与启动入口:`create_server(name, version)` 注册 `/health` 路由;`serve(mcp, prog, description)` 解析 `--transport stdio|http` / `--host` / `--port` / `--path` 并启动
|
|
26
|
+
|
|
27
|
+
认证方式:
|
|
28
|
+
|
|
29
|
+
- stdio:环境变量 `REDFOX_API_KEY`
|
|
30
|
+
- HTTP:请求头 `X-API-Key: ak_xxx`(或 `Authorization: Bearer ak_xxx`)
|
|
31
|
+
|
|
32
|
+
API Key 获取:https://redfox.hk/settings/api-keys?source=mcp
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# redfox-mcp-core
|
|
2
|
+
|
|
3
|
+
RedFoxHub 各平台 MCP server 的共享基础包,不直接面向终端用户。
|
|
4
|
+
|
|
5
|
+
提供能力:
|
|
6
|
+
|
|
7
|
+
- RedFoxClient 管理:stdio 模式全局单例;HTTP 模式按请求头 API Key 的多租户缓存(LRU,上限 1000)
|
|
8
|
+
- 统一异常封装:SDK 异常转为结构化结果(auth_failed / rate_limited / api_error),agent 可直接读取出错引导
|
|
9
|
+
- 异步任务辅助:提交后自动轮询至完成,超时返回 taskId 供 result 工具补查
|
|
10
|
+
- server 工厂与启动入口:`create_server(name, version)` 注册 `/health` 路由;`serve(mcp, prog, description)` 解析 `--transport stdio|http` / `--host` / `--port` / `--path` 并启动
|
|
11
|
+
|
|
12
|
+
认证方式:
|
|
13
|
+
|
|
14
|
+
- stdio:环境变量 `REDFOX_API_KEY`
|
|
15
|
+
- HTTP:请求头 `X-API-Key: ak_xxx`(或 `Authorization: Bearer ak_xxx`)
|
|
16
|
+
|
|
17
|
+
API Key 获取:https://redfox.hk/settings/api-keys?source=mcp
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "redfox-mcp-core"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Shared runtime for RedFoxHub MCP servers: client management, multi-tenant auth, async task polling, stdio/HTTP serving"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
keywords = ["mcp", "redfox"]
|
|
13
|
+
dependencies = [
|
|
14
|
+
"fastmcp>=2.0",
|
|
15
|
+
# Intel Mac 上 cryptography 49+ 无预编译 wheel 会触发源码构建,钉住 48.x
|
|
16
|
+
"cryptography<49; platform_machine == 'x86_64' and sys_platform == 'darwin'",
|
|
17
|
+
"redfox-python-sdk",
|
|
18
|
+
"httpx>=0.24",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.urls]
|
|
22
|
+
Homepage = "https://github.com/redfox-data/redfox-mcp"
|
|
23
|
+
Website = "https://redfox.hk/apis/?source=mcp"
|
|
24
|
+
|
|
25
|
+
[tool.hatch.build.targets.wheel]
|
|
26
|
+
packages = ["src/redfox_mcp_core"]
|
|
27
|
+
|
|
28
|
+
[tool.hatch.build.targets.sdist]
|
|
29
|
+
include = ["src/redfox_mcp_core", "README.md"]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""RedFox MCP 共享基础设施 — 各平台 MCP server 的统一运行时与启动入口"""
|
|
2
|
+
|
|
3
|
+
from redfox_mcp_core.runtime import (
|
|
4
|
+
API_KEY_GUIDE,
|
|
5
|
+
API_KEY_GUIDE_HTTP,
|
|
6
|
+
TASK_PENDING_MSG,
|
|
7
|
+
call,
|
|
8
|
+
get_client,
|
|
9
|
+
is_done,
|
|
10
|
+
poll,
|
|
11
|
+
run_task,
|
|
12
|
+
set_transport,
|
|
13
|
+
)
|
|
14
|
+
from redfox_mcp_core.server import create_server, serve
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"API_KEY_GUIDE",
|
|
18
|
+
"API_KEY_GUIDE_HTTP",
|
|
19
|
+
"TASK_PENDING_MSG",
|
|
20
|
+
"call",
|
|
21
|
+
"get_client",
|
|
22
|
+
"is_done",
|
|
23
|
+
"poll",
|
|
24
|
+
"run_task",
|
|
25
|
+
"set_transport",
|
|
26
|
+
"create_server",
|
|
27
|
+
"serve",
|
|
28
|
+
]
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""RedFox MCP 共享运行时
|
|
2
|
+
|
|
3
|
+
为各平台 MCP server 提供统一的基础设施:
|
|
4
|
+
- RedFoxClient 管理(stdio 单例 / HTTP 多租户按 key 缓存)
|
|
5
|
+
- 统一异常转结构化结果(agent 可直接读取出错引导)
|
|
6
|
+
- 异步任务提交 + 自动轮询
|
|
7
|
+
|
|
8
|
+
认证:stdio 模式读环境变量 REDFOX_API_KEY;
|
|
9
|
+
HTTP 模式从请求头 X-API-Key(或 Authorization: Bearer <key>)取 key。
|
|
10
|
+
获取地址 https://redfox.hk/settings/api-keys?source=mcp
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import threading
|
|
14
|
+
import time
|
|
15
|
+
from collections import OrderedDict
|
|
16
|
+
from typing import Any, Callable, Dict, Optional
|
|
17
|
+
|
|
18
|
+
from redfox import RedFoxClient
|
|
19
|
+
from redfox.exceptions import RedFoxAPIError, RedFoxAuthError, RedFoxRateLimitError
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
from fastmcp.server.dependencies import get_http_request
|
|
23
|
+
except ImportError: # 老版本 fastmcp 无此接口
|
|
24
|
+
get_http_request = None
|
|
25
|
+
|
|
26
|
+
API_KEY_GUIDE = (
|
|
27
|
+
"REDFOX_API_KEY 未配置或无效。请前往 "
|
|
28
|
+
"https://redfox.hk/settings/api-keys?source=mcp 注册并获取 API Key,"
|
|
29
|
+
"然后设置环境变量 REDFOX_API_KEY 后重启本服务。"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
API_KEY_GUIDE_HTTP = (
|
|
33
|
+
"未在请求头中检测到 API Key。请前往 "
|
|
34
|
+
"https://redfox.hk/settings/api-keys?source=mcp 注册并获取 API Key,"
|
|
35
|
+
"然后在 MCP 客户端的请求头中配置 X-API-Key(或 Authorization: Bearer <key>)。"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
TASK_PENDING_MSG = (
|
|
39
|
+
"任务仍在进行中,已超过本次等待时间。"
|
|
40
|
+
"请稍后使用对应的 result 工具并传入此 taskId 查询结果。"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
TERMINAL_STATUSES = {
|
|
44
|
+
"succeeded", "success", "completed", "complete", "done",
|
|
45
|
+
"failed", "error", "cancelled", "canceled",
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
_TRANSPORT = "stdio" # serve() 启动时按实际 transport 设置
|
|
49
|
+
|
|
50
|
+
_client: Optional[RedFoxClient] = None # stdio 模式:全局单例
|
|
51
|
+
_tenants: "OrderedDict[str, RedFoxClient]" = OrderedDict() # http 模式:按 key 缓存
|
|
52
|
+
_tenants_lock = threading.Lock()
|
|
53
|
+
_TENANT_MAX = 1000
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def set_transport(transport: str) -> None:
|
|
57
|
+
"""由 serve() 在启动时调用,按实际 transport 设置全局模式"""
|
|
58
|
+
global _TRANSPORT
|
|
59
|
+
_TRANSPORT = transport
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _auth_guide() -> str:
|
|
63
|
+
return API_KEY_GUIDE if _TRANSPORT == "stdio" else API_KEY_GUIDE_HTTP
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _request_key() -> Optional[str]:
|
|
67
|
+
"""HTTP 模式下从当前请求头取 key:X-API-Key 优先,Authorization: Bearer 回退"""
|
|
68
|
+
if get_http_request is None:
|
|
69
|
+
return None
|
|
70
|
+
try:
|
|
71
|
+
req = get_http_request()
|
|
72
|
+
except Exception: # 非 HTTP 上下文
|
|
73
|
+
return None
|
|
74
|
+
key = req.headers.get("x-api-key")
|
|
75
|
+
if key and key.strip():
|
|
76
|
+
return key.strip()
|
|
77
|
+
auth = req.headers.get("authorization", "")
|
|
78
|
+
if auth.lower().startswith("bearer "):
|
|
79
|
+
return auth[7:].strip() or None
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def get_client() -> RedFoxClient:
|
|
84
|
+
global _client
|
|
85
|
+
if _TRANSPORT == "stdio":
|
|
86
|
+
if _client is None:
|
|
87
|
+
_client = RedFoxClient() # 零配置:自动读环境变量 REDFOX_API_KEY
|
|
88
|
+
return _client
|
|
89
|
+
# HTTP 多租户:按请求头中的 key 建独立 client,互不共享额度
|
|
90
|
+
key = _request_key()
|
|
91
|
+
if not key:
|
|
92
|
+
raise ValueError("missing API key in request header")
|
|
93
|
+
with _tenants_lock:
|
|
94
|
+
cli = _tenants.get(key)
|
|
95
|
+
if cli is None:
|
|
96
|
+
cli = RedFoxClient(api_key=key)
|
|
97
|
+
_tenants[key] = cli
|
|
98
|
+
if len(_tenants) > _TENANT_MAX:
|
|
99
|
+
_tenants.popitem(last=False) # 淘汰最久未使用的租户
|
|
100
|
+
else:
|
|
101
|
+
_tenants.move_to_end(key)
|
|
102
|
+
return cli
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def call(fn_factory: Callable[[], Callable], **kwargs) -> Dict[str, Any]:
|
|
106
|
+
"""统一调用 SDK 方法并把异常转为结构化结果,agent 可直接读取出错引导"""
|
|
107
|
+
try:
|
|
108
|
+
return fn_factory()(**{k: v for k, v in kwargs.items() if v is not None})
|
|
109
|
+
except (RedFoxAuthError, ValueError):
|
|
110
|
+
return {"error": "auth_failed", "message": _auth_guide()}
|
|
111
|
+
except RedFoxRateLimitError:
|
|
112
|
+
return {"error": "rate_limited", "message": "请求频率超限,请稍后重试"}
|
|
113
|
+
except RedFoxAPIError as e:
|
|
114
|
+
return {"error": "api_error", "code": e.code, "message": e.message}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def is_done(res: Any) -> bool:
|
|
118
|
+
"""判断异步任务是否到达终态(兼容各品类不同的响应结构)"""
|
|
119
|
+
if not isinstance(res, dict):
|
|
120
|
+
return True
|
|
121
|
+
if res.get("error"):
|
|
122
|
+
return True
|
|
123
|
+
if res.get("completed") is True:
|
|
124
|
+
return True
|
|
125
|
+
status = res.get("status")
|
|
126
|
+
if isinstance(status, str) and status.lower() in TERMINAL_STATUSES:
|
|
127
|
+
return True
|
|
128
|
+
for field in ("content", "imagePaths", "images", "videoUrl", "videoUrls", "video"):
|
|
129
|
+
if res.get(field):
|
|
130
|
+
return True
|
|
131
|
+
return False
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def poll(result_fn: Callable[[], Callable], task_id: str,
|
|
135
|
+
timeout_seconds: int, interval: float = 3.0) -> Dict[str, Any]:
|
|
136
|
+
deadline = time.monotonic() + timeout_seconds
|
|
137
|
+
while True:
|
|
138
|
+
res = call(result_fn, task_id=task_id)
|
|
139
|
+
if is_done(res):
|
|
140
|
+
return res
|
|
141
|
+
if time.monotonic() >= deadline:
|
|
142
|
+
return {"completed": False, "taskId": task_id, "message": TASK_PENDING_MSG}
|
|
143
|
+
time.sleep(interval)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def run_task(submit_fn: Callable[[], Callable], result_fn: Callable[[], Callable],
|
|
147
|
+
timeout_seconds: int, **kwargs) -> Dict[str, Any]:
|
|
148
|
+
"""提交异步任务并自动轮询至完成;超时则返回 taskId 供 result 工具后续查询"""
|
|
149
|
+
submitted = call(submit_fn, **kwargs)
|
|
150
|
+
if not isinstance(submitted, dict) or submitted.get("error"):
|
|
151
|
+
return submitted
|
|
152
|
+
task_id = submitted.get("taskId") or submitted.get("task_id")
|
|
153
|
+
if not task_id:
|
|
154
|
+
return submitted
|
|
155
|
+
return poll(result_fn, task_id, timeout_seconds)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""RedFox MCP 共享 server 工厂与启动入口"""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
from fastmcp import FastMCP
|
|
7
|
+
from starlette.responses import JSONResponse
|
|
8
|
+
|
|
9
|
+
from redfox_mcp_core.runtime import set_transport
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def create_server(name: str, version: str) -> FastMCP:
|
|
13
|
+
"""创建一个带 /health 路由的 FastMCP 实例,各平台 server 共用"""
|
|
14
|
+
mcp = FastMCP(name)
|
|
15
|
+
|
|
16
|
+
@mcp.custom_route("/health", methods=["GET"])
|
|
17
|
+
async def _health(_request):
|
|
18
|
+
return JSONResponse({"status": "ok", "server": name, "version": version})
|
|
19
|
+
|
|
20
|
+
return mcp
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def serve(mcp: FastMCP, prog: str, description: str) -> None:
|
|
24
|
+
"""启动 MCP server:默认 stdio(本地客户端),--transport http 切换为远程多租户模式"""
|
|
25
|
+
parser = argparse.ArgumentParser(prog=prog, description=description)
|
|
26
|
+
parser.add_argument("--transport", choices=["stdio", "http"],
|
|
27
|
+
default=os.getenv("REDFOX_MCP_TRANSPORT", "stdio"))
|
|
28
|
+
parser.add_argument("--host", default=os.getenv("REDFOX_MCP_HOST", "127.0.0.1"))
|
|
29
|
+
parser.add_argument("--port", type=int, default=int(os.getenv("REDFOX_MCP_PORT", "8000")))
|
|
30
|
+
parser.add_argument("--path", default=os.getenv("REDFOX_MCP_PATH", "/mcp"))
|
|
31
|
+
args = parser.parse_args()
|
|
32
|
+
if args.transport == "http":
|
|
33
|
+
set_transport("http")
|
|
34
|
+
mcp.run(transport="streamable-http", host=args.host, port=args.port, path=args.path)
|
|
35
|
+
else:
|
|
36
|
+
mcp.run()
|