feat(*): t3code-mcp connector - machines, projects, dispatch, wait, interrupt

This commit is contained in:
hh
2026-08-29 20:36:21 +02:00
commit c86f34c9fa
21 changed files with 2891 additions and 0 deletions
+432
View File
@@ -0,0 +1,432 @@
import json
from typing import Any
import httpx
import pytest
from fastmcp import Client
from fastmcp.exceptions import ToolError
from t3code_mcp.config import Machine, ModelSpec, load_machines
from t3code_mcp.server import Registry, build_server
class FakeT3:
def __init__(
self, label: str, projects: list[dict[str, Any]], token: str = "tok"
) -> None:
self.label = label
self.token = token
self.projects = projects
self.threads: dict[str, dict[str, Any]] = {}
self.commands: list[dict[str, Any]] = []
self.polls_until_done = 2
self.pending_input = False
self.fail_with: str | None = None
def handle(self, request: httpx.Request) -> httpx.Response:
path = request.url.path
if path == "/.well-known/t3/environment":
return httpx.Response(
200,
json={
"label": self.label,
"serverVersion": "0.0.36",
"platform": {"os": "linux"},
},
)
if request.headers.get("authorization") != f"Bearer {self.token}":
return httpx.Response(
401, json={"code": "auth_invalid", "reason": "missing_credential"}
)
if path == "/api/orchestration/shell":
return httpx.Response(200, json=self._shell())
if path.startswith("/api/orchestration/threads/"):
thread_id = path.rsplit("/", 1)[1]
if thread_id not in self.threads:
return httpx.Response(
404, json={"code": "not_found", "reason": "thread_not_found"}
)
self._tick(thread_id)
return httpx.Response(
200, json={"snapshotSequence": 1, "thread": self.threads[thread_id]}
)
if path == "/api/orchestration/dispatch":
command = json.loads(request.content)
self.commands.append(command)
return self._dispatch(command)
return httpx.Response(404, json={"code": "not_found"})
def _shell(self) -> dict[str, Any]:
threads = [
{
k: v
for k, v in t.items()
if k not in ("messages", "activities", "checkpoints")
}
| {"hasPendingUserInput": self.pending_input, "hasPendingApprovals": False}
for t in self.threads.values()
]
return {
"snapshotSequence": 1,
"projects": self.projects,
"threads": threads,
"updatedAt": "now",
}
def _tick(self, thread_id: str) -> None:
thread = self.threads[thread_id]
if self.fail_with and thread.get("_pending_turn"):
thread["session"] = {"status": "stopped", "lastError": self.fail_with}
return
if thread.get("_pending_turn"):
thread["latestTurn"] = {
"turnId": thread.pop("_pending_turn"),
"state": "running",
}
return
latest = thread["latestTurn"]
if latest and latest["state"] == "running":
latest["_polls"] = latest.get("_polls", 0) + 1
if latest["_polls"] > self.polls_until_done:
latest["state"] = "completed"
thread["messages"].append(
{"role": "assistant", "text": "done: report", "streaming": False}
)
thread["session"]["status"] = "ready"
def _dispatch(self, command: dict[str, Any]) -> httpx.Response:
kind = command["type"]
if kind == "thread.create":
self.threads[command["threadId"]] = {
"id": command["threadId"],
"projectId": command["projectId"],
"title": command["title"],
"modelSelection": command["modelSelection"],
"runtimeMode": command["runtimeMode"],
"latestTurn": None,
"session": {"status": "idle", "lastError": None},
"messages": [],
"activities": [],
"checkpoints": [],
"archivedAt": None,
"updatedAt": "now",
}
elif kind == "thread.turn.start":
thread = self.threads[command["threadId"]]
thread["messages"].append(
{"role": "user", "text": command["message"]["text"], "streaming": False}
)
thread["latestTurn"] = None
thread["_pending_turn"] = "turn-" + command["commandId"][:4]
thread["session"] = {"status": "running", "lastError": None}
if "modelSelection" in command:
thread["modelSelection"] = command["modelSelection"]
elif kind == "thread.turn.interrupt":
thread = self.threads[command["threadId"]]
thread.pop("_pending_turn", None)
thread["latestTurn"] = {"turnId": "t", "state": "interrupted"}
else:
return httpx.Response(
400, json={"code": "invalid_request", "reason": "invalid_command"}
)
return httpx.Response(200, json={"sequence": len(self.commands)})
PROJECTS_MAC = [
{
"id": "p-smoke",
"title": "t3-smoke",
"workspaceRoot": "/Users/h/projects/playgrounds/t3-smoke",
"defaultModelSelection": {"instanceId": "codex", "model": "gpt-5.6-sol"},
},
{
"id": "p-secret",
"title": "secret",
"workspaceRoot": "/Users/h/secret",
"defaultModelSelection": None,
},
]
PROJECTS_DELL = [
{
"id": "p-root",
"title": "projects",
"workspaceRoot": "/root/projects",
"defaultModelSelection": None,
},
{
"id": "p-repo",
"title": "beaver/x",
"workspaceRoot": "/root/projects/beaver/x",
"defaultModelSelection": None,
},
]
class Router(httpx.AsyncBaseTransport):
def __init__(self, fakes: dict[str, FakeT3]) -> None:
self.fakes = fakes
async def handle_async_request(self, request: httpx.Request) -> httpx.Response:
return self.fakes[request.url.host].handle(request)
@pytest.fixture
def fakes(monkeypatch: pytest.MonkeyPatch) -> dict[str, FakeT3]:
monkeypatch.setenv("T3_MAC_TOKEN", "tok")
monkeypatch.setenv("T3_DELL_TOKEN", "tok")
return {"mac": FakeT3("mac", PROJECTS_MAC), "dell": FakeT3("dell", PROJECTS_DELL)}
@pytest.fixture
def registry(fakes: dict[str, FakeT3]) -> Registry:
machines = {
"mac": Machine.model_validate(
{
"name": "mac",
"url": "http://mac:3773",
"token_env": "T3_MAC_TOKEN",
"projects": ("t3-smoke",),
"model": "claudeAgent/claude-opus-5",
"options": {"effort": "high"},
}
),
"dell": Machine(
name="dell",
url="http://dell:3773",
token_env="T3_DELL_TOKEN",
projects=("/root/projects", "/root/projects/*"),
),
}
return Registry(machines, transport=Router(fakes))
@pytest.fixture
def client(registry: Registry) -> Client:
return Client(build_server(registry, poll=0.001))
def data(result: Any) -> Any:
return (
result.structured_content["result"]
if "result" in result.structured_content
else result.structured_content
)
async def test_machines_and_projects(client: Client) -> None:
async with client:
machines = data(await client.call_tool("t3_machines"))
assert [m["name"] for m in machines] == ["mac", "dell"]
assert all(m["online"] for m in machines)
projects = data(await client.call_tool("t3_projects", {"machine": "mac"}))
assert [p["title"] for p in projects] == ["t3-smoke"]
assert projects[0]["default_model"] == "codex/gpt-5.6-sol"
dell = data(await client.call_tool("t3_projects", {"machine": "dell"}))
assert [p["title"] for p in dell] == ["projects", "beaver/x"]
async def test_dispatch_wait_and_thread(
client: Client, fakes: dict[str, FakeT3]
) -> None:
async with client:
started = data(
await client.call_tool(
"t3_dispatch",
{
"machine": "mac",
"project": "t3-smoke",
"prompt": "write README\nmore",
},
)
)
assert started["title"] == "write README"
assert started["model"] == "claudeAgent/claude-opus-5 effort=high"
kinds = [c["type"] for c in fakes["mac"].commands]
assert kinds == ["thread.create", "thread.turn.start"]
assert fakes["mac"].commands[0]["modelSelection"] == {
"instanceId": "claudeAgent",
"model": "claude-opus-5",
"options": [{"id": "effort", "value": "high"}],
}
waited = data(
await client.call_tool(
"t3_wait", {"thread_id": started["thread_id"], "timeout": 5}
)
)
assert waited["state"] == "completed"
assert waited["messages"][-1]["text"] == "done: report"
assert waited["timed_out"] is False
view = data(
await client.call_tool("t3_thread", {"thread_id": started["thread_id"]})
)
assert view["project"] == "t3-smoke"
assert view["pending"] is None
async def test_follow_up_uses_same_thread(
client: Client, fakes: dict[str, FakeT3]
) -> None:
fakes["mac"].polls_until_done = 0
async with client:
started = data(
await client.call_tool(
"t3_dispatch",
{"machine": "mac", "project": "t3-smoke", "prompt": "one"},
)
)
await client.call_tool(
"t3_wait", {"thread_id": started["thread_id"], "timeout": 5}
)
again = data(
await client.call_tool(
"t3_dispatch",
{
"machine": "mac",
"project": "t3-smoke",
"prompt": "two",
"thread_id": started["thread_id"],
},
)
)
assert again["thread_id"] == started["thread_id"]
assert [c["type"] for c in fakes["mac"].commands] == [
"thread.create",
"thread.turn.start",
"thread.turn.start",
]
async def test_allowlist_and_unknown_machine(client: Client) -> None:
async with client:
with pytest.raises(ToolError, match="not available on mac"):
await client.call_tool(
"t3_dispatch", {"machine": "mac", "project": "secret", "prompt": "x"}
)
with pytest.raises(ToolError, match="unknown machine"):
await client.call_tool("t3_projects", {"machine": "rpi"})
async def test_wait_timeout_and_interrupt(
client: Client, fakes: dict[str, FakeT3]
) -> None:
fakes["dell"].polls_until_done = 10_000
async with client:
started = data(
await client.call_tool(
"t3_dispatch",
{
"machine": "dell",
"project": "/root/projects",
"prompt": "loop",
"model": "claudeAgent/claude-sonnet-5",
},
)
)
assert started["model"] == "claudeAgent/claude-sonnet-5"
waited = data(
await client.call_tool(
"t3_wait", {"thread_id": started["thread_id"], "timeout": 5}
)
)
assert waited["state"] == "running"
assert waited["timed_out"] is True
stopped = data(
await client.call_tool("t3_interrupt", {"thread_id": started["thread_id"]})
)
assert stopped["state"] == "interrupting"
assert fakes["dell"].commands[-1]["type"] == "thread.turn.interrupt"
view = data(
await client.call_tool("t3_thread", {"thread_id": started["thread_id"]})
)
assert view["state"] == "interrupted"
async def test_wait_returns_on_pending_input(
client: Client, fakes: dict[str, FakeT3]
) -> None:
fakes["mac"].polls_until_done = 10_000
fakes["mac"].pending_input = True
async with client:
started = data(
await client.call_tool(
"t3_dispatch",
{"machine": "mac", "project": "t3-smoke", "prompt": "ask"},
)
)
waited = data(
await client.call_tool(
"t3_wait", {"thread_id": started["thread_id"], "timeout": 5}
)
)
assert waited["pending"] == {
"approval": False,
"user_input": True,
"plan": False,
}
assert waited["state"] == "running"
async def test_locate_without_cache(
registry: Registry, fakes: dict[str, FakeT3]
) -> None:
fakes["dell"].polls_until_done = 0
async with Client(build_server(registry, poll=0.001)) as client:
started = data(
await client.call_tool(
"t3_dispatch",
{
"machine": "dell",
"project": "beaver/x",
"prompt": "hi",
"model": "claudeAgent/claude-opus-5",
},
)
)
registry.threads.clear()
async with Client(build_server(registry, poll=0.001)) as client:
view = data(
await client.call_tool("t3_thread", {"thread_id": started["thread_id"]})
)
assert view["machine"] == "dell"
def test_config_parsing(tmp_path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("T3_MAC_TOKEN", "abc")
cfg = tmp_path / "t3code.toml"
cfg.write_text(
'[machines.mac]\nurl = "http://x"\ntoken_env = "T3_MAC_TOKEN"\nprojects = ["a"]\nmodel = "claudeAgent/claude-opus-5"\noptions = { effort = "high" }\n'
)
machines = load_machines(cfg)
assert machines["mac"].model == ModelSpec(
instance="claudeAgent", model="claude-opus-5", options={"effort": "high"}
)
assert machines["mac"].token == "abc"
monkeypatch.delenv("T3_MAC_TOKEN")
with pytest.raises(ValueError, match="T3_MAC_TOKEN is empty"):
load_machines(cfg)
async def test_wait_returns_on_runtime_failure(
client: Client, fakes: dict[str, FakeT3]
) -> None:
fakes["dell"].fail_with = "Claude runtime stream failed."
async with client:
started = data(
await client.call_tool(
"t3_dispatch",
{
"machine": "dell",
"project": "projects",
"prompt": "x",
"model": "claudeAgent/claude-opus-5",
},
)
)
waited = data(
await client.call_tool(
"t3_wait", {"thread_id": started["thread_id"], "timeout": 5}
)
)
assert waited["state"] == "starting"
assert waited["error"] == "Claude runtime stream failed."
assert waited["waited"] < 1