fastmcp-app-launcher 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Codex MCP
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,3 @@
1
+ include LICENSE
2
+ include README.md
3
+ include mcp-apps.example.json
@@ -0,0 +1,184 @@
1
+ Metadata-Version: 2.4
2
+ Name: fastmcp-app-launcher
3
+ Version: 0.2.0
4
+ Summary: 基于 fastmcp 的跨平台应用启动 MCP 服务
5
+ Author-email: Codex MCP <codex@example.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2025 Codex MCP
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/fjc/fastmcp-app-launcher
29
+ Project-URL: Repository, https://github.com/fjc/fastmcp-app-launcher
30
+ Project-URL: Bug Tracker, https://github.com/fjc/fastmcp-app-launcher/issues
31
+ Keywords: mcp,fastmcp,automation,launcher,productivity
32
+ Classifier: Development Status :: 4 - Beta
33
+ Classifier: Intended Audience :: Developers
34
+ Classifier: License :: OSI Approved :: MIT License
35
+ Classifier: Programming Language :: Python :: 3
36
+ Classifier: Programming Language :: Python :: 3.10
37
+ Classifier: Programming Language :: Python :: 3.11
38
+ Classifier: Programming Language :: Python :: 3.12
39
+ Classifier: Operating System :: OS Independent
40
+ Classifier: Topic :: Desktop Environment
41
+ Classifier: Topic :: System :: Monitoring
42
+ Requires-Python: >=3.10
43
+ Description-Content-Type: text/markdown
44
+ License-File: LICENSE
45
+ Requires-Dist: fastmcp>=2.12.0
46
+ Requires-Dist: pydantic>=2.9.0
47
+ Provides-Extra: windows
48
+ Requires-Dist: pywin32>=305; extra == "windows"
49
+ Requires-Dist: pywinauto>=0.6.8; extra == "windows"
50
+ Provides-Extra: dev
51
+ Requires-Dist: pytest>=8.3.0; extra == "dev"
52
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
53
+ Dynamic: license-file
54
+
55
+ # FastMCP 应用启动器
56
+
57
+ 使用 [fastmcp](https://github.com/modelcontextprotocol/fastmcp) 构建的跨平台 Model Context Protocol 服务,可通过 MCP 客户端(如 Claude Desktop)列出并打开本地应用,针对 Windows 托盘应用提供热键与窗口激活增强。
58
+
59
+ ## 安装
60
+
61
+ ```bash
62
+ pip install fastmcp-app-launcher
63
+ # 或使用 uv
64
+ uv pip install fastmcp-app-launcher
65
+ ```
66
+
67
+ 安装完成后终端会提供 `app-launcher-mcp` 命令,可直接在 MCP 客户端中调用。
68
+
69
+ ## 目录结构
70
+
71
+ ```
72
+ fastmcp_app_launcher/
73
+ ├── mcp-apps.example.json # 示例配置
74
+ ├── pyproject.toml # uv/fastmcp 配置
75
+ ├── README.md
76
+ └── src/app_launcher_mcp
77
+ ├── __init__.py
78
+ ├── activator.py # Windows 激活逻辑
79
+ ├── apps.py # 配置加载与搜索
80
+ ├── server.py # FastMCP 入口
81
+ └── service.py # 业务封装
82
+ ```
83
+
84
+ ## 快速开始
85
+
86
+ 1. **创建 uv 虚拟环境(已在 `.venv` 目录演示,如需重新创建可按需运行):**
87
+
88
+ ```bash
89
+ uv venv .venv
90
+ source .venv/bin/activate # Windows 使用 .venv\\Scripts\\activate
91
+ ```
92
+
93
+ 2. **安装依赖:**
94
+
95
+ ```bash
96
+ uv sync # 或 uv pip install -r pyproject.toml
97
+ # Windows 如需托盘/热键支持,安装额外依赖:
98
+ uv pip install .[windows]
99
+ ```
100
+
101
+ 3. **运行 MCP 服务器(stdio 模式):**
102
+
103
+ ```bash
104
+ uv run app-launcher-mcp
105
+ # 或指定参数:
106
+ uv run app-launcher-mcp --no-auto-discover # 禁用自动发现
107
+ uv run app-launcher-mcp --transport stdio # 指定 Transport
108
+ ```
109
+
110
+ 4. **在 MCP 客户端中配置:**
111
+
112
+ ```json
113
+ {
114
+ "command": "uv",
115
+ "args": ["run", "--directory", "/Users/fjc/Desktop/项目/skills/python/fastmcp_app_launcher", "app-launcher-mcp"],
116
+ "env": {
117
+ "MCP_APPS": "QQ;C:/Program Files/Tencent/QQ/Bin/QQ.exe;qq,tencent;Ctrl+Alt+Z"
118
+ }
119
+ }
120
+ ```
121
+
122
+ ## 应用配置
123
+
124
+ - **环境变量 `MCP_APPS`**:支持 JSON 数组或 `name;path;keywords;hotkey|...` 简写。
125
+ - **配置文件**:程序在以下路径按顺序查找(找到即停止):
126
+ 1. `~/.mcp-apps.json`
127
+ 2. `<工作目录>/mcp-apps.json`
128
+ 3. `~/.config/mcp-apps/config.json`
129
+ - **自动发现**:
130
+ - Windows:内置 QQ、微信、VS Code 等常见路径,并扫描开始菜单 `.lnk` 快捷方式(目录可通过环境变量 `MCP_WINDOWS_SHORTCUT_DIRS` 指定,使用 `os.pathsep` 分隔)。
131
+ - macOS:默认遍历 `/Applications`、`/System/Applications`、`~/Applications` 等目录,可使用 `MCP_MAC_APP_DIRS` 覆盖;扫描数量由 `MCP_AUTO_DISCOVER_LIMIT` 控制。
132
+
133
+ 可参考 `mcp-apps.example.json` 快速自定义列表。
134
+
135
+ ## 可用工具
136
+
137
+ | 工具名 | 描述 |
138
+ | ------ | ---- |
139
+ | `list_apps_tool` | 返回当前注册的全部应用及数量 |
140
+ | `open_app_tool` | 参数 `app_name`、可选 `reload_before`,用于打开或激活应用 |
141
+ | `reload_apps_tool` | 重新加载配置/环境,并返回最新应用数 |
142
+
143
+ 工具返回 `structuredContent` 中包含的字段示例:
144
+
145
+ ```json
146
+ {
147
+ "query": "wechat",
148
+ "app": {"name": "微信", "path": "...", "hotkey": "Ctrl+Alt+W"},
149
+ "execution": {"success": true, "message": "通过热键激活窗口成功"}
150
+ }
151
+ ```
152
+
153
+ ## Windows 特性
154
+
155
+ - **托盘激活**:若安装 `pywin32`,服务会尝试发送热键、定位现有窗口并置前。
156
+ - **pywinauto 支持**:可选安装以在托盘图标无法响应热键时通过 UI 自动化置顶窗口。
157
+ - **自动回退**:当相关库缺失时,会直接调用 `os.startfile` 或 `subprocess.Popen` 启动新实例。
158
+
159
+ ## 故障排查
160
+
161
+ - `未找到匹配的应用`:检查 `MCP_APPS` / 配置文件是否已加载,或在调用 `open_app_tool` 前执行 `reload_apps_tool`。
162
+ - `pywin32 未安装`:在 Windows 环境执行 `uv pip install .[windows]`。
163
+ - `权限相关错误`:Mac/Linux 打开 `.app` 时需确认拥有执行权限,可通过 `chmod +x` 处理。
164
+
165
+ 欢迎根据需要扩展更多工具,例如批量更新应用、动态注册等。
166
+
167
+ ## 发布到 PyPI
168
+
169
+ 1. 更新 `pyproject.toml` 中的版本号,并确保代码/文档同步。
170
+ 2. 安装构建工具并生成分发包:
171
+
172
+ ```bash
173
+ uv pip install --upgrade build twine
174
+ uv build
175
+ twine check dist/*
176
+ ```
177
+
178
+ 3. 使用 PyPI API Token 上传:
179
+
180
+ ```bash
181
+ twine upload dist/*
182
+ ```
183
+
184
+ 发布成功后,用户即可通过 `pip install fastmcp-app-launcher` 获取最新版本。
@@ -0,0 +1,130 @@
1
+ # FastMCP 应用启动器
2
+
3
+ 使用 [fastmcp](https://github.com/modelcontextprotocol/fastmcp) 构建的跨平台 Model Context Protocol 服务,可通过 MCP 客户端(如 Claude Desktop)列出并打开本地应用,针对 Windows 托盘应用提供热键与窗口激活增强。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ pip install fastmcp-app-launcher
9
+ # 或使用 uv
10
+ uv pip install fastmcp-app-launcher
11
+ ```
12
+
13
+ 安装完成后终端会提供 `app-launcher-mcp` 命令,可直接在 MCP 客户端中调用。
14
+
15
+ ## 目录结构
16
+
17
+ ```
18
+ fastmcp_app_launcher/
19
+ ├── mcp-apps.example.json # 示例配置
20
+ ├── pyproject.toml # uv/fastmcp 配置
21
+ ├── README.md
22
+ └── src/app_launcher_mcp
23
+ ├── __init__.py
24
+ ├── activator.py # Windows 激活逻辑
25
+ ├── apps.py # 配置加载与搜索
26
+ ├── server.py # FastMCP 入口
27
+ └── service.py # 业务封装
28
+ ```
29
+
30
+ ## 快速开始
31
+
32
+ 1. **创建 uv 虚拟环境(已在 `.venv` 目录演示,如需重新创建可按需运行):**
33
+
34
+ ```bash
35
+ uv venv .venv
36
+ source .venv/bin/activate # Windows 使用 .venv\\Scripts\\activate
37
+ ```
38
+
39
+ 2. **安装依赖:**
40
+
41
+ ```bash
42
+ uv sync # 或 uv pip install -r pyproject.toml
43
+ # Windows 如需托盘/热键支持,安装额外依赖:
44
+ uv pip install .[windows]
45
+ ```
46
+
47
+ 3. **运行 MCP 服务器(stdio 模式):**
48
+
49
+ ```bash
50
+ uv run app-launcher-mcp
51
+ # 或指定参数:
52
+ uv run app-launcher-mcp --no-auto-discover # 禁用自动发现
53
+ uv run app-launcher-mcp --transport stdio # 指定 Transport
54
+ ```
55
+
56
+ 4. **在 MCP 客户端中配置:**
57
+
58
+ ```json
59
+ {
60
+ "command": "uv",
61
+ "args": ["run", "--directory", "/Users/fjc/Desktop/项目/skills/python/fastmcp_app_launcher", "app-launcher-mcp"],
62
+ "env": {
63
+ "MCP_APPS": "QQ;C:/Program Files/Tencent/QQ/Bin/QQ.exe;qq,tencent;Ctrl+Alt+Z"
64
+ }
65
+ }
66
+ ```
67
+
68
+ ## 应用配置
69
+
70
+ - **环境变量 `MCP_APPS`**:支持 JSON 数组或 `name;path;keywords;hotkey|...` 简写。
71
+ - **配置文件**:程序在以下路径按顺序查找(找到即停止):
72
+ 1. `~/.mcp-apps.json`
73
+ 2. `<工作目录>/mcp-apps.json`
74
+ 3. `~/.config/mcp-apps/config.json`
75
+ - **自动发现**:
76
+ - Windows:内置 QQ、微信、VS Code 等常见路径,并扫描开始菜单 `.lnk` 快捷方式(目录可通过环境变量 `MCP_WINDOWS_SHORTCUT_DIRS` 指定,使用 `os.pathsep` 分隔)。
77
+ - macOS:默认遍历 `/Applications`、`/System/Applications`、`~/Applications` 等目录,可使用 `MCP_MAC_APP_DIRS` 覆盖;扫描数量由 `MCP_AUTO_DISCOVER_LIMIT` 控制。
78
+
79
+ 可参考 `mcp-apps.example.json` 快速自定义列表。
80
+
81
+ ## 可用工具
82
+
83
+ | 工具名 | 描述 |
84
+ | ------ | ---- |
85
+ | `list_apps_tool` | 返回当前注册的全部应用及数量 |
86
+ | `open_app_tool` | 参数 `app_name`、可选 `reload_before`,用于打开或激活应用 |
87
+ | `reload_apps_tool` | 重新加载配置/环境,并返回最新应用数 |
88
+
89
+ 工具返回 `structuredContent` 中包含的字段示例:
90
+
91
+ ```json
92
+ {
93
+ "query": "wechat",
94
+ "app": {"name": "微信", "path": "...", "hotkey": "Ctrl+Alt+W"},
95
+ "execution": {"success": true, "message": "通过热键激活窗口成功"}
96
+ }
97
+ ```
98
+
99
+ ## Windows 特性
100
+
101
+ - **托盘激活**:若安装 `pywin32`,服务会尝试发送热键、定位现有窗口并置前。
102
+ - **pywinauto 支持**:可选安装以在托盘图标无法响应热键时通过 UI 自动化置顶窗口。
103
+ - **自动回退**:当相关库缺失时,会直接调用 `os.startfile` 或 `subprocess.Popen` 启动新实例。
104
+
105
+ ## 故障排查
106
+
107
+ - `未找到匹配的应用`:检查 `MCP_APPS` / 配置文件是否已加载,或在调用 `open_app_tool` 前执行 `reload_apps_tool`。
108
+ - `pywin32 未安装`:在 Windows 环境执行 `uv pip install .[windows]`。
109
+ - `权限相关错误`:Mac/Linux 打开 `.app` 时需确认拥有执行权限,可通过 `chmod +x` 处理。
110
+
111
+ 欢迎根据需要扩展更多工具,例如批量更新应用、动态注册等。
112
+
113
+ ## 发布到 PyPI
114
+
115
+ 1. 更新 `pyproject.toml` 中的版本号,并确保代码/文档同步。
116
+ 2. 安装构建工具并生成分发包:
117
+
118
+ ```bash
119
+ uv pip install --upgrade build twine
120
+ uv build
121
+ twine check dist/*
122
+ ```
123
+
124
+ 3. 使用 PyPI API Token 上传:
125
+
126
+ ```bash
127
+ twine upload dist/*
128
+ ```
129
+
130
+ 发布成功后,用户即可通过 `pip install fastmcp-app-launcher` 获取最新版本。
@@ -0,0 +1,15 @@
1
+ {
2
+ "apps": [
3
+ {
4
+ "name": "微信",
5
+ "path": "C:/Program Files/Tencent/WeChat/WeChat.exe",
6
+ "keywords": ["wechat", "weixin", "微信"],
7
+ "hotkey": "Ctrl+Alt+W"
8
+ },
9
+ {
10
+ "name": "VS Code",
11
+ "path": "C:/Program Files/Microsoft VS Code/Code.exe",
12
+ "keywords": ["code", "vscode", "编辑器"]
13
+ }
14
+ ]
15
+ }
@@ -0,0 +1,59 @@
1
+ [project]
2
+ name = "fastmcp-app-launcher"
3
+ version = "0.2.0"
4
+ description = "基于 fastmcp 的跨平台应用启动 MCP 服务"
5
+ readme = "README.md"
6
+ authors = [{ name = "Codex MCP", email = "codex@example.com" }]
7
+ license = { file = "LICENSE" }
8
+ requires-python = ">=3.10"
9
+ keywords = ["mcp", "fastmcp", "automation", "launcher", "productivity"]
10
+ classifiers = [
11
+ "Development Status :: 4 - Beta",
12
+ "Intended Audience :: Developers",
13
+ "License :: OSI Approved :: MIT License",
14
+ "Programming Language :: Python :: 3",
15
+ "Programming Language :: Python :: 3.10",
16
+ "Programming Language :: Python :: 3.11",
17
+ "Programming Language :: Python :: 3.12",
18
+ "Operating System :: OS Independent",
19
+ "Topic :: Desktop Environment",
20
+ "Topic :: System :: Monitoring",
21
+ ]
22
+ dependencies = [
23
+ "fastmcp>=2.12.0",
24
+ "pydantic>=2.9.0",
25
+ ]
26
+
27
+ [project.urls]
28
+ Homepage = "https://github.com/fjc/fastmcp-app-launcher"
29
+ Repository = "https://github.com/fjc/fastmcp-app-launcher"
30
+ "Bug Tracker" = "https://github.com/fjc/fastmcp-app-launcher/issues"
31
+
32
+ [project.optional-dependencies]
33
+ windows = [
34
+ "pywin32>=305",
35
+ "pywinauto>=0.6.8",
36
+ ]
37
+ dev = [
38
+ "pytest>=8.3.0",
39
+ "pytest-asyncio>=0.23.0",
40
+ ]
41
+
42
+ [project.scripts]
43
+ app-launcher-mcp = "app_launcher_mcp.server:main"
44
+
45
+ [tool.uv]
46
+ package = true
47
+
48
+ [tool.setuptools]
49
+ include-package-data = true
50
+
51
+ [tool.setuptools.packages.find]
52
+ where = ["src"]
53
+
54
+ [tool.setuptools.package-data]
55
+
56
+ [dependency-groups]
57
+ dev = [
58
+ "build>=1.3.0",
59
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,12 @@
1
+ """FastMCP 应用启动 MCP 服务。"""
2
+
3
+ from importlib import metadata
4
+
5
+ from .server import main
6
+
7
+ try: # pragma: no cover
8
+ __version__ = metadata.version("fastmcp-app-launcher")
9
+ except metadata.PackageNotFoundError: # pragma: no cover
10
+ __version__ = "0.0.0"
11
+
12
+ __all__ = ["main", "__version__"]
@@ -0,0 +1,198 @@
1
+ """跨平台应用激活逻辑。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import os
7
+ import platform
8
+ import subprocess
9
+ import time
10
+ from pathlib import Path
11
+ from typing import Any, Dict
12
+
13
+ from .apps import AppInfo
14
+
15
+ LOGGER = logging.getLogger(__name__)
16
+ SYSTEM = platform.system().lower()
17
+
18
+ if SYSTEM == "windows":
19
+ try: # pragma: no cover - Windows 才能导入
20
+ import win32api # type: ignore
21
+ import win32con # type: ignore
22
+ import win32gui # type: ignore
23
+ import win32process # type: ignore
24
+ HAS_WIN32 = True
25
+ except Exception: # pragma: no cover
26
+ HAS_WIN32 = False
27
+
28
+ try: # pragma: no cover
29
+ from pywinauto import Application # type: ignore
30
+
31
+ HAS_PYWINAUTO = True
32
+ except Exception: # pragma: no cover
33
+ HAS_PYWINAUTO = False
34
+ else: # 非 Windows 平台无需这些依赖
35
+ HAS_WIN32 = False
36
+ HAS_PYWINAUTO = False
37
+
38
+
39
+ class WindowsTrayActivator:
40
+ """使用 win32 API 激活托盘/后台应用。"""
41
+
42
+ def __init__(self) -> None:
43
+ self.steps: list[str] = []
44
+
45
+ def activate(self, app: AppInfo) -> Dict[str, Any]: # pragma: no cover - Windows 特有
46
+ self.steps.clear()
47
+ if not HAS_WIN32:
48
+ self.steps.append("pywin32 未安装,回退到直接启动")
49
+ launched = self.launch_process(app.path)
50
+ return self._result(launched, "pywin32 不可用,已直接启动应用")
51
+
52
+ process_name = Path(app.path).stem + ".exe"
53
+
54
+ if app.hotkey and self.send_hotkey(app.hotkey):
55
+ self.steps.append(f"已发送热键 {app.hotkey}")
56
+ if self.wait_for_window(process_name):
57
+ return self._result(True, "通过热键激活窗口成功")
58
+
59
+ hwnd = self.find_window_by_process(process_name)
60
+ if hwnd and self.bring_window_to_front(hwnd):
61
+ return self._result(True, "检测到运行中的窗口并置前")
62
+
63
+ if self.activate_with_pywinauto(process_name, app.name):
64
+ return self._result(True, "通过 pywinauto 激活窗口")
65
+
66
+ launched = self.launch_process(app.path)
67
+ if launched:
68
+ return self._result(True, "未检测到已运行实例,已重新启动应用")
69
+
70
+ return self._result(False, "无法激活或启动应用")
71
+
72
+ # --- win32 helpers -------------------------------------------------
73
+ @staticmethod
74
+ def send_hotkey(hotkey: str) -> bool:
75
+ try:
76
+ keys = [k.strip() for k in hotkey.split("+") if k.strip()]
77
+ modifiers = []
78
+ key_code = None
79
+ for key in keys:
80
+ upper = key.lower()
81
+ if upper == "ctrl":
82
+ modifiers.append(win32con.VK_CONTROL)
83
+ elif upper == "alt":
84
+ modifiers.append(win32con.VK_MENU)
85
+ elif upper == "shift":
86
+ modifiers.append(win32con.VK_SHIFT)
87
+ else:
88
+ key_code = upper
89
+ if not key_code:
90
+ return False
91
+
92
+ for mod in modifiers:
93
+ win32api.keybd_event(mod, 0, 0, 0)
94
+
95
+ vk = ord(key_code.upper())
96
+ win32api.keybd_event(vk, 0, 0, 0)
97
+ time.sleep(0.05)
98
+ win32api.keybd_event(vk, 0, win32con.KEYEVENTF_KEYUP, 0)
99
+ for mod in reversed(modifiers):
100
+ win32api.keybd_event(mod, 0, win32con.KEYEVENTF_KEYUP, 0)
101
+ return True
102
+ except Exception as exc: # pragma: no cover
103
+ LOGGER.warning("发送热键失败: %s", exc)
104
+ return False
105
+
106
+ @staticmethod
107
+ def find_window_by_process(process_name: str):
108
+ hwnds: list[int] = []
109
+
110
+ def callback(hwnd, _):
111
+ if not win32gui.IsWindowVisible(hwnd):
112
+ return True
113
+ _, pid = win32process.GetWindowThreadProcessId(hwnd)
114
+ try:
115
+ handle = win32api.OpenProcess(
116
+ win32con.PROCESS_QUERY_INFORMATION | win32con.PROCESS_VM_READ,
117
+ False,
118
+ pid,
119
+ )
120
+ exe_name = win32process.GetModuleFileNameEx(handle, 0)
121
+ if process_name.lower() in exe_name.lower():
122
+ hwnds.append(hwnd)
123
+ except Exception:
124
+ pass
125
+ return True
126
+
127
+ win32gui.EnumWindows(callback, None)
128
+ return hwnds[0] if hwnds else None
129
+
130
+ @staticmethod
131
+ def bring_window_to_front(hwnd: int) -> bool:
132
+ try:
133
+ if win32gui.IsIconic(hwnd):
134
+ win32gui.ShowWindow(hwnd, win32con.SW_RESTORE)
135
+ win32gui.SetForegroundWindow(hwnd)
136
+ return True
137
+ except Exception as exc: # pragma: no cover
138
+ LOGGER.warning("置顶窗口失败: %s", exc)
139
+ return False
140
+
141
+ @staticmethod
142
+ def activate_with_pywinauto(process_name: str, app_name: str) -> bool:
143
+ if not HAS_PYWINAUTO:
144
+ return False
145
+ try:
146
+ app = Application().connect(path=process_name)
147
+ windows = app.windows()
148
+ if windows:
149
+ windows[0].set_focus()
150
+ LOGGER.info("通过 pywinauto 激活 %s", app_name)
151
+ return True
152
+ except Exception as exc: # pragma: no cover
153
+ LOGGER.warning("pywinauto 激活失败: %s", exc)
154
+ return False
155
+
156
+ @staticmethod
157
+ def wait_for_window(process_name: str, timeout: float = 1.5) -> bool:
158
+ end = time.time() + timeout
159
+ while time.time() < end:
160
+ hwnd = WindowsTrayActivator.find_window_by_process(process_name)
161
+ if hwnd:
162
+ return True
163
+ time.sleep(0.1)
164
+ return False
165
+
166
+ @staticmethod
167
+ def launch_process(app_path: str) -> bool:
168
+ try:
169
+ if os.path.splitext(app_path)[1].lower() == ".lnk":
170
+ os.startfile(app_path) # type: ignore[attr-defined]
171
+ else:
172
+ subprocess.Popen([app_path], shell=False)
173
+ return True
174
+ except Exception as exc:
175
+ LOGGER.warning("启动应用失败: %s", exc)
176
+ return False
177
+
178
+ def _result(self, success: bool, message: str) -> Dict[str, Any]:
179
+ return {
180
+ "success": success,
181
+ "message": message,
182
+ "steps": list(self.steps),
183
+ }
184
+
185
+
186
+ def open_app(app: AppInfo) -> Dict[str, Any]:
187
+ """根据平台打开应用。"""
188
+
189
+ if SYSTEM == "windows":
190
+ activator = WindowsTrayActivator()
191
+ return activator.activate(app)
192
+
193
+ if SYSTEM == "darwin":
194
+ subprocess.run(["open", app.path], check=True)
195
+ return {"success": True, "message": f"已通过 open 启动 {app.name}", "steps": []}
196
+
197
+ subprocess.Popen([app.path], shell=False)
198
+ return {"success": True, "message": f"已执行 {app.path}", "steps": []}
@@ -0,0 +1,366 @@
1
+ """应用配置加载与搜索逻辑。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import platform
8
+ from dataclasses import dataclass, field
9
+ from pathlib import Path
10
+ from typing import Iterable, List, Sequence
11
+
12
+ DEFAULT_CONFIG_PATHS = (
13
+ Path.home() / ".mcp-apps.json",
14
+ Path.cwd() / "mcp-apps.json",
15
+ Path.home() / ".config" / "mcp-apps" / "config.json",
16
+ )
17
+
18
+ AUTO_DISCOVER_LIMIT = int(os.environ.get("MCP_AUTO_DISCOVER_LIMIT", "200"))
19
+
20
+
21
+ def _paths_from_env(env_name: str, defaults: Sequence[str]) -> tuple[Path, ...]:
22
+ raw = os.environ.get(env_name)
23
+ entries = raw.split(os.pathsep) if raw else defaults
24
+ resolved: list[Path] = []
25
+ for entry in entries:
26
+ entry = entry.strip()
27
+ if not entry:
28
+ continue
29
+ resolved.append(Path(entry).expanduser())
30
+ return tuple(resolved)
31
+
32
+ DEFAULT_WINDOWS_APPS = (
33
+ {
34
+ "name": "QQ",
35
+ "paths": [
36
+ "C:/Program Files/Tencent/QQ/Bin/QQ.exe",
37
+ "C:/Program Files (x86)/Tencent/QQ/Bin/QQ.exe",
38
+ os.path.join(os.environ.get("USERPROFILE", ""), "AppData", "Local", "Tencent", "QQ", "Bin", "QQ.exe"),
39
+ ],
40
+ "keywords": ["qq", "tencent", "腾讯"],
41
+ "hotkey": "Ctrl+Alt+Z",
42
+ },
43
+ {
44
+ "name": "微信",
45
+ "paths": [
46
+ "C:/Program Files/Tencent/WeChat/WeChat.exe",
47
+ "C:/Program Files (x86)/Tencent/WeChat/WeChat.exe",
48
+ os.path.join(os.environ.get("USERPROFILE", ""), "AppData", "Local", "Tencent", "WeChat", "WeChat.exe"),
49
+ ],
50
+ "keywords": ["微信", "wechat", "weixin"],
51
+ "hotkey": "Ctrl+Alt+W",
52
+ },
53
+ {
54
+ "name": "Visual Studio Code",
55
+ "paths": [
56
+ "C:/Program Files/Microsoft VS Code/Code.exe",
57
+ "C:/Users/Public/scoop/apps/vscode/current/code.exe",
58
+ ],
59
+ "keywords": ["vscode", "code", "编辑器"],
60
+ },
61
+ )
62
+
63
+ DEFAULT_MAC_APPS = (
64
+ {
65
+ "name": "WeChat",
66
+ "path": "/Applications/WeChat.app",
67
+ "keywords": ["wechat", "微信"],
68
+ },
69
+ {
70
+ "name": "Safari",
71
+ "path": "/Applications/Safari.app",
72
+ "keywords": ["safari", "browser", "浏览器"],
73
+ },
74
+ {
75
+ "name": "iTerm",
76
+ "path": "/Applications/iTerm.app",
77
+ "keywords": ["terminal", "iterm"],
78
+ },
79
+ )
80
+
81
+ MAC_APPLICATION_DIRS = _paths_from_env(
82
+ "MCP_MAC_APP_DIRS",
83
+ [
84
+ "/Applications",
85
+ "/Applications/Utilities",
86
+ "/System/Applications",
87
+ "~/Applications",
88
+ ],
89
+ )
90
+
91
+ WINDOWS_SHORTCUT_DEFAULTS = [
92
+ Path(os.environ.get("ProgramData", "")) / "Microsoft" / "Windows" / "Start Menu" / "Programs",
93
+ Path.home() / "AppData" / "Roaming" / "Microsoft" / "Windows" / "Start Menu" / "Programs",
94
+ ]
95
+
96
+ WINDOWS_SHORTCUT_DIRS = tuple(
97
+ Path(entry).expanduser()
98
+ for entry in (
99
+ os.environ.get("MCP_WINDOWS_SHORTCUT_DIRS").split(os.pathsep)
100
+ if os.environ.get("MCP_WINDOWS_SHORTCUT_DIRS")
101
+ else [
102
+ str(path)
103
+ for path in WINDOWS_SHORTCUT_DEFAULTS
104
+ if path and str(path).strip() not in {"", "."}
105
+ ]
106
+ )
107
+ if entry.strip()
108
+ )
109
+
110
+
111
+ @dataclass(slots=True)
112
+ class AppInfo:
113
+ """单个应用的元数据。"""
114
+
115
+ name: str
116
+ path: str
117
+ keywords: List[str] = field(default_factory=list)
118
+ hotkey: str | None = None
119
+
120
+ def score(self, query: str) -> int:
121
+ """为匹配打分,用于找到最优应用。"""
122
+
123
+ q = query.lower().strip()
124
+ if not q:
125
+ return 0
126
+
127
+ name = self.name.lower()
128
+ score = 0
129
+ if name == q:
130
+ return 100
131
+ if name.startswith(q):
132
+ score = max(score, 90)
133
+ if q in name:
134
+ score = max(score, 70)
135
+
136
+ for kw in self.keywords:
137
+ k = kw.lower()
138
+ if k == q:
139
+ score = max(score, 80)
140
+ elif k.startswith(q):
141
+ score = max(score, 60)
142
+ elif q in k:
143
+ score = max(score, 40)
144
+
145
+ return score
146
+
147
+
148
+ class AppRegistry:
149
+ """应用注册表,负责搜索与序列化。"""
150
+
151
+ def __init__(self, apps: Sequence[AppInfo] | None = None) -> None:
152
+ self._apps: List[AppInfo] = []
153
+ if apps:
154
+ self.extend(apps)
155
+
156
+ @property
157
+ def apps(self) -> List[AppInfo]:
158
+ return list(self._apps)
159
+
160
+ def extend(self, apps: Sequence[AppInfo]) -> None:
161
+ for app in apps:
162
+ self.add(app)
163
+
164
+ def add(self, app: AppInfo) -> None:
165
+ if not app.name or not app.path:
166
+ return
167
+ lower = app.name.lower()
168
+ if any(existing.name.lower() == lower or existing.path == app.path for existing in self._apps):
169
+ return
170
+ self._apps.append(app)
171
+
172
+ def find(self, query: str) -> AppInfo | None:
173
+ candidates = [
174
+ (app, app.score(query))
175
+ for app in self._apps
176
+ ]
177
+ candidates = [item for item in candidates if item[1] > 0]
178
+ candidates.sort(key=lambda item: item[1], reverse=True)
179
+ return candidates[0][0] if candidates else None
180
+
181
+ def dump(self) -> List[dict]:
182
+ return [
183
+ {
184
+ "name": app.name,
185
+ "path": app.path,
186
+ "keywords": app.keywords,
187
+ "hotkey": app.hotkey,
188
+ }
189
+ for app in self._apps
190
+ ]
191
+
192
+
193
+ def _clean_keywords(value: Iterable[str] | None) -> List[str]:
194
+ if not value:
195
+ return []
196
+ return sorted({kw.strip() for kw in value if kw and kw.strip()})
197
+
198
+
199
+ def _keywords_from_name(name: str) -> List[str]:
200
+ tokens = {name, name.lower()}
201
+ normalized = name.replace("_", " ").replace("-", " ")
202
+ tokens.update(part for part in normalized.split() if part)
203
+ return _clean_keywords(tokens)
204
+
205
+
206
+ def _app_from_mapping(data: dict) -> AppInfo | None:
207
+ name = str(data.get("name", "")).strip()
208
+ path = str(data.get("path", "")).strip()
209
+ if not name or not path:
210
+ return None
211
+ keywords = data.get("keywords", [])
212
+ hotkey = data.get("hotkey")
213
+ return AppInfo(name=name, path=path, keywords=_clean_keywords(keywords), hotkey=hotkey)
214
+
215
+
216
+ def load_from_env(var: str = "MCP_APPS") -> List[AppInfo]:
217
+ raw = os.environ.get(var)
218
+ if not raw:
219
+ return []
220
+
221
+ raw = raw.strip()
222
+ apps: List[AppInfo] = []
223
+ try:
224
+ if raw.startswith("["):
225
+ parsed = json.loads(raw)
226
+ for item in parsed:
227
+ app = _app_from_mapping(item)
228
+ if app:
229
+ apps.append(app)
230
+ else:
231
+ entries = [segment.strip() for segment in raw.split("|") if segment.strip()]
232
+ for entry in entries:
233
+ parts = [p.strip() for p in entry.split(";")]
234
+ if len(parts) < 2:
235
+ continue
236
+ name, app_path = parts[:2]
237
+ keywords = parts[2].split(",") if len(parts) > 2 else []
238
+ hotkey = parts[3] if len(parts) > 3 else None
239
+ app = AppInfo(name=name, path=app_path, keywords=_clean_keywords(keywords), hotkey=hotkey)
240
+ apps.append(app)
241
+ except json.JSONDecodeError as exc:
242
+ raise ValueError(f"无法解析环境变量 {var}: {exc}") from exc
243
+
244
+ return apps
245
+
246
+
247
+ def load_from_config(paths: Sequence[Path] = DEFAULT_CONFIG_PATHS) -> List[AppInfo]:
248
+ for path in paths:
249
+ if not path.exists():
250
+ continue
251
+ try:
252
+ data = json.loads(path.read_text(encoding="utf-8"))
253
+ except json.JSONDecodeError as exc:
254
+ raise ValueError(f"配置文件 {path} 解析失败: {exc}") from exc
255
+
256
+ payload = data.get("apps", []) if isinstance(data, dict) else data
257
+ apps = [_app_from_mapping(item) for item in payload]
258
+ return [app for app in apps if app]
259
+ return []
260
+
261
+
262
+ def _discover_windows_shortcuts(limit: int | None = None) -> List[AppInfo]:
263
+ apps: List[AppInfo] = []
264
+ max_items = limit or AUTO_DISCOVER_LIMIT
265
+ for base in WINDOWS_SHORTCUT_DIRS:
266
+ if not base or not base.exists():
267
+ continue
268
+ for shortcut in base.rglob("*.lnk"):
269
+ name = shortcut.stem.strip()
270
+ if not name:
271
+ continue
272
+ apps.append(AppInfo(name=name, path=str(shortcut), keywords=_keywords_from_name(name)))
273
+ if len(apps) >= max_items:
274
+ return apps
275
+ return apps
276
+
277
+
278
+ def discover_windows_apps(limit: int | None = None) -> List[AppInfo]:
279
+ if platform.system().lower() != "windows":
280
+ return []
281
+
282
+ max_items = limit or AUTO_DISCOVER_LIMIT
283
+ discovered: List[AppInfo] = []
284
+ for entry in DEFAULT_WINDOWS_APPS:
285
+ if len(discovered) >= max_items:
286
+ return discovered
287
+ paths = entry.get("paths", [])
288
+ if isinstance(paths, str):
289
+ paths = [paths]
290
+ for candidate in paths:
291
+ if candidate and Path(candidate).exists():
292
+ app = AppInfo(
293
+ name=entry["name"],
294
+ path=candidate,
295
+ keywords=_clean_keywords(entry.get("keywords")),
296
+ hotkey=entry.get("hotkey"),
297
+ )
298
+ discovered.append(app)
299
+ break
300
+
301
+ # Start Menu 快捷方式扫描
302
+ remaining = max_items - len(discovered)
303
+ if remaining > 0 and WINDOWS_SHORTCUT_DIRS:
304
+ discovered.extend(_discover_windows_shortcuts(limit=remaining))
305
+ return discovered
306
+
307
+
308
+ def discover_macos_apps(limit: int | None = None) -> List[AppInfo]:
309
+ if platform.system().lower() != "darwin":
310
+ return []
311
+
312
+ max_items = limit or AUTO_DISCOVER_LIMIT
313
+ apps: List[AppInfo] = []
314
+ seen_paths: set[str] = set()
315
+
316
+ def add_app(info: AppInfo) -> None:
317
+ if info.path in seen_paths:
318
+ return
319
+ seen_paths.add(info.path)
320
+ apps.append(info)
321
+
322
+ # 先加入内置常用应用,保证最小可用集合
323
+ for entry in DEFAULT_MAC_APPS:
324
+ candidate = entry["path"]
325
+ if Path(candidate).exists():
326
+ add_app(
327
+ AppInfo(
328
+ name=entry["name"],
329
+ path=candidate,
330
+ keywords=_clean_keywords(entry.get("keywords")),
331
+ )
332
+ )
333
+ if len(apps) >= max_items:
334
+ return apps
335
+
336
+ # 遍历常见应用目录
337
+ for base in MAC_APPLICATION_DIRS:
338
+ if not base.exists():
339
+ continue
340
+ for app_dir in base.rglob("*.app"):
341
+ if ".app/Contents/" in str(app_dir):
342
+ continue
343
+ if not app_dir.is_dir():
344
+ continue
345
+ add_app(
346
+ AppInfo(
347
+ name=app_dir.stem,
348
+ path=str(app_dir),
349
+ keywords=_keywords_from_name(app_dir.stem),
350
+ )
351
+ )
352
+ if len(apps) >= max_items:
353
+ return apps
354
+ return apps
355
+
356
+
357
+ def build_registry(auto_discover: bool = True) -> AppRegistry:
358
+ registry = AppRegistry()
359
+ registry.extend(load_from_config())
360
+ registry.extend(load_from_env())
361
+
362
+ if auto_discover:
363
+ registry.extend(discover_windows_apps())
364
+ registry.extend(discover_macos_apps())
365
+
366
+ return registry
@@ -0,0 +1,70 @@
1
+ """FastMCP 服务器入口。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import os
7
+ from typing import Any, Dict
8
+
9
+ from fastmcp import FastMCP
10
+
11
+ from .service import AppLauncherService
12
+
13
+ os.environ.setdefault("FASTMCP_LOG_LEVEL", "INFO")
14
+
15
+ mcp = FastMCP("FastMCP 应用启动器")
16
+ SERVICE: AppLauncherService | None = None
17
+
18
+
19
+ def require_service() -> AppLauncherService:
20
+ if SERVICE is None:
21
+ raise RuntimeError("服务尚未初始化,请先调用 main()")
22
+ return SERVICE
23
+
24
+
25
+ @mcp.tool()
26
+ async def list_apps_tool() -> Dict[str, Any]:
27
+ """列出所有可用的应用程序。"""
28
+
29
+ service = require_service()
30
+ apps = service.list_apps()
31
+ return {"count": len(apps), "apps": apps}
32
+
33
+
34
+ @mcp.tool()
35
+ async def open_app_tool(app_name: str, reload_before: bool = False) -> Dict[str, Any]:
36
+ """根据名称或关键词打开应用。"""
37
+
38
+ service = require_service()
39
+ if reload_before:
40
+ service.reload()
41
+ return await service.open_app(app_name)
42
+
43
+
44
+ @mcp.tool()
45
+ async def reload_apps_tool() -> Dict[str, Any]:
46
+ """手动重新加载配置。"""
47
+
48
+ service = require_service()
49
+ service.reload()
50
+ apps = service.list_apps()
51
+ return {"message": "已重新加载应用配置", "count": len(apps)}
52
+
53
+
54
+ def main(argv: list[str] | None = None) -> None:
55
+ parser = argparse.ArgumentParser(description="FastMCP 应用启动服务")
56
+ parser.add_argument("--no-auto-discover", action="store_true", help="禁用默认的系统应用自动发现")
57
+ parser.add_argument("--transport", default="stdio", help="MCP 传输层,默认 stdio")
58
+ args = parser.parse_args(argv)
59
+
60
+ global SERVICE
61
+ SERVICE = AppLauncherService(auto_discover=not args.no_auto_discover)
62
+
63
+ print("启动 FastMCP 应用启动器,Transport:", args.transport)
64
+ print("可用工具: list_apps_tool, open_app_tool, reload_apps_tool")
65
+
66
+ mcp.run(transport=args.transport)
67
+
68
+
69
+ if __name__ == "__main__":
70
+ main()
@@ -0,0 +1,45 @@
1
+ """封装应用注册表与激活器的业务逻辑。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from typing import Any, Dict, List
7
+
8
+ from .activator import open_app as activate_app
9
+ from .apps import AppRegistry, build_registry
10
+
11
+
12
+ class AppLauncherService:
13
+ """管理应用配置并通过激活器打开应用。"""
14
+
15
+ def __init__(self, auto_discover: bool = True) -> None:
16
+ self.auto_discover = auto_discover
17
+ self.registry: AppRegistry = build_registry(auto_discover=auto_discover)
18
+
19
+ def reload(self) -> None:
20
+ self.registry = build_registry(auto_discover=self.auto_discover)
21
+
22
+ def list_apps(self) -> List[dict[str, Any]]:
23
+ return self.registry.dump()
24
+
25
+ async def open_app(self, query: str) -> Dict[str, Any]:
26
+ app = self.registry.find(query)
27
+ if not app:
28
+ raise ValueError(f"未找到匹配的应用: {query}")
29
+
30
+ loop = asyncio.get_running_loop()
31
+ result = await loop.run_in_executor(None, activate_app, app)
32
+ payload = {
33
+ "query": query,
34
+ "app": {
35
+ "name": app.name,
36
+ "path": app.path,
37
+ "keywords": app.keywords,
38
+ "hotkey": app.hotkey,
39
+ },
40
+ "execution": result,
41
+ }
42
+
43
+ if not result.get("success"):
44
+ raise RuntimeError(f"打开应用失败: {result.get('message')}")
45
+ return payload
@@ -0,0 +1,184 @@
1
+ Metadata-Version: 2.4
2
+ Name: fastmcp-app-launcher
3
+ Version: 0.2.0
4
+ Summary: 基于 fastmcp 的跨平台应用启动 MCP 服务
5
+ Author-email: Codex MCP <codex@example.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2025 Codex MCP
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/fjc/fastmcp-app-launcher
29
+ Project-URL: Repository, https://github.com/fjc/fastmcp-app-launcher
30
+ Project-URL: Bug Tracker, https://github.com/fjc/fastmcp-app-launcher/issues
31
+ Keywords: mcp,fastmcp,automation,launcher,productivity
32
+ Classifier: Development Status :: 4 - Beta
33
+ Classifier: Intended Audience :: Developers
34
+ Classifier: License :: OSI Approved :: MIT License
35
+ Classifier: Programming Language :: Python :: 3
36
+ Classifier: Programming Language :: Python :: 3.10
37
+ Classifier: Programming Language :: Python :: 3.11
38
+ Classifier: Programming Language :: Python :: 3.12
39
+ Classifier: Operating System :: OS Independent
40
+ Classifier: Topic :: Desktop Environment
41
+ Classifier: Topic :: System :: Monitoring
42
+ Requires-Python: >=3.10
43
+ Description-Content-Type: text/markdown
44
+ License-File: LICENSE
45
+ Requires-Dist: fastmcp>=2.12.0
46
+ Requires-Dist: pydantic>=2.9.0
47
+ Provides-Extra: windows
48
+ Requires-Dist: pywin32>=305; extra == "windows"
49
+ Requires-Dist: pywinauto>=0.6.8; extra == "windows"
50
+ Provides-Extra: dev
51
+ Requires-Dist: pytest>=8.3.0; extra == "dev"
52
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
53
+ Dynamic: license-file
54
+
55
+ # FastMCP 应用启动器
56
+
57
+ 使用 [fastmcp](https://github.com/modelcontextprotocol/fastmcp) 构建的跨平台 Model Context Protocol 服务,可通过 MCP 客户端(如 Claude Desktop)列出并打开本地应用,针对 Windows 托盘应用提供热键与窗口激活增强。
58
+
59
+ ## 安装
60
+
61
+ ```bash
62
+ pip install fastmcp-app-launcher
63
+ # 或使用 uv
64
+ uv pip install fastmcp-app-launcher
65
+ ```
66
+
67
+ 安装完成后终端会提供 `app-launcher-mcp` 命令,可直接在 MCP 客户端中调用。
68
+
69
+ ## 目录结构
70
+
71
+ ```
72
+ fastmcp_app_launcher/
73
+ ├── mcp-apps.example.json # 示例配置
74
+ ├── pyproject.toml # uv/fastmcp 配置
75
+ ├── README.md
76
+ └── src/app_launcher_mcp
77
+ ├── __init__.py
78
+ ├── activator.py # Windows 激活逻辑
79
+ ├── apps.py # 配置加载与搜索
80
+ ├── server.py # FastMCP 入口
81
+ └── service.py # 业务封装
82
+ ```
83
+
84
+ ## 快速开始
85
+
86
+ 1. **创建 uv 虚拟环境(已在 `.venv` 目录演示,如需重新创建可按需运行):**
87
+
88
+ ```bash
89
+ uv venv .venv
90
+ source .venv/bin/activate # Windows 使用 .venv\\Scripts\\activate
91
+ ```
92
+
93
+ 2. **安装依赖:**
94
+
95
+ ```bash
96
+ uv sync # 或 uv pip install -r pyproject.toml
97
+ # Windows 如需托盘/热键支持,安装额外依赖:
98
+ uv pip install .[windows]
99
+ ```
100
+
101
+ 3. **运行 MCP 服务器(stdio 模式):**
102
+
103
+ ```bash
104
+ uv run app-launcher-mcp
105
+ # 或指定参数:
106
+ uv run app-launcher-mcp --no-auto-discover # 禁用自动发现
107
+ uv run app-launcher-mcp --transport stdio # 指定 Transport
108
+ ```
109
+
110
+ 4. **在 MCP 客户端中配置:**
111
+
112
+ ```json
113
+ {
114
+ "command": "uv",
115
+ "args": ["run", "--directory", "/Users/fjc/Desktop/项目/skills/python/fastmcp_app_launcher", "app-launcher-mcp"],
116
+ "env": {
117
+ "MCP_APPS": "QQ;C:/Program Files/Tencent/QQ/Bin/QQ.exe;qq,tencent;Ctrl+Alt+Z"
118
+ }
119
+ }
120
+ ```
121
+
122
+ ## 应用配置
123
+
124
+ - **环境变量 `MCP_APPS`**:支持 JSON 数组或 `name;path;keywords;hotkey|...` 简写。
125
+ - **配置文件**:程序在以下路径按顺序查找(找到即停止):
126
+ 1. `~/.mcp-apps.json`
127
+ 2. `<工作目录>/mcp-apps.json`
128
+ 3. `~/.config/mcp-apps/config.json`
129
+ - **自动发现**:
130
+ - Windows:内置 QQ、微信、VS Code 等常见路径,并扫描开始菜单 `.lnk` 快捷方式(目录可通过环境变量 `MCP_WINDOWS_SHORTCUT_DIRS` 指定,使用 `os.pathsep` 分隔)。
131
+ - macOS:默认遍历 `/Applications`、`/System/Applications`、`~/Applications` 等目录,可使用 `MCP_MAC_APP_DIRS` 覆盖;扫描数量由 `MCP_AUTO_DISCOVER_LIMIT` 控制。
132
+
133
+ 可参考 `mcp-apps.example.json` 快速自定义列表。
134
+
135
+ ## 可用工具
136
+
137
+ | 工具名 | 描述 |
138
+ | ------ | ---- |
139
+ | `list_apps_tool` | 返回当前注册的全部应用及数量 |
140
+ | `open_app_tool` | 参数 `app_name`、可选 `reload_before`,用于打开或激活应用 |
141
+ | `reload_apps_tool` | 重新加载配置/环境,并返回最新应用数 |
142
+
143
+ 工具返回 `structuredContent` 中包含的字段示例:
144
+
145
+ ```json
146
+ {
147
+ "query": "wechat",
148
+ "app": {"name": "微信", "path": "...", "hotkey": "Ctrl+Alt+W"},
149
+ "execution": {"success": true, "message": "通过热键激活窗口成功"}
150
+ }
151
+ ```
152
+
153
+ ## Windows 特性
154
+
155
+ - **托盘激活**:若安装 `pywin32`,服务会尝试发送热键、定位现有窗口并置前。
156
+ - **pywinauto 支持**:可选安装以在托盘图标无法响应热键时通过 UI 自动化置顶窗口。
157
+ - **自动回退**:当相关库缺失时,会直接调用 `os.startfile` 或 `subprocess.Popen` 启动新实例。
158
+
159
+ ## 故障排查
160
+
161
+ - `未找到匹配的应用`:检查 `MCP_APPS` / 配置文件是否已加载,或在调用 `open_app_tool` 前执行 `reload_apps_tool`。
162
+ - `pywin32 未安装`:在 Windows 环境执行 `uv pip install .[windows]`。
163
+ - `权限相关错误`:Mac/Linux 打开 `.app` 时需确认拥有执行权限,可通过 `chmod +x` 处理。
164
+
165
+ 欢迎根据需要扩展更多工具,例如批量更新应用、动态注册等。
166
+
167
+ ## 发布到 PyPI
168
+
169
+ 1. 更新 `pyproject.toml` 中的版本号,并确保代码/文档同步。
170
+ 2. 安装构建工具并生成分发包:
171
+
172
+ ```bash
173
+ uv pip install --upgrade build twine
174
+ uv build
175
+ twine check dist/*
176
+ ```
177
+
178
+ 3. 使用 PyPI API Token 上传:
179
+
180
+ ```bash
181
+ twine upload dist/*
182
+ ```
183
+
184
+ 发布成功后,用户即可通过 `pip install fastmcp-app-launcher` 获取最新版本。
@@ -0,0 +1,17 @@
1
+ LICENSE
2
+ MANIFEST.in
3
+ README.md
4
+ mcp-apps.example.json
5
+ pyproject.toml
6
+ src/app_launcher_mcp/__init__.py
7
+ src/app_launcher_mcp/activator.py
8
+ src/app_launcher_mcp/apps.py
9
+ src/app_launcher_mcp/py.typed
10
+ src/app_launcher_mcp/server.py
11
+ src/app_launcher_mcp/service.py
12
+ src/fastmcp_app_launcher.egg-info/PKG-INFO
13
+ src/fastmcp_app_launcher.egg-info/SOURCES.txt
14
+ src/fastmcp_app_launcher.egg-info/dependency_links.txt
15
+ src/fastmcp_app_launcher.egg-info/entry_points.txt
16
+ src/fastmcp_app_launcher.egg-info/requires.txt
17
+ src/fastmcp_app_launcher.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ app-launcher-mcp = app_launcher_mcp.server:main
@@ -0,0 +1,10 @@
1
+ fastmcp>=2.12.0
2
+ pydantic>=2.9.0
3
+
4
+ [dev]
5
+ pytest>=8.3.0
6
+ pytest-asyncio>=0.23.0
7
+
8
+ [windows]
9
+ pywin32>=305
10
+ pywinauto>=0.6.8