ship-detector-mcp 0.1.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.
- ship_detector_mcp-0.1.0/PKG-INFO +12 -0
- ship_detector_mcp-0.1.0/README.md +0 -0
- ship_detector_mcp-0.1.0/pyproject.toml +27 -0
- ship_detector_mcp-0.1.0/src/ship_detector_mcp/__init__.py +11 -0
- ship_detector_mcp-0.1.0/src/ship_detector_mcp/__main__.py +2 -0
- ship_detector_mcp-0.1.0/src/ship_detector_mcp/server.py +152 -0
- ship_detector_mcp-0.1.0/src/ship_detector_mcp/weights/best.pt +0 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ship-detector-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A MCP server for ship detection using YOLO
|
|
5
|
+
Author-email: xiacb <chenboxia111@163.com>
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Requires-Dist: mcp[cli]>=1.9.2
|
|
8
|
+
Requires-Dist: numpy
|
|
9
|
+
Requires-Dist: opencv-python
|
|
10
|
+
Requires-Dist: pillow
|
|
11
|
+
Requires-Dist: requests
|
|
12
|
+
Requires-Dist: ultralytics
|
|
File without changes
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "ship-detector-mcp"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "A MCP server for ship detection using YOLO"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
authors = [
|
|
8
|
+
{name = "xiacb", email = "chenboxia111@163.com"}
|
|
9
|
+
]
|
|
10
|
+
dependencies = [
|
|
11
|
+
"mcp[cli]>=1.9.2",
|
|
12
|
+
"ultralytics",
|
|
13
|
+
"opencv-python",
|
|
14
|
+
"pillow",
|
|
15
|
+
"numpy",
|
|
16
|
+
"requests",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.scripts]
|
|
20
|
+
ship-detector-mcp = "ship_detector_mcp:main"
|
|
21
|
+
|
|
22
|
+
[build-system]
|
|
23
|
+
requires = ["hatchling"]
|
|
24
|
+
build-backend = "hatchling.build"
|
|
25
|
+
|
|
26
|
+
[tool.hatch.build.targets.wheel]
|
|
27
|
+
packages = ["src/ship_detector_mcp"]
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# server.py
|
|
2
|
+
from mcp.server.fastmcp import FastMCP
|
|
3
|
+
import os
|
|
4
|
+
from ultralytics import YOLO
|
|
5
|
+
import cv2
|
|
6
|
+
import numpy as np
|
|
7
|
+
import tempfile
|
|
8
|
+
import base64
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from PIL import Image
|
|
11
|
+
from io import BytesIO
|
|
12
|
+
|
|
13
|
+
# 初始化 MCP 服务器,配置主机和端口
|
|
14
|
+
mcp = FastMCP("ShipDetector", host="0.0.0.0", port=9000, stateless_http=True)
|
|
15
|
+
|
|
16
|
+
# 加载 YOLO 模型(请替换为你的模型路径)
|
|
17
|
+
|
|
18
|
+
import importlib.resources
|
|
19
|
+
MODEL_PATH = os.getenv("MODEL_PATH", str(Path(__file__).parent / "weights" / "best.pt"))
|
|
20
|
+
model = YOLO(MODEL_PATH)
|
|
21
|
+
|
|
22
|
+
def load_image_from_input(image_input: str):
|
|
23
|
+
"""
|
|
24
|
+
智能加载图片:自动判断输入是 Base64 还是文件路径
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
image_input: Base64 字符串 或 本地文件路径
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
PIL Image 对象
|
|
31
|
+
"""
|
|
32
|
+
# 判断是否是 Base64
|
|
33
|
+
is_base64 = False
|
|
34
|
+
# 检查是否是 PNG 的 Base64 头
|
|
35
|
+
if image_input.startswith('iVBORw0KGgo'):
|
|
36
|
+
is_base64 = True
|
|
37
|
+
# 检查是否是 JPG 的 Base64 头
|
|
38
|
+
elif image_input.startswith('/9j/4'):
|
|
39
|
+
is_base64 = True
|
|
40
|
+
# 检查是否是存在的文件路径
|
|
41
|
+
elif Path(image_input).exists():
|
|
42
|
+
pass # 不是 Base64,是文件路径
|
|
43
|
+
else:
|
|
44
|
+
# 尝试作为 Base64 解码前100个字符
|
|
45
|
+
try:
|
|
46
|
+
base64.b64decode(image_input[:100])
|
|
47
|
+
is_base64 = True
|
|
48
|
+
except:
|
|
49
|
+
raise ValueError(f"输入既不是有效的 Base64,也不是存在的文件路径: {image_input[:50]}...")
|
|
50
|
+
|
|
51
|
+
if is_base64:
|
|
52
|
+
print("检测到 Base64 输入,正在解码...")
|
|
53
|
+
image_data = base64.b64decode(image_input)
|
|
54
|
+
image = Image.open(BytesIO(image_data)).convert("RGB")
|
|
55
|
+
else:
|
|
56
|
+
print(f"检测到文件路径输入: {image_input}")
|
|
57
|
+
image = Image.open(image_input).convert("RGB")
|
|
58
|
+
|
|
59
|
+
return image
|
|
60
|
+
|
|
61
|
+
@mcp.tool()
|
|
62
|
+
def detect_ship(
|
|
63
|
+
image_data: bytes = None,
|
|
64
|
+
image_path: str = None,
|
|
65
|
+
image_url: str = None, # ← 新增
|
|
66
|
+
confidence: float = 0.5
|
|
67
|
+
) -> str:
|
|
68
|
+
import requests
|
|
69
|
+
import tempfile
|
|
70
|
+
import os
|
|
71
|
+
import cv2
|
|
72
|
+
import numpy as np
|
|
73
|
+
|
|
74
|
+
temp_path = None
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
# 情况1:有 image_data
|
|
78
|
+
if image_data is not None:
|
|
79
|
+
if isinstance(image_data, str):
|
|
80
|
+
import base64
|
|
81
|
+
image_data = base64.b64decode(image_data)
|
|
82
|
+
|
|
83
|
+
with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tmp:
|
|
84
|
+
tmp.write(image_data)
|
|
85
|
+
temp_path = tmp.name
|
|
86
|
+
image_to_use = temp_path
|
|
87
|
+
print(f"[INFO] 从 image_data 创建临时文件")
|
|
88
|
+
|
|
89
|
+
# 情况2:有 image_url(从 Dify 传来)
|
|
90
|
+
elif image_url is not None:
|
|
91
|
+
# 如果是相对路径,补全完整 URL
|
|
92
|
+
if image_url.startswith('/'):
|
|
93
|
+
image_url = f"http://localhost:5001{image_url}"
|
|
94
|
+
|
|
95
|
+
print(f"[INFO] 从 URL 下载图片: {image_url}")
|
|
96
|
+
response = requests.get(image_url, timeout=30)
|
|
97
|
+
response.raise_for_status()
|
|
98
|
+
|
|
99
|
+
with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tmp:
|
|
100
|
+
tmp.write(response.content)
|
|
101
|
+
temp_path = tmp.name
|
|
102
|
+
image_to_use = temp_path
|
|
103
|
+
|
|
104
|
+
# 情况3:有 image_path
|
|
105
|
+
elif image_path is not None:
|
|
106
|
+
if not os.path.exists(image_path):
|
|
107
|
+
return f"错误:图片文件不存在 - {image_path}"
|
|
108
|
+
image_to_use = image_path
|
|
109
|
+
|
|
110
|
+
else:
|
|
111
|
+
return "错误:请提供图片数据、URL或路径"
|
|
112
|
+
|
|
113
|
+
# 运行 YOLO 检测
|
|
114
|
+
results = model(image_to_use, conf=confidence)
|
|
115
|
+
|
|
116
|
+
# 处理结果
|
|
117
|
+
if not results or len(results) == 0:
|
|
118
|
+
return "未检测到船只"
|
|
119
|
+
|
|
120
|
+
detections = []
|
|
121
|
+
for box in results[0].boxes:
|
|
122
|
+
class_id = int(box.cls[0])
|
|
123
|
+
class_name = model.names[class_id]
|
|
124
|
+
conf = float(box.conf[0])
|
|
125
|
+
bbox = box.xyxy[0].tolist()
|
|
126
|
+
detections.append({
|
|
127
|
+
"class": class_name,
|
|
128
|
+
"confidence": round(conf, 4),
|
|
129
|
+
"bbox": [int(x) for x in bbox]
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
output = f"✅ 检测到 {len(detections)} 艘船:\n"
|
|
133
|
+
for i, ship in enumerate(detections, 1):
|
|
134
|
+
output += f"{i}. {ship['class']} - 置信度 {ship['confidence'] * 100:.2f}% - 位置 {ship['bbox']}\n"
|
|
135
|
+
return output
|
|
136
|
+
|
|
137
|
+
except Exception as e:
|
|
138
|
+
print(f"[ERROR] {e}")
|
|
139
|
+
return f"检测失败: {str(e)}"
|
|
140
|
+
|
|
141
|
+
finally:
|
|
142
|
+
if temp_path and os.path.exists(temp_path):
|
|
143
|
+
os.unlink(temp_path)
|
|
144
|
+
@mcp.tool()
|
|
145
|
+
def list_available_models() -> str:
|
|
146
|
+
"""列出可用的检测模型"""
|
|
147
|
+
return f"当前模型: {MODEL_PATH}"
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
if __name__ == "__main__":
|
|
151
|
+
# 使用 streamable-http 传输协议运行
|
|
152
|
+
mcp.run(transport="streamable-http")
|
|
Binary file
|