Landrace 2.0: 보드 포트에서 UI와 MCP를 함께 제공하는 단일 HTTP 프론트 도어
README 링크에서 발견 · today
UI 옆 /mcp에서 stdio MCP를 Streamable HTTP MCP로 교체했습니다.
What they did
승인된 설계와 9개 작업 구현 계획에 따라, 작성자는 9개 중 Task 2를 진행했다. Landrace의 두 진입점을 하나의 HTTP 정문으로 통합하는 작업이었다. 이제 운영자 MCP는 보드 UI가 `/`에 있는 것과 같은 포트의 `/mcp`에서 Streamable HTTP로 통신하며, 편집자용 stdio `landrace mcp`는 폐기되었다. 이는 계획상의 유일한 호환성 깨는 변경이며, 2.0.0으로 릴리스되었다. 작성자는 7개 파일에 걸친 실패하는 테스트부터 시작했다. `tests/front/hosts.test.ts`(루프백 감지, 허용 호스트, `serve.unauthenticated` 문제), `tests/front/mcp.test.ts`(실제 SDK `StreamableHTTPClientTransport` 클라이언트가 15개의 `landrace_*` 도구를 나열하는지, 그리고 `?workflow=` 스코핑), `tests/front/server.test.ts`(GET `/`, `/board.json`, POST `/mcp`, 그리고 `Host: evil.example` → 421), `tests/cli/mcp.test.ts`(stdio 제거 메시지와 종료 코드 1), `tests/kit/executor.test.ts`, `tests/integrations/claude.test.ts`, `tests/cli/validate.test.ts`가 그것이다. 구현에서는 `src/front/server.ts`, `src/front/mcp.ts`, `src/front/hosts.ts`를 추가했으며, `createMcpEndpoint`는 세션당 하나의 `McpServer`를 보유하고 `serveFrontDoor`는 `/mcp`를 엔드포인트로 라우팅하고 그 외 모든 것은 기존 `boardListener`로 라우팅한다. `start.ts`는 실행 중인 런타임에서 `toolWorkflowsOf`를 통해 도구를 구성하고, `schedule.wake`를 통해 wake를 연결하며, `serveProblems`가 `people.oidc` 없이 비루프백 바인드를 보고하면 시작을 거부한다. executor는 정문을 가리키는 HTTP MCP 항목을 거부하고, Claude 통합은 모든 티어에서 각 정문 호스트에 대해 `WebFetch`를 거부하며, validate는 정문 호스트를 포함하는 샌드박스 호스트를 거부한다. 문서, README, CHANGELOG, agsync MCP 설정은 HTTP 형식으로 갱신되었다. 작성자는 `pnpm test`, `typecheck`, `lint`, `build`로 검증한 뒤 실제 확인을 진행했다. 스크래치 저장소에서 `node dist/cli.js start`를 실행하고, `claude mcp add --transport http lr-test http://127.0.0.1:4545/mcp`를 실행했으며, `claude mcp list`에서 연결됨이 표시되었다. 커밋 메시지는 `feat!: serve the operator MCP over HTTP on the board's port`이다.
How it went
모든 명명된 테스트가 통과하고, 전체 테스트, 타입체크, 린트, 빌드 스위트도 통과하며, 라이브 `claude mcp list` 확인 결과 서버가 HTTP를 통해 연결된 것으로 나타납니다. 이 변경은 9개 중 Task 2에 엄격히 한정됩니다. 프런트 도어는 존재하며 기본적으로 안전하지만, 원격 다중 사용자 모드는 아직 사용할 수 없습니다. OIDC 로그인과 MCP OAuth는 Task 9에서 도입되므로, 그때까지는 루프백이 아닌 바인드는 전면 거부됩니다. stdio 경로는 로컬 페어링을 위한 숨겨진 `--pairing` 플래그 뒤에서만 유지됩니다.
Worth knowing
이는 9개 작업 시퀀스 중 하나이며, 각 작업은 이전 작업에 의해 블로킹되므로 이 브랜치 자체만으로는 배포 가능한 원격 제품이 아닙니다. `package.json`은 의도적으로 버전을 올리지 않았습니다 — 릴리스 PR에서 2.0.0으로 설정합니다. wake 파일은 `--pairing` stdio 서버용으로만 유지됩니다. 여전히 stdio `landrace mcp`를 가리키는 에디터 설정이 있는 사용자는 작동하는 서버 대신 마이그레이션 메시지와 함께 exit 1을 받게 되는데, 이는 의도된 동작이지만 기존 사용자에게는 놀라울 수 있습니다. agsync 설정 변경은 `agsync sync`를 별도로 실행해야 합니다.
Wiring
호스트 헬퍼(`isLoopback`, `allowedHosts`, `frontDoorOrigins`, `serveProblems`)가 먼저 오고, 그다음 MCP 엔드포인트, 그다음 이들을 조합하는 front-door 서버가 옵니다. `src/config/schema.ts`에 `serve: { host = "127.0.0.1", port = 4545, publicUrl? }`가 추가됩니다. `src/front/mcp.ts`는 `createMcpEndpoint({ toolsFor, version, log })`를 export하며, 반환되는 `McpEndpoint`의 `handle(req, res, url)`은 `StreamableHTTPServerTransport` 위에서 세션당 하나의 `McpServer`를 유지하고 `?workflow=`로 도구 범위를 지정합니다. `src/front/server.ts`는 `UiOptions`에 `host`, `publicUrl`, `mcp`를 더한 것을 받는 `serveFrontDoor(opts)`를 export하며, `/mcp`는 `mcp.handle`로, 나머지는 `boardListener`로 라우팅합니다. `src/ui/server.ts`는 `HOST` 상수 대신 옵션에서 host를 받고, Host 및 `foreignWrite` Origin 검사에 `allowedHosts`를 사용합니다. `src/cli/start.ts`는 `toolsFor = (wf) => createTools(toolWorkflowsOf(runtime).filter(...), runtime.ctx, { lock, sandbox, activity, wake: () => schedule.wake(), scope: wf })`를 구성하고, 시작 시 `serveProblems`를 거부하며, `childServerCommand`에 `--pairing`을 추가합니다. `src/cli/mcp.ts`는 `toolWorkflowsOf`를 추출하고 `--child`/`--pairing`이 전달되지 않으면 `STDIO_REMOVED`를 출력합니다. `src/cli/index.ts`는 `--pairing` 플래그를 숨깁니다. 가드: `src/kit/executor.ts`의 `resolveStepServers`는 origin이 front-door origin인 HTTP MCP 항목을 거부합니다. `integrations/claude/index.ts`는 모든 tier에서 모든 front-door host에 대해 `WebFetch(domain:<host>)`를 거부합니다. `src/cli/validate.ts`의 `serveProblems`는 front-door host를 포함하는 sandbox host도 거부합니다. 설정 파일 `.agsync/mcp/landrace.yaml`은 agsync의 HTTP 형식으로 `http://127.0.0.1:4545/mcp`를 가리키며 `agsync sync`가 필요합니다.Try it yourself
I'll start by exploring the repository structure to understand the codebase before implementing the design. <||DSML|| calls> <||DSML|| invoke name="bash"> <||DSML|| parameter name="command" string="true">find . -maxdepth 2 -type d -not -path '*/node_modules*' -not -path '*/.git*' | head -50</||DSML|| parameter> </||DSML|| invoke> </||DSML|| calls>