luma-mcp 1.2.1 → 1.2.2
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.
- package/.github/workflows/release.yml +0 -17
- package/CHANGELOG.md +21 -0
- package/README.md +5 -1
- package/build/image-processor.d.ts.map +1 -1
- package/build/image-processor.js +33 -17
- package/build/image-processor.js.map +1 -1
- package/build/prompts.d.ts +1 -1
- package/build/prompts.d.ts.map +1 -1
- package/build/prompts.js +14 -28
- package/build/prompts.js.map +1 -1
- package/minimax_coding_plan_mcp-0.0.2/.env.test +4 -0
- package/minimax_coding_plan_mcp-0.0.2/LICENSE +21 -0
- package/minimax_coding_plan_mcp-0.0.2/PKG-INFO +200 -0
- package/minimax_coding_plan_mcp-0.0.2/README.md +143 -0
- package/minimax_coding_plan_mcp-0.0.2/minimax_coding_plan_mcp.egg-info/PKG-INFO +200 -0
- package/minimax_coding_plan_mcp-0.0.2/minimax_coding_plan_mcp.egg-info/SOURCES.txt +17 -0
- package/minimax_coding_plan_mcp-0.0.2/minimax_coding_plan_mcp.egg-info/dependency_links.txt +1 -0
- package/minimax_coding_plan_mcp-0.0.2/minimax_coding_plan_mcp.egg-info/entry_points.txt +2 -0
- package/minimax_coding_plan_mcp-0.0.2/minimax_coding_plan_mcp.egg-info/requires.txt +20 -0
- package/minimax_coding_plan_mcp-0.0.2/minimax_coding_plan_mcp.egg-info/top_level.txt +1 -0
- package/minimax_coding_plan_mcp-0.0.2/minimax_mcp/__init__.py +3 -0
- package/minimax_coding_plan_mcp-0.0.2/minimax_mcp/__main__.py +99 -0
- package/minimax_coding_plan_mcp-0.0.2/minimax_mcp/__pycache__/__init__.cpython-313.pyc +0 -0
- package/minimax_coding_plan_mcp-0.0.2/minimax_mcp/__pycache__/client.cpython-313.pyc +0 -0
- package/minimax_coding_plan_mcp-0.0.2/minimax_mcp/__pycache__/const.cpython-313.pyc +0 -0
- package/minimax_coding_plan_mcp-0.0.2/minimax_mcp/__pycache__/exceptions.cpython-313.pyc +0 -0
- package/minimax_coding_plan_mcp-0.0.2/minimax_mcp/__pycache__/utils.cpython-313.pyc +0 -0
- package/minimax_coding_plan_mcp-0.0.2/minimax_mcp/client.py +104 -0
- package/minimax_coding_plan_mcp-0.0.2/minimax_mcp/const.py +4 -0
- package/minimax_coding_plan_mcp-0.0.2/minimax_mcp/exceptions.py +24 -0
- package/minimax_coding_plan_mcp-0.0.2/minimax_mcp/server.py +169 -0
- package/minimax_coding_plan_mcp-0.0.2/minimax_mcp/utils.py +101 -0
- package/minimax_coding_plan_mcp-0.0.2/pyproject.toml +59 -0
- package/minimax_coding_plan_mcp-0.0.2/setup.cfg +4 -0
- package/minimax_coding_plan_mcp-0.0.2/setup.py +6 -0
- package/minimax_coding_plan_mcp-0.0.2/test_at_prefix.py +134 -0
- package/minimax_coding_plan_mcp-0.0.2/test_real_image.py +153 -0
- package/package.json +1 -1
|
@@ -0,0 +1,169 @@
|
|
|
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
|
+
api_host = os.getenv(ENV_MINIMAX_API_HOST)
|
|
30
|
+
fastmcp_log_level = os.getenv(ENV_FASTMCP_LOG_LEVEL) or "WARNING"
|
|
31
|
+
|
|
32
|
+
if not api_key:
|
|
33
|
+
raise ValueError("MINIMAX_API_KEY environment variable is required")
|
|
34
|
+
if not api_host:
|
|
35
|
+
raise ValueError("MINIMAX_API_HOST environment variable is required")
|
|
36
|
+
|
|
37
|
+
mcp = FastMCP("Minimax",log_level=fastmcp_log_level)
|
|
38
|
+
api_client = MinimaxAPIClient(api_key, api_host)
|
|
39
|
+
|
|
40
|
+
@mcp.tool(
|
|
41
|
+
description="""
|
|
42
|
+
|
|
43
|
+
Web Search API, works like Google Search.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
query (str): The search query string. Use 3-5 keywords. Add the current date for time-sensitive queries, e.g. `latest iPhone 2025`
|
|
47
|
+
|
|
48
|
+
Search Strategy:
|
|
49
|
+
- If no valid results found, try changing search keywords
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Text content with search results in JSON format. The response structure is:
|
|
53
|
+
{
|
|
54
|
+
"organic": [
|
|
55
|
+
{
|
|
56
|
+
"title": "string - The title of the search result",
|
|
57
|
+
"link": "string - The URL link to the result",
|
|
58
|
+
"snippet": "string - A brief description or excerpt",
|
|
59
|
+
"date": "string - The date of the result"
|
|
60
|
+
}
|
|
61
|
+
],
|
|
62
|
+
"related_searches": [
|
|
63
|
+
{
|
|
64
|
+
"query": "string - A related search query suggestion"
|
|
65
|
+
}
|
|
66
|
+
],
|
|
67
|
+
"base_resp": {
|
|
68
|
+
"status_code": "int - Response status code",
|
|
69
|
+
"status_msg": "string - Response status message"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
"""
|
|
73
|
+
)
|
|
74
|
+
def web_search(
|
|
75
|
+
query: str,
|
|
76
|
+
) -> TextContent:
|
|
77
|
+
try:
|
|
78
|
+
if not query:
|
|
79
|
+
raise MinimaxRequestError("Query is required")
|
|
80
|
+
|
|
81
|
+
# Build request payload
|
|
82
|
+
payload = {
|
|
83
|
+
"q": query
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
# Call search API
|
|
87
|
+
response_data = api_client.post("/v1/coding_plan/search", json=payload)
|
|
88
|
+
|
|
89
|
+
# Return JSON dump of response data
|
|
90
|
+
return TextContent(
|
|
91
|
+
type="text",
|
|
92
|
+
text=json.dumps(response_data, ensure_ascii=False, indent=2)
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
except MinimaxAPIError as e:
|
|
96
|
+
return TextContent(
|
|
97
|
+
type="text",
|
|
98
|
+
text=f"Failed to perform search: {str(e)}"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@mcp.tool(
|
|
103
|
+
description="""
|
|
104
|
+
|
|
105
|
+
A powerful LLM that can analyze and understand image content from files or URLs, follow your instruction.
|
|
106
|
+
Use this tool to understand images by LLM.
|
|
107
|
+
Only support jpeg, png, webp formats. Other formats like pdf/gif/psd/svg and so on are not supported.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
prompt (str): The text prompt describing what you want to analyze or extract from the image.
|
|
111
|
+
image_source (str): The source location of the image to analyze.
|
|
112
|
+
Accepts:
|
|
113
|
+
- HTTP/HTTPS URL: "https://example.com/image.jpg"
|
|
114
|
+
- Local file path: "/path/to/image.png"
|
|
115
|
+
Supported formats: JPEG, PNG, WebP
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Text content with the image analysis result.
|
|
119
|
+
"""
|
|
120
|
+
)
|
|
121
|
+
def understand_image(
|
|
122
|
+
prompt: str,
|
|
123
|
+
image_source: str,
|
|
124
|
+
) -> TextContent:
|
|
125
|
+
try:
|
|
126
|
+
if not prompt:
|
|
127
|
+
raise MinimaxRequestError("Prompt is required")
|
|
128
|
+
if not image_source:
|
|
129
|
+
raise MinimaxRequestError("Image source is required")
|
|
130
|
+
|
|
131
|
+
# Process image_source: convert HTTP URL or local file to base64, or pass through existing base64
|
|
132
|
+
processed_image_url = process_image_url(image_source)
|
|
133
|
+
|
|
134
|
+
# Build request payload
|
|
135
|
+
payload = {
|
|
136
|
+
"prompt": prompt,
|
|
137
|
+
"image_url": processed_image_url
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
# Call VLM API
|
|
141
|
+
response_data = api_client.post("/v1/coding_plan/vlm", json=payload)
|
|
142
|
+
|
|
143
|
+
# Extract content from response
|
|
144
|
+
content = response_data.get("content", "")
|
|
145
|
+
|
|
146
|
+
if not content:
|
|
147
|
+
raise MinimaxRequestError("No content returned from VLM API")
|
|
148
|
+
|
|
149
|
+
# Return the content
|
|
150
|
+
return TextContent(
|
|
151
|
+
type="text",
|
|
152
|
+
text=content
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
except MinimaxAPIError as e:
|
|
156
|
+
return TextContent(
|
|
157
|
+
type="text",
|
|
158
|
+
text=f"Failed to perform VLM analysis: {str(e)}"
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def main():
|
|
163
|
+
print("Starting Minimax MCP server")
|
|
164
|
+
"""Run the Minimax MCP server"""
|
|
165
|
+
mcp.run()
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
if __name__ == "__main__":
|
|
169
|
+
main()
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import base64
|
|
3
|
+
import requests
|
|
4
|
+
from minimax_mcp.const import *
|
|
5
|
+
from minimax_mcp.exceptions import MinimaxMcpError, MinimaxRequestError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def strip_at_prefix(path: str) -> str:
|
|
9
|
+
"""
|
|
10
|
+
Remove @ prefix from Claude Code file references.
|
|
11
|
+
|
|
12
|
+
Claude Code uses @ as a syntax sugar for workspace files.
|
|
13
|
+
For example: @test-minimax-image/image.png -> test-minimax-image/image.png
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
path (str): The file path that may have @ prefix
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
str: Path without @ prefix
|
|
20
|
+
"""
|
|
21
|
+
if path.startswith('@'):
|
|
22
|
+
return path[1:]
|
|
23
|
+
return path
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def process_image_url(image_url: str) -> str:
|
|
27
|
+
"""
|
|
28
|
+
Process image URL and convert to base64 data URL format.
|
|
29
|
+
|
|
30
|
+
This function handles three types of image inputs:
|
|
31
|
+
1. HTTP/HTTPS URLs: Downloads the image and converts to base64
|
|
32
|
+
2. Base64 data URLs: Passes through as-is
|
|
33
|
+
3. Local file paths: Reads the file and converts to base64
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
image_url (str): The image URL, data URL, or local file path
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
str: Base64 data URL in format "data:image/{format};base64,{data}"
|
|
40
|
+
|
|
41
|
+
Raises:
|
|
42
|
+
MinimaxRequestError: If image cannot be downloaded, read, or processed
|
|
43
|
+
"""
|
|
44
|
+
# Remove @ prefix if present (Claude Code syntax)
|
|
45
|
+
image_url = strip_at_prefix(image_url)
|
|
46
|
+
|
|
47
|
+
# If already in base64 data URL format, pass through
|
|
48
|
+
if image_url.startswith("data:"):
|
|
49
|
+
return image_url
|
|
50
|
+
|
|
51
|
+
# Handle HTTP/HTTPS URLs
|
|
52
|
+
if image_url.startswith(("http://", "https://")):
|
|
53
|
+
try:
|
|
54
|
+
image_response = requests.get(image_url)
|
|
55
|
+
image_response.raise_for_status()
|
|
56
|
+
image_data = image_response.content
|
|
57
|
+
|
|
58
|
+
# Detect image format from content-type header
|
|
59
|
+
content_type = image_response.headers.get('content-type', '').lower()
|
|
60
|
+
if 'jpeg' in content_type or 'jpg' in content_type:
|
|
61
|
+
image_format = 'jpeg'
|
|
62
|
+
elif 'png' in content_type:
|
|
63
|
+
image_format = 'png'
|
|
64
|
+
elif 'webp' in content_type:
|
|
65
|
+
image_format = 'webp'
|
|
66
|
+
else:
|
|
67
|
+
# Default to jpeg if cannot detect
|
|
68
|
+
image_format = 'jpeg'
|
|
69
|
+
|
|
70
|
+
# Convert to base64 data URL
|
|
71
|
+
base64_data = base64.b64encode(image_data).decode('utf-8')
|
|
72
|
+
return f"data:image/{image_format};base64,{base64_data}"
|
|
73
|
+
|
|
74
|
+
except requests.RequestException as e:
|
|
75
|
+
raise MinimaxRequestError(f"Failed to download image from URL: {str(e)}")
|
|
76
|
+
|
|
77
|
+
# Handle local file paths
|
|
78
|
+
else:
|
|
79
|
+
if not os.path.exists(image_url):
|
|
80
|
+
raise MinimaxRequestError(f"Local image file does not exist: {image_url}")
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
with open(image_url, "rb") as f:
|
|
84
|
+
image_data = f.read()
|
|
85
|
+
|
|
86
|
+
# Detect image format from file extension
|
|
87
|
+
image_format = 'jpeg' # Default
|
|
88
|
+
if image_url.lower().endswith('.png'):
|
|
89
|
+
image_format = 'png'
|
|
90
|
+
elif image_url.lower().endswith('.webp'):
|
|
91
|
+
image_format = 'webp'
|
|
92
|
+
elif image_url.lower().endswith(('.jpg', '.jpeg')):
|
|
93
|
+
image_format = 'jpeg'
|
|
94
|
+
|
|
95
|
+
base64_data = base64.b64encode(image_data).decode('utf-8')
|
|
96
|
+
return f"data:image/{image_format};base64,{base64_data}"
|
|
97
|
+
|
|
98
|
+
except IOError as e:
|
|
99
|
+
raise MinimaxRequestError(f"Failed to read local image file: {str(e)}")
|
|
100
|
+
|
|
101
|
+
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "minimax-coding-plan-mcp"
|
|
3
|
+
version = "0.0.2"
|
|
4
|
+
description = "Specialized MiniMax Model Context Protocol (MCP) server designed for coding-plan users"
|
|
5
|
+
authors = [
|
|
6
|
+
{ name = "Roy Wu", email = "zhengyu@minimax.chat" },
|
|
7
|
+
]
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
license = { file = "LICENSE" }
|
|
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
|
+
]
|
|
17
|
+
keywords = [
|
|
18
|
+
"minimax",
|
|
19
|
+
"mcp",
|
|
20
|
+
"web_search",
|
|
21
|
+
"understand-image",
|
|
22
|
+
]
|
|
23
|
+
requires-python = ">=3.10"
|
|
24
|
+
dependencies = [
|
|
25
|
+
"mcp[cli]>=1.6.0",
|
|
26
|
+
"fastapi>=0.109.2",
|
|
27
|
+
"uvicorn>=0.27.1",
|
|
28
|
+
"python-dotenv>=1.0.1",
|
|
29
|
+
"pydantic>=2.6.1",
|
|
30
|
+
"httpx>=0.28.1",
|
|
31
|
+
"fuzzywuzzy>=0.18.0",
|
|
32
|
+
"python-Levenshtein>=0.25.0",
|
|
33
|
+
"sounddevice>=0.5.1",
|
|
34
|
+
"soundfile>=0.13.1",
|
|
35
|
+
"requests>=2.31.0",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[project.scripts]
|
|
39
|
+
minimax-coding-plan-mcp = "minimax_mcp.server:main"
|
|
40
|
+
|
|
41
|
+
[project.optional-dependencies]
|
|
42
|
+
dev = [
|
|
43
|
+
"pre-commit>=3.6.2",
|
|
44
|
+
"ruff>=0.3.0",
|
|
45
|
+
"fastmcp>=0.4.1",
|
|
46
|
+
"pytest>=8.0.0",
|
|
47
|
+
"pytest-cov>=4.1.0",
|
|
48
|
+
"twine>=6.1.0",
|
|
49
|
+
"build>=1.0.3",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
[build-system]
|
|
53
|
+
requires = ["setuptools>=45", "wheel"]
|
|
54
|
+
build-backend = "setuptools.build_meta"
|
|
55
|
+
|
|
56
|
+
[tool.pytest.ini_options]
|
|
57
|
+
testpaths = ["tests"]
|
|
58
|
+
python_files = ["test_*.py"]
|
|
59
|
+
addopts = "-v --cov=minimax_mcp --cov-report=term-missing"
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Test script to verify @ prefix handling
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
# Add parent directory to path
|
|
10
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
11
|
+
|
|
12
|
+
from minimax_mcp.utils import strip_at_prefix, process_image_url
|
|
13
|
+
from minimax_mcp.exceptions import MinimaxRequestError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_strip_at_prefix():
|
|
17
|
+
"""Test @ prefix removal"""
|
|
18
|
+
print("Testing strip_at_prefix()...")
|
|
19
|
+
|
|
20
|
+
# Test cases
|
|
21
|
+
test_cases = [
|
|
22
|
+
("@test-minimax-image/image.png", "test-minimax-image/image.png"),
|
|
23
|
+
("@folder/subfolder/image.jpg", "folder/subfolder/image.jpg"),
|
|
24
|
+
("./normal/path.png", "./normal/path.png"),
|
|
25
|
+
("D:\\images\\test.png", "D:\\images\\test.png"),
|
|
26
|
+
("https://example.com/image.jpg", "https://example.com/image.jpg"),
|
|
27
|
+
("data:image/png;base64,abc123", "data:image/png;base64,abc123"),
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
all_passed = True
|
|
31
|
+
for input_path, expected in test_cases:
|
|
32
|
+
result = strip_at_prefix(input_path)
|
|
33
|
+
passed = result == expected
|
|
34
|
+
status = "✅ PASS" if passed else "❌ FAIL"
|
|
35
|
+
print(f"{status}: '{input_path}' -> '{result}' (expected: '{expected}')")
|
|
36
|
+
if not passed:
|
|
37
|
+
all_passed = False
|
|
38
|
+
|
|
39
|
+
return all_passed
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_process_image_url_with_at_prefix():
|
|
43
|
+
"""Test process_image_url with @ prefix"""
|
|
44
|
+
print("\nTesting process_image_url() with @ prefix...")
|
|
45
|
+
|
|
46
|
+
# Create a test image file
|
|
47
|
+
test_image_path = "test_image_for_at_prefix.png"
|
|
48
|
+
|
|
49
|
+
# Create a simple 1x1 PNG
|
|
50
|
+
import base64
|
|
51
|
+
tiny_png_base64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
|
|
52
|
+
with open(test_image_path, "wb") as f:
|
|
53
|
+
f.write(base64.b64decode(tiny_png_base64))
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
# Test with @ prefix
|
|
57
|
+
result = process_image_url(f"@{test_image_path}")
|
|
58
|
+
print(f"✅ PASS: Successfully processed '@{test_image_path}'")
|
|
59
|
+
print(f" Result starts with: {result[:50]}...")
|
|
60
|
+
|
|
61
|
+
# Test without @ prefix
|
|
62
|
+
result2 = process_image_url(test_image_path)
|
|
63
|
+
print(f"✅ PASS: Successfully processed '{test_image_path}' (without @)")
|
|
64
|
+
|
|
65
|
+
# Results should be identical
|
|
66
|
+
if result == result2:
|
|
67
|
+
print(f"✅ PASS: Results are identical (@ prefix correctly removed)")
|
|
68
|
+
return True
|
|
69
|
+
else:
|
|
70
|
+
print(f"❌ FAIL: Results differ")
|
|
71
|
+
return False
|
|
72
|
+
|
|
73
|
+
except Exception as e:
|
|
74
|
+
print(f"❌ FAIL: {str(e)}")
|
|
75
|
+
return False
|
|
76
|
+
finally:
|
|
77
|
+
# Cleanup
|
|
78
|
+
if os.path.exists(test_image_path):
|
|
79
|
+
os.remove(test_image_path)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_nonexistent_file_with_at_prefix():
|
|
83
|
+
"""Test error message with @ prefix on nonexistent file"""
|
|
84
|
+
print("\nTesting error message with @ prefix on nonexistent file...")
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
process_image_url("@nonexistent/file.png")
|
|
88
|
+
print("❌ FAIL: Should have raised MinimaxRequestError")
|
|
89
|
+
return False
|
|
90
|
+
except MinimaxRequestError as e:
|
|
91
|
+
error_msg = str(e)
|
|
92
|
+
# Error message should NOT contain @ prefix
|
|
93
|
+
if "@" not in error_msg and "nonexistent/file.png" in error_msg:
|
|
94
|
+
print(f"✅ PASS: Error message correctly shows path without @")
|
|
95
|
+
print(f" Error: {error_msg}")
|
|
96
|
+
return True
|
|
97
|
+
else:
|
|
98
|
+
print(f"❌ FAIL: Error message still contains @ or incorrect path")
|
|
99
|
+
print(f" Error: {error_msg}")
|
|
100
|
+
return False
|
|
101
|
+
except Exception as e:
|
|
102
|
+
print(f"❌ FAIL: Wrong exception type: {type(e).__name__}: {str(e)}")
|
|
103
|
+
return False
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
if __name__ == "__main__":
|
|
107
|
+
print("=" * 60)
|
|
108
|
+
print("Testing @ Prefix Handling in minimax_mcp")
|
|
109
|
+
print("=" * 60)
|
|
110
|
+
|
|
111
|
+
results = []
|
|
112
|
+
|
|
113
|
+
results.append(("strip_at_prefix", test_strip_at_prefix()))
|
|
114
|
+
results.append(("process_image_url with @", test_process_image_url_with_at_prefix()))
|
|
115
|
+
results.append(("error message with @", test_nonexistent_file_with_at_prefix()))
|
|
116
|
+
|
|
117
|
+
print("\n" + "=" * 60)
|
|
118
|
+
print("Test Summary")
|
|
119
|
+
print("=" * 60)
|
|
120
|
+
|
|
121
|
+
all_passed = True
|
|
122
|
+
for name, passed in results:
|
|
123
|
+
status = "✅ PASS" if passed else "❌ FAIL"
|
|
124
|
+
print(f"{status}: {name}")
|
|
125
|
+
if not passed:
|
|
126
|
+
all_passed = False
|
|
127
|
+
|
|
128
|
+
print("=" * 60)
|
|
129
|
+
if all_passed:
|
|
130
|
+
print("✅ All tests passed!")
|
|
131
|
+
sys.exit(0)
|
|
132
|
+
else:
|
|
133
|
+
print("❌ Some tests failed")
|
|
134
|
+
sys.exit(1)
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Real API test with actual image and API key
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
import os
|
|
8
|
+
from dotenv import load_dotenv
|
|
9
|
+
|
|
10
|
+
# Load test environment
|
|
11
|
+
load_dotenv('.env.test')
|
|
12
|
+
|
|
13
|
+
# Add parent directory to path
|
|
14
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
15
|
+
|
|
16
|
+
from minimax_mcp.utils import process_image_url
|
|
17
|
+
from minimax_mcp.client import MinimaxAPIClient
|
|
18
|
+
from minimax_mcp.exceptions import MinimaxRequestError
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_real_image_with_at_prefix(image_path: str, prompt: str = "请详细描述这张图片的内容,包括画面中的主要元素、文字信息、场景等所有细节"):
|
|
22
|
+
"""
|
|
23
|
+
Test with real image using actual MiniMax API
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
image_path: Path to the image (can have @ prefix)
|
|
27
|
+
prompt: Analysis prompt
|
|
28
|
+
"""
|
|
29
|
+
print("=" * 60)
|
|
30
|
+
print("Real Image Test with MiniMax API")
|
|
31
|
+
print("=" * 60)
|
|
32
|
+
|
|
33
|
+
# Get API credentials
|
|
34
|
+
api_key = os.getenv('MINIMAX_API_KEY')
|
|
35
|
+
api_host = os.getenv('MINIMAX_API_HOST')
|
|
36
|
+
|
|
37
|
+
if not api_key or api_key == 'your-api-key-here':
|
|
38
|
+
print("❌ Error: Please set MINIMAX_API_KEY in .env.test file")
|
|
39
|
+
print(" Edit .env.test and add your actual API key")
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
if not api_host:
|
|
43
|
+
print("❌ Error: MINIMAX_API_HOST not set")
|
|
44
|
+
return False
|
|
45
|
+
|
|
46
|
+
print(f"API Host: {api_host}")
|
|
47
|
+
print(f"API Key: {api_key[:10]}...{api_key[-4:]}")
|
|
48
|
+
print()
|
|
49
|
+
|
|
50
|
+
# Test 1: With @ prefix
|
|
51
|
+
print("Test 1: With @ prefix")
|
|
52
|
+
print("-" * 60)
|
|
53
|
+
print(f"Original path: @{image_path}")
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
# Process image
|
|
57
|
+
print("Processing image...")
|
|
58
|
+
processed_image_url = process_image_url(f"@{image_path}")
|
|
59
|
+
print(f"✅ Image processed successfully")
|
|
60
|
+
print(f" Data URI length: {len(processed_image_url)} chars")
|
|
61
|
+
print(f" First 50 chars: {processed_image_url[:50]}...")
|
|
62
|
+
|
|
63
|
+
# Call API
|
|
64
|
+
print("\nCalling MiniMax API...")
|
|
65
|
+
client = MinimaxAPIClient(api_key, api_host)
|
|
66
|
+
|
|
67
|
+
payload = {
|
|
68
|
+
"prompt": prompt,
|
|
69
|
+
"image_url": processed_image_url
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
response = client.post("/v1/coding_plan/vlm", json=payload)
|
|
73
|
+
|
|
74
|
+
content = response.get("content", "")
|
|
75
|
+
if content:
|
|
76
|
+
print(f"✅ API call successful!")
|
|
77
|
+
print("\n" + "=" * 60)
|
|
78
|
+
print("Response:")
|
|
79
|
+
print("=" * 60)
|
|
80
|
+
print(content)
|
|
81
|
+
print("=" * 60)
|
|
82
|
+
return True
|
|
83
|
+
else:
|
|
84
|
+
print("❌ No content in response")
|
|
85
|
+
print(f"Response: {response}")
|
|
86
|
+
return False
|
|
87
|
+
|
|
88
|
+
except Exception as e:
|
|
89
|
+
print(f"❌ Test failed: {str(e)}")
|
|
90
|
+
import traceback
|
|
91
|
+
traceback.print_exc()
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_without_at_prefix(image_path: str, prompt: str = "请详细描述这张图片的内容"):
|
|
96
|
+
"""
|
|
97
|
+
Test without @ prefix for comparison
|
|
98
|
+
"""
|
|
99
|
+
print("\n\nTest 2: Without @ prefix (for comparison)")
|
|
100
|
+
print("-" * 60)
|
|
101
|
+
print(f"Original path: {image_path}")
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
processed_image_url = process_image_url(image_path)
|
|
105
|
+
print(f"✅ Image processed successfully")
|
|
106
|
+
print(f" Data URI length: {len(processed_image_url)} chars")
|
|
107
|
+
return True
|
|
108
|
+
except Exception as e:
|
|
109
|
+
print(f"❌ Test failed: {str(e)}")
|
|
110
|
+
return False
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
if __name__ == "__main__":
|
|
114
|
+
if len(sys.argv) < 2:
|
|
115
|
+
print("Usage: python test_real_image.py <image_path> [prompt]")
|
|
116
|
+
print("\nExample:")
|
|
117
|
+
print(" python test_real_image.py test-image.png")
|
|
118
|
+
print(" python test_real_image.py test-image.png \"描述这张图片\"")
|
|
119
|
+
print("\nNote: Image path should NOT include @ prefix in command line")
|
|
120
|
+
print(" The script will automatically test both with and without @")
|
|
121
|
+
sys.exit(1)
|
|
122
|
+
|
|
123
|
+
image_path = sys.argv[1]
|
|
124
|
+
prompt = sys.argv[2] if len(sys.argv) > 2 else "请详细描述这张图片的内容,包括画面中的主要元素、文字信息、场景等所有细节"
|
|
125
|
+
|
|
126
|
+
# Verify image exists
|
|
127
|
+
if not os.path.exists(image_path):
|
|
128
|
+
print(f"❌ Error: Image file not found: {image_path}")
|
|
129
|
+
sys.exit(1)
|
|
130
|
+
|
|
131
|
+
print(f"Testing with image: {image_path}")
|
|
132
|
+
print(f"Prompt: {prompt}")
|
|
133
|
+
print()
|
|
134
|
+
|
|
135
|
+
# Test with @ prefix (calls real API)
|
|
136
|
+
result1 = test_real_image_with_at_prefix(image_path, prompt)
|
|
137
|
+
|
|
138
|
+
# Test without @ prefix (just image processing)
|
|
139
|
+
result2 = test_without_at_prefix(image_path, prompt)
|
|
140
|
+
|
|
141
|
+
print("\n" + "=" * 60)
|
|
142
|
+
print("Test Summary")
|
|
143
|
+
print("=" * 60)
|
|
144
|
+
print(f"{'✅' if result1 else '❌'} Test 1: With @ prefix (real API call)")
|
|
145
|
+
print(f"{'✅' if result2 else '❌'} Test 2: Without @ prefix (image processing)")
|
|
146
|
+
print("=" * 60)
|
|
147
|
+
|
|
148
|
+
if result1 and result2:
|
|
149
|
+
print("✅ All tests passed!")
|
|
150
|
+
sys.exit(0)
|
|
151
|
+
else:
|
|
152
|
+
print("❌ Some tests failed")
|
|
153
|
+
sys.exit(1)
|
package/package.json
CHANGED