아키텍처 분석 · gosuda/flats v0.4.0

Flats ↔ 에이전트 연동 구조

에이전트는 Flats에 무엇으로 붙고, 어디까지 할 수 있으며, 어디서 사람이 반드시 개입하는가.

기준 커밋 192e758 (v0.4.0 = main) 근거: internal/mcpx, internal/cli/mcpbridge.go, internal/core, plugins/flats, docs/agent

한 줄 요약 다섯 개

  1. 통로는 MCP 하나예요. 호스트가 /mcp에서 MCP 서버를 직접 띄우고, 도구 27개가 모두 같은 core.Service를 호출해요.
  2. 플러그인은 얇은 껍데기예요. 스킬 하나와 flats mcp를 실행하는 .mcp.json뿐이고, 호스트 주소는 담지 않아요.
  3. flats mcp는 stdio↔HTTP 중계기예요. 표준입력 JSON-RPC를 호스트의 Streamable HTTP로 그대로 넘겨서, 도구·지침이 호스트와 항상 일치해요.
  4. 지식은 호스트가 들고 있어요. 짧은 상시 지침 + 필요할 때 읽는 guide 토픽 + 오류별 refusal.* 안내라서 스킬이 낡아도 동작이 안 바뀌어요.
  5. 공개·활성화·삭제는 사람만 승인해요. 에이전트는 Draft 저장과 요청까지만 하고, 승인 도구는 MCP에 아예 없어요.

1. 전체 그림

에이전트 쪽에는 연결 정보만 있고, 기능과 규칙은 전부 Flats 호스트에 있어요. 아래 그림에서 왼쪽 파란 영역이 에이전트 머신, 오른쪽 초록 영역이 Flats 호스트, 보라색이 운영자(사람)예요.

Flats와 에이전트 연동 전체 구조도 에이전트 클라이언트가 flats-deploy 스킬과 .mcp.json을 읽고 flats mcp 프로세스를 stdio로 실행한다. flats mcp는 HTTPS로 호스트의 /mcp에 중계한다. 호스트의 mcpx 핸들러는 core.Service를 ViaMCP로 호출한다. 운영자는 콘솔에서 승인하며 이는 ViaConsole로 core.Service를 호출한다. core.Service는 store, 런타임, 네트워크 공급자를 관리한다. 에이전트 머신 Claude Code · Codex · Cursor · 기타 MCP 클라이언트 에이전트 (LLM 클라이언트) 도구 호출 · 응답 해석 approval_url을 사용자에게 전달 스킬 flats-deploy 연결 안내만 .mcp.json flats mcp 호스트 주소 없음 flats mcp (stdio 브리지) stdin JSON-RPC 한 줄 → POST /mcp 응답 → stdout 주소: --url › FLATS_URL › connect › 기본값 connection.json <config dir>/flats-client/ 주소 읽기 HTTPS (tailnet) Streamable HTTP Mcp-Session-Id Flats 호스트 (flats serve) 하나의 HTTP mux /mcp mcpx 핸들러 /llms.txt MCP 없는 에이전트용 / · /api/ 콘솔 · CLI API core.Service 모든 입구가 같은 서비스를 호출 — 호출자를 via로 구분 ViaMCP ViaCLI ViaConsole store flats.db · 버전 승인 · secret.key 런타임 정적 서빙 JS/WASM 워커 네트워크 공급자 local · tailscale funnel · portal 각 flat = 별도 origin <slug>.localhost · <slug>.<tailnet>.ts.net · portal 도메인 운영자 (사람) 브라우저로 콘솔을 열어 approval_url의 요청을 승인/거절 비밀값 · 네트워크 허용 · 공급자 권한도 여기서만 사용자 대화창 에이전트가 approval_url을 그대로 전달 링크 클릭
그림 1. 에이전트는 MCP로 “요청”만 만들고, 실제 공개/활성화는 운영자가 콘솔(ViaConsole)에서 실행해요.
에이전트 영역 Flats 호스트 운영자 경로

핵심은 “입구는 여러 개, 서비스는 하나”예요. MCP, CLI, 콘솔이 모두 core.Service의 같은 메서드를 부르고, 호출 출처(via)만 다르게 넘겨요. 권한 검사는 각 입구가 아니라 서비스 안에서 via를 보고 해요. 그래서 MCP에 도구를 하나 더 붙여도 권한 규칙을 우회할 수 없어요.

2. 플러그인: 무엇이 설치되나

레포의 plugins/flats 하나가 세 클라이언트용 매니페스트를 모두 갖고 있어요. 내용물은 놀랄 만큼 작아요.

plugins/flats/
├── .claude-plugin/plugin.json   # Claude Code
├── .codex-plugin/plugin.json    # Codex (skills, mcpServers, UI 문구)
├── .cursor-plugin/plugin.json   # Cursor
├── .mcp.json                    # {"flats": {"command": "flats", "args": ["mcp"]}}
└── skills/flats-deploy/SKILL.md # 연결 안내 + 승인 규칙 요약

스킬이 하는 일

“도구가 있으면 guide topic.index부터 읽어라, 없으면 flats connect로 호스트를 확인해라”가 전부예요. 실제 배포 방법은 적혀 있지 않아요.

.mcp.json이 하는 일

flats mcp를 stdio MCP 서버로 실행해요. 주소를 넣지 않아서, 플러그인 하나로 어떤 호스트에도 붙어요.

왜 이렇게 얇은가

도구 목록·설명·규칙이 호스트 버전과 함께 바뀌어요. 플러그인을 업데이트하지 않아도 호스트를 올리면 에이전트가 새 기능을 봐요.

플러그인 없이도 연결할 수 있어요. MCP 클라이언트에 http://127.0.0.1:7878/mcp(또는 콘솔의 tailnet 주소 + /mcp)를 HTTP 서버로 등록하면 되고, flats mcp-config가 클라이언트별 설정을 출력해요. MCP 자체가 없으면 <console>/llms.txt가 같은 안내를 텍스트로 줘요.

3. flats mcp 브리지

Claude Code, Codex, Cursor 플러그인이 공통으로 실행할 수 있는 MCP 형태는 stdio예요. 그래서 Flats는 호스트에 HTTP MCP를 두고, 클라이언트 쪽에는 그 앞에 붙는 stdio↔HTTP 중계기를 둬요. 중계기는 메시지를 해석하지 않고 그대로 넘겨요.

flats mcp 브리지 시퀀스 에이전트가 initialize를 stdin으로 보내면 브리지가 POST /mcp로 전달하고, 호스트가 Mcp-Session-Id와 프로토콜 버전을 돌려준다. 이후 메시지는 initialize 응답을 기다린 뒤 같은 세션 헤더로 전달된다. tools/call 응답은 JSON 또는 SSE로 돌아와 stdout으로 쓰인다. stdin EOF가 오면 브리지는 진행 중 호출을 정리하고 호스트 세션을 끝낸다. 에이전트 flats mcp 호스트 /mcp stdin: initialize POST /mcp Accept: json, event-stream ← Mcp-Session-Id · 프로토콜 버전 · 지침 stdout: 결과 + instructions 이후 메시지는 initialize 응답 대기 stdin: tools/call guide POST /mcp + 세션 헤더 ← JSON 또는 SSE stdout: 응답 stdin EOF 진행 중 호출 정리 → 세션 종료
그림 2. 브리지는 입력 순서를 지켜 보내고, 응답은 동시에 받아 stdout으로 써요.

호스트 선택 순서

플러그인은 주소를 모르기 때문에, 브리지가 매번 아래 순서로 정해요. 실행 시 stderr에 flats mcp: relaying to … (출처)를 찍어요.

순위출처특징
1--url 플래그MCP 설정의 args에 직접 넣는 방식. 가장 명시적이에요.
2FLATS_URL 환경변수MCP 클라이언트는 셸 환경을 넘기지 않는 경우가 많아 플러그인에서는 기대하기 어려워요.
3flats connect 저장값<user config dir>/flats-client/connection.json. 저장 전에 그 주소가 Flats 호스트로 응답하는지 확인해요. 문서가 권장하는 방식이에요.
4기본값http://127.0.0.1:7878. 호스트 머신 자체에서 쓰는 경우예요.

구현에서 눈여겨볼 점

4. 호스트의 MCP 서버

호스트는 공식 Go SDK(modelcontextprotocol/go-sdk)로 /mcp에 Streamable HTTP 서버를 둬요. 설정이 꽤 보수적이에요.

Stateless + JSON 응답

서버 쪽 세션 상태에 기대지 않아요. 호스트 재시작이나 프록시가 끼어도 다시 붙기 쉬워요.

요청 크기 = 업로드 한도 기준

본문 한도를 업로드 한도 × 1.5 + 1 MiB로 요청마다 계산해요. base64 팽창분을 감안한 값이에요.

지침이 설정과 동기화

업로드 한도가 바뀌면 MCP 서버 객체를 다시 만들어요. 그래서 지침에 적힌 한도와 실제 한도가 항상 같아요.

loopback 판별

양 끝이 모두 loopback이고 Forwarded, X-Forwarded-For, X-Real-Ip, Tailscale-User-Login 헤더가 없을 때만 “이 머신의 프로세스”로 봐요.

loopback 판별은 save_version_from_dir(호스트 디스크의 디렉터리를 바로 업로드) 같은 도구를 호스트 자신에게만 허용하는 데 쓰여요. tailnet 너머의 에이전트는 파일을 인라인으로 보내야 해요.

5. 지식 전달: 지침 → guide → refusal

Flats는 에이전트 컨텍스트를 아끼려고 지식을 세 층으로 나눠요. 항상 필요한 것만 처음에 주고, 나머지는 에이전트가 필요할 때 꺼내 읽게 해요.

지식 전달 계층 1층 MCP instructions는 initialize 때 항상 전달된다. 2층 guide 도구는 topic.index에서 시작해 16개 토픽을 필요할 때 읽는다. 3층 참조 문서는 MCP 리소스와 읽기 전용 도구로 제공된다. 도구 오류는 category를 반환하고 guide refusal.category가 해결 방법을 설명한다. 1층 · 항상 MCP instructions initialize 때 자동 전달 첫 행동: guide topic.index 승인 규칙 · Public≠접근통제 업로드 한도 · URL로 상태 추측 금지 2층 · 필요할 때 guide topic.index가 작업별로 라우팅 토픽 16개 (static, server.db, docs, approvals, design …) 한 번에 여러 개 요청 가능 3층 · 전체 계약 참조 문서 flats://docs/runtime-api/v1 flats://docs/content-types/v1 get_runtime_reference get_content_types (읽기 전용) 오류 → 해결 안내 모든 도구 오류는 category를 반환 → guide refusal.<category> 로 고치는 법을 읽음 conflict · stale_approval · provider_not_permitted … 23종
그림 3. 상시 지침은 짧게, 세부 내용은 guide 토픽으로. 토픽 원본은 docs/agent/ 한 곳이고 guide·llms.txt·참조 문서가 모두 여기서 나와요.

이 구조 덕분에 스킬 파일이 아무리 오래돼도 에이전트가 읽는 규칙은 지금 돌아가는 호스트 버전의 것이에요. 스킬에 배포 절차를 써 두면 호스트가 바뀔 때 어긋나는데, Flats는 그걸 구조적으로 막았어요.

6. 도구 27개와 권한

v0.4.0 호스트가 노출하는 도구는 27개예요(핵심 24 + guide, get_runtime_reference, get_content_types). MCP 어노테이션으로 읽기 전용/쓰기/파괴적 동작을 표시해요. 중요한 건 “쓰기”라도 대부분 Draft만 바꾼다는 점이에요.

구분도구효과
읽기list_flats get_flat list_versions get_draft get_logs get_approval list_env get_network list_secrets guide get_runtime_reference get_content_types상태 조회. 비밀값은 이름만 보여요.
Draft 쓰기create_flat save_draft save_version save_version_from_dir* save_document open_preview set_env delete_env비공개 Draft·미리보기·일반 환경변수만 바꿔요. 라이브는 그대로예요. (*호스트 loopback 전용)
승인 요청publish deploy rollback set_visibility delete_flatpending_approval + approval_url만 돌려줘요. 승인 전에는 아무것도 안 바뀌어요.
예외update_document실행 중인 문서 flat의 본문을 승인 없이 즉시 수정해요. Public이면 바로 모두에게 보여요.
운영자 전용(MCP에 없음)승인 결정, 비밀값 설정·삭제, 외부 네트워크 허용 목록, 공급자(tailscale·funnel·portal) 권한. 서비스가 via를 보고 MCP 호출을 거절해요.

7. 배포 흐름과 승인 게이트

guide의 topic.index가 제시하는 표준 흐름은 다섯 단계예요. 에이전트가 할 수 있는 구간과 사람이 해야 하는 구간이 명확히 나뉘어요.

배포 흐름과 승인 게이트 1 Draft 저장, 2 미리보기 검토는 에이전트가 한다. 3 publish 요청은 pending_approval과 approval_url을 반환한다. 승인 게이트에서 운영자가 콘솔로 승인하면 Draft revision과 hash가 고정된 상태로 vN이 생성되고 health check 후 활성화된다. 4 에이전트는 get_approval을 폴링하고, 5 get_flat과 실제 fetch로 검증한다. 승인 대상이 바뀌면 stale_approval로 실패한다. 에이전트가 혼자 할 수 있음 사람만 에이전트가 확인 ① Draft save_draft 빌드 전체 업로드 warnings 확인 ② 검토 open_preview version 0 = Draft 브라우저로 확인 ③ 요청 publish pending_approval + approval_url 승인 게이트 콘솔에서 승인 (ViaConsole) revision·hash 고정 → vN 생성 → health → 활성화 ④ 대기 get_approval approved / rejected / failed ⑤ 검증 get_flat live_version + 실제 fetch 고쳐서 다시 저장 같은 게이트를 지나는 요청 deploy {version: N} · rollback (restore_data 포함) set_visibility (양방향) · delete_flat 승인 대상이 승인 전에 바뀌면 stale_approval로 실패 → 다시 요청 “저장 성공 ≠ 라이브” live_version이 바뀌고 endpoint가 ready이며 직접 열어본 뒤에만 “배포됨”이라고 보고해요.
그림 4. 에이전트가 진행할 수 있는 건 ①~③과 ④~⑤예요. 게이트 통과는 사람만 해요.

8. 신뢰 경계와 보안

신뢰 경계 tailnet ACL이 바깥 경계다. 그 안에서 MCP 경로는 요청만 만들 수 있고, 콘솔 경로는 동일 출처 CSRF 검사만 있을 뿐 별도 인증이 없다. 호스트 로컬 프로세스는 콘솔 헤더를 보낼 수 있으므로 신뢰할 수 있는 에이전트만 호스트에서 돌려야 한다. 비밀값은 secret.key로 암호화되지만 데이터 디렉터리를 읽을 수 있으면 복구할 수 있다. tailnet ACL — 실질적인 바깥 경계 원격 에이전트 (MCP) 요청만 만들 수 있음 승인 도구 없음 비밀값은 이름만 loopback 전용 도구 불가 위험도 낮음 콘솔 (브라우저) 승인·비밀값·공급자 권한 same-origin + 콘솔 헤더 검사 = CSRF 방어일 뿐 별도 로그인 없음 ACL로 접근 제한 필요 호스트 로컬 프로세스 콘솔 헤더를 흉내 낼 수 있음 데이터 디렉터리 읽기 가능 시 secret.key로 비밀값 복구 가능 컨테이너 exec 권한도 동일 신뢰하는 것만 실행
그림 5. 승인 게이트는 “정직한 에이전트의 실수”를 막아요. 악의적인 로컬 프로세스까지 막는 장치는 아니에요.
주의: 문서 스스로 밝히듯 콘솔 승인은 인증이 아니라 CSRF 방어예요. 콘솔에 닿을 수 있는 사람은 승인할 수 있어요. 그래서 MCP 지침도 “콘솔 라우트를 부르거나 콘솔 헤더를 보내지 말 것, MCP 클라이언트의 자동 승인은 Flats 승인을 대신하지 않음”을 명시해요. 또 Public은 접근 통제가 아니고, Private도 “소유자 전용”이 아니라 tailnet ACL이 허용하는 사람 전부예요.

9. 배치 형태: 호스트에서 vs 원격에서

같은 플러그인이 두 가지 배치에서 모두 동작해요. 차이는 flats mcp가 어디로 중계하느냐와, 호스트 디스크를 직접 쓸 수 있느냐뿐이에요.

Flats 배치 형태 두 가지 A. 에이전트가 Flats 호스트와 같은 머신에서 실행되면 flats mcp가 기본값인 loopback 127.0.0.1:7878로 중계하고 save_version_from_dir로 호스트 디렉터리를 바로 올릴 수 있다. B. 에이전트가 다른 머신에 있으면 CLI만 설치하고 flats connect로 콘솔 주소를 저장한 뒤 tailnet HTTPS로 /mcp에 중계하며, 파일은 인라인으로 보낸다. A. 호스트에서 직접 한 머신 (Flats 호스트) 에이전트 + 플러그인 flats mcp (연결 설정 불필요) 127.0.0.1:7878/mcp flats serve loopback 요청 → 호스트 전용 도구 허용 save_version_from_dir {slug, dir} 빌드 디렉터리를 경로로 바로 업로드 B. 다른 머신에서 원격으로 클라이언트 머신 install.sh --no-service flats connect https://<console> 에이전트 + 플러그인 → flats mcp 파일은 인라인(utf8 / base64)으로 전송 HTTPS · tailnet ACL 안 Flats 호스트 https://<console>/mcp 프록시·Tailscale 헤더가 있으면 loopback 아님 → save_version_from_dir 사용 불가 콘솔 포트는 인터넷에 열지 않음
그림 6. 원격 배치에서는 CLI를 클라이언트 전용으로 설치하고 호스트 주소를 한 번만 저장해요.
항목A. 호스트에서 직접B. 원격 클라이언트
CLI 설치설치 스크립트 기본값 (서비스 포함)--no-service로 바이너리만
호스트 지정필요 없음 (loopback 기본값)flats connect <console URL> 또는 MCP 설정에 --url
업로드인라인 또는 save_version_from_dir인라인, 또는 CLI flats deploy <dir>
접근 제어loopbacktailnet ACL (콘솔도 같은 경계)
컨테이너로 운영할 때Linux에서는 host 네트워크를 쓰고, 콘솔 포트를 외부에 publish하지 말라고 문서가 권고해요.

플러그인 형식을 지원하지 않는 MCP 클라이언트도 같은 구성으로 붙을 수 있어요. 스킬 파일을 그 클라이언트의 스킬 위치에 두고, MCP 서버로 stdio flats mcp(또는 HTTP /mcp)를 등록하면 플러그인과 똑같이 동작해요.

10. 설계에서 배울 점과 주의점

배울 점: 지식은 서버에

도구 설명·규칙·오류 해설을 호스트가 버전과 함께 제공해요. 클라이언트 쪽 문서가 낡아서 생기는 사고가 없어요.

배울 점: 권한은 서비스 한 곳에서

via 하나로 MCP·CLI·콘솔을 구분하니, 입구가 늘어도 권한 검사가 흩어지지 않아요.

배울 점: 요청과 실행의 분리

에이전트는 결과물이 아니라 “요청”을 만들어요. 승인 시점에 대상을 고정해 바꿔치기도 막아요.

주의: 콘솔 = 신뢰 경계

콘솔에 닿을 수 있으면 승인할 수 있어요. tailnet ACL과 호스트에서 돌리는 프로세스 관리가 실제 보안이에요.

주의: update_document

유일하게 승인 없이 라이브를 바꾸는 도구예요. Public 문서 flat이면 즉시 공개돼요.

주의: 버전 맞추기

원격 클라이언트의 CLI는 호스트와 따로 업그레이드돼요. 브리지는 내용을 해석하지 않아 대부분 괜찮지만, CLI 명령은 버전 차이를 탈 수 있어요.