Files
t3code-mcp/src/t3code_mcp/t3.py
T

175 lines
5.0 KiB
Python

"""HTTP client for one T3 Code server (`packages/contracts/src/environmentHttp.ts`).
Four routes are used: the unauthenticated descriptor, the shell read-model
(projects + threads without bodies), one thread with a turn window, and
`dispatch` with a `ClientOrchestrationCommand`. The WebSocket RPC
(`terminal.*` and friends) is deliberately not wrapped.
"""
from __future__ import annotations
import uuid
from datetime import UTC, datetime
from typing import Any
import httpx
DESCRIPTOR = "/.well-known/t3/environment"
SHELL = "/api/orchestration/shell"
THREADS = "/api/orchestration/threads"
DISPATCH = "/api/orchestration/dispatch"
WS_TICKET = "/api/auth/websocket-ticket"
WS_PATH = "/ws"
SUBSCRIBE_SHELL = "orchestration.subscribeShell"
RUNTIME_MODE = "full-access"
INTERACTION_MODE = "default"
PROBE_TIMEOUT = 5.0
class T3Error(Exception):
def __init__(self, status: int, body: Any) -> None:
self.status = status
self.body = body
detail = (
f"{body.get('code')}: {body.get('reason') or body.get('message')}"
if isinstance(body, dict)
else str(body)[:300]
)
super().__init__(f"T3 responded {status} - {detail}")
def now_iso() -> str:
return datetime.now(UTC).isoformat(timespec="milliseconds").replace("+00:00", "Z")
def new_id() -> str:
return str(uuid.uuid4())
def thread_create(
thread_id: str, project_id: str, title: str, selection: dict[str, Any]
) -> dict[str, Any]:
return {
"type": "thread.create",
"commandId": new_id(),
"threadId": thread_id,
"projectId": project_id,
"title": title,
"modelSelection": selection,
"runtimeMode": RUNTIME_MODE,
"interactionMode": INTERACTION_MODE,
"branch": None,
"worktreePath": None,
"createdAt": now_iso(),
}
def turn_start(
thread_id: str,
text: str,
selection: dict[str, Any] | None = None,
title_seed: str | None = None,
) -> dict[str, Any]:
command: dict[str, Any] = {
"type": "thread.turn.start",
"commandId": new_id(),
"threadId": thread_id,
"message": {
"messageId": new_id(),
"role": "user",
"text": text,
"attachments": [],
},
"runtimeMode": RUNTIME_MODE,
"interactionMode": INTERACTION_MODE,
"createdAt": now_iso(),
}
if selection is not None:
command["modelSelection"] = selection
if title_seed:
command["titleSeed"] = title_seed
return command
def turn_interrupt(thread_id: str, turn_id: str | None = None) -> dict[str, Any]:
command: dict[str, Any] = {
"type": "thread.turn.interrupt",
"commandId": new_id(),
"threadId": thread_id,
"createdAt": now_iso(),
}
if turn_id:
command["turnId"] = turn_id
return command
def user_input_respond(
thread_id: str, request_id: str, answers: dict[str, Any]
) -> dict[str, Any]:
return {
"type": "thread.user-input.respond",
"commandId": new_id(),
"threadId": thread_id,
"requestId": request_id,
"answers": answers,
"createdAt": now_iso(),
}
class T3Client:
def __init__(
self,
url: str,
token: str,
*,
transport: httpx.AsyncBaseTransport | None = None,
timeout: float = 30.0,
) -> None:
self.url = url.rstrip("/")
self._http = httpx.AsyncClient(
base_url=self.url,
headers={"Authorization": f"Bearer {token}"},
timeout=timeout,
transport=transport,
)
async def aclose(self) -> None:
await self._http.aclose()
async def descriptor(self) -> dict[str, Any]:
response = await self._http.get(
DESCRIPTOR, headers={"Authorization": ""}, timeout=PROBE_TIMEOUT
)
return self._json(response)
async def shell(self) -> dict[str, Any]:
return self._json(await self._http.get(SHELL))
async def thread(
self, thread_id: str, turn_limit: int | None = None
) -> dict[str, Any] | None:
params = {"turnLimit": turn_limit} if turn_limit else None
response = await self._http.get(f"{THREADS}/{thread_id}", params=params)
if response.status_code == httpx.codes.NOT_FOUND:
return None
return self._json(response)
async def dispatch(self, command: dict[str, Any]) -> dict[str, Any]:
return self._json(await self._http.post(DISPATCH, json=command))
async def ws_url(self) -> str:
ticket = self._json(await self._http.post(WS_TICKET))["ticket"]
scheme = "wss" if self.url.startswith("https") else "ws"
return f"{scheme}://{self.url.split('://', 1)[1]}{WS_PATH}?wsTicket={ticket}"
@staticmethod
def _json(response: httpx.Response) -> dict[str, Any]:
try:
body: Any = response.json()
except ValueError:
body = response.text
if response.is_error or not isinstance(body, dict):
raise T3Error(response.status_code, body)
return body