docs(mcp): add bridge implementation plans

This commit is contained in:
Guillaume ARM 2026-06-10 20:07:54 +02:00
parent b7187e0155
commit 217b0e4e83
2 changed files with 501 additions and 0 deletions

259
.plans/mcp-bridge-plan.md Normal file
View File

@ -0,0 +1,259 @@
# MCP Bridge Plan
## Goal
Build a small TypeScript bridge that lets MCP clients call tools backed by live ComputerCraft computers.
The first MCP tool is `probe-computers`. It broadcasts a probe request to every linked computer and returns one line per response:
```text
pong from <computerId> (Label: <computerLabel>)
```
## Decisions
- Host-side implementation lives in `tools/mcp-bridge/`.
- MCP clients connect on `MCP_PORT`, default `3000`.
- ComputerCraft computers connect on `CC_LINK_PORT`, default `3001`.
- The MCP port and ComputerCraft link port are separate listeners.
- No auth is required for the first bridge implementation.
- `probe-computers` takes no arguments and probes every currently linked computer.
- The bridge owns real HTTP/MCP transport. ComputerCraft computers connect outbound over WebSocket.
## Background
CC:Tweaked computers cannot bind a real TCP/HTTP port with the standard APIs. They can make outbound HTTP/WebSocket connections. Therefore the host bridge exposes real MCP over HTTP and maintains outbound WebSocket links from computers.
The current MCP TypeScript SDK situation is split:
- `@modelcontextprotocol/sdk` v1 is stable and commonly used by existing examples.
- Newer docs describe split packages such as `@modelcontextprotocol/server` and `@modelcontextprotocol/node`, but the upstream README says that branch is v2/pre-alpha as of this research.
Prefer stable v1 for the first implementation unless opencode compatibility requires the newer packages. Re-check package docs immediately before implementation.
## Architecture
```text
[ opencode / MCP client ] --HTTP MCP--> [ tools/mcp-bridge ] --WebSocket--> [ CC computer(s) ]
:3000 :3001
```
The bridge has two independent surfaces:
- MCP listener: streamable HTTP MCP endpoint for agents.
- Link listener: WebSocket endpoint for ComputerCraft computers.
The bridge keeps an in-memory registry of connected computers:
```ts
type ComputerConnection = {
computerId: number;
label: string | null;
ws: WebSocket;
connectedAt: number;
lastSeenAt: number;
};
```
## Link Protocol
All ComputerCraft link messages are JSON text frames.
### Computer Hello
Sent by the Lua program immediately after opening the WebSocket:
```json
{
"type": "hello",
"computerId": 12,
"computerLabel": "base-turtle"
}
```
Bridge behavior:
- Validate `computerId` is a number.
- Treat missing/empty `computerLabel` as `null`.
- Register or replace the existing connection for that `computerId`.
- Reply with `hello-ok`.
### Hello OK
```json
{
"type": "hello-ok"
}
```
### Probe Request
Sent by the bridge when MCP tool `probe-computers` is called:
```json
{
"type": "request",
"id": "req-123",
"method": "ping"
}
```
### Probe Response
Sent by each Lua computer:
```json
{
"type": "response",
"id": "req-123",
"ok": true,
"result": "pong from 12 (Label: base-turtle)"
}
```
Error shape, reserved for later commands:
```json
{
"type": "response",
"id": "req-123",
"ok": false,
"error": "unknown method"
}
```
## MCP Tool Behavior
Tool name: `probe-computers`.
Input schema: no arguments.
Behavior:
1. Snapshot the current connected computer registry.
2. If none are connected, return `No computers connected.`.
3. Generate one request id per computer, or one shared batch id with per-computer tracking.
4. Send a `ping` request to every linked computer.
5. Wait for responses until either all answer or the probe timeout expires.
6. Return text with one line per successful response.
7. Include timeout lines for computers that did not answer.
Suggested timeout: `CC_PROBE_TIMEOUT_MS`, default `2000`.
Example output:
```text
pong from 12 (Label: base-turtle)
pong from 13 (Label: miner-1)
timeout from 14 (Label: farm-turtle)
```
## Project Layout
```text
tools/mcp-bridge/
package.json
package-lock.json
tsconfig.json
src/
index.ts
link-server.ts
mcp-server.ts
protocol.ts
test/
protocol.test.ts
probe-computers.test.ts
```
Keep the first implementation minimal. Collapse files if the code is clearer in fewer modules.
## Implementation Sections
### 1. Tooling Setup
- Create `tools/mcp-bridge/package.json`.
- Use TypeScript with strict checks.
- Use Node's built-in test runner if practical, or Vitest if SDK/test ergonomics require it.
- Add scripts:
- `npm run build`
- `npm test`
- `npm run dev`
- `npm start`
### 2. Link Server
- Start WebSocket server on `CC_LINK_PORT`.
- Accept JSON text frames only.
- Register computers after valid `hello`.
- Remove computers on close/error.
- Track pending request resolvers by request id.
- Ensure malformed frames do not crash the process.
### 3. MCP Server
- Start MCP HTTP transport on `MCP_PORT`.
- Register tool `probe-computers`.
- Implement tool response as MCP text content.
- Keep ordinary `/health` endpoint available on the MCP listener if the selected transport/framework allows it without fighting the SDK.
`/health` response should be normal HTTP, not MCP:
```json
{
"ok": true,
"computers": 2
}
```
### 4. Configuration
Environment variables:
- `MCP_HOST`, default `127.0.0.1`.
- `MCP_PORT`, default `3000`.
- `CC_LINK_HOST`, default `0.0.0.0`.
- `CC_LINK_PORT`, default `3001`.
- `CC_PROBE_TIMEOUT_MS`, default `2000`.
### 5. Verification
Unit tests:
- Protocol validation accepts valid `hello` and rejects invalid frames.
- `probe-computers` returns `No computers connected.` when registry is empty.
- `probe-computers` aggregates multiple successful responses.
- `probe-computers` reports timeout for a connected computer that does not answer.
Integration tests:
- Start bridge on ephemeral ports.
- Connect fake WebSocket computer clients.
- Call the probe path through the MCP handler, or through a thin internal function if the SDK transport is hard to drive in tests.
Manual/agent checks:
- `npm run build` from `tools/mcp-bridge`.
- `npm test` from `tools/mcp-bridge`.
- Use opencode headless mode only for critical MCP compatibility checks after the bridge can run locally.
## Open Questions To Re-check During Implementation
- Exact HTTP MCP transport API for the chosen stable SDK version.
- Whether opencode expects streamable HTTP only, SSE compatibility, or can use stdio. This bridge should prioritize HTTP because the requirement is an MCP port.
- Whether tool names with hyphens are accepted cleanly by the MCP clients we care about. If not, challenge `probe-computers` and use `probe_computers`.
## Out Of Scope For First Pass
- Authentication and authorization.
- Persistent computer registry.
- Commands other than `ping`/`probe-computers`.
- Multi-bridge clustering.
- TLS termination. Put the bridge behind a reverse proxy if needed later.
## Deliverables
- `tools/mcp-bridge/` TypeScript project.
- MCP HTTP listener on port `3000` by default.
- Computer WebSocket link listener on port `3001` by default.
- MCP tool `probe-computers` returning live ComputerCraft pong lines.
- Tests for protocol parsing and probe aggregation.

View File

@ -0,0 +1,242 @@
# MCP Computer Lua Plan
## Goal
Add a TrapOS sandbox program that links a ComputerCraft computer to the host-side MCP bridge over WebSocket.
The first supported command is `ping`, used by the bridge's MCP tool `probe-computers`. The Lua program answers:
```text
pong from <computerId> (Label: <computerLabel>)
```
## Decisions
- Lua program path: `programs/mcp-computer.lua`.
- Package: `trapos-sandbox`.
- The program connects outbound to the bridge's ComputerCraft link port.
- The bridge link default is `ws://<host>:3001`.
- The host-side MCP tool is named `probe-computers` and broadcasts to every connected computer.
- No link auth for the first version.
- Keep the first Lua implementation as a program, not an autostart server, until the bridge loop is proven.
## Background
CC:Tweaked cannot listen on real HTTP/TCP ports with standard APIs. It can connect outward using `http.websocket`. This program is therefore the in-game half of the bridge:
```text
[ CC computer ] --outbound WebSocket--> [ tools/mcp-bridge link port :3001 ]
```
The Lua program is effectively an agent. It identifies itself to the bridge, waits for JSON request frames, executes supported methods, and sends JSON response frames.
## User Interface
Program usage:
```text
mcp-computer <ws-url>
mcp-computer -url <ws-url>
mcp-computer --version
mcp-computer --help
```
Examples:
```text
mcp-computer ws://192.168.1.20:3001
mcp-computer -url ws://mcp-bridge.local:3001
```
The program should print clear status lines:
```text
mcp-computer v0.1.1 connecting to ws://192.168.1.20:3001
linked as 12 (Label: base-turtle)
waiting for requests... Press Ctrl+T to stop.
```
If `http` or `http.websocket` is unavailable, print a clear error explaining that CC:Tweaked HTTP/WebSocket must be enabled.
## Link Protocol
All frames are JSON strings compatible with `textutils.serializeJSON` and `textutils.unserializeJSON`.
### Computer Hello
Sent immediately after opening the websocket:
```json
{
"type": "hello",
"computerId": 12,
"computerLabel": "base-turtle"
}
```
Lua fields:
- `computerId`: `os.getComputerID()`.
- `computerLabel`: `os.getComputerLabel()`, or `nil` encoded as JSON null/omitted depending on `textutils.serializeJSON` behavior.
### Hello OK
Expected from bridge:
```json
{
"type": "hello-ok"
}
```
The first version may continue even if `hello-ok` is not received immediately, but implementation should prefer waiting briefly so connection problems are obvious.
### Probe Request
Received from bridge:
```json
{
"type": "request",
"id": "req-123",
"method": "ping"
}
```
### Probe Response
Sent back by Lua:
```json
{
"type": "response",
"id": "req-123",
"ok": true,
"result": "pong from 12 (Label: base-turtle)"
}
```
Unknown method response:
```json
{
"type": "response",
"id": "req-123",
"ok": false,
"error": "unknown method"
}
```
## Implementation Sections
### 1. Program Skeleton
- Add `programs/mcp-computer.lua`.
- Support `--help`, `-help`, `help`, `--version`, `-version`, and `version`.
- Version output uses `require('/apis/libversion')().forSelf()`.
- Parse URL from first positional argument or `-url <ws-url>`.
- Reject missing URL with usage.
### 2. Connection Handling
- Check `http` exists.
- Check `http.websocket` exists.
- Call `http.websocket(url)`.
- On failure, print the returned error and exit non-zero if possible.
- Send `hello` frame.
- Optionally wait up to a short timeout for `hello-ok`.
### 3. Request Loop
- Receive websocket messages in a loop.
- Decode JSON with `textutils.unserializeJSON` inside `pcall`.
- Ignore or respond with error for malformed messages.
- Handle `terminate` cleanly if using `parallel.waitForAny` around websocket receive and terminate watcher.
- For `request` with `method = 'ping'`, send success response.
- For unknown request methods, send error response.
- Close websocket on exit.
### 4. Pong Formatting
Implement a local function:
```lua
local function formatPong()
local label = os.getComputerLabel();
if not label or label == '' then
label = 'nil';
end
return 'pong from ' .. tostring(os.getComputerID()) .. ' (Label: ' .. tostring(label) .. ')';
end
```
Keep the output exactly aligned with the bridge plan.
### 5. Packaging
- Add `programs/mcp-computer.lua` to `packages/trapos-sandbox/ccpm.json`.
- Bump `trapos-sandbox` version.
- Mirror the version bump in `packages/index.json`.
- Do not add it to `autostart` in the first pass.
- Do not add `trapos-sandbox` as a `trapos` dependency unless explicitly desired later.
## Testing Strategy
### Unit-Style Lua Tests
Prefer moving deterministic protocol helpers into `apis/libmcpcomputer.lua` only if testing the program directly becomes awkward. The minimal testable API could expose:
- `formatPong(osLike)`.
- `handleRequest(request, osLike)`.
- `encode/decode` helpers if needed.
If a helper API is added, include it in `trapos-sandbox` and add tests under `tests/` using `/apis/libtest.lua`.
If keeping everything in one program, at least verify syntax and help/version behavior with CraftOS-PC probes.
### CraftOS-PC Probes
Use automated probes only; do not run `just repl`.
Examples after implementation:
```bash
just trapos-exec 'shell.run("/programs/mcp-computer.lua", "--help")'
just trapos-exec 'shell.run("/programs/mcp-computer.lua", "--version")'
```
For an end-to-end probe, run the TS bridge on localhost and use CraftOS-PC with HTTP local access enabled if the harness permits it. If localhost access is blocked by CC:Tweaked config, document the limitation and rely on an in-game/manual validation for the WebSocket connection.
### Required Repo Checks
After editing Lua or package descriptors:
- `just check`
- `just test` if tests are added or changed.
## Manual Validation
1. Start the TypeScript bridge.
2. On one ComputerCraft computer, run `mcp-computer ws://<bridge-host>:3001`.
3. Confirm the bridge logs the computer registration.
4. Connect an MCP client to the bridge MCP port.
5. Call `probe-computers`.
6. Confirm response includes the computer id and label.
7. Connect a second computer and confirm the tool returns two lines.
## Out Of Scope For First Pass
- Autostart server behavior.
- Reconnect/backoff loop.
- Authentication.
- Commands that mutate the world.
- Turtle/peripheral control.
- MCP protocol implementation inside Lua. Lua only speaks the simple bridge link protocol.
## Deliverables
- `programs/mcp-computer.lua` in `trapos-sandbox`.
- Package version bump for `trapos-sandbox`.
- Optional helper API and tests if implementation benefits from unit coverage.
- Working WebSocket link to `tools/mcp-bridge`.
- Correct response to bridge `ping` requests.