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.
- fastmcp_app_launcher-0.2.0/LICENSE +21 -0
- fastmcp_app_launcher-0.2.0/MANIFEST.in +3 -0
- fastmcp_app_launcher-0.2.0/PKG-INFO +184 -0
- fastmcp_app_launcher-0.2.0/README.md +130 -0
- fastmcp_app_launcher-0.2.0/mcp-apps.example.json +15 -0
- fastmcp_app_launcher-0.2.0/pyproject.toml +59 -0
- fastmcp_app_launcher-0.2.0/setup.cfg +4 -0
- fastmcp_app_launcher-0.2.0/src/app_launcher_mcp/__init__.py +12 -0
- fastmcp_app_launcher-0.2.0/src/app_launcher_mcp/activator.py +198 -0
- fastmcp_app_launcher-0.2.0/src/app_launcher_mcp/apps.py +366 -0
- fastmcp_app_launcher-0.2.0/src/app_launcher_mcp/py.typed +0 -0
- fastmcp_app_launcher-0.2.0/src/app_launcher_mcp/server.py +70 -0
- fastmcp_app_launcher-0.2.0/src/app_launcher_mcp/service.py +45 -0
- fastmcp_app_launcher-0.2.0/src/fastmcp_app_launcher.egg-info/PKG-INFO +184 -0
- fastmcp_app_launcher-0.2.0/src/fastmcp_app_launcher.egg-info/SOURCES.txt +17 -0
- fastmcp_app_launcher-0.2.0/src/fastmcp_app_launcher.egg-info/dependency_links.txt +1 -0
- fastmcp_app_launcher-0.2.0/src/fastmcp_app_launcher.egg-info/entry_points.txt +2 -0
- fastmcp_app_launcher-0.2.0/src/fastmcp_app_launcher.egg-info/requires.txt +10 -0
- fastmcp_app_launcher-0.2.0/src/fastmcp_app_launcher.egg-info/top_level.txt +1 -0
|
@@ -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,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,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
|
|
File without changes
|
|
@@ -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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
app_launcher_mcp
|