Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
1 change: 0 additions & 1 deletion src/tether/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -7744,6 +7744,5 @@ def data_revoke() -> None:
set_contribute_data(False)
console.print(f"Revoked: {removed} files deleted. Data contribution disabled.")


if __name__ == "__main__":
app()
19 changes: 18 additions & 1 deletion src/tether/mcp/server.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
"""FastMCP server factory bound to a live TetherServer.

Exposes 6 tools + 1 resource to MCP-compatible agents (Phase 1 + Phase 1.5):
Exposes 6 tools + 2 resources to MCP-compatible agents (Phase 1 + Phase 1.5):

Phase 1 (consumer-side):
- tool: `act(instruction, image_b64, state, episode_id?)` → action chunk +
policy_version + inference_ms
- tool: `health()` → {state, model_version, uptime_seconds, cuda_graphs_active}
- tool: `models_list()` → [{id, hf_id, size_gb_fp16, hardware_fit}, ...]
- tool: `validate_dataset(dataset_path)` → {summary, checks: [...]}
- resource: `version://current` → current package/runtime version
- resource: `metrics://prometheus` → current Prometheus exposition text

Phase 1.5 (producer-side, agents-can-plan-without-executing):
Expand Down Expand Up @@ -57,6 +58,7 @@
- validate_dataset: pre-flight check a LeRobot v3.0 training dataset

Available resources:
- version://current: fastcrest-tether package version (for client compatibility checks)
- metrics://prometheus: current Prometheus metrics in text exposition format

Comment on lines 58 to 63
Safety note: tool `act` returns actions but does NOT actuate them. The caller is
Expand Down Expand Up @@ -501,6 +503,21 @@ async def export_estimate(
"notes": notes,
}

@mcp.resource("version://current")
async def version_resource() -> str:
"""Current fastcrest-tether package version.

Clients can read this resource to check compatibility before issuing
tool calls. Returns a JSON string with ``version`` and ``package`` keys.
"""
import json
from tether import __version__
return json.dumps({
"version": __version__,
"package": "fastcrest-tether",
"service": "tether",
})

@mcp.resource("metrics://prometheus")
async def prometheus_metrics() -> str:
"""Current Prometheus metrics in text exposition format.
Expand Down
56 changes: 47 additions & 9 deletions tests/test_mcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,52 @@ async def test_validate_dataset_tool_returns_error_on_missing_path():
assert ("error" in payload) or ("decision" in payload) or ("summary" in payload)


# ---------------------------------------------------------------------------
# Resource: version://current
# ---------------------------------------------------------------------------


def _resource_text(result) -> str:
if isinstance(result, list):
contents = result
elif hasattr(result, "contents"):
contents = result.contents
else:
return result.text if hasattr(result, "text") else str(result)
return "".join(
(
item.text
if hasattr(item, "text")
else item.content
if hasattr(item, "content")
else str(item)
)
for item in contents
)


@pytest.mark.asyncio
async def test_version_resource_registered():
mcp = create_mcp_server(_mock_tether_server())
resources = await mcp.list_resources()
uris = {str(r.uri) for r in resources}
assert "version://current" in uris


@pytest.mark.asyncio
async def test_version_resource_returns_package_version():
import json
from tether import __version__

mcp = create_mcp_server(_mock_tether_server())
result = await mcp.read_resource("version://current")

data = json.loads(_resource_text(result))
assert data["version"] == __version__
assert data["package"] == "fastcrest-tether"
assert data["service"] == "tether"


# ---------------------------------------------------------------------------
# Resource: metrics://prometheus
# ---------------------------------------------------------------------------
Expand All @@ -256,15 +302,7 @@ async def test_metrics_resource_returns_prometheus_text():
mcp = create_mcp_server(_mock_tether_server())
# Read the metrics resource
result = await mcp.read_resource("metrics://prometheus")
# read_resource returns a list of ReadResourceContents or strings depending
# on FastMCP version — handle both
if isinstance(result, list):
payload = "".join(
(item.text if hasattr(item, "text") else str(item))
for item in result
)
else:
payload = result.text if hasattr(result, "text") else str(result)
payload = _resource_text(result)
# Real Prometheus exposition starts with # HELP or # TYPE comments
assert isinstance(payload, str)
assert len(payload) > 0
Expand Down
Loading