agentleFS
PsychQuantPsychQuant/che-ical-mcp/CLAUDE.md

che-ical-mcp

CLAUDE.md44 starsChanged 8 months agoMITView on GitHub

This project uses Spectra for Spec-Driven Development(SDD). Specs live in openspec/specs/, change proposals in openspec/changes/. discuss? → propose → apply ⇄ ingest → archive Changes can be parked(暫存)— temporarily moved out of openspec/changes/. Parked changes won't appear in spectra list but can be found with spectra list --parked. To restore: spectra unpark <name>. The /spectra:apply and /spectra:ingest skills handle parked changes automatically. Binary 更新後 session 內 MCP server 仍是舊版(重啟才換 — 且重啟也不保證換:plugin wrapper 的 auto-download 會被 Claude Code 的 15 分鐘…

The file

We split this CLAUDE.md into the rules it gives an agent

3 rules. About 0 are common, 0 are distinctive, and 3 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈0

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 3

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    When adding a test, ask: am I exercising a pure function (→ *Tests), the handler boundary (→ *HandlerTests), the outer dispatch shell (→ …

    line 55
  2. Very specific:

    Injection point: add a constructor parameter with EventKitManager.shared as the default.

    line 68
  3. Very specific:

    If a future use case argues for --setup triggering banner emission, revisit by writing a follow-up issue with the user-flow rationale — …

    line 86

≈ “Common” is an estimate: at least 1 in 5 of the 26,980 CLAUDE.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
CLAUDE.md
<!-- SPECTRA:START v1.0.1 -->

Spectra Instructions

This project uses Spectra for Spec-Driven Development(SDD). Specs live in openspec/specs/, change proposals in openspec/changes/.

Use /spectra:* skills when:

  • A discussion needs structure before coding → /spectra:discuss
  • User wants to plan, propose, or design a change → /spectra:propose
  • Tasks are ready to implement → /spectra:apply
  • There's an in-progress change to continue → /spectra:ingest
  • User asks about specs or how something works → /spectra:ask
  • Implementation is done → /spectra:archive

Workflow

discuss? → propose → apply ⇄ ingest → archive

  • discuss is optional — skip if requirements are clear
  • Requirements change mid-work? Plan mode → ingest → resume apply

Parked Changes

Changes can be parked(暫存)— temporarily moved out of openspec/changes/. Parked changes won't appear in spectra list but can be found with spectra list --parked. To restore: spectra unpark <name>. The /spectra:apply and /spectra:ingest skills handle parked changes automatically.

<!-- SPECTRA:END -->

MCP 功能驗證的表面選擇(.claude/rules/mcp-binary-version-testing.md)

Binary 更新後 session 內 MCP server 仍是舊版(重啟才換 — 且重啟也不保證換:plugin wrapper 的 auto-download 會被 Claude Code 的 15 分鐘 connection-failure cache 安靜跳過)。 驗證新功能前必做行為探測(呼叫新版才有的欄位看回應)確認測試表面版本,再依功能性質 選路徑:stateless → --cli 指 repo 內剛 build 的 binary;stateful(undo 等)→ 重啟後的 session MCP tools(--cli per-process stack 結構上驗不了;手動 spawn stdio 有 read-after-write confound)。三表面特性表、wrapper failure-cache 坑與 2026-08-31 #186/v1.16.1 事故記錄見 .claude/rules/mcp-binary-version-testing.md。

Test Naming Convention

Tests/CheICalMCPTests/ follows four filename suffixes that signal what each test exercises. The suffix is load-bearing — pick the right one when adding a new file so reviewers (and Claude) can locate the right test layer fast.

Filename patternLayerWhat it tests
<Subject>Tests.swiftPure unitFree functions / value types / pure helpers. No FakeEventKitManager and no EventKitManager instances (static-utility access like EventKitManager.setupCommandHint(binaryPath:) / resolvedSetupCommandHint() in SetupCommandTests is fine — it's a type-level probe, not handler integration). Examples: BatchDeleteFilterTests, ReminderCleanupTests, EventKitErrorSanitizerTests, ParticipantFormattingTests, SetupCommandTests.
<Subject>HandlerTests.swiftHandler integrationhandle* methods from CheICalMCPServer driven through FakeEventKitManager-equivalent doubles. Tests the handler's full sanitize → dispatch → response shape, not the EventKit manager itself. Canonical example: CleanupHandlerTests.swift — copy its structure when adding a new handler test.
<Subject>DispatchTests.swiftOuter-catch / dispatchhandleToolCallForTesting outer-catch and the dispatch JSON envelope. Probes what surfaces when a handler throws an unexpected error type. Examples: OuterCatchDispatchTests, DispatchRoundTripTests.
<Subject>WiringTests.swiftSource-scan wiring pinCode an in-memory event store cannot run (the order of calls in a store path, which helper a handler goes through), pinned by reading Sources/ with Helpers/SourcePins.swift / Helpers/SourceScan.swift. Examples: UndoBatchWiringTests, ReminderUndoWiringTests. A single source pin inside another *Tests file is fine; a file made of them takes this suffix. The helpers' own tests (SourcePinsTests, SourceScanTests) are pure *Tests.

Helper files (e.g., FakeEventKitManager.swift) carry no *Tests suffix.

Helpers/ subdirectory (Tests/CheICalMCPTests/Helpers/, #83): shared test infrastructure that is NOT a test class is exempt from the *Tests.swift naming convention. Files here provide cross-test utilities (e.g. StderrCaptureHarness.swift centralizing the dup2/Pipe stderr-capture pattern that the sanitizer cluster's carve-out tests share). The subdirectory's existence signals "library code for tests, not tests itself" — auditors looking for tests should search the parent directory; auditors looking for shared utilities should look here.

When adding a test, ask: am I exercising a pure function (→ *Tests), the handler boundary (→ *HandlerTests), the outer dispatch shell (→ *DispatchTests), or wiring only a source read can show (→ *WiringTests)? Mismatched suffix is a idd-verify finding worth flagging. Helpers go in Helpers/ regardless of suffix.

Test Seam Convention (DI for handlers)

CheICalMCPServer exposes EventKit primitives via two concurrent paths:

  1. Concrete singleton — eventKitManager: EventKitManager (used by 30+ handlers). Production behavior. Not unit-testable in isolation.
  2. Per-feature narrow protocol — e.g. reminderCleanupSource: any EventKitManaging (#31), used only by handleCleanupCompletedReminders. Defaults to EventKitManager.shared so production callers don't see the seam; tests inject a fake.

When a handler needs a test fake, introduce a new narrow *Source protocol scoped to that handler's surface area (1–3 methods is typical). Do NOT widen EventKitManaging to cover the new handler's needs — #31 D1 deliberately keeps that protocol tight to avoid forcing every fake to stub unrelated methods.

Naming: <Domain>Source (e.g. EventBatchDeletionSource, ReminderTagSource). The naming guidance applies to new protocols going forward; the existing EventKitManaging (#31, scoped narrow per its origin) keeps its name for compatibility — the protocol-typed parameter name (reminderCleanupSource) need not match the protocol's name (EventKitManaging). When introducing a new test seam, name the new protocol per <Domain>Source rather than reusing EventKitManaging.

Injection point: add a constructor parameter with EventKitManager.shared as the default. The concrete eventKitManager property stays — these coexist.

Canonical example: reminderCleanupSource in CheICalMCPServer.init + Tests/CheICalMCPTests/CleanupHandlerTests.swift. New handler tests should mirror that structure (the injection pattern — narrow protocol, default to shared singleton, inject in tests). The protocol name EventKitManaging is grandfathered; new protocols should use <Domain>Source.

This convention is per-handler doc, not a refactor: existing 30+ handlers continue to use eventKitManager directly — no migration debt. The seam appears only when a handler graduates into the test surface.

Closure-seam variant(#182):當要隔離測試的是純序列邏輯(而非 handler 的 sanitize→dispatch→envelope 面)時,可用 closure-based executor 取代 *Source protocol — 例:ExclusionExecutor(Sources/CheICalMCP/EventKit/ExclusionExecutor.swift),generic over occurrence type、以 resolve/remove/rollback 三個 closure 注入,ExclusionExecutorTests 以 stub closures pin two-pass 排序與 rollback 語意,零 EventKit 依賴。判準:protocol seam 給「handler 需要 fake 一組 manager 方法」;closure seam 給「manager 內部有一段可抽出的純協調邏輯」。兩者都不得擴 EventKitManaging。

Startup Banner CLI Skip List (#122 + #130)

The TCC drift detector startup banner (#122) intentionally does NOT run for these CLI side-channels: --version, --help, --setup, --print-tcc-path, --self-update, --cli. All of them exit before reaching the MCP server default branch where emitStartupBanner() lives.

--setup deserves explicit justification (#130 — surfaced as Codex M5 finding in idd-verify #122): the Strategy Lock comment on #122 included --setup running drift check at start as Acceptance Criterion #7, but the Plan tier (and shipped implementation) silently retracted it. The retraction is intentional and stands:

  • --setup is the manual remediation path, not a diagnostic surface. Its job is to drive the macOS TCC permission dialog round-trip; the banner's drift signals are noise during that dialog flow and risk distracting users who are mid-prompt.
  • Drift signals address "the running binary may not be the one TCC granted access to" — but --setup is specifically the binary the user just decided to authorize. Re-surfacing drift here would be cargo-culted output, not actionable.
  • The diagnostic path that does surface drift signals is --print-tcc-path (which prints the banner-style binary path + bundle ID + TCC state). Users who want to see drift before running --setup already have a clear ordering: --print-tcc-path → review → --setup.

If a future use case argues for --setup triggering banner emission, revisit by writing a follow-up issue with the user-flow rationale — don't flip the skip list silently.

Test coverage of the skip list is currently partial: testNoBannerForVersionFlag + testNoBannerForHelpFlag spawn the binary and exercise --version + --help. testNoBannerForSetupFlag checks --setup structurally over the source (#249): the --setup block sits above the top-level emitStartupBanner() call, its non-interactive branch ends in exit( and its interactive branch calls SetupRunner.runInteractive(), declared -> Never. It does not spawn the binary: in a GUI session --setup runs the interactive SetupWindow app, which takes focus and never exits by itself, and a test cannot make the child non-interactive (#143 keeps CI from doing so). No test opens the setup window. The other 3 skip targets (--print-tcc-path, --self-update, --cli) are skipped structurally because they call exit(0) before emitStartupBanner() is reached (control-flow guarantee in main.swift), with no test of their own. Adding testNoBannerForXxxFlag tests for them is tracked as a follow-up (low priority — the control-flow guarantee is straightforward to read).

#131 update (banner tests now run on CI): the TCCDriftDetectorBannerTests binary-spawn tests previously carried a runtime skipIfCI() guard while #131's GHA hang was unexplained. #131 is resolved (root cause = AuthorizationGate blocking on requestFullAccess for .notDetermined in a non-interactive session, now fast-failing), so the skipIfCI() guard is removed — all banner tests in the file run on CI. The earlier "skip limits GHA-hang exposure" caveat no longer applies: spawnAndCaptureStderr bounds every wait with a maxWait cap + SIGTERM→SIGKILL escalation + 3s hard waitUntilExit cap, and the spawned binary inherits the same EventKit fast-fail under CI=1. Since #233 the helper waits for the child to exit (it exits on stdin EOF right after the banner) or for an optional until: predicate to match, and maxWait (default 10s) only caps a hung child; the returned timedOut flag says when the cap ended the wait, and tests whose child must exit by itself assert it is false. The former fixed 1s window SIGTERM'd the child before its single banner write whenever that write came later than 1s, which read as "no banner". A healthy banner run already takes roughly 0.4–1.0s from spawn to exit (about 0.02s with the banner suppressed), so that window had little margin even on a lightly loaded host, and under contention the banner arrived as late as 1.67s. A hung child now costs at most ~16.5s per helper call (10s cap + 0.5s SIGTERM grace + 3s wait cap + 1s force-reap + two 1s drain bounds; the same breakdown with the old 1s cap gave 7.5s), never the 20m job timeout; most tests make one call, and the warmed default-mode test makes two (warm-up + timed spawn), so its worst case is ~33s. testBannerAppearsInDefaultMCPServerMode runs an untimed --version warm-up (so a first-exec Gatekeeper assessment stays out of the clock) and then times the banner's arrival via until: against a 5s tripwire, deliberately looser than #127's former 1.5s bound on the helper's whole run, which real arrivals on a busy host exceeded.

Manifest display_name — no XML metacharacters (#166 CONFIRMED ROOT CAUSE)

Invariant: mcpb/manifest.json display_name MUST NOT contain &, <, or >. A literal & in display_name makes Claude Desktop 1.18286.0's tool-injection layer silently drop the entire server from every conversation — the transport handshake + tools/list complete (Desktop receives the full list) but no tool is ever injected, and nothing surfaces in any log (the drop is above the MCP-protocol layer). Claude Code has no such layer, so it worked end-to-end throughout.

This is the confirmed #166 root cause, proven by single-variable intervention on the exact failing Desktop install (2026-07-03): with the 29-tool binary and manifest otherwise byte-identical, changing display_name from "macOS Calendar & Reminders" → "macOS Calendar and Reminders" flipped the server from dropped → injecting real EventKit data. It also explains the regression timeline exactly — the & predated the 2026-07-02 Desktop update; the update changed how the injection layer handles it. ManifestParityTests.testDisplayNameHasNoXMLMetacharacters guards this at swift test time. Only & is confirmed-breaking; </> are guarded alongside as same-class (XML/HTML metacharacter) defense-in-depth.

Method note (reusable): this was settled with a local Developer-ID hotswap (build + codesign --sign "$DEVELOPER_ID" --options runtime, then cp into the installed .mcpb server/ dir + hand-edit the installed manifest.json, then user Cmd+Q → reopen). Notarization (403, expired Apple agreement) blocks distribution to other machines, NOT local testing — a locally-placed Developer-ID binary runs on macOS 26 and keeps its TCC grant (csreq keyed to Identifier + TeamID, not cdhash). This is how a Desktop-only injection bug was root-caused without a notarized release. Desktop-injection causation can only be settled by direct single-variable intervention — two compelling correlations (H6 serverInfo.name n=4; tool schema-depth/desc-length outliers) both misled and were empirically refuted.

Umbrella note (#166 display_name sweep — RESOLVED 2026-07-04): a sweep of all che-mcps mcpb/manifest.json files had found one other server with a & in display_name — che-duckdb-mcp ("DuckDB Documentation & Database"), which would have dropped identically under Desktop 1.18286.0. Now fixed: che-duckdb-mcp commit 82ab882 (& → and; display_name is now "DuckDB Documentation and Database"). A re-sweep of every che-mcps submodule manifest on 2026-07-04 came back clean — no che-mcps display_name carries &/</> any more.

MCP Server Identity Convention (#166 — hygiene, NOT the Desktop-drop cause)

Invariant: the value passed as Server(name:) (i.e. the runtime serverInfo.name) MUST equal mcpb/manifest.json name. Both are che-ical-mcp (kebab). This alignment was #166's earlier leading hypothesis (H6) — that Desktop 1.18286.0 reconciles serverInfo.name against the manifest id and drops the server on mismatch — but it was empirically refuted: fixing the name and swapping the corrected binary into the failing Desktop install did NOT restore injection; only removing the & from display_name did. The invariant is retained anyway because a server's serverInfo.name matching its manifest id is a baseline MCP expectation independent of the Desktop bug.

Two distinct name constants in Version.swift — do not conflate them:

ConstantValueFeedsMust match
AppVersion.mcpServerNameche-ical-mcp (kebab)Server(name:) → serverInfo.namemcpb/manifest.json name
AppVersion.nameCheICalMCP (Pascal)--version, --help Usage lines, argv0 fallbacksthe on-disk binary filename

AppVersion.name is the binary/product name and is correctly PascalCase (the executable is CheICalMCP); mcpServerName is the MCP protocol identity and must be the kebab manifest id. ManifestParityTests.testServerInfoNameMatchesManifestName enforces the invariant at swift test time (alongside the existing tools[].name parity guard). When cloning this repo's structure to a new MCP server, wire Server(name:) to a kebab constant that equals the manifest, not to the binary name.

Umbrella note (#166 sister sweep): the same serverInfo.name ≠ manifest-id mismatch exists in che-apple-notes-mcp (CheAppleNotesMCP) and che-xcode-mcp (CheXcodeMCP), and likely che-contacts-mcp (CheContactsMCP). This is now moot for the Desktop-drop symptom — the mismatch is refuted as the cause (the actual cause is the display_name &, above). These alignments remain worthwhile hygiene and ride along on each server's next release, but they are NOT a Desktop-injection fix. The one sibling that carried the actual Desktop-drop & was che-duckdb-mcp, now fixed (82ab882, 2026-07-04) — the umbrella display_name sweep is clean (see the note above).

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.