Skip to content

Commit 0269993

Browse files
committed
feat: add search_mcp_servers tool with related demo and tests
1 parent c24dcbb commit 0269993

9 files changed

Lines changed: 331 additions & 7 deletions

File tree

.vscode/tasks.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
"problemMatcher": []
3737
},
3838
{
39-
"label": "uv run python demo.py",
39+
"label": "uv run python demo.py --full",
4040
"detail": "Run demo.py",
4141
"type": "shell",
4242
"command": "uv",

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ A Model Context Protocol (MCP) server that integrates with [ModelScope](https://
1111

1212
- 🔐 **User Authentication** - Retrieve information about the currently authenticated ModelScope user
1313
- 🎨 **AI Image Generation** - Generate images from text prompts or transform existing images using AIGC models (supports both text-to-image and image-to-image generation)
14-
- 🔍 **Model Search** - Search for machine learning models on ModelScope with advanced filtering options (task type, author, inference support, etc.)
15-
- 📚 **Research Paper Search** - Search for arXiv papers indexed in ModelScope with comprehensive metadata
14+
- 🔍 **Resource Discovery** - Search and discover ModelScope resources including machine learning models, research papers, and MCP servers with advanced filtering options
15+
- 📋 **Resource Details** _(Coming Soon)_ - Get comprehensive details for specific resources including model specifications, paper abstracts, and MCP server configurations
1616
- 📖 **Documentation Search** _(Coming Soon)_ - Semantic search for ModelScope documentation and articles
17-
- 🚀 **Gradio API Integration** _(Coming Soon)_ - Invoke Gradio APIs exposed by any pre-configured ModelScope studio
17+
- 🚀 **Gradio API Integration** _(Coming Soon)_ - Invoke Gradio APIs exposed by any pre-configured ModelScope studio (AI app)
1818

1919
## 🚀 Quick Start
2020

demo.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,41 @@ async def demo_search_papers(client: Client) -> None:
9595
print()
9696

9797

98+
async def demo_search_mcp_servers(client: Client) -> None:
99+
"""Demo: Search MCP servers using various parameters."""
100+
print("4. 🛠️ Tool: search_mcp_servers")
101+
print(
102+
" • Task: 🔍 Search MCP servers (keyword='Chrome', category='browser-automation', limit 3 results)"
103+
)
104+
105+
result = await client.call_tool(
106+
"search_mcp_servers",
107+
{
108+
"search": "Chrome",
109+
"category": "browser-automation",
110+
"limit": 3,
111+
},
112+
)
113+
114+
if result.content and len(result.content) > 0:
115+
servers = json.loads(result.content[0].text) # type: ignore
116+
server_summary = []
117+
for server in servers:
118+
name = server.get("chinese_name", server.get("name", "N/A"))
119+
publisher = server.get("publisher", "N/A")
120+
views = server.get("view_count", 0)
121+
server_summary.append(f"{name} by {publisher} (Views {views:,})")
122+
print(
123+
f" • Result: Found {len(servers)} servers - {' | '.join(server_summary)}"
124+
)
125+
else:
126+
print(" • Result: No MCP servers found")
127+
print()
128+
129+
98130
async def demo_generate_image(client: Client) -> None:
99131
"""Demo: Generate image URL from text prompt."""
100-
print("4. 🛠️ Tool: generate_image")
132+
print("5. 🛠️ Tool: generate_image")
101133
print(
102134
" • Task: 🎨 Generate image (prompt='A curious cat wearing a tiny wizard hat in candy cloud kingdom')"
103135
)
@@ -152,6 +184,7 @@ async def main():
152184
await demo_get_current_user(client)
153185
await demo_search_models(client)
154186
await demo_search_papers(client)
187+
await demo_search_mcp_servers(client)
155188

156189
if args.full:
157190
await demo_generate_image(client)

src/modelscope_mcp_server/server.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from ._version import __version__
1313
from .settings import settings
1414
from .tools.aigc import register_aigc_tools
15+
from .tools.mcp import register_mcp_tools
1516
from .tools.model import register_model_tools
1617
from .tools.paper import register_paper_tools
1718
from .tools.user import register_user_tools
@@ -41,6 +42,7 @@ def create_mcp_server() -> FastMCP:
4142
register_user_tools(mcp)
4243
register_model_tools(mcp)
4344
register_paper_tools(mcp)
45+
register_mcp_tools(mcp)
4446
register_aigc_tools(mcp)
4547

4648
return mcp
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
"""
2+
ModelScope MCP Server MCP tools.
3+
4+
Provides tools for MCP-related operations in the ModelScope MCP Server, such as searching for MCP servers.
5+
"""
6+
7+
from typing import Annotated, Literal
8+
9+
import requests
10+
from fastmcp import FastMCP
11+
from fastmcp.utilities import logging
12+
from pydantic import Field
13+
14+
from ..constants import MODELSCOPE_DOMAIN, MODELSCOPE_OPENAPI_ENDPOINT
15+
from ..types import McpServer
16+
17+
logger = logging.get_logger(__name__)
18+
19+
20+
def register_mcp_tools(mcp: FastMCP) -> None:
21+
"""
22+
Register all MCP-related tools with the MCP server.
23+
24+
Args:
25+
mcp (FastMCP): The MCP server instance
26+
"""
27+
28+
@mcp.tool(
29+
annotations={
30+
"title": "Search MCP Servers",
31+
}
32+
)
33+
async def search_mcp_servers(
34+
search: Annotated[
35+
str,
36+
Field(description="Search keyword for MCP servers"),
37+
] = "",
38+
category: Annotated[
39+
(
40+
Literal[
41+
"browser-automation",
42+
"search",
43+
"communication",
44+
"customer-and-marketing",
45+
"developer-tools",
46+
"entertainment-and-media",
47+
"file-systems",
48+
"finance",
49+
"knowledge-and-memory",
50+
"location-services",
51+
"art-and-culture",
52+
"research-and-data",
53+
"calendar-management",
54+
"other",
55+
]
56+
| None
57+
),
58+
Field(description=("Filter by category")),
59+
] = None,
60+
is_hosted: Annotated[
61+
bool | None,
62+
Field(description="Filter by hosted status"),
63+
] = None,
64+
limit: Annotated[
65+
int, Field(description="Maximum number of servers to return", ge=1, le=100)
66+
] = 10,
67+
) -> list[McpServer]:
68+
"""
69+
Search for MCP servers on ModelScope.
70+
"""
71+
url = f"{MODELSCOPE_OPENAPI_ENDPOINT}/mcp/servers"
72+
73+
headers = {
74+
"Content-Type": "application/json",
75+
"User-Agent": "modelscope-mcp-server",
76+
}
77+
78+
# Build filter object
79+
filter_obj = {}
80+
if category is not None:
81+
filter_obj["category"] = category
82+
if is_hosted is not None:
83+
filter_obj["is_hosted"] = is_hosted
84+
85+
request_data = {
86+
"filter": filter_obj,
87+
"page_number": 1,
88+
"page_size": limit,
89+
"search": search,
90+
}
91+
92+
try:
93+
response = requests.put(url, json=request_data, headers=headers, timeout=10)
94+
except requests.exceptions.Timeout:
95+
raise TimeoutError("Request timeout - please try again later")
96+
97+
if response.status_code != 200:
98+
raise Exception(
99+
f"Server returned non-200 status code: {response.status_code} {response.text}"
100+
)
101+
102+
data = response.json()
103+
104+
if data.get("code") != 200:
105+
raise Exception(
106+
f"Server returned error: {data.get('message', 'Unknown error')}"
107+
)
108+
109+
result_data = data.get("data", {})
110+
servers_data = result_data.get("mcp_server_list", [])
111+
112+
servers = []
113+
for server_data in servers_data:
114+
id = server_data.get("id", "")
115+
modelscope_url = f"{MODELSCOPE_DOMAIN}/mcp/servers/{id}"
116+
117+
server = McpServer(
118+
id=id,
119+
modelscope_url=modelscope_url,
120+
name=server_data.get("name", ""),
121+
chinese_name=server_data.get("chinese_name", ""),
122+
description=server_data.get("description", ""),
123+
publisher=server_data.get("publisher", ""),
124+
tags=server_data.get("tags", []),
125+
view_count=server_data.get("view_count", 0),
126+
)
127+
servers.append(server)
128+
129+
return servers

src/modelscope_mcp_server/tools/model.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ async def search_models(
5050
Field(description="Sort order"),
5151
] = "Default",
5252
limit: Annotated[
53-
int, Field(description="Number of models to return", ge=1, le=30)
53+
int, Field(description="Maximum number of models to return", ge=1, le=30)
5454
] = 10,
5555
) -> list[Model]:
5656
"""

src/modelscope_mcp_server/tools/paper.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ async def search_papers(
3838
Field(description="Sort order"),
3939
] = "default",
4040
limit: Annotated[
41-
int, Field(description="Number of papers to return", ge=1, le=100)
41+
int, Field(description="Maximum number of papers to return", ge=1, le=100)
4242
] = 10,
4343
) -> list[Paper]:
4444
"""

src/modelscope_mcp_server/types.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,24 @@ class Paper(BaseModel):
7979
comment_count: Annotated[int, Field(description="Comment count")] = 0
8080

8181

82+
class McpServer(BaseModel):
83+
"""MCP Server information."""
84+
85+
# Basic information
86+
id: Annotated[str, Field(description="MCP Server ID")]
87+
name: Annotated[str, Field(description="MCP Server name")]
88+
chinese_name: Annotated[str, Field(description="Chinese name")]
89+
description: Annotated[str, Field(description="Description")]
90+
publisher: Annotated[str, Field(description="Publisher")]
91+
tags: Annotated[list[str], Field(description="Tags")] = []
92+
93+
# Links
94+
modelscope_url: Annotated[str, Field(description="ModelScope page URL")]
95+
96+
# Metrics
97+
view_count: Annotated[int, Field(description="View count")] = 0
98+
99+
82100
class ImageGenerationResult(BaseModel):
83101
"""Image generation result."""
84102

0 commit comments

Comments
 (0)