Skip to content

Add the friction scan and a read-only MCP surface - #77

Merged
heznpc merged 3 commits into
mainfrom
feat/friction-and-mcp
Aug 19, 2026
Merged

Add the friction scan and a read-only MCP surface#77
heznpc merged 3 commits into
mainfrom
feat/friction-and-mcp

Conversation

@heznpc

@heznpc heznpc commented Aug 19, 2026

Copy link
Copy Markdown
Owner

canary(~/IdeaProjects/Paper/canary)는 논문 아카이브로 동결됐고, 도구 계층을 Modore로 옮긴다. 두 레포를 대조하면 canary는 Modore의 부분집합이었다 — 세션↔프로젝트 조인과 미푸시 워크트리 유일본 판정은 이미 scree.py가 하고 있고, retention forecast·orphan 탐지·preserve 내보내기는 Modore에만 있다. 겹치지 않는 고유 기능은 friction scanner 하나였고, canary를 존치시켜온 실용적 이유는 그 판정이 세션 중 MCP로 질의 가능했다는 점 하나였다. 둘 다 닫는다.

scripts/friction.py

canary/lib/sessions/friction.ts의 결정론적 포팅. 9종 분류 × 심각도 1–3, 키워드·톤 매칭만 쓰고 판정 경로에 모델이 없다.

  • Claude Code·Codex 세션은 scree.collect_claude / collect_codex를 그대로 재사용한다(중복 순회 없음).
  • Gemini CLI 채팅과 Claude Desktop 로컬 에이전트 세션은 여기서 새로 수집한다 — scree는 둘 다 순회하지 않는다. collect_gemini~/.gemini/projects.json 레지스트리만 읽고(트랜스크립트 없음), Claude Desktop 수집기는 아예 없다. Gemini 트랜스크립트에는 cwd가 없어 워크스페이스는 레지스트리의 alias로 조인한다.
  • 사용자 턴 내용을 읽는 것은 preserve에 이은 scree 무내용 계약의 두 번째 명시적 예외다. 같은 방식으로 제한한다: 사용자 작성 턴만, 인용 200자 상한, 기본 scree.mask_text 마스킹, --raw-quotes가 명시적 opt-out, 아무것도 쓰지 않음.
  • --json으로 구조화 출력. 30일 창 실측: 세션 200개 / 사용자 턴 3,563 / 발견 430건(rage 247 · irritation 60 · correction 123).

분류 근거 수치(2026-07, 2,630턴/515건 인간 감사)는 canary 기록에서 인용만 하고 재도출하지 않았다.

scripts/mcp_server.py

의존성 0의 stdio JSON-RPC 서버. 읽기 전용 도구 셋:

도구 내용
scree_report 조인·유지기간 예보·유일본/고아/lineage 판정, 섹션 선택, 절단은 항상 명시
friction_scan 위 분류 체계, 스토어·카테고리·최소 심각도 필터
system_scan_summary 디스크에 이미 있는 스캔 결과 요약 + 그 파일의 나이
  • 얇은 층: 각 도구는 scree.py --json / friction.py --json을 실행해 출력을 전달할 뿐이다. 판정 재구현이 없으므로 CLI·Mac 앱·MCP가 서로 다른 사실을 말할 수 없다.
  • 실행 미노출: cleanup·삭제·스캔 실행은 없다. Modore의 파괴 경로는 사람이 화면에서 승인하는 토큰에 걸려 있고, 에이전트가 우회 가능하면 그 계약이 깨진다. 모든 도구를 실제로 호출해 스폰되는 프로세스가 두 판정 스크립트뿐임을 확인하는 테스트로 고정했다.
  • 모든 결과는 untrusted 데이터로 펜싱된다.

설계가 브리핑과 어긋난 지점

  1. scree가 gemini/desktop 트랜스크립트를 순회한다는 전제는 사실이 아니었다 (위 참조). 셋 중 둘을 friction 쪽에 새로 만들었고, 이미 배포되어 Swift UI가 소비하는 scree 리포트를 넓히지는 않았다.

  2. AirMCP의 MCP 서버는 둘이고, 운영 중인 쪽은 TypeScript다. .mcp.json이 등록하는 것, 자기 macOS 앱의 ServerManager.swiftnode/npx로 띄우는 것, npm에 배포되는 것 전부 TS 서버(@modelcontextprotocol/sdk, 260+ 도구)다. 이 PR이 따른 도구 서술자·readOnlyHint 애노테이션·untrusted 펜싱 패턴은 여기서 왔다. 별개로 ios/Sources/AirMCPServer에 Swift MCP 서버가 하나 더 있다 — MCPServer.swift 229줄, initialize·tools/list·tools/call·ping, protocolVersion 2025-03-26, Hummingbird HTTP. README가 "iOS runtime is in preview"로 표시하는 프리뷰 런타임이다. 이 PR의 프로토콜 계층 형태(SDK 없이 손으로 짠 dispatch + 작은 도구 테이블)와 아래 등록 시점 게이트는 이쪽에서 가져왔다.

    Modore가 어느 쪽 언어도 따라가지 않은 이유는 언어 자체가 아니라, 판정이 Python 스크립트이고 이 레포가 런타임 외부 의존성 0을 명시적으로 고정하고 있기 때문이다.

    읽기 전용 계약을 등록 시점으로 옮겼다 (커밋 cb37e4d). Swift 서버의 registerToolIOSPreviewContract를 참조해 이름 allowlist에 없거나 read-only·non-destructive 애노테이션이 없는 도구를 등록 자체에서 거부한다 — tools/list에서 숨기는 게 아니라. 이 표면은 같은 성질을 "그런 도구를 안 만들었다"는 의도 + 테스트로만 갖고 있었는데, 이제 contract_allows가 핸들러 맵과 서술자 목록을 게이트하고, 거부된 도구는 미노출이 아니라 도달 불가이며, REJECTED_TOOLS--tools 출력에 드러난다.

    이력 정정: 커밋 cb37e4d의 메시지는 "AirMCP is not a Swift MCP server라고 말한 것은 틀렸다"고 적혀 있다. 그 자체가 틀렸다 — 운영 중인 서버가 TS라는 원래 진술은 맞았고, 프리뷰 Swift 서버의 존재는 그 진술을 뒤집는 게 아니라 보완한다. 푸시된 커밋 메시지는 force push 없이 못 고치므로 여기서 정정한다. cb37e4d가 넣은 코드(등록 시점 게이트)는 출처와 무관하게 유효해서 그대로 둔다.

  3. scree.py_extract_turn은 Codex 파일에서 동작하지 않는다 (별건, 이 PR에서 고치지 않음). Codex event_msgpayload.message가 문자열이고 response_iteminput_text/output_text 블록을 쓰는데, _extract_turncontent 리스트의 type == "text"만 본다. 따라서 Codex 세션에 preserve를 걸면 "no recognizable turns"가 나온다. friction의 Codex 파서는 두 채널을 모두 처리한다.

검증

  • python3 -I -B -m pytest tests/ -q444 passed (friction 39 + MCP 34 신규)
  • python3 -m compileall -q scripts/, python3 -I -B scripts/release_smoke.py → 통과
  • friction CLI를 4개 스토어 실데이터에 대해 실행 확인
  • MCP 서버를 실제 stdio 핸드셰이크(initialize → notifications/initialized → tools/call ×3)로 구동 확인. 프로토콜 협상은 지원 버전 에코, 미지원이면 최신으로 폴백
  • 미검증: MCP 클라이언트에 등록한 상태에서의 동작(세션 재시작 필요)

Swift UI 노출은 범위 밖이라 두 스크립트 모두 서명 앱 런타임에 넣지 않았고, 런타임 완결성 테스트의 제외 목록에 "Swift 호출자가 생기면 RUNTIME_FILES로 옮긴다"는 조건과 함께 등록했다.

heznpc added 3 commits August 19, 2026 15:41
canary is frozen as a paper archive and its tool layer moves here. Comparing
the two, canary was already a subset of Modore -- session/project join and
unpushed-worktree sole-copy judgment are scree's, and retention forecast,
orphan detection, and preserve export exist only here. The one capability
that did not overlap was the operator-friction scan, and the one practical
reason to keep canary alive was that its judgments were reachable from an
MCP client mid-session while Modore's were terminal-only. Both are closed.

friction.py ports canary/lib/sessions/friction.ts: the nine-category
pushback taxonomy at severity 1-3, keyword and tone matching only, no model
in the judgment path. Claude Code and Codex sessions come from scree's own
collectors rather than a second traversal. Gemini CLI chats and Claude
Desktop local-agent sessions are collected here because scree does not
traverse either -- its Gemini collector reads only ~/.gemini/projects.json,
and it has no Claude Desktop collector at all, so the brief's premise that
all four stores were already walked did not hold. Gemini transcripts carry
no cwd, so the workspace is joined through the project registry's alias.

Reading user turns is the second deliberate exception to scree's no-content
contract, after preserve, and is bounded the same way: user-authored turns
only, quotes capped at 200 characters and masked through scree.mask_text by
default, --raw-quotes as the explicit opt-out, nothing written.

mcp_server.py is a zero-dependency stdio JSON-RPC server exposing three
read-only tools -- scree_report, friction_scan, system_scan_summary. Each
runs scree.py --json or friction.py --json and forwards the output, so no
judgment is reimplemented and the CLI, the app, and this surface cannot
disagree. Cleanup, deletion, and scan execution are absent: Modore gates
destruction on an approval a human grants on screen, and an agent-reachable
bypass would void that. A test exercises every tool and asserts the only
processes ever spawned are the two judgment scripts. Results are fenced as
untrusted machine-derived data.

AirMCP was read as the precedent for tool descriptors, read-only
annotations, and untrusted-content fencing, but its server is TypeScript on
the MCP SDK; this repository ships no Node toolchain and pins zero runtime
dependencies, so the protocol layer is stdlib Python instead.

Neither script joins the signed app runtime -- no Swift view calls either --
so both are listed in the runtime-completeness exclusions with the condition
for moving them in.
Correcting the previous commit message: AirMCP does have a Swift MCP server.
ios/Sources/AirMCPServer/MCPServer.swift is a real one -- initialize,
tools/list, tools/call, ping, protocolVersion 2025-03-26, over HTTP, with a
JSONRPCTypes layer and Calendar/Contacts/Health/Location/Reminder tools. The
TypeScript server (the npm package) is the macOS one; the Swift server is the
iOS one. Saying AirMCP "is not a Swift MCP server" was wrong.

Reading it changes the design in one place worth taking. Its registerTool
consults IOSPreviewContract (ios/Sources/AirMCPServer/PreviewTools.swift) and
refuses to register anything that is not on an explicit name allowlist and
annotated read-only and non-destructive -- failing closed at registration
rather than hiding the tool from tools/list. This surface had the same
property only by intention plus a test asserting nobody had broken it. Now it
is a mechanism: contract_allows gates the handler map and the descriptor list,
a rejected tool is unreachable rather than unlisted, and REJECTED_TOOLS makes
the rejection visible in --tools instead of silent.

The language choice stands and is in fact closer to the precedent than the
previous message claimed: the Swift server is 229 lines of hand-rolled
dispatch over a small tool table with no SDK, which is exactly the shape of
this one. Python remains right here because Modore's judgments are Python
scripts and this repository pins zero runtime dependencies.
The previous commit's message retracted an accurate finding. Checked again,
with the evidence this time:

  .mcp.json                     -> npx [email protected]          (TypeScript)
  app/.../ServerManager.swift   -> node <entry> --http / npx   (TypeScript)
  npm package "airmcp"                                          (TypeScript)
  ios/Sources/AirMCPServer      -> Swift, Hummingbird HTTP, README: "iOS
                                   runtime is in preview"

So AirMCP has two MCP servers. The one in operation -- the one its own app
spawns, the one this session's airmcp tools come from -- is TypeScript, which
is what the first commit said. The Swift server exists and is real, but it is
the preview iOS runtime; finding it does not make the original statement
wrong, it makes it incomplete.

The docstring now states both, and says which one each borrowed pattern came
from: descriptors, read-only annotations, and untrusted fencing from the
TypeScript server; the hand-rolled dispatch and the registration-time contract
from the Swift one.

cb37e4d's message cannot be fixed without a force push, so the correction is
recorded in the PR body instead. The code it added stands on its own.
@heznpc
heznpc merged commit d396431 into main Aug 19, 2026
8 checks passed
@heznpc
heznpc deleted the feat/friction-and-mcp branch August 19, 2026 07:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant