misc: display name + error handling
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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),
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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"),
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
}
|
||||||
|
|||||||
@@ -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=[
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user