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) @dataclass(slots=True)
class NodeCatalogEntry: class NodeCatalogEntry:
name: str name: str
display_name: str | None
description: str | None description: str | None
outcomes: tuple[str, ...] outcomes: tuple[str, ...]
input_schema: dict[str, Any] input_schema: dict[str, Any]
@@ -18,6 +19,7 @@ class NodeCatalogEntry:
def from_spec(cls, spec: NodeSpec[Any, Any]) -> "NodeCatalogEntry": def from_spec(cls, spec: NodeSpec[Any, Any]) -> "NodeCatalogEntry":
return cls( return cls(
name=spec.name, name=spec.name,
display_name=None,
description=spec.description, description=spec.description,
outcomes=spec.outcomes, outcomes=spec.outcomes,
input_schema=spec.input_model.model_json_schema(), input_schema=spec.input_model.model_json_schema(),
@@ -41,6 +43,7 @@ class NodeCatalog:
"nodes": [ "nodes": [
{ {
"name": entry.name, "name": entry.name,
"display_name": entry.display_name,
"description": entry.description, "description": entry.description,
"outcomes": list(entry.outcomes), "outcomes": list(entry.outcomes),
"input_schema": entry.input_schema, "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 mcp.server.fastmcp import FastMCP
from .error_info import error_payload
from .mcp_sdk_adapter import McpSdkAdapter from .mcp_sdk_adapter import McpSdkAdapter
from .models import BrokerConfig, ConnectionConfig from .models import BrokerConfig, ConnectionConfig
from .service import WfMcpService from .service import WfMcpService
@@ -63,8 +64,7 @@ def create_broker_server(service: WfMcpService) -> FastMCP:
return { return {
"connection_id": connection_id, "connection_id": connection_id,
"refreshed": False, "refreshed": False,
"error_type": type(exc).__name__, **error_payload(exc),
"error": str(exc),
} }
snapshot = service.get_connection_snapshot(connection_id) snapshot = service.get_connection_snapshot(connection_id)
if snapshot is None: if snapshot is None:
+6
View File
@@ -7,6 +7,7 @@ from typing import Any
@dataclass(slots=True) @dataclass(slots=True)
class DiscoveredTool: class DiscoveredTool:
name: str name: str
display_name: str | None
description: str | None description: str | None
input_schema: dict[str, Any] input_schema: dict[str, Any]
output_schema: dict[str, Any] output_schema: dict[str, Any]
@@ -18,6 +19,7 @@ class DiscoveredTool:
class DiscoveredResource: class DiscoveredResource:
uri: str uri: str
name: str name: str
display_name: str | None
description: str | None description: str | None
mime_type: str | None = None mime_type: str | None = None
metadata: dict[str, Any] = field(default_factory=dict) metadata: dict[str, Any] = field(default_factory=dict)
@@ -26,6 +28,7 @@ class DiscoveredResource:
@dataclass(slots=True) @dataclass(slots=True)
class DiscoveredPrompt: class DiscoveredPrompt:
name: str name: str
display_name: str | None
description: str | None description: str | None
arguments: list[dict[str, Any]] = field(default_factory=list) arguments: list[dict[str, Any]] = field(default_factory=list)
metadata: dict[str, Any] = field(default_factory=dict) metadata: dict[str, Any] = field(default_factory=dict)
@@ -36,6 +39,7 @@ class CatalogNodeEntry:
qualified_name: str qualified_name: str
connection_id: str connection_id: str
local_name: str local_name: str
display_name: str | None
description: str | None description: str | None
outcomes: tuple[str, ...] outcomes: tuple[str, ...]
input_schema: dict[str, Any] input_schema: dict[str, Any]
@@ -47,6 +51,7 @@ class CatalogResourceEntry:
qualified_name: str qualified_name: str
connection_id: str connection_id: str
local_name: str local_name: str
display_name: str | None
uri: str uri: str
description: str | None description: str | None
mime_type: str | None = None mime_type: str | None = None
@@ -58,6 +63,7 @@ class CatalogPromptEntry:
qualified_name: str qualified_name: str
connection_id: str connection_id: str
local_name: str local_name: str
display_name: str | None
description: str | None description: str | None
arguments: list[dict[str, Any]] = field(default_factory=list) arguments: list[dict[str, Any]] = field(default_factory=list)
metadata: dict[str, Any] = field(default_factory=dict) metadata: dict[str, Any] = field(default_factory=dict)
+10
View File
@@ -20,6 +20,7 @@ def snapshot_from_specs(
connection_id: str, connection_id: str,
*, *,
specs: dict[str, NodeSpec[Any, Any]], specs: dict[str, NodeSpec[Any, Any]],
tool_display_names: dict[str, str | None] | None = None,
resources: list[DiscoveredResource] | None = None, resources: list[DiscoveredResource] | None = None,
prompts: list[DiscoveredPrompt] | None = None, prompts: list[DiscoveredPrompt] | None = None,
metadata: dict[str, Any] | None = None, metadata: dict[str, Any] | None = None,
@@ -34,6 +35,10 @@ def snapshot_from_specs(
else qualify_node_name(connection_id, entry.name), else qualify_node_name(connection_id, entry.name),
connection_id=connection_id, connection_id=connection_id,
local_name=entry.name.removeprefix(f"{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, description=entry.description,
outcomes=entry.outcomes, outcomes=entry.outcomes,
input_schema=entry.input_schema, input_schema=entry.input_schema,
@@ -46,6 +51,7 @@ def snapshot_from_specs(
qualified_name=qualify_node_name(connection_id, resource.name), qualified_name=qualify_node_name(connection_id, resource.name),
connection_id=connection_id, connection_id=connection_id,
local_name=resource.name, local_name=resource.name,
display_name=resource.display_name,
uri=resource.uri, uri=resource.uri,
description=resource.description, description=resource.description,
mime_type=resource.mime_type, mime_type=resource.mime_type,
@@ -58,6 +64,7 @@ def snapshot_from_specs(
qualified_name=qualify_node_name(connection_id, prompt.name), qualified_name=qualify_node_name(connection_id, prompt.name),
connection_id=connection_id, connection_id=connection_id,
local_name=prompt.name, local_name=prompt.name,
display_name=prompt.display_name,
description=prompt.description, description=prompt.description,
arguments=prompt.arguments, arguments=prompt.arguments,
metadata=prompt.metadata, metadata=prompt.metadata,
@@ -116,6 +123,7 @@ class CombinedCatalog:
"qualified_name": entry.qualified_name, "qualified_name": entry.qualified_name,
"connection_id": entry.connection_id, "connection_id": entry.connection_id,
"local_name": entry.local_name, "local_name": entry.local_name,
"display_name": entry.display_name,
"description": entry.description, "description": entry.description,
"outcomes": list(entry.outcomes), "outcomes": list(entry.outcomes),
"input_schema": entry.input_schema, "input_schema": entry.input_schema,
@@ -128,6 +136,7 @@ class CombinedCatalog:
"qualified_name": entry.qualified_name, "qualified_name": entry.qualified_name,
"connection_id": entry.connection_id, "connection_id": entry.connection_id,
"local_name": entry.local_name, "local_name": entry.local_name,
"display_name": entry.display_name,
"uri": entry.uri, "uri": entry.uri,
"description": entry.description, "description": entry.description,
"mime_type": entry.mime_type, "mime_type": entry.mime_type,
@@ -140,6 +149,7 @@ class CombinedCatalog:
"qualified_name": entry.qualified_name, "qualified_name": entry.qualified_name,
"connection_id": entry.connection_id, "connection_id": entry.connection_id,
"local_name": entry.local_name, "local_name": entry.local_name,
"display_name": entry.display_name,
"description": entry.description, "description": entry.description,
"arguments": entry.arguments, "arguments": entry.arguments,
"metadata": entry.metadata, "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", "type": "object",
"properties": {"content": {"type": "array"}}, "properties": {"content": {"type": "array"}},
} }
display_name = (
tool.annotations.title
if tool.annotations is not None and tool.annotations.title
else tool.title
)
return DiscoveredTool( return DiscoveredTool(
name=tool.name, name=tool.name,
display_name=display_name,
description=tool.description, description=tool.description,
input_schema=tool.inputSchema, input_schema=tool.inputSchema,
output_schema=output_schema, output_schema=output_schema,
@@ -56,6 +62,7 @@ def _resource_to_discovered(resource: McpResource) -> DiscoveredResource:
return DiscoveredResource( return DiscoveredResource(
uri=str(resource.uri), uri=str(resource.uri),
name=local_name, name=local_name,
display_name=resource.title,
description=resource.description, description=resource.description,
mime_type=resource.mimeType, mime_type=resource.mimeType,
metadata=resource.model_dump(by_alias=True, mode="json"), metadata=resource.model_dump(by_alias=True, mode="json"),
@@ -69,6 +76,7 @@ def _prompt_to_discovered(prompt: McpPrompt) -> DiscoveredPrompt:
] ]
return DiscoveredPrompt( return DiscoveredPrompt(
name=prompt.name, name=prompt.name,
display_name=prompt.title,
description=prompt.description, description=prompt.description,
arguments=arguments, arguments=arguments,
metadata=prompt.model_dump(by_alias=True, mode="json"), 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 .catalog import CombinedCatalog, snapshot_from_specs
from .connections import ConnectionRegistry, parse_connection_id, qualify_node_name from .connections import ConnectionRegistry, parse_connection_id, qualify_node_name
from .discovery import discover_connection_capabilities, specs_from_discovered_tools from .discovery import discover_connection_capabilities, specs_from_discovered_tools
from .error_info import error_payload
from .events import McpEvent, make_event from .events import McpEvent, make_event
from .models import ( from .models import (
AuthRecord, AuthRecord,
@@ -313,6 +314,9 @@ class WfMcpService:
snapshot = snapshot_from_specs( snapshot = snapshot_from_specs(
connection_id, connection_id,
specs=self.specs_by_connection.get(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, resources=capabilities.resources,
prompts=capabilities.prompts, prompts=capabilities.prompts,
metadata=capabilities.metadata, metadata=capabilities.metadata,
@@ -336,10 +340,7 @@ class WfMcpService:
make_event( make_event(
"catalog_refresh_failed", "catalog_refresh_failed",
connection_id=connection_id, connection_id=connection_id,
payload={ payload=error_payload(exc),
"error_type": type(exc).__name__,
"error": str(exc),
},
) )
) )
raise raise
+12
View File
@@ -10,6 +10,7 @@ from wf_mcp import (
RawWorkflowPlan, RawWorkflowPlan,
WfMcpService, WfMcpService,
) )
from wf_mcp.error_info import error_payload
from test_wf_mcp_support import ( from test_wf_mcp_support import (
FailingDiscoveryAdapter, FailingDiscoveryAdapter,
@@ -115,6 +116,7 @@ def test_service_refreshes_catalog_from_adapter() -> None:
"qualified_name": "demo.personal.echo_tool", "qualified_name": "demo.personal.echo_tool",
"connection_id": "demo.personal", "connection_id": "demo.personal",
"local_name": "echo_tool", "local_name": "echo_tool",
"display_name": "Echo Tool",
"description": "Echo text back", "description": "Echo text back",
"outcomes": ["ok"], "outcomes": ["ok"],
"input_schema": { "input_schema": {
@@ -138,6 +140,7 @@ def test_service_refreshes_catalog_from_adapter() -> None:
"qualified_name": "demo.personal.resource.welcome", "qualified_name": "demo.personal.resource.welcome",
"connection_id": "demo.personal", "connection_id": "demo.personal",
"local_name": "resource.welcome", "local_name": "resource.welcome",
"display_name": "Welcome Resource",
"uri": "demo://docs/welcome", "uri": "demo://docs/welcome",
"description": "Welcome resource", "description": "Welcome resource",
"mime_type": "text/plain", "mime_type": "text/plain",
@@ -149,6 +152,7 @@ def test_service_refreshes_catalog_from_adapter() -> None:
"qualified_name": "demo.personal.prompt.summarize", "qualified_name": "demo.personal.prompt.summarize",
"connection_id": "demo.personal", "connection_id": "demo.personal",
"local_name": "prompt.summarize", "local_name": "prompt.summarize",
"display_name": "Summarize Prompt",
"description": "Summarize text", "description": "Summarize text",
"arguments": [ "arguments": [
{ {
@@ -343,3 +347,11 @@ def test_service_records_catalog_refresh_failures() -> None:
"error_type": "PermissionError", "error_type": "PermissionError",
"error": "Access is denied", "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 [ return [
DiscoveredTool( DiscoveredTool(
name="echo_tool", name="echo_tool",
display_name="Echo Tool",
description="Echo text back", description="Echo text back",
input_schema={ input_schema={
"type": "object", "type": "object",
@@ -128,6 +129,7 @@ class FakeAdapter:
DiscoveredResource( DiscoveredResource(
uri="demo://docs/welcome", uri="demo://docs/welcome",
name="resource.welcome", name="resource.welcome",
display_name="Welcome Resource",
description="Welcome resource", description="Welcome resource",
mime_type="text/plain", mime_type="text/plain",
metadata={"kind": "static"}, metadata={"kind": "static"},
@@ -142,6 +144,7 @@ class FakeAdapter:
return [ return [
DiscoveredPrompt( DiscoveredPrompt(
name="prompt.summarize", name="prompt.summarize",
display_name="Summarize Prompt",
description="Summarize text", description="Summarize text",
arguments=[ arguments=[
{ {