minimax-coding-plan-mcp 0.0.1__py3-none-any.whl
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.
- minimax_coding_plan_mcp-0.0.1.dist-info/METADATA +202 -0
- minimax_coding_plan_mcp-0.0.1.dist-info/RECORD +13 -0
- minimax_coding_plan_mcp-0.0.1.dist-info/WHEEL +5 -0
- minimax_coding_plan_mcp-0.0.1.dist-info/entry_points.txt +2 -0
- minimax_coding_plan_mcp-0.0.1.dist-info/licenses/LICENSE +21 -0
- minimax_coding_plan_mcp-0.0.1.dist-info/top_level.txt +1 -0
- minimax_mcp/__init__.py +3 -0
- minimax_mcp/__main__.py +100 -0
- minimax_mcp/client.py +95 -0
- minimax_mcp/const.py +30 -0
- minimax_mcp/exceptions.py +24 -0
- minimax_mcp/server.py +170 -0
- minimax_mcp/utils.py +251 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: minimax-coding-plan-mcp
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Specialized MiniMax Model Context Protocol (MCP) server designed for coding-plan users
|
|
5
|
+
Author-email: Roy Wu <zhengyu@minimax.chat>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright 2025 MiniMax AI.
|
|
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
|
+
Keywords: minimax,mcp,coding-plan-search,coding-plan-vlm
|
|
29
|
+
Classifier: Development Status :: 4 - Beta
|
|
30
|
+
Classifier: Intended Audience :: Developers
|
|
31
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
32
|
+
Classifier: Programming Language :: Python :: 3
|
|
33
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
34
|
+
Requires-Python: >=3.10
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
License-File: LICENSE
|
|
37
|
+
Requires-Dist: mcp[cli]>=1.6.0
|
|
38
|
+
Requires-Dist: fastapi>=0.109.2
|
|
39
|
+
Requires-Dist: uvicorn>=0.27.1
|
|
40
|
+
Requires-Dist: python-dotenv>=1.0.1
|
|
41
|
+
Requires-Dist: pydantic>=2.6.1
|
|
42
|
+
Requires-Dist: httpx>=0.28.1
|
|
43
|
+
Requires-Dist: fuzzywuzzy>=0.18.0
|
|
44
|
+
Requires-Dist: python-Levenshtein>=0.25.0
|
|
45
|
+
Requires-Dist: sounddevice>=0.5.1
|
|
46
|
+
Requires-Dist: soundfile>=0.13.1
|
|
47
|
+
Requires-Dist: requests>=2.31.0
|
|
48
|
+
Provides-Extra: dev
|
|
49
|
+
Requires-Dist: pre-commit>=3.6.2; extra == "dev"
|
|
50
|
+
Requires-Dist: ruff>=0.3.0; extra == "dev"
|
|
51
|
+
Requires-Dist: fastmcp>=0.4.1; extra == "dev"
|
|
52
|
+
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
53
|
+
Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
|
|
54
|
+
Requires-Dist: twine>=6.1.0; extra == "dev"
|
|
55
|
+
Requires-Dist: build>=1.0.3; extra == "dev"
|
|
56
|
+
Dynamic: license-file
|
|
57
|
+
|
|
58
|
+

|
|
59
|
+
|
|
60
|
+
<div align="center" style="line-height: 1;">
|
|
61
|
+
<a href="https://www.minimax.io" target="_blank" style="margin: 2px; color: var(--fgColor-default);">
|
|
62
|
+
<img alt="Homepage" src="https://img.shields.io/badge/_Homepage-MiniMax-FF4040?style=flat-square&labelColor=2C3E50&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgNDkwLjE2IDQxMS43Ij48ZGVmcz48c3R5bGU+LmNscy0xe2ZpbGw6I2ZmZjt9PC9zdHlsZT48L2RlZnM+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNMjMzLjQ1LDQwLjgxYTE3LjU1LDE3LjU1LDAsMSwwLTM1LjEsMFYzMzEuNTZhNDAuODIsNDAuODIsMCwwLDEtODEuNjMsMFYxNDVhMTcuNTUsMTcuNTUsMCwxLDAtMzUuMDksMHY3OS4wNmE0MC44Miw0MC44MiwwLDAsMS04MS42MywwVjE5NS40MmExMS42MywxMS42MywwLDAsMSwyMy4yNiwwdjI4LjY2YTE3LjU1LDE3LjU1LDAsMCwwLDM1LjEsMFYxNDVBNDAuODIsNDAuODIsMCwwLDEsMTQwLDE0NVYzMzEuNTZhMTcuNTUsMTcuNTUsMCwwLDAsMzUuMSwwVjIxNy41aDBWNDAuODFhNDAuODEsNDAuODEsMCwxLDEsODEuNjIsMFYyODEuNTZhMTEuNjMsMTEuNjMsMCwxLDEtMjMuMjYsMFptMjE1LjksNjMuNEE0MC44Niw0MC44NiwwLDAsMCw0MDguNTMsMTQ1VjMwMC44NWExNy41NSwxNy41NSwwLDAsMS0zNS4wOSwwdi0yNjBhNDAuODIsNDAuODIsMCwwLDAtODEuNjMsMFYzNzAuODlhMTcuNTUsMTcuNTUsMCwwLDEtMzUuMSwwVjMzMGExMS42MywxMS42MywwLDEsMC0yMy4yNiwwdjQwLjg2YTQwLjgxLDQwLjgxLDAsMCwwLDgxLjYyLDBWNDAuODFhMTcuNTUsMTcuNTUsMCwwLDEsMzUuMSwwdjI2MGE0MC44Miw0MC44MiwwLDAsMCw4MS42MywwVjE0NWExNy41NSwxNy41NSwwLDEsMSwzNS4xLDBWMjgxLjU2YTExLjYzLDExLjYzLDAsMCwwLDIzLjI2LDBWMTQ1QTQwLjg1LDQwLjg1LDAsMCwwLDQ0OS4zNSwxMDQuMjFaIi8+PC9zdmc+&logoWidth=20" style="display: inline-block; vertical-align: middle;"/>
|
|
63
|
+
</a>
|
|
64
|
+
<a href="https://arxiv.org/abs/2501.08313" target="_blank" style="margin: 2px;">
|
|
65
|
+
<img alt="Paper" src="https://img.shields.io/badge/π_Paper-MiniMax--01-FF4040?style=flat-square&labelColor=2C3E50" style="display: inline-block; vertical-align: middle;"/>
|
|
66
|
+
</a>
|
|
67
|
+
<a href="https://chat.minimax.io/" target="_blank" style="margin: 2px;">
|
|
68
|
+
<img alt="Chat" src="https://img.shields.io/badge/_MiniMax_Chat-FF4040?style=flat-square&labelColor=2C3E50&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgNDkwLjE2IDQxMS43Ij48ZGVmcz48c3R5bGU+LmNscy0xe2ZpbGw6I2ZmZjt9PC9zdHlsZT48L2RlZnM+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNMjMzLjQ1LDQwLjgxYTE3LjU1LDE3LjU1LDAsMSwwLTM1LjEsMFYzMzEuNTZhNDAuODIsNDAuODIsMCwwLDEtODEuNjMsMFYxNDVhMTcuNTUsMTcuNTUsMCwxLDAtMzUuMDksMHY3OS4wNmE0MC44Miw0MC44MiwwLDAsMS04MS42MywwVjE5NS40MmExMS42MywxMS42MywwLDAsMSwyMy4yNiwwdjI4LjY2YTE3LjU1LDE3LjU1LDAsMCwwLDM1LjEsMFYxNDVBNDAuODIsNDAuODIsMCwwLDEsMTQwLDE0NVYzMzEuNTZhMTcuNTUsMTcuNTUsMCwwLDAsMzUuMSwwVjIxNy41aDBWNDAuODFhNDAuODEsNDAuODEsMCwxLDEsODEuNjIsMFYyODEuNTZhMTEuNjMsMTEuNjMsMCwxLDEtMjMuMjYsMFptMjE1LjksNjMuNEE0MC44Niw0MC44NiwwLDAsMCw0MDguNTMsMTQ1VjMwMC44NWExNy41NSwxNy41NSwwLDAsMS0zNS4wOSwwdi0yNjBhNDAuODIsNDAuODIsMCwwLDAtODEuNjMsMFYzNzAuODlhMTcuNTUsMTcuNTUsMCwwLDEtMzUuMSwwVjMzMGExMS42MywxMS42MywwLDEsMC0yMy4yNiwwdjQwLjg2YTQwLjgxLDQwLjgxLDAsMCwwLDgxLjYyLDBWNDAuODFhMTcuNTUsMTcuNTUsMCwwLDEsMzUuMSwwdjI2MGE0MC44Miw0MC44MiwwLDAsMCw4MS42MywwVjE0NWExNy41NSwxNy41NSwwLDEsMSwzNS4xLDBWMjgxLjU2YTExLjYzLDExLjYzLDAsMCwwLDIzLjI2LDBWMTQ1QTQwLjg1LDQwLjg1LDAsMCwwLDQ0OS4zNSwxMDQuMjFaIi8+PC9zdmc+&logoWidth=20" style="display: inline-block; vertical-align: middle;"/>
|
|
69
|
+
</a>
|
|
70
|
+
<a href="https://www.minimax.io/platform" style="margin: 2px;">
|
|
71
|
+
<img alt="API" src="https://img.shields.io/badge/β‘_API-Platform-FF4040?style=flat-square&labelColor=2C3E50" style="display: inline-block; vertical-align: middle;"/>
|
|
72
|
+
</a>
|
|
73
|
+
</div>
|
|
74
|
+
<div align="center" style="line-height: 1;">
|
|
75
|
+
<a href="https://huggingface.co/MiniMaxAI" target="_blank" style="margin: 2px;">
|
|
76
|
+
<img alt="Hugging Face" src="https://img.shields.io/badge/π€_Hugging_Face-MiniMax-FF4040?style=flat-square&labelColor=2C3E50" style="display: inline-block; vertical-align: middle;"/>
|
|
77
|
+
</a>
|
|
78
|
+
<a href="https://github.com/MiniMax-AI/MiniMax-AI.github.io/blob/main/images/wechat-qrcode.jpeg" target="_blank" style="margin: 2px;">
|
|
79
|
+
<img alt="WeChat" src="https://img.shields.io/badge/_WeChat-MiniMax-FF4040?style=flat-square&labelColor=2C3E50" style="display: inline-block; vertical-align: middle;"/>
|
|
80
|
+
</a>
|
|
81
|
+
<a href="https://www.modelscope.cn/organization/MiniMax" target="_blank" style="margin: 2px;">
|
|
82
|
+
<img alt="ModelScope" src="https://img.shields.io/badge/_ModelScope-MiniMax-FF4040?style=flat-square&labelColor=2C3E50" style="display: inline-block; vertical-align: middle;"/>
|
|
83
|
+
</a>
|
|
84
|
+
</div>
|
|
85
|
+
<div align="center" style="line-height: 1;">
|
|
86
|
+
<a href="https://github.com/MiniMax-AI/MiniMax-MCP/blob/main/LICENSE" style="margin: 2px;">
|
|
87
|
+
<img alt="Code License" src="https://img.shields.io/badge/_Code_License-MIT-FF4040?style=flat-square&labelColor=2C3E50" style="display: inline-block; vertical-align: middle;"/>
|
|
88
|
+
</a>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<p align="center">
|
|
92
|
+
Specialized MiniMax Model Context Protocol (MCP) server designed for <b>coding-plan</b> users, featuring AI-powered search and vision analysis APIs optimized for code development workflows. Unlike the standard <a href="https://github.com/MiniMax-AI/MiniMax-MCP">MiniMax-MCP</a>, this version provides coding-specific tools (for examples: <code>coding_plan_search</code> and <code>coding_plan_vlm</code>) that integrate seamlessly with MCP clients like <a href="https://www.anthropic.com/claude">Claude Desktop</a>, <a href="https://www.cursor.so">Cursor</a>, <a href="https://codeium.com/windsurf">Windsurf</a>, <a href="https://github.com/openai/openai-agents-python">OpenAI Agents</a> and others to enhance your coding experience.
|
|
93
|
+
</p>
|
|
94
|
+
|
|
95
|
+
## Documentation
|
|
96
|
+
- [δΈζζζ‘£](README-CN.md)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
## Quickstart with MCP Client
|
|
100
|
+
1. Get your API key from [MiniMax](https://www.minimax.io/platform/user-center/basic-information/interface-key).
|
|
101
|
+
2. Install `uv` (Python package manager), install with `curl -LsSf https://astral.sh/uv/install.sh | sh` or see the `uv` [repo](https://github.com/astral-sh/uv) for additional install methods.
|
|
102
|
+
3. **Important**: The API host and key vary by region and must match; otherwise, you'll encounter an `Invalid API key` error.
|
|
103
|
+
|
|
104
|
+
|Region| Global | Mainland |
|
|
105
|
+
|:--|:-----|:-----|
|
|
106
|
+
|MINIMAX_API_KEY| go get from [MiniMax Global](https://www.minimax.io/platform/user-center/basic-information/interface-key) | go get from [MiniMax](https://platform.minimaxi.com/user-center/basic-information/interface-key) |
|
|
107
|
+
|MINIMAX_API_HOST| https://api.minimax.io | https://api.minimaxi.com |
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
### Claude Desktop
|
|
111
|
+
Go to `Claude > Settings > Developer > Edit Config > claude_desktop_config.json` to include the following:
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
{
|
|
115
|
+
"mcpServers": {
|
|
116
|
+
"MiniMax": {
|
|
117
|
+
"command": "uvx",
|
|
118
|
+
"args": [
|
|
119
|
+
"minimax-coding-plan-mcp",
|
|
120
|
+
"-y"
|
|
121
|
+
],
|
|
122
|
+
"env": {
|
|
123
|
+
"MINIMAX_API_KEY": "insert-your-api-key-here",
|
|
124
|
+
"MINIMAX_MCP_BASE_PATH": "local-output-dir-path, such as /User/xxx/Desktop",
|
|
125
|
+
"MINIMAX_API_HOST": "api host, https://api.minimax.io | https://api.minimaxi.com",
|
|
126
|
+
"MINIMAX_API_RESOURCE_MODE": "optional, [url|local], url is default, audio/image/video are downloaded locally or provided in URL format"
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
β οΈ Warning: The API key needs to match the host. If an error "API Error: invalid api key" occurs, please check your api host:
|
|
134
|
+
- Global HostοΌ`https://api.minimax.io`
|
|
135
|
+
- Mainland HostοΌ`https://api.minimaxi.com`
|
|
136
|
+
|
|
137
|
+
If you're using Windows, you will have to enable "Developer Mode" in Claude Desktop to use the MCP server. Click "Help" in the hamburger menu in the top left and select "Enable Developer Mode".
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
### Cursor
|
|
141
|
+
Go to `Cursor -> Preferences -> Cursor Settings -> MCP -> Add new global MCP Server` to add above config.
|
|
142
|
+
|
|
143
|
+
That's it. Your MCP client can now interact with MiniMax through these tools:
|
|
144
|
+
|
|
145
|
+
## Transport
|
|
146
|
+
We support two transport types: stdio and sse.
|
|
147
|
+
| stdio | SSE |
|
|
148
|
+
|:-----|:-----|
|
|
149
|
+
| Run locally | Can be deployed locally or in the cloud |
|
|
150
|
+
| Communication through `stdout` | Communication through `network` |
|
|
151
|
+
| Input: Supports processing `local files` or valid `URL` resources | Input: When deployed in the cloud, it is recommended to use `URL` for input |
|
|
152
|
+
|
|
153
|
+
## Available Tools
|
|
154
|
+
| tool | description |
|
|
155
|
+
|-|-|
|
|
156
|
+
|`coding_plan_search`|Performs web searches and returns organic search results along with related search queries|
|
|
157
|
+
|`coding_plan_vlm`|Analyzes images with AI based on text prompts, extracts information and answers questions about images|
|
|
158
|
+
|
|
159
|
+
## Release Notes
|
|
160
|
+
|
|
161
|
+
### November 20, 2025
|
|
162
|
+
|
|
163
|
+
#### π What's New
|
|
164
|
+
- **Web Search**: New `coding_plan_search` tool - perform web searches and get organic results with related search queries
|
|
165
|
+
- **Vision Language Model**: New `coding_plan_vlm` tool - analyze images using AI based on text prompts
|
|
166
|
+
|
|
167
|
+
#### π Features
|
|
168
|
+
- `coding_plan_search` - Search the web and get structured results including titles, links, snippets, and related searches
|
|
169
|
+
- `coding_plan_vlm` - Analyze images from URLs or local files, supporting JPEG, PNG, GIF, and WebP formats
|
|
170
|
+
|
|
171
|
+
## FAQ
|
|
172
|
+
### 1. invalid api key
|
|
173
|
+
Please ensure your API key and API host are regionally aligned
|
|
174
|
+
|Region| Global | Mainland |
|
|
175
|
+
|:--|:-----|:-----|
|
|
176
|
+
|MINIMAX_API_KEY| go get from [MiniMax Global](https://www.minimax.io/platform/user-center/basic-information/interface-key) | go get from [MiniMax](https://platform.minimaxi.com/user-center/basic-information/interface-key) |
|
|
177
|
+
|MINIMAX_API_HOST| https://api.minimax.io | https://api.minimaxi.com |
|
|
178
|
+
|
|
179
|
+
### 2. spawn uvx ENOENT
|
|
180
|
+
Please confirm its absolute path by running this command in your terminal:
|
|
181
|
+
```sh
|
|
182
|
+
which uvx
|
|
183
|
+
```
|
|
184
|
+
Once you obtain the absolute path (e.g., /usr/local/bin/uvx), update your configuration to use that path (e.g., "command": "/usr/local/bin/uvx").
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
## Example usage
|
|
189
|
+
|
|
190
|
+
β οΈ Warning: Using these tools may incur costs.
|
|
191
|
+
|
|
192
|
+
### 1. Web Search
|
|
193
|
+
Use the `coding_plan_search` tool to search for information on the web:
|
|
194
|
+
|
|
195
|
+
<img src="https://cdn.hailuoai.video/moss/prod/2025-11-20-15/user/multi_chat_file/99ff15e3-e5d6-48e2-8216-38c6abf03c71.image/png" style="display:
|
|
196
|
+
inline-block; vertical-align: middle; "/>
|
|
197
|
+
|
|
198
|
+
### 2. Image Analysis
|
|
199
|
+
Use the `coding_plan_vlm` tool to analyze images with AI:
|
|
200
|
+
|
|
201
|
+
<img src="https://cdn.hailuoai.video/moss/prod/2025-11-20-15/user/multi_chat_file/1f8174a2-68d5-4576-b886-86933eab5280.image/png" style="display:
|
|
202
|
+
inline-block; vertical-align: middle; "/>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
minimax_coding_plan_mcp-0.0.1.dist-info/licenses/LICENSE,sha256=qROMAfPCJkGsj4_i8-x13gtKBJT_gejaQ7PMIJGTWsM,1064
|
|
2
|
+
minimax_mcp/__init__.py,sha256=CKIi_HLF6Kg0jwf_Mq4H50_YqbadAzELljOlfvBoFAI,69
|
|
3
|
+
minimax_mcp/__main__.py,sha256=t-bxkqivbV-ri9pZADozQS2eUpjq5_cWTJXuG-vclmQ,3021
|
|
4
|
+
minimax_mcp/client.py,sha256=CNQRaiDoplLOueK29YsefN_kZLFLr_BZd-CLSwW2a-M,3675
|
|
5
|
+
minimax_mcp/const.py,sha256=mPzsI7CgHw9uaxJ4egkRAhmYaF8yCPmEBWaBepGE5KU,830
|
|
6
|
+
minimax_mcp/exceptions.py,sha256=f02y8FAKo7WS9jqhQD1_hFkPHWFX4-j5nEvZ_rBmFCU,547
|
|
7
|
+
minimax_mcp/server.py,sha256=wXW_4XEThyrqthKyLDQSCqpyvb33zd-LBrKbiw5raXE,5484
|
|
8
|
+
minimax_mcp/utils.py,sha256=YndfmVoeFyIHmyPcMe2uQke0C-r17eq288_2wRb3PUs,8504
|
|
9
|
+
minimax_coding_plan_mcp-0.0.1.dist-info/METADATA,sha256=NaxK9rYdwg2Eoy2dqGI6lsjsfywuLS48Zl-aQycJ0JY,12902
|
|
10
|
+
minimax_coding_plan_mcp-0.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
11
|
+
minimax_coding_plan_mcp-0.0.1.dist-info/entry_points.txt,sha256=nBQn7kfqXxlmJCWcPmnLA9nyy-wGLUuI0p4daZ6_OHg,68
|
|
12
|
+
minimax_coding_plan_mcp-0.0.1.dist-info/top_level.txt,sha256=PQ7pnbym7cuF8XXvQ-yLu2czj9EDuAhkTbbwtoeEKsA,12
|
|
13
|
+
minimax_coding_plan_mcp-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright 2025 MiniMax AI.
|
|
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 @@
|
|
|
1
|
+
minimax_mcp
|
minimax_mcp/__init__.py
ADDED
minimax_mcp/__main__.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import sys
|
|
5
|
+
from dotenv import load_dotenv
|
|
6
|
+
import argparse
|
|
7
|
+
|
|
8
|
+
load_dotenv()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_claude_config_path() -> Path | None:
|
|
12
|
+
"""Get the Claude config directory based on platform."""
|
|
13
|
+
if sys.platform == "win32":
|
|
14
|
+
path = Path(Path.home(), "AppData", "Roaming", "Claude")
|
|
15
|
+
elif sys.platform == "darwin":
|
|
16
|
+
path = Path(Path.home(), "Library", "Application Support", "Claude")
|
|
17
|
+
elif sys.platform.startswith("linux"):
|
|
18
|
+
path = Path(
|
|
19
|
+
os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config"), "Claude"
|
|
20
|
+
)
|
|
21
|
+
else:
|
|
22
|
+
return None
|
|
23
|
+
|
|
24
|
+
if path.exists():
|
|
25
|
+
return path
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_python_path():
|
|
30
|
+
return sys.executable
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def generate_config(api_key: str | None = None):
|
|
34
|
+
module_dir = Path(__file__).resolve().parent
|
|
35
|
+
server_path = module_dir / "server.py"
|
|
36
|
+
python_path = get_python_path()
|
|
37
|
+
|
|
38
|
+
final_api_key = api_key or os.environ.get("MINIMAX_API_KEY")
|
|
39
|
+
if not final_api_key:
|
|
40
|
+
print("Error: Minimax API key is required.")
|
|
41
|
+
print("Please either:")
|
|
42
|
+
print(" 1. Pass the API key using --api-key argument, or")
|
|
43
|
+
print(" 2. Set the MINIMAX_API_KEY environment variable, or")
|
|
44
|
+
print(" 3. Add MINIMAX_API_KEY to your .env file")
|
|
45
|
+
sys.exit(1)
|
|
46
|
+
|
|
47
|
+
config = {
|
|
48
|
+
"mcpServers": {
|
|
49
|
+
"Minimax": {
|
|
50
|
+
"command": "uvx",
|
|
51
|
+
"args": [
|
|
52
|
+
"minimax-coding-plan-mcp",
|
|
53
|
+
],
|
|
54
|
+
|
|
55
|
+
"env": {
|
|
56
|
+
"MINIMAX_API_KEY": final_api_key,
|
|
57
|
+
"MINIMAX_MCP_BASE_PATH": "",
|
|
58
|
+
"MINIMAX_API_HOST": "https://api.minimax.chat",
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return config
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
if __name__ == "__main__":
|
|
68
|
+
parser = argparse.ArgumentParser()
|
|
69
|
+
parser.add_argument(
|
|
70
|
+
"--print",
|
|
71
|
+
action="store_true",
|
|
72
|
+
help="Print config to screen instead of writing to file",
|
|
73
|
+
)
|
|
74
|
+
parser.add_argument(
|
|
75
|
+
"--api-key",
|
|
76
|
+
help="Minimax API key (alternatively, set MINIMAX_API_KEY environment variable)",
|
|
77
|
+
)
|
|
78
|
+
parser.add_argument(
|
|
79
|
+
"--config-path",
|
|
80
|
+
type=Path,
|
|
81
|
+
help="Custom path to Claude config directory",
|
|
82
|
+
)
|
|
83
|
+
args = parser.parse_args()
|
|
84
|
+
|
|
85
|
+
config = generate_config(args.api_key)
|
|
86
|
+
|
|
87
|
+
if args.print:
|
|
88
|
+
print(json.dumps(config, indent=2))
|
|
89
|
+
else:
|
|
90
|
+
claude_path = args.config_path if args.config_path else get_claude_config_path()
|
|
91
|
+
if claude_path is None:
|
|
92
|
+
print(
|
|
93
|
+
"Could not find Claude config path automatically. Please specify it using --config-path argument. The argument should be an absolute path of the claude_desktop_config.json file."
|
|
94
|
+
)
|
|
95
|
+
sys.exit(1)
|
|
96
|
+
|
|
97
|
+
claude_path.mkdir(parents=True, exist_ok=True)
|
|
98
|
+
print("Writing config to", claude_path / "claude_desktop_config.json")
|
|
99
|
+
with open(claude_path / "claude_desktop_config.json", "w") as f:
|
|
100
|
+
json.dump(config, f, indent=2)
|
minimax_mcp/client.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Minimax API client base class."""
|
|
2
|
+
|
|
3
|
+
import requests
|
|
4
|
+
from typing import Any, Dict
|
|
5
|
+
from minimax_mcp.exceptions import MinimaxAuthError, MinimaxRequestError
|
|
6
|
+
|
|
7
|
+
class MinimaxAPIClient:
|
|
8
|
+
"""Base client for making requests to Minimax API."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, api_key: str, api_host: str):
|
|
11
|
+
"""Initialize the API client.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
api_key: The API key for authentication
|
|
15
|
+
api_host: The API host URL
|
|
16
|
+
"""
|
|
17
|
+
self.api_key = api_key
|
|
18
|
+
self.api_host = api_host
|
|
19
|
+
self.session = requests.Session()
|
|
20
|
+
self.session.headers.update({
|
|
21
|
+
'Authorization': f'Bearer {api_key}',
|
|
22
|
+
'MM-API-Source': 'Minimax-MCP'
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
def _make_request(
|
|
26
|
+
self,
|
|
27
|
+
method: str,
|
|
28
|
+
endpoint: str,
|
|
29
|
+
**kwargs
|
|
30
|
+
) -> Dict[str, Any]:
|
|
31
|
+
"""Make an HTTP request to the Minimax API.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
method: HTTP method (GET, POST, etc.)
|
|
35
|
+
endpoint: API endpoint path
|
|
36
|
+
**kwargs: Additional arguments to pass to requests
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
API response data as dictionary
|
|
40
|
+
|
|
41
|
+
Raises:
|
|
42
|
+
MinimaxAuthError: If authentication fails
|
|
43
|
+
MinimaxRequestError: If the request fails
|
|
44
|
+
"""
|
|
45
|
+
url = f"{self.api_host}{endpoint}"
|
|
46
|
+
|
|
47
|
+
# Set Content-Type based on whether files are being uploaded
|
|
48
|
+
files = kwargs.get('files')
|
|
49
|
+
if not files:
|
|
50
|
+
self.session.headers['Content-Type'] = 'application/json'
|
|
51
|
+
else:
|
|
52
|
+
# Remove Content-Type header for multipart/form-data
|
|
53
|
+
# requests library will set it automatically with the correct boundary
|
|
54
|
+
self.session.headers.pop('Content-Type', None)
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
response = self.session.request(method, url, **kwargs)
|
|
58
|
+
|
|
59
|
+
# Check for other HTTP errors
|
|
60
|
+
response.raise_for_status()
|
|
61
|
+
|
|
62
|
+
data = response.json()
|
|
63
|
+
|
|
64
|
+
# Check API-specific error codes
|
|
65
|
+
base_resp = data.get("base_resp", {})
|
|
66
|
+
if base_resp.get("status_code") != 0:
|
|
67
|
+
match base_resp.get("status_code"):
|
|
68
|
+
case 1004:
|
|
69
|
+
raise MinimaxAuthError(
|
|
70
|
+
f"API Error: {base_resp.get('status_msg')}, please check your API key and API host."
|
|
71
|
+
f"Trace-Id: {response.headers.get('Trace-Id')}"
|
|
72
|
+
)
|
|
73
|
+
case 2038:
|
|
74
|
+
raise MinimaxRequestError(
|
|
75
|
+
f"API Error: {base_resp.get('status_msg')}, should complete real-name verification on the open-platform(https://platform.minimaxi.com/user-center/basic-information)."
|
|
76
|
+
f"Trace-Id: {response.headers.get('Trace-Id')}"
|
|
77
|
+
)
|
|
78
|
+
case _:
|
|
79
|
+
raise MinimaxRequestError(
|
|
80
|
+
f"API Error: {base_resp.get('status_code')}-{base_resp.get('status_msg')} "
|
|
81
|
+
f"Trace-Id: {response.headers.get('Trace-Id')}"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
return data
|
|
85
|
+
|
|
86
|
+
except requests.exceptions.RequestException as e:
|
|
87
|
+
raise MinimaxRequestError(f"Request failed: {str(e)}")
|
|
88
|
+
|
|
89
|
+
def get(self, endpoint: str, **kwargs) -> Dict[str, Any]:
|
|
90
|
+
"""Make a GET request."""
|
|
91
|
+
return self._make_request("GET", endpoint, **kwargs)
|
|
92
|
+
|
|
93
|
+
def post(self, endpoint: str, **kwargs) -> Dict[str, Any]:
|
|
94
|
+
"""Make a POST request."""
|
|
95
|
+
return self._make_request("POST", endpoint, **kwargs)
|
minimax_mcp/const.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# speech model default values
|
|
2
|
+
DEFAULT_VOICE_ID = "female-shaonv"
|
|
3
|
+
DEFAULT_SPEECH_MODEL = "speech-02-hd"
|
|
4
|
+
DEFAULT_MUSIC_MODEL = "music-1.5"
|
|
5
|
+
DEFAULT_SPEED = 1.0
|
|
6
|
+
DEFAULT_VOLUME = 1.0
|
|
7
|
+
DEFAULT_PITCH = 0
|
|
8
|
+
DEFAULT_EMOTION = "happy"
|
|
9
|
+
DEFAULT_SAMPLE_RATE = 32000
|
|
10
|
+
DEFAULT_BITRATE = 128000
|
|
11
|
+
DEFAULT_CHANNEL = 1
|
|
12
|
+
DEFAULT_FORMAT = "mp3"
|
|
13
|
+
DEFAULT_LANGUAGE_BOOST = "auto"
|
|
14
|
+
|
|
15
|
+
# video model default values
|
|
16
|
+
DEFAULT_T2V_MODEL = "T2V-01"
|
|
17
|
+
|
|
18
|
+
# image model default values
|
|
19
|
+
DEFAULT_T2I_MODEL = "image-01"
|
|
20
|
+
|
|
21
|
+
# ENV variables
|
|
22
|
+
ENV_MINIMAX_API_KEY = "MINIMAX_API_KEY"
|
|
23
|
+
ENV_MINIMAX_API_HOST = "MINIMAX_API_HOST"
|
|
24
|
+
ENV_MINIMAX_MCP_BASE_PATH = "MINIMAX_MCP_BASE_PATH"
|
|
25
|
+
ENV_RESOURCE_MODE = "MINIMAX_API_RESOURCE_MODE"
|
|
26
|
+
|
|
27
|
+
RESOURCE_MODE_LOCAL = "local" # save resource to local file system
|
|
28
|
+
RESOURCE_MODE_URL = "url" # provide resource url
|
|
29
|
+
|
|
30
|
+
ENV_FASTMCP_LOG_LEVEL = "FASTMCP_LOG_LEVEL"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Custom exceptions for Minimax MCP."""
|
|
2
|
+
|
|
3
|
+
class MinimaxAPIError(Exception):
|
|
4
|
+
"""Base exception for Minimax API errors."""
|
|
5
|
+
pass
|
|
6
|
+
|
|
7
|
+
class MinimaxAuthError(MinimaxAPIError):
|
|
8
|
+
"""Authentication related errors."""
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
class MinimaxRequestError(MinimaxAPIError):
|
|
12
|
+
"""Request related errors."""
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
class MinimaxTimeoutError(MinimaxAPIError):
|
|
16
|
+
"""Timeout related errors."""
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
class MinimaxValidationError(MinimaxAPIError):
|
|
20
|
+
"""Validation related errors."""
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
class MinimaxMcpError(MinimaxAPIError):
|
|
24
|
+
pass
|
minimax_mcp/server.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MiniMax Coding Plan MCP Server
|
|
3
|
+
|
|
4
|
+
β οΈ IMPORTANT: This server connects to Minimax API endpoints which may involve costs.
|
|
5
|
+
Any tool that makes an API call is clearly marked with a cost warning. Please follow these guidelines:
|
|
6
|
+
|
|
7
|
+
1. Only use these tools when users specifically ask for them
|
|
8
|
+
2. For audio generation tools, be mindful that text length affects the cost
|
|
9
|
+
3. Voice cloning features are charged upon first use after cloning
|
|
10
|
+
|
|
11
|
+
Note: Tools without cost warnings are free to use as they only read existing data.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import json
|
|
16
|
+
from dotenv import load_dotenv
|
|
17
|
+
from mcp.server.fastmcp import FastMCP
|
|
18
|
+
from mcp.types import TextContent
|
|
19
|
+
from minimax_mcp.utils import (
|
|
20
|
+
process_image_url,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
from minimax_mcp.const import *
|
|
24
|
+
from minimax_mcp.exceptions import MinimaxAPIError, MinimaxRequestError
|
|
25
|
+
from minimax_mcp.client import MinimaxAPIClient
|
|
26
|
+
|
|
27
|
+
load_dotenv()
|
|
28
|
+
api_key = os.getenv(ENV_MINIMAX_API_KEY)
|
|
29
|
+
base_path = os.getenv(ENV_MINIMAX_MCP_BASE_PATH) or "~/Desktop"
|
|
30
|
+
api_host = os.getenv(ENV_MINIMAX_API_HOST)
|
|
31
|
+
resource_mode = os.getenv(ENV_RESOURCE_MODE) or RESOURCE_MODE_URL
|
|
32
|
+
fastmcp_log_level = os.getenv(ENV_FASTMCP_LOG_LEVEL) or "WARNING"
|
|
33
|
+
|
|
34
|
+
if not api_key:
|
|
35
|
+
raise ValueError("MINIMAX_API_KEY environment variable is required")
|
|
36
|
+
if not api_host:
|
|
37
|
+
raise ValueError("MINIMAX_API_HOST environment variable is required")
|
|
38
|
+
|
|
39
|
+
mcp = FastMCP("Minimax",log_level=fastmcp_log_level)
|
|
40
|
+
api_client = MinimaxAPIClient(api_key, api_host)
|
|
41
|
+
|
|
42
|
+
@mcp.tool(
|
|
43
|
+
description="""Search for information using a search query.
|
|
44
|
+
|
|
45
|
+
This tool performs web searches and returns organic search results along with related search queries.
|
|
46
|
+
|
|
47
|
+
COST WARNING: This tool makes an API call to Minimax which may incur costs. Only use when explicitly requested by the user.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
query (str): The search query string.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Text content with search results in JSON format. The response structure is:
|
|
54
|
+
{
|
|
55
|
+
"organic": [
|
|
56
|
+
{
|
|
57
|
+
"title": "string - The title of the search result",
|
|
58
|
+
"link": "string - The URL link to the result",
|
|
59
|
+
"snippet": "string - A brief description or excerpt",
|
|
60
|
+
"date": "string - The date of the result"
|
|
61
|
+
}
|
|
62
|
+
],
|
|
63
|
+
"related_searches": [
|
|
64
|
+
{
|
|
65
|
+
"query": "string - A related search query suggestion"
|
|
66
|
+
}
|
|
67
|
+
],
|
|
68
|
+
"base_resp": {
|
|
69
|
+
"status_code": "int - Response status code",
|
|
70
|
+
"status_msg": "string - Response status message"
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
"""
|
|
74
|
+
)
|
|
75
|
+
def coding_plan_search(
|
|
76
|
+
query: str,
|
|
77
|
+
) -> TextContent:
|
|
78
|
+
try:
|
|
79
|
+
if not query:
|
|
80
|
+
raise MinimaxRequestError("Query is required")
|
|
81
|
+
|
|
82
|
+
# Build request payload
|
|
83
|
+
payload = {
|
|
84
|
+
"q": query
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
# Call search API
|
|
88
|
+
response_data = api_client.post("/v1/coding_plan/search", json=payload)
|
|
89
|
+
|
|
90
|
+
# Return JSON dump of response data
|
|
91
|
+
return TextContent(
|
|
92
|
+
type="text",
|
|
93
|
+
text=json.dumps(response_data, ensure_ascii=False, indent=2)
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
except MinimaxAPIError as e:
|
|
97
|
+
return TextContent(
|
|
98
|
+
type="text",
|
|
99
|
+
text=f"Failed to perform search: {str(e)}"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@mcp.tool(
|
|
104
|
+
description="""Analyze an image with AI based on your text prompt.
|
|
105
|
+
|
|
106
|
+
This tool analyzes images and answers questions or extracts information based on your prompt.
|
|
107
|
+
|
|
108
|
+
COST WARNING: This tool makes an API call to Minimax which may incur costs. Only use when explicitly requested by the user.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
prompt (str): The text prompt describing what you want to analyze or extract from the image.
|
|
112
|
+
image_source (str): The source location of the image to analyze.
|
|
113
|
+
Accepts:
|
|
114
|
+
- HTTP/HTTPS URL: "https://example.com/image.jpg"
|
|
115
|
+
- Local file path: "/path/to/image.png"
|
|
116
|
+
Supported formats: JPEG, PNG, GIF, WebP
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Text content with the VLM analysis result containing the content field.
|
|
120
|
+
"""
|
|
121
|
+
)
|
|
122
|
+
def coding_plan_vlm(
|
|
123
|
+
prompt: str,
|
|
124
|
+
image_source: str,
|
|
125
|
+
) -> TextContent:
|
|
126
|
+
try:
|
|
127
|
+
if not prompt:
|
|
128
|
+
raise MinimaxRequestError("Prompt is required")
|
|
129
|
+
if not image_source:
|
|
130
|
+
raise MinimaxRequestError("Image source is required")
|
|
131
|
+
|
|
132
|
+
# Process image_source: convert HTTP URL or local file to base64, or pass through existing base64
|
|
133
|
+
processed_image_url = process_image_url(image_source)
|
|
134
|
+
|
|
135
|
+
# Build request payload
|
|
136
|
+
payload = {
|
|
137
|
+
"prompt": prompt,
|
|
138
|
+
"image_url": processed_image_url
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
# Call VLM API
|
|
142
|
+
response_data = api_client.post("/v1/coding_plan/vlm", json=payload)
|
|
143
|
+
|
|
144
|
+
# Extract content from response
|
|
145
|
+
content = response_data.get("content", "")
|
|
146
|
+
|
|
147
|
+
if not content:
|
|
148
|
+
raise MinimaxRequestError("No content returned from VLM API")
|
|
149
|
+
|
|
150
|
+
# Return the content
|
|
151
|
+
return TextContent(
|
|
152
|
+
type="text",
|
|
153
|
+
text=content
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
except MinimaxAPIError as e:
|
|
157
|
+
return TextContent(
|
|
158
|
+
type="text",
|
|
159
|
+
text=f"Failed to perform VLM analysis: {str(e)}"
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def main():
|
|
164
|
+
print("Starting Minimax MCP server")
|
|
165
|
+
"""Run the Minimax MCP server"""
|
|
166
|
+
mcp.run()
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
if __name__ == "__main__":
|
|
170
|
+
main()
|
minimax_mcp/utils.py
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from fuzzywuzzy import fuzz
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
import base64
|
|
8
|
+
import requests
|
|
9
|
+
from typing import Iterator, Union
|
|
10
|
+
from minimax_mcp.const import *
|
|
11
|
+
from minimax_mcp.exceptions import MinimaxMcpError, MinimaxRequestError
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def is_file_writeable(path: Path) -> bool:
|
|
15
|
+
if path.exists():
|
|
16
|
+
return os.access(path, os.W_OK)
|
|
17
|
+
parent_dir = path.parent
|
|
18
|
+
return os.access(parent_dir, os.W_OK)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def build_output_file(
|
|
22
|
+
tool: str, text: str, output_path: Path, extension: str, full_id: bool = False
|
|
23
|
+
) -> Path:
|
|
24
|
+
id = text if full_id else text[:10]
|
|
25
|
+
|
|
26
|
+
output_file_name = f"{tool}_{id.replace(' ', '_')}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.{extension}"
|
|
27
|
+
return output_path / output_file_name
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def build_output_path(
|
|
31
|
+
output_directory: str | None, base_path: str | None = None, is_test: bool = False
|
|
32
|
+
) -> Path:
|
|
33
|
+
# Set default base_path to desktop if not provided
|
|
34
|
+
if base_path is None:
|
|
35
|
+
base_path = str(Path.home() / "Desktop")
|
|
36
|
+
|
|
37
|
+
# Handle output path based on output_directory
|
|
38
|
+
if output_directory is None:
|
|
39
|
+
output_path = Path(os.path.expanduser(base_path))
|
|
40
|
+
elif not os.path.isabs(os.path.expanduser(output_directory)):
|
|
41
|
+
output_path = Path(os.path.expanduser(base_path)) / Path(output_directory)
|
|
42
|
+
else:
|
|
43
|
+
output_path = Path(os.path.expanduser(output_directory))
|
|
44
|
+
|
|
45
|
+
# Safety checks and directory creation
|
|
46
|
+
if is_test:
|
|
47
|
+
return output_path
|
|
48
|
+
if not is_file_writeable(output_path):
|
|
49
|
+
raise MinimaxMcpError(f"Directory ({output_path}) is not writeable")
|
|
50
|
+
output_path.mkdir(parents=True, exist_ok=True)
|
|
51
|
+
return output_path
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def find_similar_filenames(
|
|
55
|
+
target_file: str, directory: Path, threshold: int = 70
|
|
56
|
+
) -> list[tuple[str, int]]:
|
|
57
|
+
"""
|
|
58
|
+
Find files with names similar to the target file using fuzzy matching.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
target_file (str): The reference filename to compare against
|
|
62
|
+
directory (str): Directory to search in (defaults to current directory)
|
|
63
|
+
threshold (int): Similarity threshold (0 to 100, where 100 is identical)
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
list: List of similar filenames with their similarity scores
|
|
67
|
+
"""
|
|
68
|
+
target_filename = os.path.basename(target_file)
|
|
69
|
+
similar_files = []
|
|
70
|
+
for root, _, files in os.walk(directory):
|
|
71
|
+
for filename in files:
|
|
72
|
+
if (
|
|
73
|
+
filename == target_filename
|
|
74
|
+
and os.path.join(root, filename) == target_file
|
|
75
|
+
):
|
|
76
|
+
continue
|
|
77
|
+
similarity = fuzz.token_sort_ratio(target_filename, filename)
|
|
78
|
+
|
|
79
|
+
if similarity >= threshold:
|
|
80
|
+
file_path = Path(root) / filename
|
|
81
|
+
similar_files.append((file_path, similarity))
|
|
82
|
+
|
|
83
|
+
similar_files.sort(key=lambda x: x[1], reverse=True)
|
|
84
|
+
|
|
85
|
+
return similar_files
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def try_find_similar_files(
|
|
89
|
+
filename: str, directory: Path, take_n: int = 5
|
|
90
|
+
) -> list[Path]:
|
|
91
|
+
similar_files = find_similar_filenames(filename, directory)
|
|
92
|
+
if not similar_files:
|
|
93
|
+
return []
|
|
94
|
+
|
|
95
|
+
filtered_files = []
|
|
96
|
+
|
|
97
|
+
for path, _ in similar_files[:take_n]:
|
|
98
|
+
if check_audio_file(path):
|
|
99
|
+
filtered_files.append(path)
|
|
100
|
+
|
|
101
|
+
return filtered_files
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def check_audio_file(path: Path) -> bool:
|
|
105
|
+
audio_extensions = {
|
|
106
|
+
".wav",
|
|
107
|
+
".mp3",
|
|
108
|
+
".m4a",
|
|
109
|
+
".aac",
|
|
110
|
+
".ogg",
|
|
111
|
+
".flac",
|
|
112
|
+
".mp4",
|
|
113
|
+
".avi",
|
|
114
|
+
".mov",
|
|
115
|
+
".wmv",
|
|
116
|
+
}
|
|
117
|
+
return path.suffix.lower() in audio_extensions
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def process_input_file(file_path: str, audio_content_check: bool = True) -> Path:
|
|
121
|
+
if not os.path.isabs(file_path) and not os.environ.get(ENV_MINIMAX_MCP_BASE_PATH):
|
|
122
|
+
raise MinimaxMcpError(
|
|
123
|
+
"File path must be an absolute path if MINIMAX_MCP_BASE_PATH is not set"
|
|
124
|
+
)
|
|
125
|
+
path = Path(file_path)
|
|
126
|
+
if not path.exists() and path.parent.exists():
|
|
127
|
+
parent_directory = path.parent
|
|
128
|
+
similar_files = try_find_similar_files(path.name, parent_directory)
|
|
129
|
+
similar_files_formatted = ",".join([str(file) for file in similar_files])
|
|
130
|
+
if similar_files:
|
|
131
|
+
raise MinimaxMcpError(
|
|
132
|
+
f"File ({path}) does not exist. Did you mean any of these files: {similar_files_formatted}?"
|
|
133
|
+
)
|
|
134
|
+
raise MinimaxMcpError(f"File ({path}) does not exist")
|
|
135
|
+
elif not path.exists():
|
|
136
|
+
raise MinimaxMcpError(f"File ({path}) does not exist")
|
|
137
|
+
elif not path.is_file():
|
|
138
|
+
raise MinimaxMcpError(f"File ({path}) is not a file")
|
|
139
|
+
|
|
140
|
+
if audio_content_check and not check_audio_file(path):
|
|
141
|
+
raise MinimaxMcpError(f"File ({path}) is not an audio or video file")
|
|
142
|
+
return path
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def is_installed(lib_name: str) -> bool:
|
|
146
|
+
return shutil.which(lib_name) is not None
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def play(
|
|
150
|
+
audio: Union[bytes, Iterator[bytes]]
|
|
151
|
+
) -> None:
|
|
152
|
+
if isinstance(audio, Iterator):
|
|
153
|
+
audio = b"".join(audio)
|
|
154
|
+
|
|
155
|
+
if not is_installed("ffplay"):
|
|
156
|
+
message = (
|
|
157
|
+
"ffplay from ffmpeg not found, necessary to play audio. "
|
|
158
|
+
"mac: install it with 'brew install ffmpeg'. "
|
|
159
|
+
"linux or windows: install it from https://ffmpeg.org/"
|
|
160
|
+
)
|
|
161
|
+
raise ValueError(message)
|
|
162
|
+
|
|
163
|
+
args = ["ffplay", "-autoexit", "-", "-nodisp"]
|
|
164
|
+
proc = subprocess.Popen(
|
|
165
|
+
args=args,
|
|
166
|
+
stdout=subprocess.PIPE,
|
|
167
|
+
stdin=subprocess.PIPE,
|
|
168
|
+
stderr=subprocess.PIPE,
|
|
169
|
+
)
|
|
170
|
+
out, err = proc.communicate(input=audio)
|
|
171
|
+
|
|
172
|
+
proc.poll()
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def process_image_url(image_url: str) -> str:
|
|
176
|
+
"""
|
|
177
|
+
Process image URL and convert to base64 data URL format.
|
|
178
|
+
|
|
179
|
+
This function handles three types of image inputs:
|
|
180
|
+
1. HTTP/HTTPS URLs: Downloads the image and converts to base64
|
|
181
|
+
2. Base64 data URLs: Passes through as-is
|
|
182
|
+
3. Local file paths: Reads the file and converts to base64
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
image_url (str): The image URL, data URL, or local file path
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
str: Base64 data URL in format "data:image/{format};base64,{data}"
|
|
189
|
+
|
|
190
|
+
Raises:
|
|
191
|
+
MinimaxRequestError: If image cannot be downloaded, read, or processed
|
|
192
|
+
"""
|
|
193
|
+
# If already in base64 data URL format, pass through
|
|
194
|
+
if image_url.startswith("data:"):
|
|
195
|
+
return image_url
|
|
196
|
+
|
|
197
|
+
# Handle HTTP/HTTPS URLs
|
|
198
|
+
if image_url.startswith(("http://", "https://")):
|
|
199
|
+
try:
|
|
200
|
+
image_response = requests.get(image_url)
|
|
201
|
+
image_response.raise_for_status()
|
|
202
|
+
image_data = image_response.content
|
|
203
|
+
|
|
204
|
+
# Detect image format from content-type header
|
|
205
|
+
content_type = image_response.headers.get('content-type', '').lower()
|
|
206
|
+
if 'jpeg' in content_type or 'jpg' in content_type:
|
|
207
|
+
image_format = 'jpeg'
|
|
208
|
+
elif 'png' in content_type:
|
|
209
|
+
image_format = 'png'
|
|
210
|
+
elif 'gif' in content_type:
|
|
211
|
+
image_format = 'gif'
|
|
212
|
+
elif 'webp' in content_type:
|
|
213
|
+
image_format = 'webp'
|
|
214
|
+
else:
|
|
215
|
+
# Default to jpeg if cannot detect
|
|
216
|
+
image_format = 'jpeg'
|
|
217
|
+
|
|
218
|
+
# Convert to base64 data URL
|
|
219
|
+
base64_data = base64.b64encode(image_data).decode('utf-8')
|
|
220
|
+
return f"data:image/{image_format};base64,{base64_data}"
|
|
221
|
+
|
|
222
|
+
except requests.RequestException as e:
|
|
223
|
+
raise MinimaxRequestError(f"Failed to download image from URL: {str(e)}")
|
|
224
|
+
|
|
225
|
+
# Handle local file paths
|
|
226
|
+
else:
|
|
227
|
+
if not os.path.exists(image_url):
|
|
228
|
+
raise MinimaxRequestError(f"Local image file does not exist: {image_url}")
|
|
229
|
+
|
|
230
|
+
try:
|
|
231
|
+
with open(image_url, "rb") as f:
|
|
232
|
+
image_data = f.read()
|
|
233
|
+
|
|
234
|
+
# Detect image format from file extension
|
|
235
|
+
image_format = 'jpeg' # Default
|
|
236
|
+
if image_url.lower().endswith('.png'):
|
|
237
|
+
image_format = 'png'
|
|
238
|
+
elif image_url.lower().endswith('.gif'):
|
|
239
|
+
image_format = 'gif'
|
|
240
|
+
elif image_url.lower().endswith('.webp'):
|
|
241
|
+
image_format = 'webp'
|
|
242
|
+
elif image_url.lower().endswith(('.jpg', '.jpeg')):
|
|
243
|
+
image_format = 'jpeg'
|
|
244
|
+
|
|
245
|
+
base64_data = base64.b64encode(image_data).decode('utf-8')
|
|
246
|
+
return f"data:image/{image_format};base64,{base64_data}"
|
|
247
|
+
|
|
248
|
+
except IOError as e:
|
|
249
|
+
raise MinimaxRequestError(f"Failed to read local image file: {str(e)}")
|
|
250
|
+
|
|
251
|
+
|