misc: display name + error handling

This commit is contained in:
lda
2026-04-30 00:44:20 +07:00 Verified
parent 449e61b0d5
commit 0afd61cf27
9 changed files with 70 additions and 6 deletions
+3
View File
@@ -9,6 +9,7 @@ from .spec import NodeSpec
@dataclass(slots=True)
class NodeCatalogEntry:
name: str
display_name: str | None
description: str | None
outcomes: tuple[str, ...]
input_schema: dict[str, Any]
@@ -18,6 +19,7 @@ class NodeCatalogEntry:
def from_spec(cls, spec: NodeSpec[Any, Any]) -> "NodeCatalogEntry":
return cls(
name=spec.name,
display_name=None,
description=spec.description,
outcomes=spec.outcomes,
input_schema=spec.input_model.model_json_schema(),
@@ -41,6 +43,7 @@ class NodeCatalog:
"nodes": [
{
"name": entry.name,
"display_name": entry.display_name,
"description": entry.description,
"outcomes": list(entry.outcomes),
"input_schema": entry.input_schema,
+2 -2
View File
@@ -8,6 +8,7 @@ from typing import Any, Literal
from mcp.server.fastmcp import FastMCP
from .error_info import error_payload
from .mcp_sdk_adapter import McpSdkAdapter
from .models import BrokerConfig, ConnectionConfig
from .service import WfMcpService
@@ -63,8 +64,7 @@ def create_broker_server(service: WfMcpService) -> FastMCP:
return {
"connection_id": connection_id,
"refreshed": False,
"error_type": type(exc).__name__,
"error": str(exc),
**error_payload(exc),
}
snapshot = service.get_connection_snapshot(connection_id)
if snapshot is None:
+6
View File
@@ -7,6 +7,7 @@ from typing import Any
@dataclass(slots=True)
class DiscoveredTool:
name: str
display_name: str | None
description: str | None
input_schema: dict[str, Any]
output_schema: dict[str, Any]
@@ -18,6 +19,7 @@ class DiscoveredTool:
class DiscoveredResource:
uri: str
name: str
display_name: str | None
description: str | None
mime_type: str | None = None
metadata: dict[str, Any] = field(default_factory=dict)
@@ -26,6 +28,7 @@ class DiscoveredResource:
@dataclass(slots=True)
class DiscoveredPrompt:
name: str
display_name: str | None
description: str | None
arguments: list[dict[str, Any]] = field(default_factory=list)
metadata: dict[str, Any] = field(default_factory=dict)
@@ -36,6 +39,7 @@ class CatalogNodeEntry:
qualified_name: str
connection_id: str
local_name: str
display_name: str | None
description: str | None
outcomes: tuple[str, ...]
input_schema: dict[str, Any]
@@ -47,6 +51,7 @@ class CatalogResourceEntry:
qualified_name: str
connection_id: str
local_name: str
display_name: str | None
uri: str
description: str | None
mime_type: str | None = None
@@ -58,6 +63,7 @@ class CatalogPromptEntry:
qualified_name: str
connection_id: str
local_name: str
display_name: str | None
description: str | None
arguments: list[dict[str, Any]] = field(default_factory=list)
metadata: dict[str, Any] = field(default_factory=dict)
+10
View File
@@ -20,6 +20,7 @@ def snapshot_from_specs(
connection_id: str,
*,
specs: dict[str, NodeSpec[Any, Any]],
tool_display_names: dict[str, str | None] | None = None,
resources: list[DiscoveredResource] | None = None,
prompts: list[DiscoveredPrompt] | None = None,
metadata: dict[str, Any] | None = None,
@@ -34,6 +35,10 @@ def snapshot_from_specs(
else qualify_node_name(connection_id, entry.name),
connection_id=connection_id,
local_name=entry.name.removeprefix(f"{connection_id}."),
display_name=(tool_display_names or {}).get(
entry.name.removeprefix(f"{connection_id}."),
entry.display_name,
),
description=entry.description,
outcomes=entry.outcomes,
input_schema=entry.input_schema,
@@ -46,6 +51,7 @@ def snapshot_from_specs(
qualified_name=qualify_node_name(connection_id, resource.name),
connection_id=connection_id,
local_name=resource.name,
display_name=resource.display_name,
uri=resource.uri,
description=resource.description,
mime_type=resource.mime_type,
@@ -58,6 +64,7 @@ def snapshot_from_specs(
qualified_name=qualify_node_name(connection_id, prompt.name),
connection_id=connection_id,
local_name=prompt.name,
display_name=prompt.display_name,
description=prompt.description,
arguments=prompt.arguments,
metadata=prompt.metadata,
@@ -116,6 +123,7 @@ class CombinedCatalog:
"qualified_name": entry.qualified_name,
"connection_id": entry.connection_id,
"local_name": entry.local_name,
"display_name": entry.display_name,
"description": entry.description,
"outcomes": list(entry.outcomes),
"input_schema": entry.input_schema,
@@ -128,6 +136,7 @@ class CombinedCatalog:
"qualified_name": entry.qualified_name,
"connection_id": entry.connection_id,
"local_name": entry.local_name,
"display_name": entry.display_name,
"uri": entry.uri,
"description": entry.description,
"mime_type": entry.mime_type,
@@ -140,6 +149,7 @@ class CombinedCatalog:
"qualified_name": entry.qualified_name,
"connection_id": entry.connection_id,
"local_name": entry.local_name,
"display_name": entry.display_name,
"description": entry.description,
"arguments": entry.arguments,
"metadata": entry.metadata,
+21
View File
@@ -0,0 +1,21 @@
from __future__ import annotations
def root_exception(exc: BaseException) -> BaseException:
current: BaseException = exc
while isinstance(current, ExceptionGroup) and current.exceptions:
nested = current.exceptions[0]
if isinstance(nested, BaseException):
current = nested
continue
break
return current
def error_payload(exc: BaseException) -> dict[str, str]:
root = root_exception(exc)
return {
"error_type": type(root).__name__,
"error": str(root),
}
+8
View File
@@ -41,8 +41,14 @@ def _tool_to_discovered(tool: McpTool) -> DiscoveredTool:
"type": "object",
"properties": {"content": {"type": "array"}},
}
display_name = (
tool.annotations.title
if tool.annotations is not None and tool.annotations.title
else tool.title
)
return DiscoveredTool(
name=tool.name,
display_name=display_name,
description=tool.description,
input_schema=tool.inputSchema,
output_schema=output_schema,
@@ -56,6 +62,7 @@ def _resource_to_discovered(resource: McpResource) -> DiscoveredResource:
return DiscoveredResource(
uri=str(resource.uri),
name=local_name,
display_name=resource.title,
description=resource.description,
mime_type=resource.mimeType,
metadata=resource.model_dump(by_alias=True, mode="json"),
@@ -69,6 +76,7 @@ def _prompt_to_discovered(prompt: McpPrompt) -> DiscoveredPrompt:
]
return DiscoveredPrompt(
name=prompt.name,
display_name=prompt.title,
description=prompt.description,
arguments=arguments,
metadata=prompt.model_dump(by_alias=True, mode="json"),
+5 -4
View File
@@ -11,6 +11,7 @@ from .adapters import BackendAdapter
from .catalog import CombinedCatalog, snapshot_from_specs
from .connections import ConnectionRegistry, parse_connection_id, qualify_node_name
from .discovery import discover_connection_capabilities, specs_from_discovered_tools
from .error_info import error_payload
from .events import McpEvent, make_event
from .models import (
AuthRecord,
@@ -313,6 +314,9 @@ class WfMcpService:
snapshot = snapshot_from_specs(
connection_id,
specs=self.specs_by_connection.get(connection_id, {}),
tool_display_names={
tool.name: tool.display_name for tool in capabilities.tools
},
resources=capabilities.resources,
prompts=capabilities.prompts,
metadata=capabilities.metadata,
@@ -336,10 +340,7 @@ class WfMcpService:
make_event(
"catalog_refresh_failed",
connection_id=connection_id,
payload={
"error_type": type(exc).__name__,
"error": str(exc),
},
payload=error_payload(exc),
)
)
raise
+12
View File
@@ -10,6 +10,7 @@ from wf_mcp import (
RawWorkflowPlan,
WfMcpService,
)
from wf_mcp.error_info import error_payload
from test_wf_mcp_support import (
FailingDiscoveryAdapter,
@@ -115,6 +116,7 @@ def test_service_refreshes_catalog_from_adapter() -> None:
"qualified_name": "demo.personal.echo_tool",
"connection_id": "demo.personal",
"local_name": "echo_tool",
"display_name": "Echo Tool",
"description": "Echo text back",
"outcomes": ["ok"],
"input_schema": {
@@ -138,6 +140,7 @@ def test_service_refreshes_catalog_from_adapter() -> None:
"qualified_name": "demo.personal.resource.welcome",
"connection_id": "demo.personal",
"local_name": "resource.welcome",
"display_name": "Welcome Resource",
"uri": "demo://docs/welcome",
"description": "Welcome resource",
"mime_type": "text/plain",
@@ -149,6 +152,7 @@ def test_service_refreshes_catalog_from_adapter() -> None:
"qualified_name": "demo.personal.prompt.summarize",
"connection_id": "demo.personal",
"local_name": "prompt.summarize",
"display_name": "Summarize Prompt",
"description": "Summarize text",
"arguments": [
{
@@ -343,3 +347,11 @@ def test_service_records_catalog_refresh_failures() -> None:
"error_type": "PermissionError",
"error": "Access is denied",
}
def test_error_payload_unwraps_exception_group() -> None:
exc = ExceptionGroup("outer", [PermissionError("Access is denied")])
assert error_payload(exc) == {
"error_type": "PermissionError",
"error": "Access is denied",
}
+3
View File
@@ -105,6 +105,7 @@ class FakeAdapter:
return [
DiscoveredTool(
name="echo_tool",
display_name="Echo Tool",
description="Echo text back",
input_schema={
"type": "object",
@@ -128,6 +129,7 @@ class FakeAdapter:
DiscoveredResource(
uri="demo://docs/welcome",
name="resource.welcome",
display_name="Welcome Resource",
description="Welcome resource",
mime_type="text/plain",
metadata={"kind": "static"},
@@ -142,6 +144,7 @@ class FakeAdapter:
return [
DiscoveredPrompt(
name="prompt.summarize",
display_name="Summarize Prompt",
description="Summarize text",
arguments=[
{