Skip to content

Commit baf7da2

Browse files
authored
Fix/apps sdk (#427)
* skybridge enhancments+testing * sk capability, stdio symbol fix
1 parent 852aebb commit baf7da2

File tree

9 files changed

+394
-25
lines changed

9 files changed

+394
-25
lines changed

src/fast_agent/agents/mcp_agent.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -668,13 +668,13 @@ async def run_tools(self, request: PromptMessageExtended) -> PromptMessageExtend
668668
result = await self.call_tool(tool_name, tool_args)
669669
tool_results[correlation_id] = result
670670

671-
# Get skybridge config for this tool if available
671+
# Show tool result (like ToolAgent does)
672672
skybridge_config = None
673673
if namespaced_tool:
674-
server_name = namespaced_tool.server_name
675-
skybridge_config = self._aggregator._skybridge_configs.get(server_name)
674+
skybridge_config = await self._aggregator.get_skybridge_config(
675+
namespaced_tool.server_name
676+
)
676677

677-
# Show tool result (like ToolAgent does)
678678
self.display.show_tool_result(
679679
name=self._name,
680680
result=result,

src/fast_agent/mcp/mcp_aggregator.py

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ def __init__(
162162
server_names: List[str],
163163
connection_persistence: bool = True,
164164
context: Optional["Context"] = None,
165-
name: str = None,
165+
name: str | None = None,
166166
config: Optional[Any] = None, # Accept the agent config for elicitation_handler access
167167
**kwargs,
168168
) -> None:
@@ -523,7 +523,8 @@ async def _evaluate_skybridge_for_server(
523523
)
524524
)
525525

526-
supports_resources = await self.server_supports_feature(server_name, "resources")
526+
raw_resources_capability = await self.server_supports_feature(server_name, "resources")
527+
supports_resources = bool(raw_resources_capability)
527528
config.supports_resources = supports_resources
528529
config.tools = tool_configs
529530

@@ -582,7 +583,10 @@ async def _evaluate_skybridge_for_server(
582583
sky_resource.mime_type = seen_mime_types[0]
583584

584585
if not sky_resource.is_skybridge:
585-
warning = "ui:// detected but resource is not of type 'text/html+skybridge'"
586+
observed_type = sky_resource.mime_type or "unknown MIME type"
587+
warning = (
588+
f"served as '{observed_type}' instead of '{SKYBRIDGE_MIME_TYPE}'"
589+
)
586590
sky_resource.warning = warning
587591
config.warnings.append(f"{uri_str}: {warning}")
588592

@@ -608,7 +612,8 @@ async def _evaluate_skybridge_for_server(
608612
if not resource_match.is_skybridge:
609613
warning = (
610614
f"Tool '{tool_config.namespaced_tool_name}' references resource "
611-
f"'{resource_match.uri}' that is not Skybridge MIME type"
615+
f"'{resource_match.uri}' served as '{resource_match.mime_type or 'unknown'}' "
616+
f"instead of '{SKYBRIDGE_MIME_TYPE}'"
612617
)
613618
tool_config.warning = warning
614619
config.warnings.append(warning)
@@ -691,7 +696,15 @@ async def server_supports_feature(self, server_name: str, feature: str) -> bool:
691696
if not capabilities:
692697
return False
693698

694-
return getattr(capabilities, feature, False)
699+
feature_value = getattr(capabilities, feature, False)
700+
if isinstance(feature_value, bool):
701+
return feature_value
702+
if feature_value is None:
703+
return False
704+
try:
705+
return bool(feature_value)
706+
except Exception: # noqa: BLE001
707+
return True
695708

696709
async def list_servers(self) -> List[str]:
697710
"""Return the list of server names aggregated by this agent."""
@@ -995,14 +1008,20 @@ async def get_skybridge_configs(self) -> Dict[str, SkybridgeServerConfig]:
9951008
await self.load_servers()
9961009
return dict(self._skybridge_configs)
9971010

1011+
async def get_skybridge_config(self, server_name: str) -> SkybridgeServerConfig | None:
1012+
"""Return the Skybridge configuration for a specific server, loading if necessary."""
1013+
if not self.initialized:
1014+
await self.load_servers()
1015+
return self._skybridge_configs.get(server_name)
1016+
9981017
async def _execute_on_server(
9991018
self,
10001019
server_name: str,
10011020
operation_type: str,
10021021
operation_name: str,
10031022
method_name: str,
10041023
method_args: Dict[str, Any] = None,
1005-
error_factory: Callable[[str], R] = None,
1024+
error_factory: Callable[[str], R] | None = None,
10061025
progress_callback: ProgressFnT | None = None,
10071026
) -> R:
10081027
"""

src/fast_agent/ui/console_display.py

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from enum import Enum
22
from json import JSONDecodeError
3-
from typing import TYPE_CHECKING, Any, Dict, List, Mapping, Optional, Set, Union
3+
from typing import TYPE_CHECKING, Any, Dict, List, Mapping, Optional, Set, Tuple, Union
44

55
from mcp.types import CallToolResult
66
from rich.panel import Panel
@@ -870,19 +870,18 @@ def _create_combined_separator_status(self, left_content: str, right_info: str =
870870
console.console.print(combined, markup=self._markup)
871871
console.console.print()
872872

873-
def show_skybridge_summary(
874-
self,
875-
agent_name: str,
873+
@staticmethod
874+
def summarize_skybridge_configs(
876875
configs: Mapping[str, "SkybridgeServerConfig"] | None,
877-
) -> None:
878-
"""Display Skybridge availability and warnings."""
879-
if configs is None:
880-
return
881-
876+
) -> Tuple[List[Dict[str, Any]], List[str]]:
877+
"""Convert raw Skybridge configs into display-friendly summary data."""
882878
server_rows: List[Dict[str, Any]] = []
883879
warnings: List[str] = []
884880
warning_seen: Set[str] = set()
885881

882+
if not configs:
883+
return server_rows, warnings
884+
886885
def add_warning(message: str) -> None:
887886
formatted = message.strip()
888887
if not formatted:
@@ -931,6 +930,16 @@ def add_warning(message: str) -> None:
931930
message = f"{server_name} {message}"
932931
add_warning(message)
933932

933+
return server_rows, warnings
934+
935+
def show_skybridge_summary(
936+
self,
937+
agent_name: str,
938+
configs: Mapping[str, "SkybridgeServerConfig"] | None,
939+
) -> None:
940+
"""Display Skybridge availability and warnings."""
941+
server_rows, warnings = self.summarize_skybridge_configs(configs)
942+
934943
if not server_rows and not warnings:
935944
return
936945

@@ -943,7 +952,6 @@ def add_warning(message: str) -> None:
943952
else:
944953
for row in server_rows:
945954
server_name = row["server_name"]
946-
config = row["config"]
947955
resource_count = row["valid_resource_count"]
948956
total_resource_count = row["total_resource_count"]
949957
tool_infos = row["active_tools"]
@@ -985,14 +993,14 @@ def add_warning(message: str) -> None:
985993
console.console.print(
986994
(
987995
"[dim] ▶ "
988-
f"{invalid_count} {invalid_word} detected with non-skybridge MIME type[/dim]"
996+
f"[/dim][cyan]{invalid_count}[/cyan][dim] {invalid_word} detected with non-skybridge MIME type[/dim]"
989997
),
990998
markup=self._markup,
991999
)
9921000

9931001
for warning_entry in warnings:
9941002
console.console.print(
995-
f"[yellow]skybridge warning[/yellow] {warning_entry}",
1003+
f"[dim red] ▶ [/dim red][red]warning[/red] [dim]{warning_entry}[/dim]",
9961004
markup=self._markup,
9971005
)
9981006

src/fast_agent/ui/mcp_display.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ class Colours:
3939
# Capability token states
4040
TOKEN_ERROR = "bright_red"
4141
TOKEN_WARNING = "bright_cyan"
42+
TOKEN_CAUTION = "bright_yellow"
4243
TOKEN_DISABLED = "dim"
4344
TOKEN_HIGHLIGHTED = "bright_yellow"
4445
TOKEN_ENABLED = "bright_green"
@@ -232,6 +233,18 @@ def _format_capability_shorthand(
232233
else:
233234
entries.append(("In", "blue", False))
234235

236+
skybridge_config = getattr(status, "skybridge", None)
237+
if not skybridge_config:
238+
entries.append(("Sk", False, False))
239+
else:
240+
has_warnings = bool(getattr(skybridge_config, "warnings", None))
241+
if has_warnings:
242+
entries.append(("Sk", "warn", False))
243+
elif getattr(skybridge_config, "enabled", False):
244+
entries.append(("Sk", True, False))
245+
else:
246+
entries.append(("Sk", False, False))
247+
235248
if status.roots_configured:
236249
entries.append(("Ro", True, False))
237250
else:
@@ -260,6 +273,8 @@ def token_style(supported, highlighted) -> str:
260273
return Colours.TOKEN_ERROR
261274
if supported == "blue":
262275
return Colours.TOKEN_WARNING
276+
if supported == "warn":
277+
return Colours.TOKEN_CAUTION
263278
if not supported:
264279
return Colours.TOKEN_DISABLED
265280
if highlighted:
@@ -652,6 +667,8 @@ def _render_channel_summary(status: ServerStatus, indent: str, total_width: int)
652667
symbol = SYMBOL_ERROR
653668
elif name == "ping":
654669
symbol = SYMBOL_PING
670+
elif is_stdio and name == "activity":
671+
symbol = SYMBOL_STDIO_ACTIVITY
655672
else:
656673
symbol = SYMBOL_RESPONSE
657674
footer.append(symbol, style=f"{color}")
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
default_model: passthrough
2+
3+
logger:
4+
level: "info"
5+
progress_display: false
6+
show_chat: false
7+
show_tools: false
8+
9+
mcp:
10+
servers:
11+
skybridge_valid:
12+
command: "uv"
13+
args: ["run", "skybridge_test_server.py", "valid"]
14+
description: "Skybridge server with valid resource/tool pairing"
15+
skybridge_invalid_mime:
16+
command: "uv"
17+
args: ["run", "skybridge_test_server.py", "invalid-mime"]
18+
description: "Skybridge server exposing ui:// resource without Skybridge MIME type"
19+
skybridge_missing_resource:
20+
command: "uv"
21+
args: ["run", "skybridge_test_server.py", "missing-resource"]
22+
description: "Skybridge server with Skybridge resources but missing tool linkage"
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
#!/usr/bin/env python3
2+
"""Skybridge-focused MCP test server exposing multiple scenarios."""
3+
4+
from __future__ import annotations
5+
6+
import argparse
7+
from typing import TYPE_CHECKING
8+
9+
from mcp.server.fastmcp import FastMCP
10+
11+
if TYPE_CHECKING:
12+
from mcp.types import Tool as MCPTool
13+
14+
SKYBRIDGE_MIME_TYPE = "text/html+skybridge"
15+
16+
17+
class SkybridgeTestServer(FastMCP):
18+
"""FastMCP server that decorates tool listings with Skybridge meta tags."""
19+
20+
def __init__(self, *args, tool_templates: dict[str, str] | None = None, **kwargs) -> None:
21+
super().__init__(*args, **kwargs)
22+
self._tool_templates = tool_templates or {}
23+
24+
async def list_tools(self) -> list[MCPTool]:
25+
tools = await super().list_tools()
26+
for tool in tools:
27+
template = self._tool_templates.get(tool.name)
28+
if template:
29+
tool.meta = {"openai/outputTemplate": template}
30+
return tools
31+
32+
33+
def build_valid_scenario() -> SkybridgeTestServer:
34+
server = SkybridgeTestServer(
35+
name="Skybridge Valid Scenario",
36+
tool_templates={"render_valid_widget": "ui://skybridge/widget-valid"},
37+
)
38+
39+
@server.tool(name="render_valid_widget", description="Return HTML for a valid Skybridge widget")
40+
def render_valid_widget() -> str:
41+
return "<html><body><h1>Valid Skybridge Widget</h1></body></html>"
42+
43+
@server.resource(
44+
"ui://skybridge/widget-valid",
45+
description="Valid Skybridge UI resource",
46+
mime_type=SKYBRIDGE_MIME_TYPE,
47+
)
48+
def valid_widget_resource() -> str:
49+
return "<html><body><h1>Valid Skybridge Widget</h1></body></html>"
50+
51+
return server
52+
53+
54+
def build_invalid_mime_scenario() -> SkybridgeTestServer:
55+
server = SkybridgeTestServer(
56+
name="Skybridge Invalid MIME Scenario",
57+
tool_templates={"render_invalid_widget": "ui://skybridge/widget-invalid"},
58+
)
59+
60+
@server.tool(
61+
name="render_invalid_widget", description="Return HTML that lacks the Skybridge MIME type"
62+
)
63+
def render_invalid_widget() -> str:
64+
return "<html><body><h1>Invalid MIME</h1></body></html>"
65+
66+
@server.resource(
67+
"ui://skybridge/widget-invalid",
68+
description="Resource served with a non-Skybridge mime type",
69+
mime_type="text/html",
70+
)
71+
def invalid_widget_resource() -> str:
72+
return "<html><body><h1>Invalid MIME</h1></body></html>"
73+
74+
return server
75+
76+
77+
def build_missing_resource_scenario() -> SkybridgeTestServer:
78+
server = SkybridgeTestServer(
79+
name="Skybridge Missing Resource Scenario",
80+
tool_templates={"render_missing_widget": "ui://skybridge/widget-missing"},
81+
)
82+
83+
@server.tool(
84+
name="render_missing_widget",
85+
description="Advertises a template that does not exist on the server",
86+
)
87+
def render_missing_widget() -> str:
88+
return "<html><body><h1>Missing Resource</h1></body></html>"
89+
90+
@server.resource(
91+
"ui://skybridge/orphan-widget",
92+
description="Orphaned Skybridge resource with no tool linkage",
93+
mime_type=SKYBRIDGE_MIME_TYPE,
94+
)
95+
def orphan_widget_resource() -> str:
96+
return "<html><body><h1>Orphan Widget</h1></body></html>"
97+
98+
return server
99+
100+
101+
SCENARIO_BUILDERS = {
102+
"valid": build_valid_scenario,
103+
"invalid-mime": build_invalid_mime_scenario,
104+
"missing-resource": build_missing_resource_scenario,
105+
}
106+
107+
108+
def main() -> None:
109+
parser = argparse.ArgumentParser(description="Skybridge MCP test server scenarios")
110+
parser.add_argument(
111+
"scenario",
112+
choices=SCENARIO_BUILDERS.keys(),
113+
help="Which Skybridge scenario to run",
114+
)
115+
args = parser.parse_args()
116+
117+
server_factory = SCENARIO_BUILDERS[args.scenario]
118+
app = server_factory()
119+
app.run(transport="stdio")
120+
121+
122+
if __name__ == "__main__":
123+
main()

0 commit comments

Comments
 (0)