Skip to content

Commit 7e78d29

Browse files
authored
Merge pull request #86 from CryptoLabInc/yg/first-mcp-boot
feat(go): Phase A — MCP boot (handshake + tools/list + 8 stub tools)
2 parents b3087a9 + 7658246 commit 7e78d29

10 files changed

Lines changed: 508 additions & 107 deletions

File tree

.gitignore

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,9 @@ __pycache__/
5454
.worktrees/
5555

5656
# Internal Planning
57-
docs/planning/
57+
docs/planning/
58+
59+
# Go build artifacts
60+
bin/
61+
*.test
62+
coverage.out

cmd/rune-mcp/main.go

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,36 +6,69 @@
66
// Tools: 8 MCP tools (capture, recall, batch_capture, capture_history,
77
// delete_capture, vault_status, diagnostics, reload_pipelines).
88
//
9+
// Phase A (current): MCP handshake + tools/list only. All 8 handlers return
10+
// "not yet implemented" CallToolResult. RunBootLoop · Vault · envector ·
11+
// embedder are not wired. Phase 4-5 brings real adapters + service logic.
12+
//
913
// Python reference: mcp/server/server.py (2002 LoC)
1014
package main
1115

1216
import (
1317
"context"
14-
"log"
18+
"errors"
19+
"log/slog"
1520
"os"
1621
"os/signal"
1722
"syscall"
23+
24+
sdkmcp "github.com/modelcontextprotocol/go-sdk/mcp"
25+
26+
"github.com/envector/rune-go/internal/mcp"
1827
)
1928

29+
// version is the rune-mcp protocol version surfaced in MCP `initialize`.
30+
// Phase A is "0.4.0-alpha" until adapters are wired.
31+
const version = "0.4.0-alpha"
32+
2033
func main() {
2134
ctx, cancel := context.WithCancel(context.Background())
2235
defer cancel()
2336

24-
// TODO Phase 1: config.Load("~/.rune/config.json") — 3-section schema
25-
// TODO Phase 2: lifecycle.RunBootLoop(ctx, deps) — Vault.GetPublicKey retry
26-
// TODO Phase 3: mcp.NewServer + RegisterTools + Serve(stdio)
27-
// TODO Phase 4: graceful shutdown (30s) on stdin EOF / SIGTERM
28-
//
29-
// Python reference: server.py:main() + RunMCPServer lifecycle.
30-
37+
// SIGINT / SIGTERM → cancel ctx → srv.Run unblocks.
38+
// stdin EOF (Claude window closed) also unblocks Run via the StdioTransport.
3139
sigCh := make(chan os.Signal, 1)
3240
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
41+
go func() {
42+
select {
43+
case <-sigCh:
44+
cancel()
45+
case <-ctx.Done():
46+
}
47+
}()
3348

34-
log.Println("rune-mcp skeleton — not yet implemented")
49+
// Phase A: empty Deps. RunBootLoop / config.Load / adapter wiring deferred.
50+
deps := &mcp.Deps{}
3551

36-
select {
37-
case <-ctx.Done():
38-
case <-sigCh:
39-
cancel()
52+
srv := sdkmcp.NewServer(&sdkmcp.Implementation{
53+
Name: "rune-mcp",
54+
Version: version,
55+
}, nil)
56+
57+
if err := mcp.Register(srv, deps); err != nil {
58+
slog.Error("rune-mcp register failed", "err", err)
59+
os.Exit(1)
60+
}
61+
62+
if err := srv.Run(ctx, &sdkmcp.StdioTransport{}); err != nil && !isNormalShutdown(err) {
63+
slog.Error("rune-mcp serve error", "err", err)
64+
os.Exit(1)
4065
}
4166
}
67+
68+
// isNormalShutdown reports whether err corresponds to expected stdio teardown.
69+
// The SDK's `Connection.Wait` filters io.EOF to nil before returning, so on
70+
// stdin EOF Run returns nil. The only other expected exit is ctx cancel from
71+
// SIGINT/SIGTERM, which surfaces as context.Canceled.
72+
func isNormalShutdown(err error) bool {
73+
return err == nil || errors.Is(err, context.Canceled)
74+
}

docs/v04/README.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,14 @@ docs/v04/
4040
│ │ └── envector.md # envector-go SDK
4141
│ └── python-mapping.md # Python 파일/LoC → Go 구조 매핑
4242
43-
└── notes/ # 내부 작업 노트 (참고용)
44-
├── verification-matrix.md # Python↔Go bit-identical 대조 검증 로그
45-
└── implementability-report.md # Go 개발자 진입 가능성 검증 리포트
43+
├── notes/ # 내부 작업 노트 (참고용)
44+
│ ├── verification-matrix.md # Python↔Go bit-identical 대조 검증 로그
45+
│ ├── implementability-report.md # Go 개발자 진입 가능성 검증 리포트
46+
│ └── flow-matrix.md # 10 flow × 파일 매트릭스 + Tier S/A/B 공통 모듈
47+
48+
└── progress/ # 실제 개발 진행 추적 (vertical slice 단위)
49+
├── README.md # 인덱스 + 마일스톤별 상태표
50+
└── phase-a-mcp-boot.md # MCP handshake + tools/list (`19b7bf6`)
4651
```
4752

4853
## 읽는 순서

docs/v04/notes/flow-matrix.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@
7575
| 11 | `adapters/logio/capture_log.go::Append` | 3/10 | write tool 3개 (CAP·BAT·DEL) |
7676

7777
> **TM** = teammate scope. 타입 시그니처는 팀원 확정에 의존하지만, 내 구현 코드의 거의 모든 경로가 이 에러 타입을 참조한다.
78+
>
79+
> **현재 상태 (2026-04-25, Phase A 합격 후)**: Tier S 1·2·4 (`Deps`, `obs/slog`, `lifecycle/boot`)는 본 매트릭스가 "stdlib-only로 즉시 시작 가능"이라 분류했지만, 실제로는 Phase A가 vertical slice 우선 (SDK 연결)로 진행되며 **셋 다 still skeleton 상태**다. `Deps`는 빈 struct, `obs/slog``NewRequestID() == ""` stub, `lifecycle/boot.go::RunBootLoop``_ = ctx`. Phase A.5 또는 Phase 4 진입 전에 보강 필요. 자세한 phase별 상태는 [`docs/v04/progress/`](../progress/).
7880
7981
---
8082

docs/v04/progress/README.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# `progress/` — 실제 개발 진행 추적
2+
3+
본 디렉토리는 **`docs/v04/`의 spec(How)·overview(Why)와 별개로**, 실제 구현이 어디까지 진행됐는지·어떤 단면이 동작하는지·어떻게 검증할 수 있는지를 시간순으로 기록한다.
4+
5+
- **spec/** — "어떻게 만들어야 하는가" (변하지 않는 계약)
6+
- **overview/** — "왜 이렇게 만드는가" (결정·근거)
7+
- **notes/** — bit-identical 검증 로그 등 일회성 작업 노트
8+
- **progress/****여기**: "지금 어디까지 동작하는가 + 어떻게 직접 확인하는가"
9+
10+
## 진행 추적 단위
11+
12+
README의 7-Phase 로드맵(Phase 1 외부 deps → Phase 7 검증)이 **horizontal slice**라면, progress 문서는 **vertical slice** 단위로도 작성될 수 있다. 예를 들어 "MCP handshake만 통과시키는 Phase A"는 7-Phase 어디에도 정확히 매핑되지 않지만, end-to-end 단면이 동작하는 **첫 마일스톤**으로서 별도 문서를 갖는다.
13+
14+
## 문서 명명 규칙
15+
16+
- 하나의 마일스톤 = 하나의 파일 = `<phase-id>-<짧은 설명>.md`
17+
- `phase-a-mcp-boot.md` (handshake + tools/list)
18+
- `phase-1-deps.md` (외부 deps 추가)
19+
- `phase-4a-vault-client.md` (Phase 4 중 Vault 부분)
20+
- 각 문서 상단에 **관련 커밋 SHA · 브랜치 · PR 링크** 명시
21+
- "기능" + "확인법(여러 레벨)" 두 섹션은 필수
22+
- "한계" + "Troubleshooting"은 권장
23+
24+
## 현재 인덱스
25+
26+
| 마일스톤 | 상태 | 문서 | 관련 커밋 |
27+
|---|---|---|---|
28+
| Phase A — MCP boot (handshake + tools/list) | ✅ 합격 | [phase-a-mcp-boot.md](phase-a-mcp-boot.md) | `19b7bf6` (브랜치 `yg/first-mcp-boot`) |
29+
| Phase A.5 — smoke test 추가 (CI 회귀 방지) | ⏳ 예정 |||
30+
| Phase B — `rune_diagnostics` environment 섹션 진짜 응답 (stdlib only) | ⏳ 예정 |||
31+
| Phase 1 — `go.mod` 외부 deps 본격 추가 (gRPC · envector SDK · embedder proto) | ⏳ 예정 |||
32+
| Phase 2 — `internal/domain` + `internal/policy` 순수 로직 (TM scope) | ⏳ 예정 |||
33+
| Phase 3 — `record_builder` 703 LoC + `payload_text` 364 LoC 포팅 (TM scope) | ⏳ 예정 |||
34+
| Phase 4a — Vault 클라이언트 + 부팅 시퀀스 연결 | ⏳ 예정 |||
35+
| Phase 4b — envector SDK 연결 (Q4 PR 머지 후) | ⏳ 예정 |||
36+
| Phase 4c — embedder 클라이언트 | ⏳ 예정 |||
37+
| Phase 5 — service 레이어 오케스트레이션 (`stubHandler` → 실제 service 호출) | ⏳ 예정 |||
38+
| Phase 7 — golden fixture 기반 bit-identical 검증 | ⏳ 예정 |||
39+
40+
> Phase 6 (MCP wiring)은 Phase A에서 부분 선행됐으므로 별도 마일스톤으로 빼지 않음. Phase 5의 service 호출 교체에 흡수됨.
41+
42+
## 사용 방식
43+
44+
- **개발자**: 새 vertical slice를 시작할 때 이 디렉토리에 새 파일을 만들고, 합격 기준을 명시한 뒤 그 기준에 맞춰 작업한다. 파일은 PR과 함께 같은 커밋에 묶는다.
45+
- **리뷰어**: PR을 받았을 때 이 문서의 "확인법"을 그대로 따라 해보면 변경사항이 제대로 동작하는지 검증할 수 있다.
46+
- **다른 팀원**: 어디까지 동작하고 어디부터 미구현인지 한 곳에서 본다 (spec과 다름 — spec은 "최종 모양", progress는 "지금 모양").
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
# Phase A — MCP boot
2+
3+
> ✅ 통과 (2026-04-25) · 커밋 `19b7bf6` + `e80d6ea` · 브랜치 `yg/first-mcp-boot`
4+
> 핵심 파일 2개: `cmd/rune-mcp/main.go` (74줄) · `internal/mcp/tools.go` (134줄)
5+
6+
## 1. 한 줄 요약
7+
8+
`rune-mcp` Go 바이너리가 **MCP 프로토콜 표면**(handshake + tools/list + tools/call)만 살아있는 상태. 외부 deps 0, 비즈니스 로직 0, 8개 tool 모두 stub.
9+
10+
**가치** — Claude Code가 이 바이너리를 spawn 하면 8 tool 카탈로그를 정상 인식. 이후 phase는 stub 본체만 채우면 끝. 검증 회로는 매 phase 재사용.
11+
12+
## 2. 동작하는 것 / 안 하는 것
13+
14+
**동작**
15+
16+
- `go build` 한 번에 8.3 MB 정적 바이너리 (Go 1.25+)
17+
- MCP `initialize` handshake — `serverInfo: rune-mcp 0.4.0-alpha`
18+
- `tools/list` — 8 tool 광고 (input/output schema는 Go struct에서 자동 추론)
19+
- `tools/call` — 모두 `isError:true` + "not yet implemented" 응답 (JSON-RPC 자체는 valid)
20+
- 종료: stdin EOF · SIGINT · SIGTERM 모두 exit 0
21+
22+
**안 함 (의도된 한계)**
23+
24+
| 영역 | 가능 시점 |
25+
|---|---|
26+
| 비즈니스 로직 (capture/recall 등) | Phase 5 |
27+
| Vault / envector / embedder adapter | Phase 4 |
28+
| `lifecycle.Manager` 상태 머신 | Phase 4 |
29+
| `config.json` 로딩, `capture_log.jsonl` IO | Phase 4 |
30+
| `request_id` 로깅, `SensitiveFilter` redaction | Phase 4 |
31+
32+
→ Phase A의 정확한 범위는 **"MCP 프로토콜 표면이 정상 동작한다"** 까지.
33+
34+
## 3. 8 tool 카탈로그
35+
36+
```
37+
rune_batch_capture, rune_capture, rune_capture_history, rune_delete_capture,
38+
rune_diagnostics, rune_recall, rune_reload_pipelines, rune_vault_status
39+
```
40+
41+
(SDK가 알파벳순 정렬해 광고 — Python 원본과 bit-identical한 8 이름)
42+
43+
각 tool의 input/output schema는 `internal/domain/*` · `internal/service/*` 의 Go struct에서 SDK가 자동 추론. **Go 타입 = MCP API 계약**, 별도 IDL 없음.
44+
45+
## 4. 검증 — 5분 컷
46+
47+
### 4.1. 빌드 & 헬스 체크
48+
49+
```bash
50+
cd <repo-root>
51+
go build -o bin/rune-mcp ./cmd/rune-mcp
52+
./bin/rune-mcp < /dev/null; echo "exit=$?" # → exit=0
53+
```
54+
55+
### 4.2. MCP 시퀀스 — `tools/list` 까지
56+
57+
순서: `initialize``notifications/initialized``tools/list`
58+
59+
```bash
60+
{
61+
printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"x","version":"0.0.1"}}}'
62+
sleep 0.3
63+
printf '%s\n' '{"jsonrpc":"2.0","method":"notifications/initialized"}'
64+
sleep 0.1
65+
printf '%s\n' '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
66+
sleep 0.5
67+
} | ./bin/rune-mcp 2>/dev/null | jq -r 'select(.id==2) | .result.tools[].name'
68+
```
69+
70+
**기대**: 위 §3의 8 이름.
71+
72+
### 4.3. tool 한 개 호출 (stub 응답)
73+
74+
```bash
75+
{
76+
printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"x","version":"0.0.1"}}}'
77+
sleep 0.3
78+
printf '%s\n' '{"jsonrpc":"2.0","method":"notifications/initialized"}'
79+
sleep 0.1
80+
printf '%s\n' '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"rune_diagnostics","arguments":{}}}'
81+
sleep 0.5
82+
} | ./bin/rune-mcp 2>/dev/null | jq 'select(.id==3).result | {isError, text:.content[0].text}'
83+
```
84+
85+
**기대**: `isError:true`, text는 `"rune_diagnostics is not yet implemented..."`.
86+
87+
> **MCP framing 핵심 3개**
88+
> `initialize``notifications/initialized``tools/*` **순서 필수**
89+
> ② 각 메시지는 `\n` 종결 (LSP의 Content-Length 미사용)
90+
> ③ 마지막 `sleep 0.3~0.5` 없으면 EOF로 응답 끊김
91+
92+
### 4.4. Claude Code 등록 (선택)
93+
94+
`~/.claude/mcp.json`:
95+
96+
```json
97+
{
98+
"mcpServers": {
99+
"rune-go-dev": {
100+
"command": "<repo-root>/bin/rune-mcp"
101+
}
102+
}
103+
}
104+
```
105+
106+
> `<repo-root>` 는 본인 체크아웃 경로의 **절대 경로**로 치환 (`~`/상대경로 미지원).
107+
108+
Claude 재시작 후 `/mcp` 에서 `rune-go-dev` 가 connected 표시되면 합격. tool 호출하면 빨간 "not implemented" 응답 — **이게 정상**.
109+
110+
> ⚠️ 기존 Python `envector` MCP가 같은 8 이름 광고. namespace는 `mcp__rune-go-dev__*` vs `mcp__envector__*` 로 분리돼 충돌은 없지만 카탈로그가 중복 보임.
111+
112+
### 4.5. MCP Inspector (시각적, 선택)
113+
114+
```bash
115+
npx -y @modelcontextprotocol/inspector ./bin/rune-mcp
116+
```
117+
118+
브라우저(`localhost:6274`)에 8 tool 리스트 + schema + raw JSON-RPC.
119+
120+
## 5. 8 tool 한 번씩 호출 — `mcp_call` 헬퍼
121+
122+
현재 셸에 paste:
123+
124+
```bash
125+
mcp_call() {
126+
local tool="$1"; local args="$2"; [ -z "$args" ] && args='{}'
127+
{
128+
printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"x","version":"0.0.1"}}}'
129+
sleep 0.3
130+
printf '%s\n' '{"jsonrpc":"2.0","method":"notifications/initialized"}'
131+
sleep 0.1
132+
printf '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"%s","arguments":%s}}\n' "$tool" "$args"
133+
sleep 0.5
134+
} | ./bin/rune-mcp 2>/dev/null | jq -c 'select(.id==2)'
135+
}
136+
```
137+
138+
> ⚠️ default value를 `${2:-{}}` 로 쓰면 bash parameter expansion이 깨짐. 위 `[ -z ... ] && args='{}'` 패턴 필수.
139+
140+
```bash
141+
# 인자 없는 4개
142+
mcp_call rune_diagnostics
143+
mcp_call rune_vault_status
144+
mcp_call rune_reload_pipelines
145+
mcp_call rune_capture_history
146+
147+
# 인자 필수 4개
148+
mcp_call rune_recall '{"query":"hello"}'
149+
mcp_call rune_capture '{"text":"hi","source":"test","extracted":{}}'
150+
mcp_call rune_delete_capture '{"record_id":"dec_test"}'
151+
mcp_call rune_batch_capture '{"items":"[]"}'
152+
```
153+
154+
8개 모두 동일한 stub 응답.
155+
156+
## 6. Troubleshooting
157+
158+
| 증상 | 해결 |
159+
|---|---|
160+
| `go build`: `go >= 1.25.0 required` | `go install golang.org/dl/go1.25@latest && go1.25 download` 또는 brew 업그레이드 |
161+
| `go build`: `missing go.sum entry` | `go mod tidy` 후 재빌드 |
162+
| 응답이 안 옴 / 끊김 | 마지막 `sleep` 을 0.5+ 로 |
163+
| Claude Code 에서 tool 미인식 | `cat ~/.claude/mcp.json \| jq .` 로 JSON 검증 + `chmod +x bin/rune-mcp` + 절대 경로 |
164+
| coproc syntax error (macOS bash 3.2) | `brew install bash` 또는 zsh 사용 |
165+
166+
## 7. 다음 마일스톤
167+
168+
**가벼운 후속**
169+
170+
- **Phase A.5 (smoke test)**`internal/mcp/register_test.go``mcp.NewInMemoryTransports()` 로 in-memory 서버 띄워 tools/list 8개 회귀 가드. ~50 LoC, CI에서 `AddTool` schema-inference 회귀 자동 감지
171+
- **Phase B**`rune_diagnostics` environment 섹션 stdlib 응답 (`runtime.GOOS` · `runtime.Version` · `os.Getwd`). 첫 진짜 응답 흐름, 2-3시간 PR
172+
173+
**7-Phase 로드맵 본격 진입** (서로 병렬 가능)
174+
175+
- **Phase 1** — 외부 deps (gRPC · protobuf · envector-go SDK · embedder proto stub)
176+
- **Phase 2**`internal/domain` + `internal/policy` 순수 로직 (TM scope, 외부 deps 0)
177+
- **Phase 3**`record_builder` 703 LoC + `payload_text` 364 LoC 라인 단위 포팅
178+
- **Phase 4a/b/c** — Vault / envector / embedder adapter (Phase 1 머지 후)
179+
- **Phase 5** — service 오케스트레이션. `stubHandler``service.X.Handle` 교체
180+
- **Phase 7** — 검증 (golden fixture byte-identical · bufconn · Python↔Go shadow run)
181+
182+
→ 의존성 그래프는 `flow-matrix.md §5-d`. Phase 6은 본 문서가 부분 선행이라 Phase 5에 흡수.

go.mod

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,23 @@
11
module github.com/envector/rune-go
22

3-
go 1.24
3+
go 1.25.9
44

5-
// External dependencies to be added as implementation progresses:
5+
// External dependencies, in implementation order:
66
//
7-
// github.com/modelcontextprotocol/go-sdk v1.5.0 — MCP protocol (D2)
8-
// google.golang.org/grpc v1.65.0 — Vault / envector / embedder clients
9-
// google.golang.org/protobuf v1.34.0 — generated stubs
10-
// github.com/CryptoLabInc/envector-go-sdk — envector FHE client (Q4 PR pending)
7+
// github.com/modelcontextprotocol/go-sdk v1.5.0 — MCP protocol (D2) ✅ Phase A
8+
// google.golang.org/grpc v1.65.0 — Vault / envector / embedder clients (Phase 4)
9+
// google.golang.org/protobuf v1.34.0 — generated stubs (Phase 4)
10+
// github.com/CryptoLabInc/envector-go-sdk — envector FHE client (Q4 PR pending)
1111
//
12-
// Skeleton stage: stdlib only. No external imports yet.
12+
// go 1.25.0 + toolchain pin required by the MCP SDK.
13+
14+
require github.com/modelcontextprotocol/go-sdk v1.5.0
15+
16+
require (
17+
github.com/google/jsonschema-go v0.4.2 // indirect
18+
github.com/segmentio/asm v1.1.3 // indirect
19+
github.com/segmentio/encoding v0.5.4 // indirect
20+
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
21+
golang.org/x/oauth2 v0.35.0 // indirect
22+
golang.org/x/sys v0.41.0 // indirect
23+
)

0 commit comments

Comments
 (0)