CrownyAI 브라우저 — "MCP 연결 가능 웹브라우저" 아키텍처 설계
- 날짜: 2026-07-22
- 배경: 사장님 지시(2026-07-22) — 애플 앱스토어에 "MCP 연결 가능한 웹브라우저"로 CrownyAI(구 CrownyBrowser)를 등록하려 함. 이번 문서는 설계·조사만(코드 미변경, 산출물 1개).
- 결론 한 줄: 크라우니MCP.py(stdio, 12도구)를 CrownyAI 집사가 붙여 1개 도구를 왕복시키는 것이 MVP이고, 필요 부품(NSTask stdio 클라이언트 관용구, 자연어 라우터, SSE 트랜스포트, MCP 라우팅 로직)은 전부 기존 자산에 선례가 있어 신규 발명이 거의 없다. 앱스토어 심사에서는 로컬 프로세스 실행(stdio)이 샌드박스와 충돌할 가능성이 높아 HTTP+SSE 경로가 배포판의 실제 주력이 될 것으로 보인다(추정).
0. 조사한 기존 자산 (읽기 전용 확인 — 근거 인용)
| 자산 | 경로 | 확인한 내용 |
|---|---|---|
| 크라우니MCP.py | /Users/ef/CrownyOS/crownyc/크라우니MCP.py | stdio JSON-RPC 2.0 서버. initialize/tools/list/tools/call/notifications/initialized 4개 메서드 처리(91~112행). 12도구(crowny_respond~crowny_verse, 15~46행) — 전부 철학엔진.sh(한선씨 crownyc_big) 위임. 이건 "브라우저가 서버"가 아니라 "크라우니 엔진 자체가 MCP서버, 외부 LLM 클라이언트(제미나이/클로드데스크톱)가 붙는" 기존 자산 — 이번 설계의 B층(브라우저가 서버)과는 방향이 반대이고, A층(브라우저가 클라이언트)의 최초 접속 대상 서버로 그대로 쓸 수 있음. |
| 크라우니MCP설정.json | /Users/ef/CrownyOS/crownyc/크라우니MCP설정.json | 클라이언트측 등록 포맷: {"mcpServers":{"크라우니":{"command":"python3","args":[".../크라우니MCP.py"]}}}. 클로드데스크톱 claude_desktop_config.json에 병합하는 표준 MCP client config 그대로 — 레지스트리 스키마 설계의 출발점. |
| MCP브리지 학습패턴 | crownycode-learn.sh 학습DB 파트너도구_MCP브리지 | 이미 학습된 한선씨 함수: 메서드라우팅(요청메서드)(initialize→서버정보응답 / tools/list→도구목록응답 / tools/call→도구호출실행 / notifications/*→무응답 / 그외→미지원오류) + 호출인자변환(도구이름,인자문자열)(crowny_ 접두 벗기기 + 도구별 인자 포맷 변환). 이번 설계의 "MCP판정.한선" 정본이 거의 이 함수를 그대로 재사용하면 된다 — 신규 발명 아님. |
| 2026-07-03 요청기록 | /Users/ef/CrownyDoc/projects/2026-07-03-요청-MCP브리지-SLM폴백.md | "MCP 서버 등록(crowny-tools, user 스코프, Connected) — 전 세션 20도구 네이티브 장착. 한선씨 정본 MCP브리지.한선 7/7". claude mcp list 라이브 확인 결과 crowny-tools: node /Users/ef/Downloads/CrownyTVM/crownycode-agent/partner-tools/mcp-server.js — Connected. 이건 이 Claude Code 세션이 쓰는 별도 MCP 자산(node, partner-tools 디렉토리에 MCP브리지.rpn.한선도 존재) — CrownyAI 브라우저와는 무관하지만 "MCP 서버를 stdio로 노출하는 패턴"의 두 번째 선례. |
| 2026-07-03 요청기록 | /Users/ef/CrownyDoc/projects/2026-07-03-요청-크라우니-범용부스터-MCP-부스트업.md | 원 기획 의도: "제미나이용 크라우니MCP, 오푸스용 페블상회 크라우니부스트업 — 완전무인 자율화설계". 즉 크라우니MCP.py는 처음부터 "브라우저"가 아니라 "외부 프론티어 LLM 강화용 서버"로 기획됨 — 이번 브라우저 통합은 그 자산을 재활용하는 것이지 원 기획과 충돌하지 않음. |
| 집사CLI브릿지.sh + cbRunLocalCLI | /Users/ef/CrownyBrowser/tools/집사CLI브릿지.sh, native/crowny-ai-butler.m:1047-1120 | A층(MCP 클라이언트) 트랜스포트의 정확한 템플릿: NSTask 백그라운드 실행(dispatch_get_global_queue) + env 4키 주입(CROWNY_CLI_CMD/MODEL/PERSONA, CLAUDE_CONFIG_DIR 패스스루) + 120초 dispatch_after 타임아웃([t terminate]) + NSPipe stdout 캡처 + exit코드 규약(0성공/2빈입력/3CLI없음/4타임아웃/5비정상종료) + main큐 UI 갱신. MCP stdio 클라이언트는 이 구조에서 단발 실행→응답 파싱을 "지속 프로세스+라인단위 JSON-RPC 왕복"으로만 바꾸면 된다(NSPipe를 유지한 채 여러 번 write/read). |
| cbButlerRespond 라우터 | native/crowny-ai-butler.m:863-936 | 자연어 → "액션\|인자\|사용자메시지" 3필드 파이프 포맷 라우팅(has() 블록 키워드 매칭 체인, 872~933행). cbExecuteButlerAction:(941~969행)이 act별로 분기(nav/slm/cli/msg/quantum/search...). "mcp" 액션을 이 체인에 한 항목 추가하면 자연어 라우팅이 즉시 붙는다(예: ! 프리픽스가 cli로 직행하듯, "MCP " 접두나 has(@"MCP")로 분기). |
| SSE.한선 | /Users/ef/CrownyOS/crownyc/libs/SSE.한선 | 서버측: SSE시작(소켓)/SSE이벤트전송(소켓,이벤트이름,데이터)/SSE데이터전송/SSE아이디이벤트/SSE핑/SSE종료. 클라이언트측: SSE클라이언트생성(호스트,포트,경로)/SSE파싱(버퍼)/SSE수신대기(소켓,핸들러). HTTP+SSE 트랜스포트(MCP 2025-03-26 스펙의 구 전송방식)를 한선씨로 이미 구현해 둔 라이브러리가 존재 — B층(브라우저가 MCP서버 노출)과 A층의 원격 서버 접속 양쪽에 재사용 가능. |
| WKScriptMessageHandler 선례 | native/crowny-ai-content.m:663, 2439 | window.webkit.messageHandlers.caRetry.postMessage(...) 패턴이 이미 인라인 오류카드 재시도 버튼에 쓰이고 있음(2439행 부근) — C층(window.crownyMCP) 페이지↔네이티브 브릿지의 정확한 선례. |
| mcporter 스킬 | /Users/ef/crowny-genesis/skills/mcporter/SKILL.md | Crowny 자산 아님(node 패키지, openclaw 생태계 범용 MCP CLI). list/call/config add|remove|import|login|logout/daemon start|stop/generate-cli 커맨드 셋 보유. 직접 재사용 대상은 아니고, "MCP 클라이언트 서브시스템이 최소 갖춰야 할 커맨드 표면(등록·조회·호출·인증·데몬)"의 참고 모델로만 인용. |
| 앱스토어 샌드박스 | find CrownyBrowser -iname *.entitlements → 0건 | 현재 CrownyBrowser/CrownyAI 어떤 변종에도 .entitlements 파일이 없다 = 지금은 샌드박스 미적용 직접배포 상태. App Store 등록 시 com.apple.security.app-sandbox 진입이 필요해지며, 이때 NSTask(자식 프로세스 실행)가 제한받는다(§5 참조, 확정 아님 — 별도 App Store gap 조사 필요). |
1. "MCP 연결 가능 브라우저"의 의미 — 3층 정의
A층 — 브라우저가 MCP 클라이언트 (핵심 셀링포인트)
집사(CAButlerDock)가 사용자가 등록한 외부 MCP 서버(로컬 stdio 프로세스 또는 원격 HTTP+SSE 엔드포인트)에 연결해, 그 서버가 노출하는 도구를 자연어 지시로 호출한다.
- 예: 사용자가 Linear MCP 서버를 등록 → "MCP 리니어에서 내 이슈 목록 보여줘" → 집사가
tools/call(list_issues)실행 → 결과를 말풍선으로 응답. - 이게 앱스토어 리스팅 문구 "MCP 연결 가능한 웹브라우저"의 실제 의미에 가장 가깝다. 클로드데스크톱/커서 등이 하는 것과 동일한 역할을 브라우저 안의 집사가 수행.
B층 — 브라우저가 MCP 서버 노출
CrownyAI가 자신의 상태(현재 페이지 본문, 열린 탭 목록, 북마크, 히스토리, 네비게이션 제어)를 MCP 도구로 노출해, 외부 LLM(클로드데스크톱, 제미나이 등)이 그 도구를 호출해 브라우저를 원격 조작/열람할 수 있게 한다.
- 도구 예:
page.read(현재 탭 본문),tabs.list(열린 탭),navigate(URL 이동),bookmarks.list,history.search. - 크라우니MCP.py 패턴의 직접 확장: 같은
initialize/tools/list/tools/callstdio 서버 골격에,TOOLS배열만 브라우저 도메인 도구로 교체하면 된다(엔진 위임 대신 브라우저 프로세스 내부 상태 조회로 교체).
C층 — 웹페이지 JS ↔ MCP 브릿지 (window.crownyMCP)
crowny:// 페이지(또는 권한을 부여받은 일반 웹페이지)의 JS가 window.crownyMCP.call(server, tool, args) 같은 API로 집사/MCP 계층에 접근한다. 임의 웹사이트가 무단으로 로컬 MCP 서버(파일시스템 등)를 두드리지 못하도록 권한 게이트가 최우선 전제.
- WKScriptMessageHandler 선례(
caRetry)를 그대로 확장 —window.webkit.messageHandlers.crownyMCP.postMessage({server, tool, args})→ 네이티브가 A층 클라이언트를 호출 →evaluateJavaScript로 콜백 반환.
2. 각 층의 구현 설계
2.1 트랜스포트
| 트랜스포트 | 구현 | 근거/재사용 |
|---|---|---|
| stdio (로컬 프로세스) | NSTask 지속 프로세스 + NSPipe 라인단위 JSON-RPC 왕복 | cbRunLocalCLI(단발 실행)를 "프로세스를 죽이지 않고 유지 + write 후 read" 구조로 확장. env 주입·타임아웃·exit코드 관용구 그대로. |
| HTTP+SSE (원격 서버) | NSURLSession(dataTask, 스트리밍 델리게이트) + 파싱은 한선씨 SSE파싱(버퍼) 로직을 native 쪽에 이식하거나, 별도 crownyc 헬퍼 프로세스(crownyc run SSE클라이언트.toau)를 NSTask로 띄워 파이프로 중계 | SSE.한선이 서버·클라 양쪽 로직을 이미 보유 — 네이티브 NSURLSession은 신규지만 파싱 규칙은 재사용. |
| (참고) Streamable HTTP | 최신 MCP 스펙의 단일 POST+SSE 겸용 방식 — 이번 설계는 구 HTTP+SSE로 시작(SSE.한선 즉시 재사용), Streamable HTTP는 다음 단계 |
2.2 MCP 프로토콜 시퀀스 (공통)
1. initialize → {protocolVersion, capabilities, clientInfo} 교환
2. tools/list → 서버가 노출하는 도구 스키마 수신, 로컬 캐시
3. tools/call → {name, arguments} 요청, {content:[{type:"text",...}]} 응답
4. notifications/initialized → 무응답(fire-and-forget)
id 상관관계는 크라우니MCP.py가 이미 하듯 요청 id를 그대로 응답에 반사(req.get("id")).
2.3 집사 자연어 라우팅
cbButlerRespond:(863행)에 신규 분기 추가(설계만, 미구현):
objc// "MCP <서버명> [에서/의] <도구명> [인자...]" 패턴
if (has(@"MCP") || has(@"mcp")) {
// MCP판정.한선(§3) 위임 또는 간단 정규식으로 서버:도구:인자 파싱
return [NSString stringWithFormat:@"mcp|%@|MCP 서버에 연결해 도구를 호출합니다", c];
}
cbExecuteButlerAction:(941행)에 대응 분기:
objc} else if ([act isEqualToString:@"mcp"]) {
[self cbRunMCPTool:arg]; // 신설 — cbRunLocalCLI 자매 함수
}
우선순위 주의: 기존 "?"/"설명"/"물어" 같은 범용 질의 규칙(906행)보다 먼저 검사해야 "MCP ~"가 slmtext로 새지 않는다(집사CLI브릿지의 "!상태" 우선 검사 사례와 동일 원칙).
2.4 서버 레지스트리
- 스키마 확장: 크라우니MCP설정.json 포맷을 그대로 물려받아 사용자 서버까지 담는다.
json { "mcpServers": {
"크라우니": {"command":"python3","args":[".../크라우니MCP.py"]},
"리니어": {"transport":"sse","url":"https://mcp.linear.app/sse"},
"노션": {"transport":"sse","url":"https://mcp.notion.com/mcp"}
}}
- 저장 위치:
~/Library/Application Support/CrownyAI/mcp_servers.json(사용자 편집 대상) — 크라우니MCP설정.json 자체는 "정본 자산"이라 건드리지 않고 별도 사용자 레지스트리 파일 신설 제안. - 설정 UI:
crowny://settings하위에 "MCP 서버" 섹션 신설 — 목록/추가(이름+command·args 또는 이름+url)/편집/삭제/연결테스트(initialize왕복 1회로 OK/실패 표시). N8의NSUserDefaults계정 프로필 UI(crowny.ai.cliCommand등)와 동일한 설정 화면 패턴.
2.5 보안 게이트 — 도구 호출 승인 4상
집사CLI브릿지의 4상 관용구(티=즉답/옴=폴백/타=거부/음=이관)를 도구 호출 승인에 적용:
| 상 | 조건 | 동작 |
|---|---|---|
| 티 | 사용자가 화이트리스트에 등록한 (서버,도구) 쌍, 읽기 전용류 | 자동 허용, 승인 팝업 없이 실행 |
| 옴 | 미분류 도구, 또는 부작용 있어 보이는 도구(이름에 write/delete/send 등 포함) | 매 호출 승인 팝업("MCP 리니어가 create_issue를 호출하려 합니다 — 허용?") |
| 타 | 블랙리스트(사용자가 명시적으로 금지) | 거부, 사용자에게 사유 표시 |
| 음 | 서버 자체가 미등록/연결 실패 | 조용한 대체응답 금지 — "MCP 서버 '리니어'가 연결되어 있지 않습니다" 정직 표시(가드레일 §허위배선 원칙과 동일) |
3. 집사 연동 — 한선씨 정본 제안
3.1 cbRunMCPTool: (네이티브, 설계만)
cbRunLocalCLI:(1047행)를 자매 복제한 - (void)cbRunMCPTool:(NSString *)spec — spec은 "서버명:도구명:JSON인자" 형태. 내부에서:
- 레지스트리(§2.4)에서 서버 설정 조회
- stdio면 지속
NSTask확보(세션 중 유지, 최초 1회initialize) 또는 HTTP+SSE면NSURLSession요청 tools/call전송 → 응답 파싱 →logAppend:who:@"집사·MCP"
3.2 MCP판정.한선 (정본 제안 — 한선씨, 4상)
신규 발명이 아니라 학습DB 파트너도구_MCP브리지에 이미 있는 메서드라우팅()/호출인자변환()을 그대로 이식 + 4상 승인 판정만 추가:
가져오기 "문자열.한선"
// 기존 학습패턴 그대로: JSON-RPC 메서드 → 처리 분기
함수 메서드라우팅(요청메서드) {
만약 (요청메서드 == "initialize") { 반환 "서버정보응답" }
만약 (요청메서드 == "tools/list") { 반환 "도구목록응답" }
만약 (요청메서드 == "tools/call") { 반환 "도구호출실행" }
만약 (시작하는가(요청메서드, "notifications/")) { 반환 "무응답" }
반환 "미지원오류"
}
// 신규 — 도구 호출 승인 4상 판정 (화이트/블랙리스트 PSV 조회)
함수 승인판정(서버명, 도구이름, 화이트, 블랙) {
만약 (포함하나(블랙, 서버명 + ":" + 도구이름) == 1) { 반환 0 - 1 } // 타
만약 (포함하나(화이트, 서버명 + ":" + 도구이름) == 1) { 반환 1 } // 티
만약 (포함하나(도구이름, "write") == 1) { 반환 0 } // 옴(승인필요)
만약 (포함하나(도구이름, "delete") == 1) { 반환 0 }
만약 (포함하나(도구이름, "send") == 1) { 반환 0 }
반환 0 // 기본=옴(매 호출 승인)
}
이 파일은 문서 산출 대상(설계)일 뿐 이번 작업에서 작성하지 않음 — 다음 구현 단계에서 CrownyBrowser/src/MCP판정.한선(탭종류판정.한선/별칭판정.한선과 같은 자리)으로 신설 제안.
4. MVP 정의 — 가장 작은 데모
"크라우니MCP.py를 브라우저 집사가 stdio로 붙여 crowny_respond 1개 호출 → 말풍선"
단계:
- (확인 완료) 크라우니MCP.py 단독 실행 확인 —
python3 크라우니MCP.py에 stdin으로{"jsonrpc":"2.0","id":1,"method":"initialize",...}파이핑해 응답 확인(기존 자산이라 이미 동작 검증됨, 설정.json 존재가 방증). cbRunMCPTool:최소 버전 — 서버 1개(크라우니MCP.py) 하드코딩,NSTask+NSPipe로initialize→tools/call(crowny_respond,{concept:"..."})왕복, 결과를logAppend.cbButlerRespond:에 "MCP" 키워드 분기 1줄 추가,cbExecuteButlerAction:에mcp액션 1줄 추가.- 레지스트리 JSON 1파일(§2.4) — 서버 1개만.
- 승인 게이트(§2.5) — 이 MVP는
crowny_respond가 읽기전용이므로 티(자동허용)로 시작.
- 다서버 레지스트리 + 설정 UI(§2.4)
- HTTP+SSE 트랜스포트(원격 MCP 서버, 예: 리니어/노션) —
SSE.한선로직 native 이식 - B층: 브라우저 자체 MCP 서버 노출(탭/북마크/히스토리 도구) — 크라우니MCP.py 골격 재사용
- C층:
window.crownyMCP웹페이지 브릿지 —caRetryWKScriptMessageHandler 패턴 확장 + 페이지 출처 권한 게이트 - 4상 승인 UI(팝업), 화이트/블랙리스트 PSV 영속화
- 앱스토어용 sandboxed 빌드에서 stdio 제거/대체(§5)
5. 앱스토어 제약과의 접점
CrownyBrowser 어떤 변종(.app)에도 .entitlements 파일이 없음(실측, find -iname *.entitlements 0건) — 지금은 샌드박스 미적용 직접배포. App Store 심사는 com.apple.security.app-sandbox 진입을 요구한다.NSTask로 임의 로컬 프로세스(사용자가 지정한 command+args, 예: npx some-mcp-server)를 실행하는 것은 App Sandbox 원칙(자식 프로세스 spawn 제한, 특히 임의 실행파일 경로)과 충돌할 가능성이 높다. com.apple.security.temporary-exception.* 예외를 심사에서 받기 어려울 수 있음 — 이 항목은 확정이 아니라 추정이며, 정확한 entitlement 요건 확인은 별도 "App Store gap" 조사 트랙의 몫으로 남긴다(이 문서는 그 결론을 인용하지 않고 이슈만 제기).com.apple.security.network.client entitlement만으로 샌드박스 호환 가능성이 높다. 따라서:CrownyBrowser-Public.app 계열, 3변종 패키징 CLAUDE.md 참조)은 HTTP+SSE(원격) MCP 서버만 지원.CrownyBrowser-Crowny.app/-Universal.app)은 stdio(로컬 프로세스) MCP 서버도 지원하는 상위 기능으로 이원화.crownyc run 웹서버v2 위에 크라우니MCP.py 로직을 얹은 로컬 loopback 서버, CROWNY_TCP_LOOPBACK=1)로 우회하는 안이 필요 — 별도 후속 설계 대상.6. 선례 정합 — "이미 있음" vs "신규 필요분"
| 필요 요소 | 상태 | 근거 |
|---|---|---|
| MCP 서버 골격(stdio JSON-RPC 2.0, initialize/tools/list/tools/call) | ✅ 이미 있음 | 크라우니MCP.py |
| MCP 서버 등록 스키마(mcpServers JSON) | ✅ 이미 있음(확장만 필요) | 크라우니MCP설정.json |
| MCP 메서드 라우팅 로직(한선씨) | ✅ 이미 학습됨(재사용) | 학습DB 파트너도구_MCP브리지 |
| stdio 클라이언트 실행 관용구(NSTask+env+타임아웃+exit규약) | ✅ 이미 있음(자매 함수로 복제) | cbRunLocalCLI(crowny-ai-butler.m:1047) |
| 자연어 → 액션 라우터 확장점 | ✅ 이미 있음(분기 1줄 추가) | cbButlerRespond:/cbExecuteButlerAction: |
| HTTP+SSE 트랜스포트 로직 | ✅ 이미 있음(한선씨, native 이식 필요) | libs/SSE.한선 |
| 페이지 JS ↔ 네이티브 브릿지 패턴 | ✅ 이미 있음(선례 1건) | caRetry WKScriptMessageHandler |
| 설정 UI에 새 섹션 추가하는 방식 | ✅ 이미 있음(패턴 재사용) | N8 계정 프로필 설정(crowny.ai.cliCommand 등) |
| 신규 필요: MCP판정.한선(승인 4상 포함 정본 파일) | ❌ 신규(§3.2 초안 제공) | — |
신규 필요: cbRunMCPTool: / mcp 액션 분기 코드 | ❌ 신규(§2.3/§3.1 설계만) | — |
| 신규 필요: 서버 레지스트리 UI + 저장 파일 | ❌ 신규(§2.4) | — |
| 신규 필요: NSURLSession 기반 SSE native 클라이언트 | ❌ 신규(SSE.한선 로직 이식) | — |
| 신규 필요: B층(브라우저 자체 MCP 서버 노출) | ❌ 신규(크라우니MCP.py 골격 확장) | — |
| 신규 필요: 샌드박스 entitlements 결정 | ❌ 별도 트랙(App Store gap 조사) | — |
크라우니코드 보고
이번 작업은 설계·조사 문서 산출(코드 미작성)이라 크라우니코드 lookup/learn 대상 코드 생성이 없음. 대신 §0/§3.2에서 기존 학습DB 패턴 1건(파트너도구_MCP브리지)을 lookup으로 확인해 그대로 재사용 제안함 — 크라우니코드: lookup HIT 1건(재사용, 신규 생성 0건) / learn 추가 0건(코드 미작성).