Skip to content

Commit faec0c0

Browse files
authored
Merge pull request #16 from kagent-dev/peterj/mcpservers
Add /toolservers endpoint
2 parents 2f804d7 + dc7211b commit faec0c0

9 files changed

Lines changed: 232 additions & 25 deletions

File tree

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from ._tool_server import ToolServer
2+
3+
__all__ = ["ToolServer"]
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
2+
from autogen_core import ComponentBase, Component
3+
from typing import Protocol
4+
from abc import ABC
5+
from pydantic import BaseModel
6+
7+
class ToolDiscovery(Protocol):
8+
async def discover_tools(self) -> list[Component]:
9+
...
10+
11+
class ToolServer(ABC, ToolDiscovery, ComponentBase[BaseModel]):
12+
component_type = "tool_server"

python/packages/autogen-studio/autogenstudio/datamodel/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from .db import Gallery, Message, Run, RunStatus, Session, Settings, Team, Tool
1+
from .db import Gallery, Message, Run, RunStatus, Session, Settings, Team, Tool, ToolServer
22
from .types import (
33
EnvironmentVariable,
44
GalleryComponents,
@@ -34,4 +34,5 @@
3434
"Settings",
3535
"EnvironmentVariable",
3636
"Gallery",
37+
"ToolServer",
3738
]

python/packages/autogen-studio/autogenstudio/datamodel/db.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,8 +133,25 @@ class Tool(SQLModel, table=True):
133133
user_id: Optional[str] = None
134134
version: Optional[str] = "0.0.1"
135135
component: Union[ComponentModel, dict] = Field(sa_column=Column(JSON))
136-
user_id: Optional[str] = None
137136

137+
server_id: Optional[int] = Field(default=None, foreign_key="toolserver.id", index=True)
138+
139+
class ToolServer(SQLModel, table=True):
140+
"""Represents a tool server that provides tools"""
141+
142+
__table_args__ = {"sqlite_autoincrement": True}
143+
144+
id: Optional[int] = Field(default=None, primary_key=True)
145+
created_at: datetime = Field(
146+
default_factory=datetime.now, sa_column=Column(DateTime(timezone=True), server_default=func.now())
147+
)
148+
updated_at: datetime = Field(
149+
default_factory=datetime.now, sa_column=Column(DateTime(timezone=True), onupdate=func.now())
150+
)
151+
user_id: Optional[str] = None
152+
last_connected: Optional[datetime] = None
153+
version: Optional[str] = "0.0.1"
154+
component: Union[ComponentModel, dict] = Field(sa_column=Column(JSON))
138155

139156
class Gallery(SQLModel, table=True):
140157
__table_args__ = {"sqlite_autoincrement": True}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .toolserver_manager import ToolServerManager
2+
3+
__all__ = ["ToolServerManager"]
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from typing import Union
2+
3+
from autogen_core import Component, ComponentModel
4+
from autogen_ext.tool_servers import ToolServer
5+
6+
7+
class ToolServerManager:
8+
"""ToolServerManager manages tool servers and tool discovery from those servers."""
9+
10+
async def _create_tool_server(
11+
self,
12+
tool_server_config: Union[dict, ComponentModel],
13+
):
14+
"""Create a tool server from the given configuration."""
15+
if not tool_server_config:
16+
raise Exception("Tool server config is required")
17+
18+
if isinstance(tool_server_config, dict):
19+
config = tool_server_config
20+
else:
21+
config = tool_server_config.model_dump()
22+
23+
try:
24+
server = ToolServer.load_component(config)
25+
return server
26+
except Exception as e:
27+
raise Exception(f"Failed to create tool server: {e}") from e
28+
29+
async def discover_tools(self, tool_server_config: Union[dict, ComponentModel]) -> list[Component]:
30+
"""Discover tools from the given tool server."""
31+
try:
32+
server = await self._create_tool_server(tool_server_config)
33+
return await server.discover_tools()
34+
except Exception as e:
35+
raise Exception(f"Failed to discover tools: {e}") from e

python/packages/autogen-studio/autogenstudio/web/app.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from .config import settings
1414
from .deps import cleanup_managers, init_managers
1515
from .initialization import AppInitializer
16-
from .routes import gallery, runs, sessions, settingsroute, teams, tools, validation, ws
16+
from .routes import gallery, runs, sessions, settingsroute, teams, tools, validation, ws, tool_servers
1717

1818
# Initialize application
1919
app_file_path = os.path.dirname(os.path.abspath(__file__))
@@ -135,6 +135,13 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
135135
responses={404: {"description": "Not found"}},
136136
)
137137

138+
api.include_router(
139+
tool_servers.router,
140+
prefix="/toolservers",
141+
tags=["tool servers"],
142+
responses={404: {"description": "Not found"}},
143+
)
144+
138145
# Version endpoint
139146

140147

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
from typing import Dict
2+
3+
from fastapi import APIRouter, Depends, HTTPException
4+
5+
from ...datamodel import Tool, ToolServer
6+
from ...toolservermanager import ToolServerManager
7+
from ..deps import get_db
8+
9+
router = APIRouter()
10+
11+
12+
@router.get("/")
13+
async def list_servers(user_id: str, db=Depends(get_db)) -> Dict:
14+
response = db.get(ToolServer, filters={"user_id": user_id})
15+
return {"status": True, "data": response.data}
16+
17+
18+
@router.get("/{server_id}")
19+
async def get_server(server_id: int, user_id: str, db=Depends(get_db)) -> Dict:
20+
response = db.get(ToolServer, filters={"id": server_id, "user_id": user_id})
21+
if not response.status or not response.data:
22+
raise HTTPException(status_code=404, detail="Server not found")
23+
return {"status": True, "data": response.data[0]}
24+
25+
26+
@router.post("/")
27+
async def create_server(server: ToolServer, db=Depends(get_db)) -> Dict:
28+
response = db.upsert(server)
29+
if not response.status:
30+
raise HTTPException(status_code=400, detail=response.message)
31+
return {"status": True, "data": response.data}
32+
33+
34+
@router.put("/{server_id}")
35+
async def update_server(server_id: int, server: ToolServer, user_id: str, db=Depends(get_db)) -> Dict:
36+
# Ensure the server exists and belongs to the user
37+
check_response = db.get(ToolServer, filters={"id": server_id, "user_id": user_id})
38+
if not check_response.status or not check_response.data:
39+
raise HTTPException(status_code=404, detail="Server not found")
40+
41+
# Update the server
42+
server.id = server_id # Ensure the ID is set correctly
43+
server.user_id = user_id # Ensure the user_id is set correctly
44+
response = db.upsert(server)
45+
46+
if not response.status:
47+
raise HTTPException(status_code=400, detail=response.message)
48+
return {"status": True, "data": response.data}
49+
50+
51+
@router.delete("/{server_id}")
52+
async def delete_server(server_id: int, user_id: str, db=Depends(get_db)) -> Dict:
53+
# Get all tools associated with this server
54+
tools_response = db.get(Tool, filters={"server_id": server_id, "user_id": user_id})
55+
56+
# Delete the tools first
57+
if tools_response.status and tools_response.data:
58+
for tool in tools_response.data:
59+
db.delete(filters={"id": tool.id}, model_class=Tool)
60+
61+
# Then delete the server
62+
db.delete(filters={"id": server_id, "user_id": user_id}, model_class=ToolServer)
63+
return {"status": True, "message": "Server and associated tools deleted successfully"}
64+
65+
66+
@router.get("/{server_id}/tools")
67+
async def get_server_tools(server_id: int, user_id: str, db=Depends(get_db)) -> Dict:
68+
# First check if server exists
69+
server_response = db.get(ToolServer, filters={"id": server_id, "user_id": user_id})
70+
if not server_response.status or not server_response.data:
71+
raise HTTPException(status_code=404, detail="Server not found")
72+
73+
tools_response = db.get(Tool, filters={"server_id": server_id, "user_id": user_id})
74+
return {"status": True, "data": tools_response.data}
75+
76+
77+
@router.post("/{server_id}/refresh")
78+
async def refresh_server_tools(server_id: int, user_id: str, db=Depends(get_db)) -> Dict:
79+
"""Refresh tools for an existing server"""
80+
81+
server_response = db.get(ToolServer, filters={"id": server_id, "user_id": user_id})
82+
if not server_response.status or not server_response.data:
83+
raise HTTPException(status_code=404, detail="Server not found")
84+
85+
server = server_response.data[0]
86+
tsm = ToolServerManager()
87+
88+
try:
89+
# Use the same discovery logic as the tools endpoint
90+
tools_components = await tsm.discover_tools(server.component)
91+
92+
# Update server last_connected timestamp
93+
from datetime import datetime
94+
95+
server.last_connected = datetime.now()
96+
db.upsert(server)
97+
98+
updated_count = 0
99+
created_count = 0
100+
101+
for tool_component in tools_components:
102+
# Generate a unique identifier for the tool from its component
103+
component_data = tool_component.dump_component().model_dump()
104+
105+
# Check if the tool already exists based on id/name
106+
component_config = component_data.get("config", {})
107+
tool_config = component_config.get("tool", {})
108+
tool_name = tool_config.get("name", None)
109+
110+
# First get all tools for this server and user
111+
existing_tool_response = db.get(Tool, filters={"server_id": server_id, "user_id": user_id})
112+
113+
matching_tools = []
114+
if existing_tool_response.status and existing_tool_response.data:
115+
for tool in existing_tool_response.data:
116+
try:
117+
tool_comp = tool.component
118+
if tool_comp.get("config", {}).get("tool", {}).get("name") == tool_name:
119+
matching_tools.append(tool)
120+
except Exception:
121+
pass
122+
123+
# Update existing_tool_response to use our filtered results
124+
existing_tool_response.data = matching_tools
125+
126+
if existing_tool_response.status and existing_tool_response.data:
127+
# Tool exists, update it
128+
existing_tool = existing_tool_response.data[0]
129+
existing_tool.component = component_data
130+
db.upsert(existing_tool)
131+
updated_count += 1
132+
else:
133+
# Tool does not exist, create new
134+
new_tool = Tool(user_id=user_id, server_id=server_id, component=component_data)
135+
# print(f"Creating new tool: {new_tool}")
136+
db.upsert(new_tool)
137+
created_count += 1
138+
139+
return {
140+
"status": True,
141+
"message": "Server refreshed successfully.",
142+
"data": {
143+
"total_count": len(tools_components),
144+
"updated_count": updated_count,
145+
"created_count": created_count,
146+
},
147+
}
148+
except Exception as e:
149+
raise HTTPException(status_code=400, detail=f"Failed to refresh server: {str(e)}") from e

python/packages/autogen-studio/autogenstudio/web/routes/tools.py

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,12 @@
1-
from typing import Dict, Literal
2-
3-
from autogen_ext.tools.mcp._config import SseServerParams, StdioServerParams
4-
from autogen_ext.tools.mcp._factory import mcp_server_tools
1+
from typing import Dict
52
from fastapi import APIRouter, Depends, HTTPException
6-
from pydantic import BaseModel
73

84
from ...datamodel import Tool
95
from ..deps import get_db
106

117
router = APIRouter()
128

139

14-
class McpToolParams(BaseModel):
15-
type: Literal["stdio", "sse"]
16-
server_params: SseServerParams | StdioServerParams
17-
18-
19-
@router.post("/discover")
20-
async def resolve_mcp_tool(params: McpToolParams):
21-
try:
22-
tools = await mcp_server_tools(params.server_params)
23-
if not tools:
24-
raise HTTPException(status_code=400, detail="Failed to retrieve tools")
25-
return [tool.dump_component() for tool in tools]
26-
except Exception as e:
27-
raise HTTPException(status_code=400, detail=str(e))
28-
29-
3010
@router.get("/")
3111
async def list_tools(user_id: str, db=Depends(get_db)) -> Dict:
3212
response = db.get(Tool, filters={"user_id": user_id})
@@ -53,7 +33,7 @@ async def create_tool(tool: Tool, db=Depends(get_db)) -> Dict:
5333
async def create_tools(tools: list[Tool], db=Depends(get_db)) -> Dict:
5434
for tool in tools:
5535
db.upsert(tool)
56-
return {"status": True, "message": "Tools created successfully"}
36+
return {"status": True, "data": tools}
5737

5838

5939
@router.delete("/{tool_id}")

0 commit comments

Comments
 (0)