mcp-server-core 0.2.2__tar.gz → 0.3.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.
- {mcp_server_core-0.2.2/mcp_server_core.egg-info → mcp_server_core-0.3.0}/PKG-INFO +4 -1
- mcp_server_core-0.3.0/mcp_framework/launcher.py +1 -0
- mcp_server_core-0.3.0/mcp_framework/response.py +1 -0
- mcp_server_core-0.3.0/mcp_framework/tray.py +1 -0
- {mcp_server_core-0.2.2 → mcp_server_core-0.3.0}/mcp_server_core/__init__.py +7 -0
- mcp_server_core-0.3.0/mcp_server_core/launcher.py +103 -0
- mcp_server_core-0.3.0/mcp_server_core/response.py +59 -0
- {mcp_server_core-0.2.2 → mcp_server_core-0.3.0}/mcp_server_core/server.py +10 -5
- mcp_server_core-0.3.0/mcp_server_core/tray.py +534 -0
- {mcp_server_core-0.2.2 → mcp_server_core-0.3.0/mcp_server_core.egg-info}/PKG-INFO +4 -1
- {mcp_server_core-0.2.2 → mcp_server_core-0.3.0}/mcp_server_core.egg-info/SOURCES.txt +6 -0
- {mcp_server_core-0.2.2 → mcp_server_core-0.3.0}/mcp_server_core.egg-info/requires.txt +4 -0
- {mcp_server_core-0.2.2 → mcp_server_core-0.3.0}/pyproject.toml +5 -1
- {mcp_server_core-0.2.2 → mcp_server_core-0.3.0}/LICENSE +0 -0
- {mcp_server_core-0.2.2 → mcp_server_core-0.3.0}/README.md +0 -0
- {mcp_server_core-0.2.2 → mcp_server_core-0.3.0}/mcp_framework/__init__.py +0 -0
- {mcp_server_core-0.2.2 → mcp_server_core-0.3.0}/mcp_framework/env.py +0 -0
- {mcp_server_core-0.2.2 → mcp_server_core-0.3.0}/mcp_framework/http_server.py +0 -0
- {mcp_server_core-0.2.2 → mcp_server_core-0.3.0}/mcp_framework/oauth.py +0 -0
- {mcp_server_core-0.2.2 → mcp_server_core-0.3.0}/mcp_framework/process.py +0 -0
- {mcp_server_core-0.2.2 → mcp_server_core-0.3.0}/mcp_framework/server.py +0 -0
- {mcp_server_core-0.2.2 → mcp_server_core-0.3.0}/mcp_framework/tunnel.py +0 -0
- {mcp_server_core-0.2.2 → mcp_server_core-0.3.0}/mcp_server_core/env.py +0 -0
- {mcp_server_core-0.2.2 → mcp_server_core-0.3.0}/mcp_server_core/http_server.py +0 -0
- {mcp_server_core-0.2.2 → mcp_server_core-0.3.0}/mcp_server_core/oauth.py +0 -0
- {mcp_server_core-0.2.2 → mcp_server_core-0.3.0}/mcp_server_core/process.py +0 -0
- {mcp_server_core-0.2.2 → mcp_server_core-0.3.0}/mcp_server_core/tunnel.py +0 -0
- {mcp_server_core-0.2.2 → mcp_server_core-0.3.0}/mcp_server_core.egg-info/dependency_links.txt +0 -0
- {mcp_server_core-0.2.2 → mcp_server_core-0.3.0}/mcp_server_core.egg-info/top_level.txt +0 -0
- {mcp_server_core-0.2.2 → mcp_server_core-0.3.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcp-server-core
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Universal MCP Server Framework: Streamable HTTP ASGI, Cloudflare Tunnel, OAuth 2.1 & Process Supervisor
|
|
5
5
|
Author: yimmr
|
|
6
6
|
License-Expression: MIT
|
|
@@ -19,6 +19,9 @@ Requires-Dist: uvicorn>=0.27.0
|
|
|
19
19
|
Requires-Dist: pywin32>=306; platform_system == "Windows"
|
|
20
20
|
Provides-Extra: test
|
|
21
21
|
Requires-Dist: httpx>=0.27.0; extra == "test"
|
|
22
|
+
Provides-Extra: tray
|
|
23
|
+
Requires-Dist: pystray>=0.19.5; extra == "tray"
|
|
24
|
+
Requires-Dist: pillow>=10.0.0; extra == "tray"
|
|
22
25
|
Dynamic: license-file
|
|
23
26
|
|
|
24
27
|
# MCP Server Core 🚀
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from mcp_server_core.launcher import * # noqa: F401, F403
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from mcp_server_core.response import * # noqa: F401, F403
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from mcp_server_core.tray import * # noqa: F401, F403
|
|
@@ -25,6 +25,8 @@ from .process import (
|
|
|
25
25
|
kill_stale_server_process,
|
|
26
26
|
stop_mcp_service,
|
|
27
27
|
)
|
|
28
|
+
from .launcher import launch_service
|
|
29
|
+
from .response import ToolResultListDict
|
|
28
30
|
from .server import (
|
|
29
31
|
build_mcp_server_app,
|
|
30
32
|
record_pid,
|
|
@@ -33,6 +35,7 @@ from .server import (
|
|
|
33
35
|
serve_http,
|
|
34
36
|
serve_stdio,
|
|
35
37
|
)
|
|
38
|
+
from .tray import BaseTrayApp, ServerProcessSupervisor
|
|
36
39
|
from .tunnel import (
|
|
37
40
|
CloudflareTunnel,
|
|
38
41
|
TunnelHeartbeatMonitor,
|
|
@@ -64,6 +67,10 @@ __all__ = [
|
|
|
64
67
|
"get_client_ip",
|
|
65
68
|
"get_server_public_url",
|
|
66
69
|
"verify_pkce",
|
|
70
|
+
"launch_service",
|
|
71
|
+
"ToolResultListDict",
|
|
72
|
+
"BaseTrayApp",
|
|
73
|
+
"ServerProcessSupervisor",
|
|
67
74
|
"build_mcp_server_app",
|
|
68
75
|
"record_pid",
|
|
69
76
|
"cleanup_pid",
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Unified MCP Service Launcher and Process Lifecycle CLI."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Callable
|
|
8
|
+
|
|
9
|
+
from .env import DeploymentEnvConfig, get_runtime_dir
|
|
10
|
+
from .process import check_service_status, stop_mcp_service
|
|
11
|
+
from .tunnel import ensure_tunnel_running, start_tunnel_heartbeat_monitor
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def launch_service(
|
|
15
|
+
service_name: str,
|
|
16
|
+
server_runner: Callable[[], Any],
|
|
17
|
+
default_port: int = 8788,
|
|
18
|
+
root_dir: Path | str | None = None,
|
|
19
|
+
mutex_name: str | None = None,
|
|
20
|
+
) -> None:
|
|
21
|
+
"""Launch an MCP service in console mode with tunnel supervisor and single-instance guards.
|
|
22
|
+
|
|
23
|
+
Handles:
|
|
24
|
+
- sys.platform Windows console UTF-8 safety
|
|
25
|
+
- `--stop`: Terminate running server on port
|
|
26
|
+
- `--test`: Pre-flight check and exit
|
|
27
|
+
- Single-instance detection with helpful console alerts
|
|
28
|
+
- Background shared Cloudflare Tunnel check & heartbeat monitor
|
|
29
|
+
- Seamless delegation to server_runner with `--no-tunnel`
|
|
30
|
+
"""
|
|
31
|
+
if sys.platform == "win32":
|
|
32
|
+
try:
|
|
33
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
34
|
+
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
|
|
35
|
+
except Exception:
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
cwd = Path(root_dir).resolve() if root_dir else Path.cwd().resolve()
|
|
39
|
+
os.chdir(cwd)
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
env_cfg = DeploymentEnvConfig.load(cwd)
|
|
43
|
+
port = env_cfg.port or default_port
|
|
44
|
+
key_configured = bool(env_cfg.api_key)
|
|
45
|
+
secret_key = env_cfg.api_key if "--show-key" in sys.argv else ("已配置(不显示密钥)" if key_configured else "未配置")
|
|
46
|
+
public_endpoint = env_cfg.upstream_url or f"http://127.0.0.1:{port}/mcp"
|
|
47
|
+
except Exception:
|
|
48
|
+
port = default_port
|
|
49
|
+
api_key_env = os.getenv("MCP_API_KEY", "")
|
|
50
|
+
key_configured = bool(api_key_env)
|
|
51
|
+
secret_key = api_key_env if "--show-key" in sys.argv else ("已配置(不显示密钥)" if key_configured else "未配置")
|
|
52
|
+
public_endpoint = os.getenv("TUNNEL_PUBLIC_URL", f"http://127.0.0.1:{port}/mcp")
|
|
53
|
+
|
|
54
|
+
local_endpoint = f"http://127.0.0.1:{port}/mcp"
|
|
55
|
+
active_mutex = mutex_name or f"Local\\{service_name.replace(' ', '_')}_{port}"
|
|
56
|
+
|
|
57
|
+
if "--stop" in sys.argv:
|
|
58
|
+
print(f"[*] 正在停止 {service_name} (端口 {port})...")
|
|
59
|
+
res = stop_mcp_service(port, cwd)
|
|
60
|
+
if res.get("killed_pids"):
|
|
61
|
+
print(f"[SUCCESS] {service_name} 服务已成功停止 (已终结进程: {res['killed_pids']})!")
|
|
62
|
+
else:
|
|
63
|
+
print(f"[INFO] 未检测到端口 {port} 上正在运行的 {service_name} 服务。")
|
|
64
|
+
sys.exit(0)
|
|
65
|
+
|
|
66
|
+
is_running, mode_desc, running_pid = check_service_status(port, cwd, active_mutex)
|
|
67
|
+
if is_running:
|
|
68
|
+
print("\n" + "=" * 70)
|
|
69
|
+
print(f" 【提示】{service_name} 服务已在运行中(或正在启动),请勿重复启动!")
|
|
70
|
+
print("=" * 70)
|
|
71
|
+
print(f" * 运行模式: {mode_desc}")
|
|
72
|
+
if running_pid:
|
|
73
|
+
print(f" * 进程 PID: {running_pid}")
|
|
74
|
+
print(f" * 专属公网端点: {public_endpoint}")
|
|
75
|
+
print(f" * 安全访问密钥: {secret_key}")
|
|
76
|
+
print(f" * 本地监听地址: {local_endpoint}")
|
|
77
|
+
print("-" * 70)
|
|
78
|
+
print(" 如需停止服务,请直接运行 stop.bat 或右键右下角托盘图标退出")
|
|
79
|
+
print("=" * 70 + "\n")
|
|
80
|
+
try:
|
|
81
|
+
input("按 Enter 键退出...")
|
|
82
|
+
except Exception:
|
|
83
|
+
pass
|
|
84
|
+
sys.exit(0)
|
|
85
|
+
|
|
86
|
+
# 1. Ensure shared tunnel is active & start self-healing heartbeat monitor
|
|
87
|
+
print(f"[*] 检查并确保共享穿透隧道在线...")
|
|
88
|
+
try:
|
|
89
|
+
ensure_tunnel_running()
|
|
90
|
+
if public_endpoint:
|
|
91
|
+
start_tunnel_heartbeat_monitor(public_url=public_endpoint, check_interval=30.0)
|
|
92
|
+
except Exception as e:
|
|
93
|
+
print(f"[!] 隧道自检提示: {e}")
|
|
94
|
+
|
|
95
|
+
if "--test" in sys.argv:
|
|
96
|
+
print(f"[OK] {service_name} launcher pre-flight check passed!")
|
|
97
|
+
sys.exit(0)
|
|
98
|
+
|
|
99
|
+
# 2. Run child server runner
|
|
100
|
+
print(f"[*] 正在启动 {service_name} (端口 {port})...\n")
|
|
101
|
+
if "--no-tunnel" not in sys.argv:
|
|
102
|
+
sys.argv = [sys.argv[0], "--no-tunnel"] + [arg for arg in sys.argv[1:] if arg != "--no-tunnel"]
|
|
103
|
+
server_runner()
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Structured response containers and protocol helpers for MCP tools."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ToolResultListDict(dict):
|
|
8
|
+
"""Structured container that ensures atomic, single-block MCP response serialization while preserving list semantics.
|
|
9
|
+
|
|
10
|
+
FastMCP converts any returned Python list into multiple separate TextContent blocks
|
|
11
|
+
in the MCP protocol response. When intermediate MCP gateways or LLM clients parse
|
|
12
|
+
the response, many gateways only inspect the last TextContent block, resulting in
|
|
13
|
+
truncation (e.g. only returning the last project, file, or item).
|
|
14
|
+
|
|
15
|
+
ToolResultListDict inherits from dict so FastMCP serializes the ENTIRE structure
|
|
16
|
+
into a single TextContent JSON payload:
|
|
17
|
+
{"status": "success", "count": N, "total": N, "<key>": items, "entries": items}
|
|
18
|
+
|
|
19
|
+
Simultaneously, for Python unit tests and backward compatibility, it implements
|
|
20
|
+
list-like iteration (__iter__), indexing (__getitem__ for int), length (__len__),
|
|
21
|
+
and membership (__contains__).
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
items: list[Any],
|
|
27
|
+
key: str = "items",
|
|
28
|
+
status: str = "success",
|
|
29
|
+
**extra: Any,
|
|
30
|
+
) -> None:
|
|
31
|
+
super().__init__()
|
|
32
|
+
self["status"] = status
|
|
33
|
+
self["total"] = len(items)
|
|
34
|
+
self["count"] = len(items)
|
|
35
|
+
self[key] = items
|
|
36
|
+
if key != "entries":
|
|
37
|
+
self["entries"] = items
|
|
38
|
+
for k, v in extra.items():
|
|
39
|
+
self[k] = v
|
|
40
|
+
self._items = items
|
|
41
|
+
|
|
42
|
+
def __iter__(self):
|
|
43
|
+
return iter(self._items)
|
|
44
|
+
|
|
45
|
+
def __len__(self) -> int:
|
|
46
|
+
return len(self._items)
|
|
47
|
+
|
|
48
|
+
def __getitem__(self, item: Any) -> Any:
|
|
49
|
+
if isinstance(item, int):
|
|
50
|
+
return self._items[item]
|
|
51
|
+
return super().__getitem__(item)
|
|
52
|
+
|
|
53
|
+
def __contains__(self, item: Any) -> bool:
|
|
54
|
+
try:
|
|
55
|
+
if super().__contains__(item):
|
|
56
|
+
return True
|
|
57
|
+
except TypeError:
|
|
58
|
+
pass
|
|
59
|
+
return item in self._items
|
|
@@ -154,8 +154,10 @@ async def serve_http(
|
|
|
154
154
|
print(f" * 官方 MCP 门户: {portal_url} (ChatGPT 统一端点)")
|
|
155
155
|
if upstream_mcp_url:
|
|
156
156
|
print(f" * 内部隧道回源: {upstream_mcp_url}")
|
|
157
|
+
show_key = "--show-key" in sys.argv
|
|
157
158
|
if env_cfg.require_auth:
|
|
158
|
-
|
|
159
|
+
key_display = env_cfg.api_key if show_key else "已配置 (运行传 --show-key 查看明文)"
|
|
160
|
+
print(f" * 安全访问密钥: {key_display}")
|
|
159
161
|
print("-" * 76)
|
|
160
162
|
|
|
161
163
|
print(" 【远程 Agent 配置指引 (Remote Agent Connection Guide)】")
|
|
@@ -164,19 +166,22 @@ async def serve_http(
|
|
|
164
166
|
print(f" [ChatGPT Developer Mode / Custom Actions 连接指南]:")
|
|
165
167
|
print(f" * 服务器 URL: {primary_endpoint}")
|
|
166
168
|
print(f" * 身份验证类型: Bearer Token 或 OAuth")
|
|
167
|
-
|
|
169
|
+
key_hint = env_cfg.api_key if show_key else "请从本机安全配置获取 (日志不显示明文)"
|
|
170
|
+
print(f" * 访问密钥: {key_hint}")
|
|
168
171
|
print(f" * 备用直连回源地址: {upstream_mcp_url}\n")
|
|
169
172
|
|
|
170
173
|
if env_cfg.require_auth:
|
|
174
|
+
token_display = env_cfg.api_key if show_key else "<MCP_API_KEY>"
|
|
171
175
|
print(f" [通用远程 Agent (网页端/Cursor/Claude Desktop/API)]: ")
|
|
172
|
-
print(f" * 单 URL 快捷接入: {primary_endpoint}?token={
|
|
173
|
-
print(f" * Header 接入: Authorization: Bearer {
|
|
176
|
+
print(f" * 单 URL 快捷接入: {primary_endpoint}?token={token_display}")
|
|
177
|
+
print(f" * Header 接入: Authorization: Bearer {token_display}")
|
|
174
178
|
else:
|
|
175
179
|
print(f" URL: {primary_endpoint}")
|
|
176
180
|
else:
|
|
177
181
|
if env_cfg.require_auth:
|
|
182
|
+
token_display = env_cfg.api_key if show_key else "<MCP_API_KEY>"
|
|
178
183
|
print(f" URL: {local_mcp_url}")
|
|
179
|
-
print(f" Header: Authorization: Bearer {
|
|
184
|
+
print(f" Header: Authorization: Bearer {token_display}")
|
|
180
185
|
else:
|
|
181
186
|
print(f" URL: {local_mcp_url}")
|
|
182
187
|
|
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
"""Reusable Windows System Tray Application Base for MCP Services."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import atexit
|
|
5
|
+
import os
|
|
6
|
+
import secrets
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import threading
|
|
10
|
+
import time
|
|
11
|
+
import urllib.request
|
|
12
|
+
import webbrowser
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Callable
|
|
15
|
+
|
|
16
|
+
if sys.platform == "win32":
|
|
17
|
+
try:
|
|
18
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
19
|
+
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
|
|
20
|
+
except Exception:
|
|
21
|
+
pass
|
|
22
|
+
try:
|
|
23
|
+
import ctypes
|
|
24
|
+
try:
|
|
25
|
+
ctypes.windll.shcore.SetProcessDpiAwareness(2)
|
|
26
|
+
except Exception:
|
|
27
|
+
ctypes.windll.user32.SetProcessDPIAware()
|
|
28
|
+
except Exception:
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
from PIL import Image, ImageDraw, ImageFont
|
|
33
|
+
import pystray
|
|
34
|
+
from pystray import MenuItem as item
|
|
35
|
+
except ImportError as err:
|
|
36
|
+
pystray = None
|
|
37
|
+
item = None
|
|
38
|
+
|
|
39
|
+
if sys.platform == "win32":
|
|
40
|
+
import win32api
|
|
41
|
+
import win32clipboard
|
|
42
|
+
import win32con
|
|
43
|
+
import win32event
|
|
44
|
+
import win32job
|
|
45
|
+
import winerror
|
|
46
|
+
else:
|
|
47
|
+
win32api = win32clipboard = win32con = win32event = win32job = winerror = None
|
|
48
|
+
|
|
49
|
+
from .env import DeploymentEnvConfig, get_runtime_dir, update_env_variable
|
|
50
|
+
from .process import is_port_in_use, kill_stale_server_process
|
|
51
|
+
from .tunnel import ensure_tunnel_running, stop_tunnel_if_no_mcp
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_server_python_exe() -> str:
|
|
55
|
+
"""Ensure child server runs with python.exe (with CREATE_NO_WINDOW) rather than pythonw.exe."""
|
|
56
|
+
py_exe = sys.executable
|
|
57
|
+
if py_exe.lower().endswith("pythonw.exe"):
|
|
58
|
+
cand = py_exe[:-5] + ".exe"
|
|
59
|
+
if os.path.exists(cand):
|
|
60
|
+
return cand
|
|
61
|
+
return py_exe
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class ServerProcessSupervisor:
|
|
65
|
+
"""Supervises child MCP server subprocess lifecycle bound to a Windows Job Object."""
|
|
66
|
+
|
|
67
|
+
def __init__(self, port: int, cwd: Path, server_cmd: list[str], service_name: str):
|
|
68
|
+
self.port = port
|
|
69
|
+
self.cwd = cwd
|
|
70
|
+
self.server_cmd = server_cmd
|
|
71
|
+
self.service_name = service_name
|
|
72
|
+
self.runtime_dir = get_runtime_dir(cwd)
|
|
73
|
+
self.runtime_dir.mkdir(parents=True, exist_ok=True)
|
|
74
|
+
self.proc: subprocess.Popen | None = None
|
|
75
|
+
self.job = None
|
|
76
|
+
self.status_text = f"● 【运行中】127.0.0.1:{self.port}"
|
|
77
|
+
self._restarting = False
|
|
78
|
+
self._init_job_object()
|
|
79
|
+
|
|
80
|
+
def _init_job_object(self) -> None:
|
|
81
|
+
if sys.platform != "win32":
|
|
82
|
+
return
|
|
83
|
+
try:
|
|
84
|
+
job = win32job.CreateJobObject(None, "")
|
|
85
|
+
info = win32job.QueryInformationJobObject(job, win32job.JobObjectExtendedLimitInformation)
|
|
86
|
+
info["BasicLimitInformation"]["LimitFlags"] |= win32job.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
|
|
87
|
+
win32job.SetInformationJobObject(job, win32job.JobObjectExtendedLimitInformation, info)
|
|
88
|
+
self.job = job
|
|
89
|
+
except Exception:
|
|
90
|
+
self.job = None
|
|
91
|
+
|
|
92
|
+
def kill_stale_server_only(self) -> None:
|
|
93
|
+
kill_stale_server_process(self.port, self.cwd)
|
|
94
|
+
time.sleep(0.3)
|
|
95
|
+
|
|
96
|
+
def start(self) -> None:
|
|
97
|
+
self.kill_stale_server_only()
|
|
98
|
+
ensure_tunnel_running()
|
|
99
|
+
|
|
100
|
+
creationflags = subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0
|
|
101
|
+
env = {**os.environ, "PYTHONUTF8": "1", "PYTHONIOENCODING": "utf-8"}
|
|
102
|
+
|
|
103
|
+
self.runtime_dir.mkdir(parents=True, exist_ok=True)
|
|
104
|
+
log_file = open(self.runtime_dir / "server_error.log", "w", encoding="utf-8")
|
|
105
|
+
self.proc = subprocess.Popen(
|
|
106
|
+
self.server_cmd,
|
|
107
|
+
cwd=str(self.cwd),
|
|
108
|
+
creationflags=creationflags,
|
|
109
|
+
env=env,
|
|
110
|
+
stdout=log_file,
|
|
111
|
+
stderr=log_file,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
if self.job and self.proc and getattr(self.proc, "_handle", None):
|
|
115
|
+
try:
|
|
116
|
+
win32job.AssignProcessToJobObject(self.job, self.proc._handle)
|
|
117
|
+
except Exception:
|
|
118
|
+
pass
|
|
119
|
+
|
|
120
|
+
def stop(self) -> None:
|
|
121
|
+
if self.proc:
|
|
122
|
+
try:
|
|
123
|
+
self.proc.terminate()
|
|
124
|
+
self.proc.wait(timeout=2)
|
|
125
|
+
except Exception:
|
|
126
|
+
try:
|
|
127
|
+
self.proc.kill()
|
|
128
|
+
except Exception:
|
|
129
|
+
pass
|
|
130
|
+
self.proc = None
|
|
131
|
+
self.kill_stale_server_only()
|
|
132
|
+
stop_tunnel_if_no_mcp()
|
|
133
|
+
|
|
134
|
+
def restart_async(self, icon: Any | None = None) -> None:
|
|
135
|
+
if self._restarting:
|
|
136
|
+
return
|
|
137
|
+
threading.Thread(target=self._do_restart, args=(icon,), daemon=True).start()
|
|
138
|
+
|
|
139
|
+
def _do_restart(self, icon: Any | None = None) -> None:
|
|
140
|
+
self._restarting = True
|
|
141
|
+
self.status_text = f"● 【重启中...】127.0.0.1:{self.port}"
|
|
142
|
+
if icon:
|
|
143
|
+
try:
|
|
144
|
+
icon.update_menu()
|
|
145
|
+
icon.notify("正在重启 MCP 服务,请稍候...", self.service_name)
|
|
146
|
+
except Exception:
|
|
147
|
+
pass
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
if self.proc:
|
|
151
|
+
try:
|
|
152
|
+
self.proc.terminate()
|
|
153
|
+
self.proc.wait(timeout=2)
|
|
154
|
+
except Exception:
|
|
155
|
+
try:
|
|
156
|
+
self.proc.kill()
|
|
157
|
+
except Exception:
|
|
158
|
+
pass
|
|
159
|
+
self.proc = None
|
|
160
|
+
|
|
161
|
+
self.kill_stale_server_only()
|
|
162
|
+
ensure_tunnel_running()
|
|
163
|
+
|
|
164
|
+
creationflags = subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0
|
|
165
|
+
env = {**os.environ, "PYTHONUTF8": "1", "PYTHONIOENCODING": "utf-8"}
|
|
166
|
+
|
|
167
|
+
self.runtime_dir.mkdir(parents=True, exist_ok=True)
|
|
168
|
+
log_file = open(self.runtime_dir / "server_error.log", "w", encoding="utf-8")
|
|
169
|
+
self.proc = subprocess.Popen(
|
|
170
|
+
self.server_cmd,
|
|
171
|
+
cwd=str(self.cwd),
|
|
172
|
+
creationflags=creationflags,
|
|
173
|
+
env=env,
|
|
174
|
+
stdout=log_file,
|
|
175
|
+
stderr=log_file,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
if self.job and self.proc and getattr(self.proc, "_handle", None):
|
|
179
|
+
try:
|
|
180
|
+
win32job.AssignProcessToJobObject(self.job, self.proc._handle)
|
|
181
|
+
except Exception:
|
|
182
|
+
pass
|
|
183
|
+
|
|
184
|
+
healthy = False
|
|
185
|
+
for _ in range(20):
|
|
186
|
+
time.sleep(0.5)
|
|
187
|
+
try:
|
|
188
|
+
req = urllib.request.Request(f"http://127.0.0.1:{self.port}/health")
|
|
189
|
+
with urllib.request.urlopen(req, timeout=1) as resp:
|
|
190
|
+
if resp.status == 200:
|
|
191
|
+
healthy = True
|
|
192
|
+
break
|
|
193
|
+
except Exception:
|
|
194
|
+
pass
|
|
195
|
+
|
|
196
|
+
if healthy:
|
|
197
|
+
self.status_text = f"● 【运行中】127.0.0.1:{self.port}"
|
|
198
|
+
if icon:
|
|
199
|
+
try:
|
|
200
|
+
icon.update_menu()
|
|
201
|
+
icon.notify(f"服务已重启完成并就绪 (端口 {self.port})", self.service_name)
|
|
202
|
+
except Exception:
|
|
203
|
+
pass
|
|
204
|
+
else:
|
|
205
|
+
self.status_text = f"● 【异常】端口 {self.port} 未响应"
|
|
206
|
+
if icon:
|
|
207
|
+
try:
|
|
208
|
+
icon.update_menu()
|
|
209
|
+
icon.notify("警告:服务重启超时,健康检查未通过!", self.service_name)
|
|
210
|
+
except Exception:
|
|
211
|
+
pass
|
|
212
|
+
finally:
|
|
213
|
+
self._restarting = False
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class BaseTrayApp:
|
|
217
|
+
"""Base class for Windows System Tray applications powering local MCP servers."""
|
|
218
|
+
|
|
219
|
+
def __init__(
|
|
220
|
+
self,
|
|
221
|
+
service_name: str,
|
|
222
|
+
base_dir: Path | str | None = None,
|
|
223
|
+
default_port: int = 8788,
|
|
224
|
+
icon_char: str = "M",
|
|
225
|
+
icon_color: str = "#00b894",
|
|
226
|
+
server_module: str | None = "app",
|
|
227
|
+
server_cmd: list[str] | None = None,
|
|
228
|
+
custom_menu_builder: Callable[[BaseTrayApp], list[Any]] | None = None,
|
|
229
|
+
):
|
|
230
|
+
if pystray is None:
|
|
231
|
+
raise RuntimeError(
|
|
232
|
+
"pystray and pillow are required for BaseTrayApp. Please install them: pip install pystray pillow pywin32"
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
self.service_name = service_name
|
|
236
|
+
self.base_dir = Path(base_dir).resolve() if base_dir else Path.cwd().resolve()
|
|
237
|
+
self.runtime_dir = get_runtime_dir(self.base_dir)
|
|
238
|
+
self.runtime_dir.mkdir(parents=True, exist_ok=True)
|
|
239
|
+
self.default_port = default_port
|
|
240
|
+
self.icon_char = icon_char
|
|
241
|
+
self.icon_color = icon_color
|
|
242
|
+
self.custom_menu_builder = custom_menu_builder
|
|
243
|
+
|
|
244
|
+
self.port = self.get_port()
|
|
245
|
+
self.mutex_name = f"Local\\{self.service_name.replace(' ', '_')}_{self.port}"
|
|
246
|
+
self.tray_pid_file = self.runtime_dir / ".tray.pid"
|
|
247
|
+
|
|
248
|
+
if server_cmd is not None:
|
|
249
|
+
self.server_cmd = server_cmd
|
|
250
|
+
else:
|
|
251
|
+
py_exe = get_server_python_exe()
|
|
252
|
+
mod = server_module or "app"
|
|
253
|
+
self.server_cmd = [py_exe, "-m", mod, "--no-tunnel"]
|
|
254
|
+
|
|
255
|
+
self.supervisor = ServerProcessSupervisor(
|
|
256
|
+
port=self.port,
|
|
257
|
+
cwd=self.base_dir,
|
|
258
|
+
server_cmd=self.server_cmd,
|
|
259
|
+
service_name=self.service_name,
|
|
260
|
+
)
|
|
261
|
+
self.icon: pystray.Icon | None = None
|
|
262
|
+
|
|
263
|
+
def get_current_env(self) -> DeploymentEnvConfig | None:
|
|
264
|
+
try:
|
|
265
|
+
return DeploymentEnvConfig.load(self.base_dir)
|
|
266
|
+
except Exception:
|
|
267
|
+
return None
|
|
268
|
+
|
|
269
|
+
def get_port(self) -> int:
|
|
270
|
+
cfg = self.get_current_env()
|
|
271
|
+
if cfg and cfg.port:
|
|
272
|
+
return cfg.port
|
|
273
|
+
return self.default_port
|
|
274
|
+
|
|
275
|
+
def get_api_key(self) -> str:
|
|
276
|
+
cfg = self.get_current_env()
|
|
277
|
+
if cfg and cfg.api_key:
|
|
278
|
+
return cfg.api_key
|
|
279
|
+
return os.getenv("MCP_API_KEY", "")
|
|
280
|
+
|
|
281
|
+
def get_public_url(self) -> str:
|
|
282
|
+
cfg = self.get_current_env()
|
|
283
|
+
if cfg and cfg.upstream_url:
|
|
284
|
+
return cfg.upstream_url
|
|
285
|
+
return os.getenv("TUNNEL_PUBLIC_URL", f"http://127.0.0.1:{self.port}/mcp")
|
|
286
|
+
|
|
287
|
+
def record_tray_pid(self) -> None:
|
|
288
|
+
try:
|
|
289
|
+
self.runtime_dir.mkdir(parents=True, exist_ok=True)
|
|
290
|
+
self.tray_pid_file.write_text(str(os.getpid()), encoding="utf-8")
|
|
291
|
+
except Exception:
|
|
292
|
+
pass
|
|
293
|
+
|
|
294
|
+
def cleanup_tray_pid(self) -> None:
|
|
295
|
+
for t_file in [self.tray_pid_file, self.base_dir / ".tray.pid"]:
|
|
296
|
+
try:
|
|
297
|
+
if t_file.exists():
|
|
298
|
+
t_file.unlink()
|
|
299
|
+
except Exception:
|
|
300
|
+
pass
|
|
301
|
+
|
|
302
|
+
def ensure_single_instance(self) -> Any:
|
|
303
|
+
if sys.platform != "win32":
|
|
304
|
+
return None
|
|
305
|
+
mutex = win32event.CreateMutex(None, False, self.mutex_name)
|
|
306
|
+
if win32api.GetLastError() == winerror.ERROR_ALREADY_EXISTS:
|
|
307
|
+
try:
|
|
308
|
+
win32api.MessageBox(
|
|
309
|
+
0,
|
|
310
|
+
f"【提示】{self.service_name} 已在系统托盘运行中!\n\n请在任务栏右下角通知区域查看图标。\n如需停止,可右键托盘图标退出或运行 stop.bat。",
|
|
311
|
+
self.service_name,
|
|
312
|
+
win32con.MB_ICONINFORMATION | win32con.MB_OK | win32con.MB_SYSTEMMODAL,
|
|
313
|
+
)
|
|
314
|
+
except Exception:
|
|
315
|
+
pass
|
|
316
|
+
sys.exit(0)
|
|
317
|
+
return mutex
|
|
318
|
+
|
|
319
|
+
def copy_to_clipboard(self, text: str) -> None:
|
|
320
|
+
if sys.platform != "win32":
|
|
321
|
+
return
|
|
322
|
+
try:
|
|
323
|
+
win32clipboard.OpenClipboard()
|
|
324
|
+
win32clipboard.EmptyClipboard()
|
|
325
|
+
win32clipboard.SetClipboardText(text, win32con.CF_UNICODETEXT)
|
|
326
|
+
win32clipboard.CloseClipboard()
|
|
327
|
+
except Exception as e:
|
|
328
|
+
print("Clipboard error:", e)
|
|
329
|
+
|
|
330
|
+
def message_box(
|
|
331
|
+
self,
|
|
332
|
+
text: str,
|
|
333
|
+
title: str | None = None,
|
|
334
|
+
flags: int = win32con.MB_ICONINFORMATION | win32con.MB_OK | win32con.MB_SYSTEMMODAL,
|
|
335
|
+
) -> int:
|
|
336
|
+
if sys.platform != "win32":
|
|
337
|
+
return 0
|
|
338
|
+
return win32api.MessageBox(0, text, title or self.service_name, flags)
|
|
339
|
+
|
|
340
|
+
def choose_folder(self, title: str = "选择文件夹") -> str | None:
|
|
341
|
+
"""Open native high-DPI directory chooser dialog with Tkinter or PowerShell fallback."""
|
|
342
|
+
try:
|
|
343
|
+
import tkinter as tk
|
|
344
|
+
from tkinter import filedialog
|
|
345
|
+
root = tk.Tk()
|
|
346
|
+
root.withdraw()
|
|
347
|
+
root.attributes("-topmost", True)
|
|
348
|
+
|
|
349
|
+
ico_path = self.runtime_dir / "app.ico"
|
|
350
|
+
if not ico_path.exists():
|
|
351
|
+
try:
|
|
352
|
+
self.create_tray_icon().save(ico_path, format="ICO")
|
|
353
|
+
except Exception:
|
|
354
|
+
pass
|
|
355
|
+
if ico_path.exists():
|
|
356
|
+
try:
|
|
357
|
+
root.iconbitmap(str(ico_path))
|
|
358
|
+
except Exception:
|
|
359
|
+
pass
|
|
360
|
+
|
|
361
|
+
folder = filedialog.askdirectory(title=title, parent=root)
|
|
362
|
+
root.destroy()
|
|
363
|
+
if folder:
|
|
364
|
+
return folder
|
|
365
|
+
except Exception:
|
|
366
|
+
pass
|
|
367
|
+
|
|
368
|
+
try:
|
|
369
|
+
ps_cmd = (
|
|
370
|
+
'Add-Type -AssemblyName System.Windows.Forms; '
|
|
371
|
+
'$fb = New-Object System.Windows.Forms.FolderBrowserDialog; '
|
|
372
|
+
f'$fb.Description = "{title}"; '
|
|
373
|
+
'$fb.ShowNewFolderButton = $true; '
|
|
374
|
+
'if ($fb.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) { '
|
|
375
|
+
' Write-Output $fb.SelectedPath '
|
|
376
|
+
'}'
|
|
377
|
+
)
|
|
378
|
+
creationflags = subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0
|
|
379
|
+
res = subprocess.run(
|
|
380
|
+
["powershell", "-NoProfile", "-Command", ps_cmd],
|
|
381
|
+
capture_output=True,
|
|
382
|
+
text=True,
|
|
383
|
+
creationflags=creationflags,
|
|
384
|
+
)
|
|
385
|
+
out = res.stdout.strip()
|
|
386
|
+
return out if out else None
|
|
387
|
+
except Exception:
|
|
388
|
+
return None
|
|
389
|
+
|
|
390
|
+
def create_tray_icon(self) -> Image.Image:
|
|
391
|
+
"""Create round dynamic tray icon with customizable letter and background color."""
|
|
392
|
+
img = Image.new("RGBA", (64, 64), (0, 0, 0, 0))
|
|
393
|
+
draw = ImageDraw.Draw(img)
|
|
394
|
+
draw.ellipse((4, 4, 60, 60), fill=self.icon_color)
|
|
395
|
+
try:
|
|
396
|
+
font = ImageFont.truetype("arial.ttf", 36)
|
|
397
|
+
draw.text((18, 12), self.icon_char, fill="white", font=font)
|
|
398
|
+
except Exception:
|
|
399
|
+
draw.text((22, 16), self.icon_char, fill="white")
|
|
400
|
+
return img
|
|
401
|
+
|
|
402
|
+
def build_custom_menu(self) -> list[Any]:
|
|
403
|
+
"""Override this method or pass custom_menu_builder to inject business-specific menu items."""
|
|
404
|
+
if self.custom_menu_builder:
|
|
405
|
+
return self.custom_menu_builder(self)
|
|
406
|
+
return []
|
|
407
|
+
|
|
408
|
+
# Standard actions
|
|
409
|
+
def on_copy_url(self, icon: Any, _menu_item: Any) -> None:
|
|
410
|
+
url = self.get_public_url()
|
|
411
|
+
self.copy_to_clipboard(url)
|
|
412
|
+
try:
|
|
413
|
+
icon.notify("已复制 MCP 服务器网址:\n" + url, self.service_name)
|
|
414
|
+
except Exception:
|
|
415
|
+
pass
|
|
416
|
+
|
|
417
|
+
def on_copy_key(self, icon: Any, _menu_item: Any) -> None:
|
|
418
|
+
key = self.get_api_key()
|
|
419
|
+
self.copy_to_clipboard(key)
|
|
420
|
+
try:
|
|
421
|
+
icon.notify("已复制访问密匙:\n" + key, self.service_name)
|
|
422
|
+
except Exception:
|
|
423
|
+
pass
|
|
424
|
+
|
|
425
|
+
def on_rotate_key(self, icon: Any, _menu_item: Any) -> None:
|
|
426
|
+
new_key = f"sec_{secrets.token_urlsafe(16)}"
|
|
427
|
+
env_file = self.base_dir / ".env"
|
|
428
|
+
update_env_variable("MCP_API_KEY", new_key, env_file)
|
|
429
|
+
os.environ["MCP_API_KEY"] = new_key
|
|
430
|
+
self.copy_to_clipboard(new_key)
|
|
431
|
+
try:
|
|
432
|
+
icon.notify(
|
|
433
|
+
f"已生成新访问密匙并复制到剪贴板!\n{new_key}\n正在平滑重启服务以生效...",
|
|
434
|
+
self.service_name,
|
|
435
|
+
)
|
|
436
|
+
except Exception:
|
|
437
|
+
pass
|
|
438
|
+
self.supervisor.restart_async(icon)
|
|
439
|
+
|
|
440
|
+
def on_open_health(self, icon: Any, _menu_item: Any) -> None:
|
|
441
|
+
webbrowser.open(f"http://127.0.0.1:{self.port}/health")
|
|
442
|
+
|
|
443
|
+
def on_restart(self, icon: Any, _menu_item: Any) -> None:
|
|
444
|
+
self.supervisor.restart_async(icon)
|
|
445
|
+
|
|
446
|
+
def on_exit(self, icon: Any, _menu_item: Any) -> None:
|
|
447
|
+
def do_exit():
|
|
448
|
+
try:
|
|
449
|
+
icon.title = f"{self.service_name} (正在退出...)"
|
|
450
|
+
icon.notify(f"{self.service_name} 正在停止服务,请稍候...", self.service_name)
|
|
451
|
+
except Exception:
|
|
452
|
+
pass
|
|
453
|
+
try:
|
|
454
|
+
self.supervisor.stop()
|
|
455
|
+
except Exception:
|
|
456
|
+
pass
|
|
457
|
+
self.cleanup_tray_pid()
|
|
458
|
+
try:
|
|
459
|
+
icon.visible = False
|
|
460
|
+
icon.stop()
|
|
461
|
+
except Exception:
|
|
462
|
+
pass
|
|
463
|
+
os._exit(0)
|
|
464
|
+
|
|
465
|
+
threading.Thread(target=do_exit, daemon=True).start()
|
|
466
|
+
|
|
467
|
+
def build_menu(self) -> pystray.Menu:
|
|
468
|
+
items = []
|
|
469
|
+
|
|
470
|
+
# 1. Status header
|
|
471
|
+
items.append(item(lambda _: self.supervisor.status_text, lambda: None, enabled=False))
|
|
472
|
+
items.append(pystray.Menu.SEPARATOR)
|
|
473
|
+
|
|
474
|
+
# 2. Custom business menu items slot
|
|
475
|
+
custom_items = self.build_custom_menu()
|
|
476
|
+
if custom_items:
|
|
477
|
+
items.extend(custom_items)
|
|
478
|
+
items.append(pystray.Menu.SEPARATOR)
|
|
479
|
+
|
|
480
|
+
# 3. Standard platform tools
|
|
481
|
+
items.append(item("复制 MCP 服务器网址", self.on_copy_url))
|
|
482
|
+
items.append(item("复制访问密匙", self.on_copy_key))
|
|
483
|
+
items.append(item("重设访问密匙", self.on_rotate_key))
|
|
484
|
+
items.append(item("健康检查", self.on_open_health))
|
|
485
|
+
items.append(pystray.Menu.SEPARATOR)
|
|
486
|
+
|
|
487
|
+
# 4. Lifecycle controls
|
|
488
|
+
items.append(item("重启", self.on_restart))
|
|
489
|
+
items.append(item("退出", self.on_exit))
|
|
490
|
+
|
|
491
|
+
return pystray.Menu(*items)
|
|
492
|
+
|
|
493
|
+
def run(self) -> None:
|
|
494
|
+
mutex = self.ensure_single_instance()
|
|
495
|
+
|
|
496
|
+
if is_port_in_use(self.port):
|
|
497
|
+
try:
|
|
498
|
+
self.message_box(
|
|
499
|
+
f"{self.service_name} 已在运行中 (端口 {self.port} 已被占用)。\n\n如需切换为无黑框托盘常驻模式,请先运行 stop.bat 停止现有服务后再启动。",
|
|
500
|
+
flags=win32con.MB_ICONINFORMATION | win32con.MB_OK | win32con.MB_SYSTEMMODAL,
|
|
501
|
+
)
|
|
502
|
+
except Exception:
|
|
503
|
+
pass
|
|
504
|
+
sys.exit(0)
|
|
505
|
+
|
|
506
|
+
self.record_tray_pid()
|
|
507
|
+
atexit.register(self.cleanup_tray_pid)
|
|
508
|
+
|
|
509
|
+
self.supervisor.start()
|
|
510
|
+
|
|
511
|
+
self.icon = pystray.Icon(
|
|
512
|
+
name=f"{self.service_name.lower().replace(' ', '_')}_tray",
|
|
513
|
+
icon=self.create_tray_icon(),
|
|
514
|
+
title=f"{self.service_name} (端口 {self.port})",
|
|
515
|
+
menu=self.build_menu(),
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
try:
|
|
519
|
+
self.icon.run()
|
|
520
|
+
except Exception as e:
|
|
521
|
+
import traceback
|
|
522
|
+
try:
|
|
523
|
+
(self.runtime_dir / "tray_debug.log").write_text(
|
|
524
|
+
f"Exception: {e}\n{traceback.format_exc()}", encoding="utf-8"
|
|
525
|
+
)
|
|
526
|
+
except Exception:
|
|
527
|
+
pass
|
|
528
|
+
finally:
|
|
529
|
+
try:
|
|
530
|
+
self.supervisor.stop()
|
|
531
|
+
except Exception:
|
|
532
|
+
pass
|
|
533
|
+
self.cleanup_tray_pid()
|
|
534
|
+
os._exit(0)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcp-server-core
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Universal MCP Server Framework: Streamable HTTP ASGI, Cloudflare Tunnel, OAuth 2.1 & Process Supervisor
|
|
5
5
|
Author: yimmr
|
|
6
6
|
License-Expression: MIT
|
|
@@ -19,6 +19,9 @@ Requires-Dist: uvicorn>=0.27.0
|
|
|
19
19
|
Requires-Dist: pywin32>=306; platform_system == "Windows"
|
|
20
20
|
Provides-Extra: test
|
|
21
21
|
Requires-Dist: httpx>=0.27.0; extra == "test"
|
|
22
|
+
Provides-Extra: tray
|
|
23
|
+
Requires-Dist: pystray>=0.19.5; extra == "tray"
|
|
24
|
+
Requires-Dist: pillow>=10.0.0; extra == "tray"
|
|
22
25
|
Dynamic: license-file
|
|
23
26
|
|
|
24
27
|
# MCP Server Core 🚀
|
|
@@ -4,16 +4,22 @@ pyproject.toml
|
|
|
4
4
|
mcp_framework/__init__.py
|
|
5
5
|
mcp_framework/env.py
|
|
6
6
|
mcp_framework/http_server.py
|
|
7
|
+
mcp_framework/launcher.py
|
|
7
8
|
mcp_framework/oauth.py
|
|
8
9
|
mcp_framework/process.py
|
|
10
|
+
mcp_framework/response.py
|
|
9
11
|
mcp_framework/server.py
|
|
12
|
+
mcp_framework/tray.py
|
|
10
13
|
mcp_framework/tunnel.py
|
|
11
14
|
mcp_server_core/__init__.py
|
|
12
15
|
mcp_server_core/env.py
|
|
13
16
|
mcp_server_core/http_server.py
|
|
17
|
+
mcp_server_core/launcher.py
|
|
14
18
|
mcp_server_core/oauth.py
|
|
15
19
|
mcp_server_core/process.py
|
|
20
|
+
mcp_server_core/response.py
|
|
16
21
|
mcp_server_core/server.py
|
|
22
|
+
mcp_server_core/tray.py
|
|
17
23
|
mcp_server_core/tunnel.py
|
|
18
24
|
mcp_server_core.egg-info/PKG-INFO
|
|
19
25
|
mcp_server_core.egg-info/SOURCES.txt
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "mcp-server-core"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "Universal MCP Server Framework: Streamable HTTP ASGI, Cloudflare Tunnel, OAuth 2.1 & Process Supervisor"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -31,6 +31,10 @@ dependencies = [
|
|
|
31
31
|
test = [
|
|
32
32
|
"httpx>=0.27.0",
|
|
33
33
|
]
|
|
34
|
+
tray = [
|
|
35
|
+
"pystray>=0.19.5",
|
|
36
|
+
"pillow>=10.0.0",
|
|
37
|
+
]
|
|
34
38
|
|
|
35
39
|
[tool.setuptools.packages.find]
|
|
36
40
|
where = ["."]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mcp_server_core-0.2.2 → mcp_server_core-0.3.0}/mcp_server_core.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|