Prevent misleading real-estate self-host instructions

Tighten the real-estate skill docs so the launchd fallback stays operational
and the Onbid bid-result tools are described with the same WIP caveat the
upstream project still publishes.

Constraint: Upstream Docker compose already uses restart: unless-stopped while `docker compose ... up -d` daemonizes immediately
Rejected: Keep a separate server LaunchAgent with RunAtLoad + KeepAlive | launchd would restart-loop on the exiting compose command
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Mirror upstream capability caveats in k-skill docs and do not wrap daemonized server commands in launchd KeepAlive jobs
Tested: node --test scripts/skill-docs.test.js
Tested: npm run ci
Tested: uv sync
Tested: uv run real-estate-mcp --help
Tested: DATA_GO_KR_API_KEY=dummy uv run real-estate-mcp --transport http --host 127.0.0.1 --port 8017
Tested: curl initialize on http://127.0.0.1:8017/mcp returned protocolVersion 2024-11-05
Not-tested: Live 거래 조회 with a real DATA_GO_KR_API_KEY
This commit is contained in:
Jeffrey (Dongkyu) Kim 2026-04-05 14:25:56 +09:00
commit d656d26412
3 changed files with 38 additions and 31 deletions

View file

@ -6,7 +6,9 @@
- 아파트 전월세 조회 (`get_apartment_rent`)
- 오피스텔/연립다세대/단독주택/상업업무용 실거래가 조회
- 지역코드 조회 (`get_region_code`) 후 행정구역 기준 검색
- 청약홈/온비드 도구 연결
- 청약홈 도구 연결
- 온비드 코드/주소 조회
- 온비드 입찰결과 도구 (`get_public_auction_items`, `get_public_auction_item_detail`)는 upstream README 기준 `⚠️ WIP` 상태로 preview 안내
- hosted endpoint가 없을 때 self-host + Cloudflare Tunnel + launchd 운영
## 가장 중요한 규칙
@ -27,6 +29,7 @@
`DATA_GO_KR_API_KEY` 하나만 넣어도 기본 실거래가 조회는 시작할 수 있다.
청약홈/온비드를 더 세밀하게 나누고 싶으면 upstream 문서대로 `ODCLOUD_API_KEY`, `ODCLOUD_SERVICE_KEY`, `ONBID_API_KEY` 를 추가한다.
다만 `get_public_auction_items`, `get_public_auction_item_detail` 는 2026-04-05 기준 upstream README 에서 아직 `⚠️ WIP` 로 남아 있으므로, 안정 기능처럼 소개하지 말고 preview/실험 단계로만 설명한다.
## 가장 빠른 시작: Codex CLI stdio
@ -111,17 +114,15 @@ cloudflared tunnel run real-estate-mcp
### 4. launchd로 자동 실행
macOS 기준으로는 서버와 tunnel을 분리한 launchd 항목 두 개를 만든다.
macOS 기준으로는 **launchd 를 tunnel 전용으로만** 쓰고, upstream 서버 컨테이너 재시작은 Docker 쪽에 맡긴다.
upstream `docker/docker-compose.yml` 이 이미 `restart: unless-stopped` 를 설정하므로, `docker compose -f docker/docker-compose.yml up -d``RunAtLoad` + `KeepAlive` launchd job 에 넣으면 daemonize 직후 종료된 프로세스를 launchd 가 계속 다시 띄우는 restart loop가 생긴다.
따라서 서버 쪽은 Docker Desktop/Engine 자동 시작을 켜고 `docker compose ... up -d --build` 를 한 번 실행해 둔 뒤, `cloudflared tunnel run real-estate-mcp` 만 launchd 에 등록한다.
- `~/Library/LaunchAgents/com.kskill.real-estate-mcp.server.plist`
- `~/Library/LaunchAgents/com.kskill.real-estate-mcp.tunnel.plist`
둘 다 `RunAtLoad``KeepAlive` 를 켜고, 서버 쪽은 `docker compose -f docker/docker-compose.yml up -d`, tunnel 쪽은 `cloudflared tunnel run real-estate-mcp` 를 실행하게 둔다.
```bash
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.kskill.real-estate-mcp.server.plist
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.kskill.real-estate-mcp.tunnel.plist
launchctl enable gui/$(id -u)/com.kskill.real-estate-mcp.server
launchctl enable gui/$(id -u)/com.kskill.real-estate-mcp.tunnel
```

View file

@ -24,7 +24,7 @@ metadata:
- 단독/다가구 매매/전월세: `get_single_house_trades`, `get_single_house_rent`
- 상업업무용 매매: `get_commercial_trade`
- 청약홈 분양/당첨: `get_apt_subscription_info`, `get_apt_subscription_results`
- 공공경매/온비드: `get_public_auction_items`, `get_public_auction_item_detail`
- 공공경매/온비드 입찰결과: `get_public_auction_items`, `get_public_auction_item_detail` (`⚠️ WIP`, upstream README 기준)
- 지역코드 조회: `get_region_code`
## When to use
@ -52,6 +52,7 @@ metadata:
`DATA_GO_KR_API_KEY` 하나만 넣어도 기본 부동산 조회는 시작할 수 있다.
청약홈/온비드 키를 분리하고 싶으면 upstream 문서대로 `ODCLOUD_API_KEY`, `ODCLOUD_SERVICE_KEY`, `ONBID_API_KEY` 를 추가한다.
다만 `get_public_auction_items`, `get_public_auction_item_detail` 는 2026-04-05 기준 upstream README 에서 아직 `⚠️ WIP` 로 표시돼 있으니, production-ready 라고 단정하지 않고 preview 성격으로만 안내한다.
## Codex CLI setup (stdio)
@ -164,27 +165,10 @@ public 인터넷에 노출한다면 upstream `docs/setup-oauth.md` 대로 `AUTH_
### 3. macOS launchd 자동 실행
부팅 후 안정적으로 다시 뜨게 하려면 upstream 서버와 tunnel을 각각 launchd 로 올린다.
부팅 후 안정적으로 다시 뜨게 하려면 **launchd 는 Cloudflare Tunnel만 담당**하게 두고, upstream 서버 컨테이너는 Docker 쪽 재시작 정책에 맡긴다.
`docker/docker-compose.yml` 에 이미 `restart: unless-stopped` 가 들어 있으므로, `docker compose ... up -d``RunAtLoad` + `KeepAlive` launchd job 으로 감싸면 오히려 즉시 종료된 프로세스를 launchd 가 반복 재실행하게 된다.
`~/Library/LaunchAgents/com.kskill.real-estate-mcp.server.plist`
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key><string>com.kskill.real-estate-mcp.server</string>
<key>ProgramArguments</key>
<array>
<string>/bin/zsh</string>
<string>-lc</string>
<string>cd $HOME/src/real-estate-mcp && docker compose -f docker/docker-compose.yml up -d</string>
</array>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
</dict>
</plist>
```
즉, 서버 쪽은 Docker Desktop/Engine 이 로그인 후 자동 기동되도록 설정한 다음 위의 `docker compose ... up -d --build` 를 한 번 실행해 두고, macOS launchd 에는 long-running 프로세스인 `cloudflared tunnel run ...` 만 등록한다.
`~/Library/LaunchAgents/com.kskill.real-estate-mcp.tunnel.plist`
@ -208,13 +192,11 @@ public 인터넷에 노출한다면 upstream `docs/setup-oauth.md` 대로 `AUTH_
```
```bash
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.kskill.real-estate-mcp.server.plist
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.kskill.real-estate-mcp.tunnel.plist
launchctl enable gui/$(id -u)/com.kskill.real-estate-mcp.server
launchctl enable gui/$(id -u)/com.kskill.real-estate-mcp.tunnel
```
위 예시는 macOS 기준이다. Linux/Windows에서는 systemd 또는 서비스 관리자로 같은 역할을 분리해서 등록한다.
위 예시는 macOS 기준이다. Linux/Windows에서는 Docker 서비스 자동 시작 + systemd/서비스 관리자로 tunnel 같은 long-running 프로세스를 따로 등록한다.
## Response policy

View file

@ -1257,6 +1257,30 @@ test("repository docs advertise the real-estate-search skill and upstream self-h
assert.equal(fs.existsSync(path.join(repoRoot, "packages", "real-estate-search")), false);
});
test("real-estate-search docs keep the upstream Onbid WIP caveat and avoid launchd daemonize loops", () => {
const featureDoc = read(path.join("docs", "features", "real-estate-search.md"));
const skill = read(path.join("real-estate-search", "SKILL.md"));
for (const doc of [skill, featureDoc]) {
assert.match(doc, /get_public_auction_items/);
assert.match(doc, /get_public_auction_item_detail/);
assert.match(doc, /WIP|작업 중|준비 중/);
}
const skillLaunchdSection = skill.match(/##\s+.*launchd[\s\S]*?(?=\n##\s+|\n#\s+|$)/i)?.[0];
const featureLaunchdSection = featureDoc.match(/###+\s+.*launchd[\s\S]*?(?=\n##\s+|\n#\s+|$)/i)?.[0];
assert.ok(skillLaunchdSection, "expected skill launchd section");
assert.ok(featureLaunchdSection, "expected feature guide launchd section");
for (const section of [skillLaunchdSection, featureLaunchdSection]) {
assert.doesNotMatch(section, /com\.kskill\.real-estate-mcp\.server/);
assert.doesNotMatch(section, /launchctl .*real-estate-mcp\.server/i);
assert.match(section, /restart:\s*unless-stopped|Docker (Desktop|Engine).*재기동|Docker.*자동 재시작/i);
assert.match(section, /cloudflared[\s\S]*tunnel[\s\S]*run[\s\S]*real-estate-mcp/i);
}
});
test("repository docs advertise the shipped korean-spell-check helper assets", () => {
const readme = read("README.md");
const install = read(path.join("docs", "install.md"));