Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 72 additions & 2 deletions astrbot/core/agent/mcp_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,7 @@ def __init__(self) -> None:

self._mcp_server_config: dict | None = None
self._server_name: str | None = None
self._server_capabilities: mcp.types.ServerCapabilities | None = None
self._reconnect_lock = asyncio.Lock() # Lock for thread-safe reconnection
self._reconnecting: bool = False

Expand Down Expand Up @@ -521,6 +522,7 @@ async def _do_connect(self, mcp_server_config: dict, name: str) -> None:
"""Internal: perform the actual connection inside _run_connection's task."""
# exit_stack is always set by _run_connection before _do_connect is called.
assert self.exit_stack is not None
self._server_capabilities = None
cfg = _prepare_config(mcp_server_config.copy())

def logging_callback(
Expand Down Expand Up @@ -649,16 +651,82 @@ def callback(msg: str | mcp.types.LoggingMessageNotificationParams) -> None:
self.session = await self.exit_stack.enter_async_context(
mcp.ClientSession(*stdio_transport),
)
await self.session.initialize()
initialize_result = await self.session.initialize()
self._server_capabilities = initialize_result.capabilities

async def list_tools_and_save(self) -> mcp.ListToolsResult:
"""List all tools from the server and save them to self.tools"""
if not self.session:
raise Exception("MCP Client is not initialized")
response = await self.session.list_tools()
if (
self._server_capabilities is not None
and getattr(self._server_capabilities, "tools", None) is None
):
response = mcp.types.ListToolsResult(tools=[])
else:
response = await self.session.list_tools()
self.tools = response.tools
return response

@property
def supports_resources(self) -> bool:
"""Whether the connected server advertises MCP resources support."""
return bool(self._server_capabilities and self._server_capabilities.resources)

async def list_resources(
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
self,
cursor: str | None = None,
) -> mcp.types.ListResourcesResult:
"""List resources exposed by the connected MCP server."""
if not self.session:
raise ValueError("MCP session is not available for resource listing.")
if not self.supports_resources:
raise RuntimeError("MCP server does not advertise resources support.")

if cursor is None:
return await self.session.list_resources()

try:
return await self.session.list_resources(cursor=cursor)
except TypeError as exc:
if "unexpected keyword argument 'cursor'" not in str(exc):
raise
raise RuntimeError(
"The installed MCP SDK does not support resource pagination."
) from exc

async def list_resource_templates(
self,
cursor: str | None = None,
) -> mcp.types.ListResourceTemplatesResult:
"""List resource templates exposed by the connected MCP server."""
if not self.session:
raise ValueError(
"MCP session is not available for resource template listing."
)
if not self.supports_resources:
raise RuntimeError("MCP server does not advertise resources support.")

if cursor is None:
return await self.session.list_resource_templates()

try:
return await self.session.list_resource_templates(cursor=cursor)
except TypeError as exc:
if "unexpected keyword argument 'cursor'" not in str(exc):
raise
raise RuntimeError(
"The installed MCP SDK does not support resource template pagination."
) from exc

async def read_resource(self, uri: str) -> mcp.types.ReadResourceResult:
"""Read an MCP resource from the current connected session."""
if not self.session:
raise ValueError("MCP session is not available for resource reading.")
if not self.supports_resources:
raise RuntimeError("MCP server does not advertise resources support.")
return await self.session.read_resource(uri=uri)

def _cancel_connection_task(self, task: asyncio.Task) -> None:
"""Cancel a connection owner task and track it until it finishes."""
# Prune already-finished tasks to avoid accumulating references over
Expand Down Expand Up @@ -787,6 +855,8 @@ async def cleanup(self) -> None:
await asyncio.gather(*pending, return_exceptions=True)
self._old_connection_tasks.clear()

self._server_capabilities = None

# Set running_event to unblock any waiting tasks
self.running_event.set()

Expand Down
32 changes: 32 additions & 0 deletions astrbot/dashboard/api/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from astrbot.dashboard.async_utils import run_maybe_async
from astrbot.dashboard.responses import ApiError, ok
from astrbot.dashboard.schemas import (
McpResourceReadRequest,
McpServerByNameRequest,
McpServerRequest,
ModelScopeSyncRequest,
Expand Down Expand Up @@ -218,6 +219,37 @@ async def list_mcp_servers(
return await _run(service.get_mcp_servers)


@router.get("/mcp/resources")
async def list_mcp_resources(
server_name: str = Query(..., min_length=1),
cursor: str | None = Query(default=None),
_auth: AuthContext = Depends(require_mcp_scope),
service: ToolsService = Depends(get_service),
):
return await _run(lambda: service.list_mcp_resources(server_name, cursor))


@router.get("/mcp/resource-templates")
async def list_mcp_resource_templates(
server_name: str = Query(..., min_length=1),
cursor: str | None = Query(default=None),
_auth: AuthContext = Depends(require_mcp_scope),
service: ToolsService = Depends(get_service),
):
return await _run(lambda: service.list_mcp_resource_templates(server_name, cursor))


@router.post("/mcp/resources/read")
async def read_mcp_resource(
payload: McpResourceReadRequest,
_auth: AuthContext = Depends(require_mcp_scope),
service: ToolsService = Depends(get_service),
):
return await _run(
lambda: service.read_mcp_resource(payload.server_name, payload.uri)
)


@router.post("/mcp/servers")
async def create_mcp_server(
payload: McpServerRequest,
Expand Down
5 changes: 5 additions & 0 deletions astrbot/dashboard/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,11 @@ class McpServerByNameRequest(OpenModel):
enabled: bool | None = None


class McpResourceReadRequest(BaseModel):
server_name: str = Field(..., min_length=1)
uri: str = Field(..., min_length=1)


class ModelScopeSyncRequest(BaseModel):
access_token: str | None = None

Expand Down
Loading
Loading