Landrace 2.0: one HTTP front door serving UI and MCP on the board port
Discovered from README cross-links · today
Replaced stdio MCP with Streamable HTTP MCP at /mcp beside the UI.
What they did
Working from an approved design and a nine-task implementation plan, the author took Task 2 of 9: collapse Landrace's two entry points into one HTTP front door. The operator MCP now speaks Streamable HTTP at `/mcp` on the same port as the board UI at `/`, and stdio `landrace mcp` is retired for editors — the plan's single breaking change, released as 2.0.0. They started with failing tests across seven files: `tests/front/hosts.test.ts` (loopback detection, allowed hosts, the `serve.unauthenticated` problem), `tests/front/mcp.test.ts` (a real SDK `StreamableHTTPClientTransport` client listing the 15 `landrace_*` tools, plus `?workflow=` scoping), `tests/front/server.test.ts` (GET `/`, `/board.json`, POST `/mcp`, and a `Host: evil.example` → 421), `tests/cli/mcp.test.ts` (stdio removal message and exit 1), `tests/kit/executor.test.ts`, `tests/integrations/claude.test.ts` and `tests/cli/validate.test.ts`. Implementation added `src/front/server.ts`, `src/front/mcp.ts` and `src/front/hosts.ts`, with `createMcpEndpoint` holding one `McpServer` per session and `serveFrontDoor` routing `/mcp` to the endpoint and everything else to the existing `boardListener`. `start.ts` builds tools from the running runtime via `toolWorkflowsOf`, wires wake through `schedule.wake`, and refuses to start when `serveProblems` reports a non-loopback bind without `people.oidc`. The executor refuses HTTP MCP entries pointing at the front door, the Claude integration denies `WebFetch` for each front-door host at every tier, and validate rejects sandbox hosts that include one. Docs, README, CHANGELOG and the agsync MCP config were updated to the HTTP form. They verified with `pnpm test`, `typecheck`, `lint` and `build`, then a live check: `node dist/cli.js start` in a scratch repo, `claude mcp add --transport http lr-test http://127.0.0.1:4545/mcp`, and `claude mcp list` showing connected. Committed as `feat!: serve the operator MCP over HTTP on the board's port`.
How it went
All named tests pass, along with the full test, typecheck, lint and build suite, and a live `claude mcp list` check shows the server connected over HTTP. The change is scoped tightly to Task 2 of 9: the front door exists and is safe by default, but remote multi-person mode is not yet usable — OIDC sign-in and MCP OAuth land in Task 9, so until then any non-loopback bind is refused outright. The stdio path survives only behind a hidden `--pairing` flag for local pairing.
Worth knowing
This is one task in a nine-task sequence, each blocked by the one before, so the branch is not a shippable remote product on its own. `package.json` is deliberately not bumped — the release PR sets 2.0.0. The wake file is kept only for `--pairing` stdio servers. Anyone with an editor config still pointing at stdio `landrace mcp` gets a migration message and exit 1 rather than a working server, which is intended but will surprise existing users. The agsync config change requires running `agsync sync` separately.
Wiring
Order matters: hosts helpers first (`isLoopback`, `allowedHosts`, `frontDoorOrigins`, `serveProblems`), then the MCP endpoint, then the front-door server that composes them. `src/config/schema.ts` gains `serve: { host = "127.0.0.1", port = 4545, publicUrl? }`. `src/front/mcp.ts` exports `createMcpEndpoint({ toolsFor, version, log })` returning an `McpEndpoint` whose `handle(req, res, url)` keeps one `McpServer` per session over `StreamableHTTPServerTransport` and scopes tools by `?workflow=`. `src/front/server.ts` exports `serveFrontDoor(opts)` taking `UiOptions` plus `host`, `publicUrl` and `mcp`, routing `/mcp` to `mcp.handle` and the rest to `boardListener`. `src/ui/server.ts` takes the host from options instead of the `HOST` const, and its Host and `foreignWrite` Origin checks use `allowedHosts`. `src/cli/start.ts` builds `toolsFor = (wf) => createTools(toolWorkflowsOf(runtime).filter(...), runtime.ctx, { lock, sandbox, activity, wake: () => schedule.wake(), scope: wf })`, refuses `serveProblems` at startup, and adds `--pairing` to `childServerCommand`. `src/cli/mcp.ts` extracts `toolWorkflowsOf` and prints `STDIO_REMOVED` unless `--child`/`--pairing` is passed; `src/cli/index.ts` hides the `--pairing` flag. Guards: `resolveStepServers` in `src/kit/executor.ts` refuses HTTP MCP entries whose origin is a front-door origin; `integrations/claude/index.ts` denies `WebFetch(domain:<host>)` for every front-door host at every tier; `serveProblems` in `src/cli/validate.ts` also rejects sandbox hosts containing a front-door host. Config file `.agsync/mcp/landrace.yaml` points at `http://127.0.0.1:4545/mcp` in agsync's HTTP form and needs `agsync sync`.Try it yourself
Read the design and Task 2 in this issue, then implement it in my repo. Start by writing the failing tests it names: `tests/front/hosts.test.ts` for `isLoopback`, `allowedHosts` and `serveProblems` (rule `serve.unauthenticated`); `tests/front/mcp.test.ts` driving a real loopback server with an SDK `StreamableHTTPClientTransport` client, asserting the tool list and `?workflow=<id>` scoping; `tests/front/server.test.ts` for GET `/`, GET `/board.json`, POST `/mcp` reaching the endpoint, and `Host: evil.example` returning 421; `tests/cli/mcp.test.ts` for `runMcp` without `--child`/`--pairing` printing `STDIO_REMOVED` to stderr and exiting 1; plus the executor, Claude-integration and validate cases. Run them and confirm they fail. Then create `src/front/hosts.ts`, `src/front/mcp.ts` and `src/front/server.ts` with the interfaces the task specifies, extend `serve` in `src/config/schema.ts` to `{ host, port, publicUrl? }` defaulting to `127.0.0.1:4545`, and rewire `src/cli/start.ts` so `--ui-port` overrides `serve.port`, tools come from the running runtime via `toolWorkflowsOf`, and wake goes through `schedule.wake`. Make startup refuse `serveProblems`. Update the executor, Claude integration and validate guards, then the docs, README, CHANGELOG (Unreleased, marked Breaking) and `.agsync/mcp/landrace.yaml`. Finish with `pnpm test && pnpm typecheck && pnpm lint && pnpm build`, then a live check with `claude mcp add --transport http` and `claude mcp list`.