Skip to content

Commit 2763575

Browse files
committed
refactor: add unified ModelScope HTTP client
1 parent a03a10f commit 2763575

File tree

12 files changed

+324
-170
lines changed

12 files changed

+324
-170
lines changed

demo.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,12 @@ async def main() -> None:
180180
action="store_true",
181181
help="Run all demos including slow operations like image generation",
182182
)
183+
parser.add_argument(
184+
"--log-level",
185+
type=str,
186+
default="WARNING",
187+
help="Set log level",
188+
)
183189
args = parser.parse_args()
184190

185191
print(f"🤖 {get_server_name_with_version()} Demo")
@@ -189,8 +195,7 @@ async def main() -> None:
189195
else:
190196
print("🚀 Running all demos including slow operations")
191197

192-
# Set log level to WARNING to avoid too many logs
193-
settings.log_level = "WARNING"
198+
settings.log_level = args.log_level
194199
settings.show_settings()
195200

196201
mcp = create_mcp_server()
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
"""ModelScope HTTP client for unified API requests."""
2+
3+
import json
4+
import logging as std_logging
5+
import time
6+
from typing import Any
7+
8+
import requests
9+
from fastmcp.utilities import logging
10+
11+
from modelscope_mcp_server.utils.metadata import get_server_version
12+
13+
from .settings import settings
14+
15+
logger = logging.get_logger(__name__)
16+
17+
18+
class ModelScopeClient:
19+
"""Unified client for ModelScope API requests."""
20+
21+
def __init__(self, timeout: int = settings.default_api_timeout_seconds):
22+
"""Initialize the ModelScope client.
23+
24+
Args:
25+
timeout: Default timeout for requests in seconds
26+
27+
"""
28+
self.timeout = timeout
29+
self._session = requests.Session()
30+
31+
def _get_default_headers(self) -> dict[str, str]:
32+
"""Get default headers for all requests."""
33+
headers = {
34+
"User-Agent": f"modelscope-mcp-server/{get_server_version()}",
35+
}
36+
37+
if settings.is_api_token_configured():
38+
headers["Authorization"] = f"Bearer {settings.api_token}"
39+
# TODO: Remove this once all API endpoints support Bearer token
40+
headers["Cookie"] = f"m_session_id={settings.api_token}"
41+
42+
return headers
43+
44+
def _prepare_request_headers(
45+
self, kwargs: dict, additional_headers: dict[str, str] | None = None
46+
) -> dict[str, str]:
47+
"""Prepare headers for a request and log them if DEBUG is enabled.
48+
49+
Args:
50+
kwargs: Request kwargs, may contain 'headers' key that will be popped
51+
additional_headers: Additional headers to add to defaults
52+
53+
Returns:
54+
Final headers dict to use for the request
55+
56+
"""
57+
headers = self._get_default_headers()
58+
if additional_headers:
59+
headers.update(additional_headers)
60+
if "headers" in kwargs:
61+
headers.update(kwargs.pop("headers"))
62+
63+
# Log request headers if DEBUG level is enabled
64+
if logger.isEnabledFor(std_logging.DEBUG):
65+
headers_str = "\n".join([f" {key}: {value}" for key, value in headers.items()])
66+
logger.debug(f"Request headers:\n{headers_str}")
67+
68+
return headers
69+
70+
def _handle_response(self, response: requests.Response, start_time: float) -> dict[str, Any]:
71+
"""Handle common response processing."""
72+
# Log response basic info
73+
elapsed_time = time.time() - start_time
74+
content_length = len(response.content) if response.content else 0
75+
logger.info(
76+
f"Response: {response.status_code} {response.reason}, size: {content_length} bytes, "
77+
f"elapsed: {elapsed_time:.3f}s"
78+
)
79+
80+
# Log response headers if DEBUG level is enabled
81+
if logger.isEnabledFor(std_logging.DEBUG):
82+
headers_str = "\n".join([f" {key}: {value}" for key, value in response.headers.items()])
83+
logger.debug(f"Response headers:\n{headers_str}")
84+
85+
try:
86+
response_json = response.json()
87+
except json.JSONDecodeError as e:
88+
raise Exception(f"Failed to parse JSON response: {e}") from e
89+
90+
# Log JSON body if DEBUG level is enabled
91+
if logger.isEnabledFor(std_logging.DEBUG):
92+
formatted_json = json.dumps(response_json, indent=2, ensure_ascii=False)
93+
logger.debug(f"Response body:\n{formatted_json}")
94+
95+
# Raise an exception if the response is not successful
96+
response.raise_for_status()
97+
98+
# If 'success = false' (case-insensitive), raise an exception
99+
if isinstance(response_json, dict):
100+
for key in response_json:
101+
if key.lower() == "success" and response_json[key] is False:
102+
raise Exception(f"Server returned error: {response_json}")
103+
104+
return response_json
105+
106+
def get(
107+
self, url: str, params: dict[str, Any] | None = None, timeout: int | None = None, **kwargs
108+
) -> dict[str, Any]:
109+
"""Perform GET request.
110+
111+
Args:
112+
url: The URL to request
113+
params: Query parameters
114+
timeout: Request timeout in seconds
115+
**kwargs: Additional arguments passed to requests.get
116+
117+
Returns:
118+
Parsed JSON response
119+
120+
Raises:
121+
TimeoutError: If request times out
122+
Exception: For other request errors
123+
124+
"""
125+
logger.info(f"Sending GET request to {url} with params: {params}")
126+
start_time = time.time()
127+
try:
128+
headers = self._prepare_request_headers(kwargs)
129+
130+
response = self._session.get(url, params=params, timeout=timeout or self.timeout, headers=headers, **kwargs)
131+
return self._handle_response(response, start_time)
132+
except requests.exceptions.Timeout as e:
133+
raise TimeoutError("Request timeout - please try again later") from e
134+
135+
def post(
136+
self,
137+
url: str,
138+
json_data: dict[str, Any] | None = None,
139+
timeout: int | None = None,
140+
**kwargs,
141+
) -> dict[str, Any]:
142+
"""Perform POST request.
143+
144+
Args:
145+
url: The URL to request
146+
json_data: JSON data to send (will be serialized)
147+
timeout: Request timeout in seconds
148+
**kwargs: Additional arguments passed to requests.post
149+
150+
Returns:
151+
Parsed JSON response
152+
153+
Raises:
154+
TimeoutError: If request times out
155+
Exception: For other request errors
156+
157+
"""
158+
return self._request_with_data("POST", url, json_data, timeout, **kwargs)
159+
160+
def put(
161+
self, url: str, json_data: dict[str, Any] | None = None, timeout: int | None = None, **kwargs
162+
) -> dict[str, Any]:
163+
"""Perform PUT request.
164+
165+
Args:
166+
url: The URL to request
167+
json_data: JSON data to send
168+
timeout: Request timeout in seconds
169+
**kwargs: Additional arguments passed to requests.put
170+
171+
Returns:
172+
Parsed JSON response
173+
174+
Raises:
175+
TimeoutError: If request times out
176+
Exception: For other request errors
177+
178+
"""
179+
return self._request_with_data("PUT", url, json_data, timeout, **kwargs)
180+
181+
def _request_with_data(
182+
self,
183+
method: str,
184+
url: str,
185+
json_data: dict[str, Any] | None = None,
186+
timeout: int | None = None,
187+
**kwargs,
188+
) -> dict[str, Any]:
189+
"""Perform HTTP request with JSON data."""
190+
logger.info(f"Sending {method} request to {url} with data: {json_data}")
191+
start_time = time.time()
192+
try:
193+
headers = self._prepare_request_headers(kwargs, {"Content-Type": "application/json"})
194+
195+
response = self._session.request(
196+
method,
197+
url,
198+
data=json.dumps(json_data, ensure_ascii=False).encode("utf-8") if json_data else None,
199+
timeout=timeout or self.timeout,
200+
headers=headers,
201+
**kwargs,
202+
)
203+
return self._handle_response(response, start_time)
204+
except requests.exceptions.Timeout as e:
205+
raise TimeoutError("Request timeout - please try again later") from e
206+
207+
def close(self):
208+
"""Close the session."""
209+
self._session.close()
210+
211+
def __enter__(self):
212+
"""Context manager entry."""
213+
return self
214+
215+
def __exit__(self, exc_type, exc_val, exc_tb):
216+
"""Context manager exit."""
217+
self.close()
218+
219+
220+
# Global client instance with default settings
221+
default_client = ModelScopeClient()

src/modelscope_mcp_server/constants.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,7 @@
1212
# Default model IDs for content generation
1313
DEFAULT_TEXT_TO_IMAGE_MODEL = "MusePublic/489_ckpt_FLUX_1"
1414
DEFAULT_IMAGE_TO_IMAGE_MODEL = "black-forest-labs/FLUX.1-Kontext-dev"
15+
16+
# Default timeout for requests
17+
DEFAULT_API_TIMEOUT_SECONDS = 5
18+
DEFAULT_IMAGE_GENERATION_TIMEOUT_SECONDS = 300

src/modelscope_mcp_server/server.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33
from typing import cast
44

55
from fastmcp import FastMCP
6-
from fastmcp import settings as fastmcp_settings
76
from fastmcp.server.middleware.error_handling import ErrorHandlingMiddleware
87
from fastmcp.server.middleware.logging import LoggingMiddleware
98
from fastmcp.server.middleware.rate_limiting import RateLimitingMiddleware
109
from fastmcp.server.middleware.timing import TimingMiddleware
1110
from fastmcp.settings import LOG_LEVEL
1211
from fastmcp.utilities import logging
12+
from fastmcp.utilities.logging import configure_logging
1313

1414
from .settings import settings
1515
from .tools.aigc import register_aigc_tools
@@ -24,7 +24,7 @@
2424

2525
def create_mcp_server() -> FastMCP:
2626
"""Create and configure the MCP server with all ModelScope tools."""
27-
fastmcp_settings.log_level = cast(LOG_LEVEL, settings.log_level)
27+
configure_logging(level=cast(LOG_LEVEL, settings.log_level))
2828

2929
mcp = FastMCP(
3030
name=get_server_name_with_version(),

src/modelscope_mcp_server/settings.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
from pydantic_settings import BaseSettings, SettingsConfigDict
55

66
from .constants import (
7+
DEFAULT_API_TIMEOUT_SECONDS,
8+
DEFAULT_IMAGE_GENERATION_TIMEOUT_SECONDS,
79
DEFAULT_IMAGE_TO_IMAGE_MODEL,
810
DEFAULT_TEXT_TO_IMAGE_MODEL,
911
MODELSCOPE_API_ENDPOINT,
@@ -48,6 +50,16 @@ class Settings(BaseSettings):
4850
description="Default model for image-to-image generation",
4951
)
5052

53+
# Default timeout settings
54+
default_api_timeout_seconds: int = Field(
55+
default=DEFAULT_API_TIMEOUT_SECONDS,
56+
description="Default timeout for API requests",
57+
)
58+
default_image_generation_timeout_seconds: int = Field(
59+
default=DEFAULT_IMAGE_GENERATION_TIMEOUT_SECONDS,
60+
description="Default timeout for image generation requests",
61+
)
62+
5163
# Logging settings
5264
log_level: str = Field(default="INFO", description="Logging level")
5365

src/modelscope_mcp_server/tools/aigc.py

Lines changed: 15 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,13 @@
33
Provides MCP tools for text-to-image generation, etc.
44
"""
55

6-
import json
76
from typing import Annotated
87

9-
import requests
108
from fastmcp import FastMCP
119
from fastmcp.utilities import logging
1210
from pydantic import Field
1311

12+
from ..client import default_client
1413
from ..settings import settings
1514
from ..types import GenerationType, ImageGenerationResult
1615

@@ -87,35 +86,21 @@ async def generate_image(
8786
if generation_type == GenerationType.IMAGE_TO_IMAGE and image_url:
8887
payload["image_url"] = image_url
8988

90-
headers = {
91-
"Authorization": f"Bearer {settings.api_token}",
92-
"Content-Type": "application/json",
93-
"User-Agent": "modelscope-mcp-server",
94-
}
95-
96-
logger.info(f"Sending {generation_type.value} generation request with model '{model}' and prompt '{prompt}'")
89+
response = default_client.post(
90+
url, json_data=payload, timeout=settings.default_image_generation_timeout_seconds
91+
)
9792

98-
try:
99-
response = requests.post(
100-
url,
101-
data=json.dumps(payload, ensure_ascii=False).encode("utf-8"),
102-
headers=headers,
103-
timeout=300,
104-
)
105-
except requests.exceptions.Timeout as e:
106-
raise TimeoutError("Request timeout - please try again later") from e
93+
images_data = response.get("images", [])
10794

108-
if response.status_code != 200:
109-
raise Exception(f"Server returned non-200 status code: {response.status_code} {response.text}")
95+
if len(images_data) == 0:
96+
raise Exception(f"No images found in response: {response}")
11097

111-
response_data = response.json()
98+
generated_image_url = images_data[0].get("url", "")
99+
if len(generated_image_url) == 0:
100+
raise Exception(f"No image URL found in response: {response}")
112101

113-
if "images" in response_data and response_data["images"]:
114-
generated_image_url = response_data["images"][0]["url"]
115-
logger.info(f"Successfully generated image URL: {generated_image_url}")
116-
return ImageGenerationResult(
117-
type=generation_type,
118-
model=model,
119-
image_url=generated_image_url,
120-
)
121-
raise Exception(f"Server returned error: {response_data}")
102+
return ImageGenerationResult(
103+
type=generation_type,
104+
model=model,
105+
image_url=generated_image_url,
106+
)

0 commit comments

Comments
 (0)