MCP Events: push real-time events (mail, money, tasks, calendar, shares) to ChatGPT automations #491

Open
opened 2026-09-30 07:16:41 +00:00 by kayg · 18 comments
Owner

Request (owner, 2026-09-30): support MCP Events, "let's make mcp crazy fast"

Background: MCP Events is a draft extension in the Model Context Protocol organisation (modelcontextprotocol/experimental-ext-triggers-events, design-sketch-proposal.md). ChatGPT supports its webhook delivery (developers.openai.com/plugins/build/mcp-events). It needs MCP protocol version 2026-07-28.
Scope:

  • events/list declares calternal's events, generated from the #484 action registry: each action says which events it emits. The first set:
    • mail.received (with filters: sender, account, folder)
    • mail.statement_received and mail.receipt_received
    • money.transaction_logged, money.category_overspent, money.bill_due (N days)
    • calendar.event_starting (N minutes)
    • task.due, task.overdue, reminder.fired
    • share.item_changed and share.comment_created
    • files.added_to_folder
  • events/subscribe and unsubscribe: idempotent, bound to the authenticated User, the server grants an expiry, and ChatGPT refreshes before refreshBefore.
  • Delivery: HTTPS webhooks only, signed with Standard Webhooks HMAC (webhook-id, webhook-timestamp, webhook-signature, X-MCP-Subscription-Id). Signed challenge verification before a subscription goes live (constant-time compare). One event per request, max 256 KiB. Exponential backoff; stop on 410 and 413.
  • Security: callbacks must not reach private or loopback addresses; resolve the address once and connect to that address while TLS still checks the original hostname (reuse the webcal SSRF guard, #431). Re-check the User's access and the App Password on every delivery and stop at once on revoke. Per-User subscription cap and rate limit. No other User's data can ever enter an event (#472 matrix and #483 cover the new routes).
  • Settings: under Apps & Devices, an on/off switch and a list of active subscriptions with revoke.
  • It is a draft spec: put it behind its own switch and version the handling.
    Order: after the #484 registry core lands.
## Request (owner, 2026-09-30): support MCP Events, "let's make mcp crazy fast" **Background:** MCP Events is a draft extension in the Model Context Protocol organisation (`modelcontextprotocol/experimental-ext-triggers-events`, design-sketch-proposal.md). ChatGPT supports its webhook delivery (developers.openai.com/plugins/build/mcp-events). It needs MCP protocol version `2026-07-28`. **Scope:** - `events/list` declares calternal's events, generated from the #484 action registry: each action says which events it emits. The first set: - mail.received (with filters: sender, account, folder) - mail.statement_received and mail.receipt_received - money.transaction_logged, money.category_overspent, money.bill_due (N days) - calendar.event_starting (N minutes) - task.due, task.overdue, reminder.fired - share.item_changed and share.comment_created - files.added_to_folder - `events/subscribe` and unsubscribe: idempotent, bound to the authenticated User, the server grants an expiry, and ChatGPT refreshes before `refreshBefore`. - **Delivery:** HTTPS webhooks only, signed with Standard Webhooks HMAC (`webhook-id`, `webhook-timestamp`, `webhook-signature`, `X-MCP-Subscription-Id`). Signed challenge verification before a subscription goes live (constant-time compare). One event per request, max 256 KiB. Exponential backoff; stop on 410 and 413. - **Security:** callbacks must not reach private or loopback addresses; resolve the address once and connect to that address while TLS still checks the original hostname (reuse the webcal SSRF guard, #431). Re-check the User's access and the App Password on every delivery and stop at once on revoke. Per-User subscription cap and rate limit. No other User's data can ever enter an event (#472 matrix and #483 cover the new routes). - **Settings:** under Apps & Devices, an on/off switch and a list of active subscriptions with revoke. - It is a draft spec: put it behind its own switch and version the handling. **Order:** after the #484 registry core lands.
Author
Owner

What MCP Events is, in plain words (for the owner)

Today an AI assistant (ChatGPT, Claude) only learns about your calternal data when you ask it something: it calls calternal's MCP tools, reads, and answers. It cannot react when something happens.

MCP Events turns this around. The assistant tells calternal once: "tell me when a bank statement mail arrives" (a subscription). From then on, calternal pushes a small signed message (a webhook) to the assistant the moment that happens. The assistant then runs the automation you set up, for example: read the statement, log the transactions in Money, and message you if a category is overspent.

It is the same idea as your iOS Shortcuts → Windmill → Actual pipeline (#480), but built in: no polling, no middle server, and calternal decides exactly what each assistant may see.

What it is and is not:

  • It is a draft extension of the MCP spec (the protocol), maintained in the MCP organisation. It is not a ChatGPT-only feature. ChatGPT is the first client that supports its webhook delivery. Other MCP clients can adopt it later, and calternal's side stays the same.
  • calternal is the sender. Each User controls it in Settings: an on/off switch, a list of active subscriptions, and revoke. Revoking the App Password stops all its events at once.
  • It is safe by design:
    • every message is signed, and the receiving address is checked (never a private or local address);
    • access is re-checked before every delivery;
    • there is a per-User rate limit;
    • no other User's data can enter an event (the isolation matrix covers it).

Example events (first set): new mail (by sender or account), a statement or receipt received, a transaction logged, a category overspent, a bill due in N days, an event starting in N minutes, a task due or overdue, a reminder fired, a shared item changed or commented on, a file added to a folder.

Tonight

It builds on the one action registry from #484 (parity), which is finishing its last round. Once #484 merges, a Sol medium job (reason: signed webhooks and outbound HTTP are security-sensitive) builds:

  • the event registry;
  • subscribe/unsubscribe;
  • signed delivery with retries;
  • the outbound address guard (reusing the webcal one);
  • the Settings switch and list;
  • two-User isolation tests and a replay test against the reference receiver.

It stays behind its own switch because the spec is a draft.

Decisions for the morning (not blocking the build; defaults in brackets)

  1. Default: is the switch ON for every User [ON, opinionated default; nothing is sent until an assistant subscribes], or OFF until turned on?
  2. Mail content: does an event carry the mail subject and snippet [subject + sender only; the assistant fetches the body through a normal MCP call, which is logged], or the full body?
  3. Money amounts: do events carry transaction amounts [yes, for the User's own assistant], or only "a transaction was logged"?
## What MCP Events is, in plain words (for the owner) Today an AI assistant (ChatGPT, Claude) only learns about your calternal data when **you** ask it something: it calls calternal's MCP tools, reads, and answers. It cannot react when something happens. **MCP Events turns this around.** The assistant tells calternal once: "tell me when a bank statement mail arrives" (a *subscription*). From then on, calternal **pushes** a small signed message (a *webhook*) to the assistant the moment that happens. The assistant then runs the automation you set up, for example: read the statement, log the transactions in Money, and message you if a category is overspent. It is the same idea as your iOS Shortcuts → Windmill → Actual pipeline (#480), but built in: no polling, no middle server, and calternal decides exactly what each assistant may see. **What it is and is not:** - It is a **draft extension of the MCP spec** (the protocol), maintained in the MCP organisation. It is not a ChatGPT-only feature. ChatGPT is the first client that supports its webhook delivery. Other MCP clients can adopt it later, and calternal's side stays the same. - calternal is the **sender**. Each User controls it in Settings: an on/off switch, a list of active subscriptions, and revoke. Revoking the App Password stops all its events at once. - It is safe by design: - every message is signed, and the receiving address is checked (never a private or local address); - access is re-checked before every delivery; - there is a per-User rate limit; - no other User's data can enter an event (the isolation matrix covers it). **Example events (first set):** new mail (by sender or account), a statement or receipt received, a transaction logged, a category overspent, a bill due in N days, an event starting in N minutes, a task due or overdue, a reminder fired, a shared item changed or commented on, a file added to a folder. ## Tonight It builds on the one action registry from #484 (parity), which is finishing its last round. Once #484 merges, a Sol medium job (reason: signed webhooks and outbound HTTP are security-sensitive) builds: - the event registry; - subscribe/unsubscribe; - signed delivery with retries; - the outbound address guard (reusing the webcal one); - the Settings switch and list; - two-User isolation tests and a replay test against the reference receiver. It stays behind its own switch because the spec is a draft. ## Decisions for the morning (not blocking the build; defaults in brackets) 1. **Default:** is the switch ON for every User [ON, opinionated default; nothing is sent until an assistant subscribes], or OFF until turned on? 2. **Mail content:** does an event carry the mail subject and snippet [subject + sender only; the assistant fetches the body through a normal MCP call, which is logged], or the full body? 3. **Money amounts:** do events carry transaction amounts [yes, for the User's own assistant], or only "a transaction was logged"?
Author
Owner

Owner decisions (2026-10-01): (1) MCP Events is on by default for every User. A client that does not support the extension never calls events/subscribe, so nothing is sent to it. (2) Mail events carry the subject + sender only (the body via a normal, logged MCP call). (3) Money events include amounts for the User's own assistant. Build after #484 core merges (merge round 4).

**Owner decisions (2026-10-01):** (1) MCP Events is **on by default for every User**. A client that does not support the extension never calls `events/subscribe`, so nothing is sent to it. (2) Mail events carry the subject + sender only (the body via a normal, logged MCP call). (3) Money events include amounts for the User's own assistant. Build after #484 core merges (merge round 4).
Author
Owner

Started on job/mcp-events-491 at base 1af8ead262. Read the contract and both owner comments. Inspecting registry metadata, MCP transport, App Password policy and the webcal outbound guard before building.

Started on job/mcp-events-491 at base 1af8ead262daf0a4ad22a60564dc75bab0005cf4. Read the contract and both owner comments. Inspecting registry metadata, MCP transport, App Password policy and the webcal outbound guard before building.
Author
Owner

Finding: the webcal resolver applies an admin private-network allowlist, so reusing it directly would admit private callback addresses. Added a public-only entry that shares its resolver and address classification but does not apply the allowlist. The MCP Events switch also needs a new auth migration because existing app_surface CHECK constraints reject new surface names. Decisions: App Password is the subscription principal; short grants use bounded in-memory state and cursor null, as permitted by the draft; receiver URLs shown in Settings are reduced to their origin so callback query credentials cannot leak.

Finding: the webcal resolver applies an admin private-network allowlist, so reusing it directly would admit private callback addresses. Added a public-only entry that shares its resolver and address classification but does not apply the allowlist. The MCP Events switch also needs a new auth migration because existing app_surface CHECK constraints reject new surface names. Decisions: App Password is the subscription principal; short grants use bounded in-memory state and cursor null, as permitted by the draft; receiver URLs shown in Settings are reduced to their origin so callback query credentials cannot leak.
Author
Owner

Backend progress: signed delivery and the reference-verifier replay test passed in the server suite (113 passed, 0 failed, 3 ignored). Added source workers for due Tasks, recurring Calendar Events, Files changes, and Money bills; bill reads share the ordinary Money read lock. Found and fixed a queue race: an old in-flight ACK could remove an overflow gap. A regression test now checks that gap survives. Known source gap: the #484 registry has no comment-creation action, so share.comment_created is not advertised. Statement detection follows Mail’s conservative subject-only signals; it does not read a body.

Backend progress: signed delivery and the reference-verifier replay test passed in the server suite (113 passed, 0 failed, 3 ignored). Added source workers for due Tasks, recurring Calendar Events, Files changes, and Money bills; bill reads share the ordinary Money read lock. Found and fixed a queue race: an old in-flight ACK could remove an overflow gap. A regression test now checks that gap survives. Known source gap: the #484 registry has no comment-creation action, so share.comment_created is not advertised. Statement detection follows Mail’s conservative subject-only signals; it does not read a body.
Author
Owner

Integrated origin/dev at 6e2ed179a. Kept the upstream bounded legacy MCP sessions and added the shared Events Hub to both modes. Auth migration 12 is still free on origin/dev. The web check exposed an OpenAPI schema collision with Calendar SubscriptionView; renamed the Events schema to McpEventSubscriptionView. Added a real Mail commit regression for new delta arrivals, replay and backfill. The planned first set has thirteen event names; twelve have existing sources, and only share.comment_created remains unavailable. Queue performance profile covers 256 subscriptions and maximum-length subject/sender fields; live profile covers the full visible registry and a bounded request burst. Auth gates: 65 passed, 0 failed; server reference receiver and two-User Settings route tests pass. Final regenerated clients, per-crate gates and production screenshots are in progress.

Integrated origin/dev at 6e2ed179a. Kept the upstream bounded legacy MCP sessions and added the shared Events Hub to both modes. Auth migration 12 is still free on origin/dev. The web check exposed an OpenAPI schema collision with Calendar SubscriptionView; renamed the Events schema to McpEventSubscriptionView. Added a real Mail commit regression for new delta arrivals, replay and backfill. The planned first set has thirteen event names; twelve have existing sources, and only share.comment_created remains unavailable. Queue performance profile covers 256 subscriptions and maximum-length subject/sender fields; live profile covers the full visible registry and a bounded request burst. Auth gates: 65 passed, 0 failed; server reference receiver and two-User Settings route tests pass. Final regenerated clients, per-crate gates and production screenshots are in progress.
Author
Owner

Completion report

Branch: job/mcp-events-491. Head: dadd320e24da39698112bab839edb3d2cb746dc7.
Initial base: 1af8ead262daf0a4ad22a60564dc75bab0005cf4.
The required origin/dev integration is commit 6e2ed179a. No push or deploy.
Ten atomic work commits cover the registry, access switch, producers, server,
Settings and performance evidence. Migration 0012 was free on origin/dev at
integration. The working tree is clean. Doc comments were checked before this
report. No review artifact is committed.

Built

  • MCP Events draft 2026-02-19: initialization capability, registry-generated
    events/list, events/subscribe and events/unsubscribe. Twelve event types
    have real producers. MCP Events has a separate default-on surface switch.
  • HTTPS webhook challenge and delivery with Standard Webhooks HMAC. DNS uses
    the shared public-address guard, checks all answers and pins addresses.
    Redirects, environment proxies and private addresses are blocked. Each send
    checks the live User, App Password, plugin, both switches and item access.
  • Temporary grants, per-User limits, an Instance cap, bounded queues, gap
    events, retries, deduplication and fresh registration epochs. The epoch
    prevents a revoked or recreated grant from accepting an old delivery result.
  • Mail events project only subject and sender. Replay and backfill are silent.
    Money events carry exact integer minor-unit amounts. Calendar, Tasks, Bills,
    Files, Shares and live reminders use existing stores and authority checks.
  • Settings shows the access switch, real active subscriptions, stable Copy
    links and revoke. Admin Settings includes the separate surface policy.
  • Reference receiver replay tests, two-User route tests, bounded real-server
    draft checks, generator tests and local hot-path profiles.

Validation

All touched-crate gates passed. Server tests cover signed nonce verification,
Unicode payload replay, duplicate IDs, changed-body rejection, private/mixed
address rejection, access loss, key revoke, expiry, queue bounds, fair rate
limits, registration fencing and the two-User Settings routes. The reference
receiver runs locally in a test. It does not prove public TLS interoperability.

One bounded real-server round passed initialization, all twelve descriptors,
Settings reads, malformed params and immediate surface denial. It found a
finite SSE initialization response that lacked the draft capability. The fix
has a regression test for both JSON and SSE envelopes. No 5xx or crash remained
in this round. Only the real discovery benchmark had slow results.

Commands were cargo fmt --check, then cargo clippy -p <crate> --all-targets -- -D warnings and cargo test -p <crate> for API, Auth, Plugin, Calendar, Mail,
Money, Notifications and Server. Cargo used four jobs and no incremental
build. Web commands were bun run check, bun run test and bun run build.
The production build passed. Registry generation --check passed with
Action registry: 335 operations, 317 generated tools.

Gate output below is verbatim. The fmt line records its silent exit 0.

cargo fmt --check: exit 0 (no output)

auth
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 2m 59s
test result: ok. 65 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 56.54s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

calternal-api
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 2m 57s
test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.04s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

calternal-plugin
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 10m 07s
test result: ok. 24 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 12.58s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

calternal-plugin-calendar
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 9m 07s
test result: ok. 84 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 5.25s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.13s
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.16s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

calternal-plugin-mail
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 3m 08s
test result: ok. 41 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 3.66s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

calternal-plugin-money
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 3m 06s
test result: ok. 24 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 9.82s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

calternal-plugin-notifications
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 2m 48s
test result: ok. 24 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.41s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

final-server
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 5m 12s
test result: ok. 118 passed; 0 failed; 4 ignored; 0 measured; 0 filtered out; finished in 18.13s

web
svelte-check found 0 errors and 0 warnings
 Test Files  148 passed (148)
      Tests  1011 passed (1011)
   Duration  123.34s (transform 52%, environment 18%, import 13%, tests 9%, setup 8%)

registry
............
----------------------------------------------------------------------
Ran 12 tests in 0.383s

OK

Performance

The perf VM was unreachable (No route to host). These are local debug
samples, not release measurements. docs/perf/baseline.json has no matching
profile, so these numbers do not establish a threshold regression.

Profile p50 ms p95 ms CPU % Mean RSS MiB Peak RSS MiB
Discovery, 30 requests, concurrency 1 1542.50 2875.43 32.00 234.47 242.34
Discovery, 64 requests, concurrency 8 2781.02 5237.58 84.26 308.87 319.66
Queue average, 16 grants 0.67 1.06 combined below combined below combined below
Queue cap, 256 grants, 1024 occurrences 6.66 8.91 99.30 59.93 63.23

Discovery load average: 41.80, 32.53, 32.40. Queue load average: 11.37, 14.04,
22.55. Queue CPU/RSS cover both fixture phases. Queue measurements exclude
compilation, DNS, TLS and the receiver. The largest fixture uses 4096 bytes per
text field and checks the 32-entry queue bound. Source scan latency was not
measured. Repeat with the shared release build on the perf VM.

Known gaps

  • share.comment_created has no action or comment store in dev. It is not
    advertised. No new comment model was invented in this job.
  • Phone Settings can select the hidden App Passwords heading for another
    group. Evidence and cause are filed in #649. This shared cosmetic defect
    does not change event access or delivery.
  • Statement classification uses a conservative English subject check and can
    miss statements in other languages.
  • Registration against a real public HTTPS receiver is not measured here.
    The local reference receiver validates signatures and replay behavior.

Decisions

  • Emit-only, in-memory subscriptions last at most ten minutes. Restart clears
    them. A supplied cursor reports truncation. Refresh preserves pending state;
    signing-key rotation takes effect at once.
  • Tasks, Calendar and Files poll every 15 seconds while interests exist.
    Bills poll every 30 seconds. Date-only Tasks are due at local midnight and
    overdue at the next local midnight. Timed Tasks are overdue one second after
    their due time. Bills use the User's local day and the days filter.
  • The cap is 16 grants per User and 256 per Instance. Subscribe attempts are
    limited to ten per User per minute; delivery is limited to 60 per User per
    minute. Four outbound sockets are shared across verification and delivery.
  • Settings displays the receiver origin and keeps URL paths and secrets on
    the server. A fresh epoch fences refreshes and recreation as well as revoke.

Files

  • Cargo.lock
  • apps/web/src/routes/settings/apps/AppsSection.svelte
  • apps/web/src/routes/settings/apps/McpEventsGroup.svelte
  • apps/web/src/routes/settings/sections.ts
  • bench/mcp-events-queue.sh
  • bench/mcp-events.mjs
  • contracts/action-events.json
  • contracts/actions.json
  • contracts/openapi.json
  • crates/calternal-api/src/actions.rs
  • crates/calternal-api/src/events.rs
  • crates/calternal-api/src/lib.rs
  • crates/calternal-auth/migrations/0012_mcp_events_surface.sql
  • crates/calternal-auth/src/store.rs
  • crates/calternal-plugin/src/lib.rs
  • crates/calternal-plugin/src/mcp_events.rs
  • crates/calternal-server/Cargo.toml
  • crates/calternal-server/src/main.rs
  • crates/calternal-server/src/mcp.rs
  • crates/calternal-server/src/mcp_event_sources.rs
  • crates/calternal-server/src/mcp_events.rs
  • crates/calternal-server/src/wire.rs
  • crates/plugins/calendar/src/feeds/mod.rs
  • crates/plugins/calendar/src/feeds/subscriptions.rs
  • crates/plugins/calendar/src/lib.rs
  • crates/plugins/calendar/src/view.rs
  • crates/plugins/mail/src/cache/store.rs
  • crates/plugins/mail/src/sync.rs
  • crates/plugins/money/Cargo.toml
  • crates/plugins/money/src/events.rs
  • crates/plugins/money/src/lib.rs
  • crates/plugins/money/src/routes.rs
  • crates/plugins/notifications/src/store.rs
  • docs/action-registry.md
  • docs/mcp-events.md
  • docs/perf/2026-10-01-mcp-events-491.md
  • packages/api-client/src/generated.ts
  • scripts/action_registry.py
  • scripts/test_action_registry.py
  • tests/adversarial/authz_matrix.py
  • tests/adversarial/mcp_events.mjs

Evidence

## Completion report Branch: `job/mcp-events-491`. Head: `dadd320e24da39698112bab839edb3d2cb746dc7`. Initial base: `1af8ead262daf0a4ad22a60564dc75bab0005cf4`. The required `origin/dev` integration is commit `6e2ed179a`. No push or deploy. Ten atomic work commits cover the registry, access switch, producers, server, Settings and performance evidence. Migration 0012 was free on origin/dev at integration. The working tree is clean. Doc comments were checked before this report. No review artifact is committed. ## Built - MCP Events draft `2026-02-19`: initialization capability, registry-generated `events/list`, `events/subscribe` and `events/unsubscribe`. Twelve event types have real producers. MCP Events has a separate default-on surface switch. - HTTPS webhook challenge and delivery with Standard Webhooks HMAC. DNS uses the shared public-address guard, checks all answers and pins addresses. Redirects, environment proxies and private addresses are blocked. Each send checks the live User, App Password, plugin, both switches and item access. - Temporary grants, per-User limits, an Instance cap, bounded queues, gap events, retries, deduplication and fresh registration epochs. The epoch prevents a revoked or recreated grant from accepting an old delivery result. - Mail events project only subject and sender. Replay and backfill are silent. Money events carry exact integer minor-unit amounts. Calendar, Tasks, Bills, Files, Shares and live reminders use existing stores and authority checks. - Settings shows the access switch, real active subscriptions, stable Copy links and revoke. Admin Settings includes the separate surface policy. - Reference receiver replay tests, two-User route tests, bounded real-server draft checks, generator tests and local hot-path profiles. ## Validation All touched-crate gates passed. Server tests cover signed nonce verification, Unicode payload replay, duplicate IDs, changed-body rejection, private/mixed address rejection, access loss, key revoke, expiry, queue bounds, fair rate limits, registration fencing and the two-User Settings routes. The reference receiver runs locally in a test. It does not prove public TLS interoperability. One bounded real-server round passed initialization, all twelve descriptors, Settings reads, malformed params and immediate surface denial. It found a finite SSE initialization response that lacked the draft capability. The fix has a regression test for both JSON and SSE envelopes. No 5xx or crash remained in this round. Only the real discovery benchmark had slow results. Commands were `cargo fmt --check`, then `cargo clippy -p <crate> --all-targets -- -D warnings` and `cargo test -p <crate>` for API, Auth, Plugin, Calendar, Mail, Money, Notifications and Server. Cargo used four jobs and no incremental build. Web commands were `bun run check`, `bun run test` and `bun run build`. The production build passed. Registry generation `--check` passed with `Action registry: 335 operations, 317 generated tools`. Gate output below is verbatim. The fmt line records its silent exit 0. ```text cargo fmt --check: exit 0 (no output) auth Finished `dev` profile [unoptimized + debuginfo] target(s) in 2m 59s test result: ok. 65 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 56.54s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s calternal-api Finished `dev` profile [unoptimized + debuginfo] target(s) in 2m 57s test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.04s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s calternal-plugin Finished `dev` profile [unoptimized + debuginfo] target(s) in 10m 07s test result: ok. 24 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 12.58s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s calternal-plugin-calendar Finished `dev` profile [unoptimized + debuginfo] target(s) in 9m 07s test result: ok. 84 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 5.25s test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.13s test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.16s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s calternal-plugin-mail Finished `dev` profile [unoptimized + debuginfo] target(s) in 3m 08s test result: ok. 41 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 3.66s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s calternal-plugin-money Finished `dev` profile [unoptimized + debuginfo] target(s) in 3m 06s test result: ok. 24 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 9.82s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s calternal-plugin-notifications Finished `dev` profile [unoptimized + debuginfo] target(s) in 2m 48s test result: ok. 24 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.41s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s final-server Finished `dev` profile [unoptimized + debuginfo] target(s) in 5m 12s test result: ok. 118 passed; 0 failed; 4 ignored; 0 measured; 0 filtered out; finished in 18.13s web svelte-check found 0 errors and 0 warnings Test Files 148 passed (148) Tests 1011 passed (1011) Duration 123.34s (transform 52%, environment 18%, import 13%, tests 9%, setup 8%) registry ............ ---------------------------------------------------------------------- Ran 12 tests in 0.383s OK ``` ## Performance The perf VM was unreachable (`No route to host`). These are local debug samples, not release measurements. `docs/perf/baseline.json` has no matching profile, so these numbers do not establish a threshold regression. | Profile | p50 ms | p95 ms | CPU % | Mean RSS MiB | Peak RSS MiB | | --- | ---: | ---: | ---: | ---: | ---: | | Discovery, 30 requests, concurrency 1 | 1542.50 | 2875.43 | 32.00 | 234.47 | 242.34 | | Discovery, 64 requests, concurrency 8 | 2781.02 | 5237.58 | 84.26 | 308.87 | 319.66 | | Queue average, 16 grants | 0.67 | 1.06 | combined below | combined below | combined below | | Queue cap, 256 grants, 1024 occurrences | 6.66 | 8.91 | 99.30 | 59.93 | 63.23 | Discovery load average: 41.80, 32.53, 32.40. Queue load average: 11.37, 14.04, 22.55. Queue CPU/RSS cover both fixture phases. Queue measurements exclude compilation, DNS, TLS and the receiver. The largest fixture uses 4096 bytes per text field and checks the 32-entry queue bound. Source scan latency was not measured. Repeat with the shared release build on the perf VM. ## Known gaps - `share.comment_created` has no action or comment store in dev. It is not advertised. No new comment model was invented in this job. - Phone Settings can select the hidden App Passwords heading for another group. Evidence and cause are filed in #649. This shared cosmetic defect does not change event access or delivery. - Statement classification uses a conservative English subject check and can miss statements in other languages. - Registration against a real public HTTPS receiver is not measured here. The local reference receiver validates signatures and replay behavior. ## Decisions - Emit-only, in-memory subscriptions last at most ten minutes. Restart clears them. A supplied cursor reports truncation. Refresh preserves pending state; signing-key rotation takes effect at once. - Tasks, Calendar and Files poll every 15 seconds while interests exist. Bills poll every 30 seconds. Date-only Tasks are due at local midnight and overdue at the next local midnight. Timed Tasks are overdue one second after their due time. Bills use the User's local day and the days filter. - The cap is 16 grants per User and 256 per Instance. Subscribe attempts are limited to ten per User per minute; delivery is limited to 60 per User per minute. Four outbound sockets are shared across verification and delivery. - Settings displays the receiver origin and keeps URL paths and secrets on the server. A fresh epoch fences refreshes and recreation as well as revoke. ## Files - `Cargo.lock` - `apps/web/src/routes/settings/apps/AppsSection.svelte` - `apps/web/src/routes/settings/apps/McpEventsGroup.svelte` - `apps/web/src/routes/settings/sections.ts` - `bench/mcp-events-queue.sh` - `bench/mcp-events.mjs` - `contracts/action-events.json` - `contracts/actions.json` - `contracts/openapi.json` - `crates/calternal-api/src/actions.rs` - `crates/calternal-api/src/events.rs` - `crates/calternal-api/src/lib.rs` - `crates/calternal-auth/migrations/0012_mcp_events_surface.sql` - `crates/calternal-auth/src/store.rs` - `crates/calternal-plugin/src/lib.rs` - `crates/calternal-plugin/src/mcp_events.rs` - `crates/calternal-server/Cargo.toml` - `crates/calternal-server/src/main.rs` - `crates/calternal-server/src/mcp.rs` - `crates/calternal-server/src/mcp_event_sources.rs` - `crates/calternal-server/src/mcp_events.rs` - `crates/calternal-server/src/wire.rs` - `crates/plugins/calendar/src/feeds/mod.rs` - `crates/plugins/calendar/src/feeds/subscriptions.rs` - `crates/plugins/calendar/src/lib.rs` - `crates/plugins/calendar/src/view.rs` - `crates/plugins/mail/src/cache/store.rs` - `crates/plugins/mail/src/sync.rs` - `crates/plugins/money/Cargo.toml` - `crates/plugins/money/src/events.rs` - `crates/plugins/money/src/lib.rs` - `crates/plugins/money/src/routes.rs` - `crates/plugins/notifications/src/store.rs` - `docs/action-registry.md` - `docs/mcp-events.md` - `docs/perf/2026-10-01-mcp-events-491.md` - `packages/api-client/src/generated.ts` - `scripts/action_registry.py` - `scripts/test_action_registry.py` - `tests/adversarial/authz_matrix.py` - `tests/adversarial/mcp_events.mjs` ## Evidence - [final-gates.txt](https://git.kayg.org/attachments/33952ec4-6933-4b11-84be-68bc4f7863c3) - [2026-10-01-mcp-events-491.md](https://git.kayg.org/attachments/87a54c20-0708-4328-bc88-cd600a8eb487) - [performance.json](https://git.kayg.org/attachments/a93bf13c-727c-4465-a863-b1a4502a7845) - [queue-performance.log](https://git.kayg.org/attachments/1b3686f7-85aa-473f-861e-8ec919685813) - [admin-1440-dark.png](https://git.kayg.org/attachments/ae266899-21b4-4bce-b501-8a4440d7825f) - [admin-1440-light.png](https://git.kayg.org/attachments/70fb1734-6fb7-4acc-8385-80468d09b74b) - [admin-390-dark.png](https://git.kayg.org/attachments/0011b807-9796-42dd-b0f7-ed63e7e2ac3c) - [admin-390-light.png](https://git.kayg.org/attachments/89517f16-28e1-4809-81d5-d8cc5ee78885) - [admin-820-dark.png](https://git.kayg.org/attachments/a054a2de-07cf-40a1-86ab-ade44c8e4309) - [admin-820-light.png](https://git.kayg.org/attachments/4ece42f7-7a1d-4c85-8f75-1af005a61bd2) - [user-1440-dark.png](https://git.kayg.org/attachments/5558acc3-dba2-48f8-badb-5ae385c682c4) - [user-1440-light.png](https://git.kayg.org/attachments/d9c239d9-be0b-4441-984b-099b7f9faf2e) - [user-390-dark.png](https://git.kayg.org/attachments/14c4547f-3bd9-4cc3-99b9-77bc9d791296) - [user-390-light.png](https://git.kayg.org/attachments/5b6a77b9-6480-42d8-bd80-f3ccb845b291) - [user-820-dark.png](https://git.kayg.org/attachments/0cbc2086-d816-47ac-b5fc-f3d2381cc73e) - [user-820-light.png](https://git.kayg.org/attachments/54a03486-37ff-43cb-bd88-5fc183b3bfb1)
Author
Owner

Cleanup complete. cargo clean output:

     Removed 21462 files, 12.3GiB total

Web build output was removed. The applied integration stash was dropped. The working tree is clean at dadd320e24da39698112bab839edb3d2cb746dc7.

Cleanup complete. `cargo clean` output: ```text Removed 21462 files, 12.3GiB total ``` Web build output was removed. The applied integration stash was dropped. The working tree is clean at `dadd320e24da39698112bab839edb3d2cb746dc7`.
Author
Owner

Independent defensive review of #491 started on job/mcp-events-review-491, audited base dadd320e24da39698112bab839edb3d2cb746dc7. Tests and findings only; no commit to the feature branch, no push or deploy. Review covers outbound address policy, live authorization, signatures, bounded queues, producers and Settings.

Independent defensive review of #491 started on `job/mcp-events-review-491`, audited base `dadd320e24da39698112bab839edb3d2cb746dc7`. Tests and findings only; no commit to the feature branch, no push or deploy. Review covers outbound address policy, live authorization, signatures, bounded queues, producers and Settings.
Author
Owner

MCP Events independent review (#491)

Audited feature: dadd320e24da39698112bab839edb3d2cb746dc7.
Review branch: job/mcp-events-review-491. No feature implementation is changed.
DESIGN §§21, 41, 54 and 55 set the review requirements.

Findings recorded during review

  1. Endpoint verification does not bind the signing key (fix required).
    In crates/calternal-server/src/mcp_events.rs, Hub::subscribe uses
    (owner, password, url) as verify_key. A cached successful verification
    skips verify_endpoint. The registration installs the newly supplied key
    and becomes active. Changing the key therefore does not prove that the
    receiver accepts it. Bind verification to a key digest and verify a new key
    before replacing the active registration. Do not store the raw key in a
    cache key. This is a protocol and delivery-integrity defect. No cross-User
    access or SSRF bypass is established by this finding.

  2. HTTP 410 and 413 do not stop the subscription (fix required).
    finish_pending treats these statuses as a completed queue item. It does
    not disable or remove the subscription, clear the rest of its queue, or
    suppress later occurrences. The issue requires delivery to stop on these
    statuses. Stop the grant and document whether a new subscription can
    restart it. Five failed retries also drop an item without a gap or retained
    dead-letter record. A receiver cannot detect that loss from a later event.

  3. Cross-User contract inventory fails (fixed in review patch).
    The feature adds mcp_event_revoke but not its entry in ROUTE_ID_FIELDS.
    The unchanged classification suite failed with:
    OpenAPI path ID has no resource classification: DELETE /api/v1/apps/events/subscriptions/{id} (mcp_event_revoke).
    The patch adds this classification and an explicit route-policy test.
    Existing test expectations are unchanged. The Hub's existing two-User
    router test remains the live in-memory grant test; the generic external
    matrix does not create a verified public webhook fixture.

  4. Receiver instructions omit the verification contract (fix required).
    docs/mcp-events.md names Standard Webhooks and deduplication but does not
    tell a receiver to use constant-time HMAC verification, reject timestamps
    outside a replay window, or retain event IDs across retries. The existing
    reference receiver uses standardwebhooks but it is a test, not receiver
    documentation. Document a five-minute timestamp window and stable-ID
    deduplication. Rotation also permits a request already in flight to use
    the old key; state that limit instead of promising immediate rotation.

  5. Settings exposes draft and protocol terms (non-blocking).
    apps/web/src/routes/settings/apps/McpEventsGroup.svelte shows
    Signed updates for your assistants (draft) and raw event names.
    AppsSection.svelte also includes (draft). DESIGN §50 requires plain
    labels. Keep version and draft details in the technical documentation.

Validation in progress

The initial cross-User classification patch passes eight tests. Rust test
compilation is in progress. Outbound, signature, queue, polling and real-server
results will be added before the final verdict.

Decisions

  • Keep implementation changes out of this independent review. Supply test
    coverage and precise code references for the feature author.
  • Use the requested feature SHA as the audit baseline, although this worktree
    starts detached. Commit only on the new review branch.
# MCP Events independent review (#491) Audited feature: `dadd320e24da39698112bab839edb3d2cb746dc7`. Review branch: `job/mcp-events-review-491`. No feature implementation is changed. DESIGN §§21, 41, 54 and 55 set the review requirements. ## Findings recorded during review 1. **Endpoint verification does not bind the signing key (fix required).** In `crates/calternal-server/src/mcp_events.rs`, `Hub::subscribe` uses `(owner, password, url)` as `verify_key`. A cached successful verification skips `verify_endpoint`. The registration installs the newly supplied key and becomes active. Changing the key therefore does not prove that the receiver accepts it. Bind verification to a key digest and verify a new key before replacing the active registration. Do not store the raw key in a cache key. This is a protocol and delivery-integrity defect. No cross-User access or SSRF bypass is established by this finding. 2. **HTTP 410 and 413 do not stop the subscription (fix required).** `finish_pending` treats these statuses as a completed queue item. It does not disable or remove the subscription, clear the rest of its queue, or suppress later occurrences. The issue requires delivery to stop on these statuses. Stop the grant and document whether a new subscription can restart it. Five failed retries also drop an item without a gap or retained dead-letter record. A receiver cannot detect that loss from a later event. 3. **Cross-User contract inventory fails (fixed in review patch).** The feature adds `mcp_event_revoke` but not its entry in `ROUTE_ID_FIELDS`. The unchanged classification suite failed with: `OpenAPI path ID has no resource classification: DELETE /api/v1/apps/events/subscriptions/{id} (mcp_event_revoke)`. The patch adds this classification and an explicit route-policy test. Existing test expectations are unchanged. The Hub's existing two-User router test remains the live in-memory grant test; the generic external matrix does not create a verified public webhook fixture. 4. **Receiver instructions omit the verification contract (fix required).** `docs/mcp-events.md` names Standard Webhooks and deduplication but does not tell a receiver to use constant-time HMAC verification, reject timestamps outside a replay window, or retain event IDs across retries. The existing reference receiver uses `standardwebhooks` but it is a test, not receiver documentation. Document a five-minute timestamp window and stable-ID deduplication. Rotation also permits a request already in flight to use the old key; state that limit instead of promising immediate rotation. 5. **Settings exposes draft and protocol terms (non-blocking).** `apps/web/src/routes/settings/apps/McpEventsGroup.svelte` shows `Signed updates for your assistants (draft)` and raw event names. `AppsSection.svelte` also includes `(draft)`. DESIGN §50 requires plain labels. Keep version and draft details in the technical documentation. ## Validation in progress The initial cross-User classification patch passes eight tests. Rust test compilation is in progress. Outbound, signature, queue, polling and real-server results will be added before the final verdict. ## Decisions - Keep implementation changes out of this independent review. Supply test coverage and precise code references for the feature author. - Use the requested feature SHA as the audit baseline, although this worktree starts detached. Commit only on the new review branch.
Author
Owner

MCP Events independent review (#491)

Audited feature: dadd320e24da39698112bab839edb3d2cb746dc7.
Review branch: job/mcp-events-review-491. No feature implementation is changed.
DESIGN §§21, 41, 54 and 55 set the review requirements.

Findings recorded during review

  1. Endpoint verification does not bind the signing key (fix required).
    In crates/calternal-server/src/mcp_events.rs, Hub::subscribe uses
    (owner, password, url) as verify_key. A cached successful verification
    skips verify_endpoint. The registration installs the newly supplied key
    and becomes active. Changing the key therefore does not prove that the
    receiver accepts it. Bind verification to a key digest and verify a new key
    before replacing the active registration. Do not store the raw key in a
    cache key. This is a protocol and delivery-integrity defect. No cross-User
    access or SSRF bypass is established by this finding.

  2. HTTP 410 and 413 do not stop the subscription (fix required).
    finish_pending treats these statuses as a completed queue item. It does
    not disable or remove the subscription, clear the rest of its queue, or
    suppress later occurrences. The issue requires delivery to stop on these
    statuses. Stop the grant and document whether a new subscription can
    restart it. Five failed retries also drop an item without a gap or retained
    dead-letter record. A receiver cannot detect that loss from a later event.

  3. Cross-User contract inventory fails (fixed in review patch).
    The feature adds mcp_event_revoke but not its entry in ROUTE_ID_FIELDS.
    The unchanged classification suite failed with:
    OpenAPI path ID has no resource classification: DELETE /api/v1/apps/events/subscriptions/{id} (mcp_event_revoke).
    The patch adds this classification and an explicit route-policy test.
    Existing test expectations are unchanged. The Hub's existing two-User
    router test remains the live in-memory grant test; the generic external
    matrix does not create a verified public webhook fixture.

  4. Receiver instructions omit the verification contract (fix required).
    docs/mcp-events.md names Standard Webhooks and deduplication but does not
    tell a receiver to use constant-time HMAC verification, reject timestamps
    outside a replay window, or retain event IDs across retries. The existing
    reference receiver uses standardwebhooks but it is a test, not receiver
    documentation. Document a five-minute timestamp window and stable-ID
    deduplication. Rotation also permits a request already in flight to use
    the old key; state that limit instead of promising immediate rotation.

  5. Settings exposes draft and protocol terms (non-blocking).
    apps/web/src/routes/settings/apps/McpEventsGroup.svelte shows
    Signed updates for your assistants (draft) and raw event names.
    AppsSection.svelte also includes (draft). DESIGN §50 requires plain
    labels. Keep version and draft details in the technical documentation.

  6. Delivery has bounds but no fair socket allocation (fix required).
    Hub::default gives all Users one semaphore with four permits. The
    delivery tick uses try_acquire_owned before selecting a User, and
    ready_subscription uses the first ready HashMap entry. There is no
    per-User socket limit or round-robin cursor. One User can occupy all four
    permits with slow requests. DNS is bounded to 3 s and HTTP to 5 s, so each
    attempt is finite, but other Users have no reserved capacity. The existing
    rate-limit test and the review's in-flight-selection test do not prove
    socket fairness. Add fair User selection and a per-User concurrency bound.

  7. Idle Events delivery polls at 10 Hz (performance finding).
    Hub::start creates a 100 ms interval even when there are no subscriptions.
    The source worker also ticks every 15 s; the bill worker ticks every 30 s.
    These three loops schedule about 10.1 ticks/s, of which 0.1 ticks/s belong
    to the two producer loops. Empty producer interest sets cause no Index or
    Home scan. Use a Notify and a deadline for ready deliveries. For Files and
    Shares, use the existing change-feed signal to start scans; Task, Calendar
    and Bill deadlines still need scheduling. Whole-Instance idle measurements
    will not be presented as Events-only CPU attribution.

Validation in progress

The initial cross-User classification patch passes eight tests. Rust test
compilation is in progress. Outbound, signature, queue, polling and real-server
results will be added before the final verdict.

Decisions

  • Keep implementation changes out of this independent review. Supply test
    coverage and precise code references for the feature author.
  • Use the requested feature SHA as the audit baseline, although this worktree
    starts detached. Commit only on the new review branch.
# MCP Events independent review (#491) Audited feature: `dadd320e24da39698112bab839edb3d2cb746dc7`. Review branch: `job/mcp-events-review-491`. No feature implementation is changed. DESIGN §§21, 41, 54 and 55 set the review requirements. ## Findings recorded during review 1. **Endpoint verification does not bind the signing key (fix required).** In `crates/calternal-server/src/mcp_events.rs`, `Hub::subscribe` uses `(owner, password, url)` as `verify_key`. A cached successful verification skips `verify_endpoint`. The registration installs the newly supplied key and becomes active. Changing the key therefore does not prove that the receiver accepts it. Bind verification to a key digest and verify a new key before replacing the active registration. Do not store the raw key in a cache key. This is a protocol and delivery-integrity defect. No cross-User access or SSRF bypass is established by this finding. 2. **HTTP 410 and 413 do not stop the subscription (fix required).** `finish_pending` treats these statuses as a completed queue item. It does not disable or remove the subscription, clear the rest of its queue, or suppress later occurrences. The issue requires delivery to stop on these statuses. Stop the grant and document whether a new subscription can restart it. Five failed retries also drop an item without a gap or retained dead-letter record. A receiver cannot detect that loss from a later event. 3. **Cross-User contract inventory fails (fixed in review patch).** The feature adds `mcp_event_revoke` but not its entry in `ROUTE_ID_FIELDS`. The unchanged classification suite failed with: `OpenAPI path ID has no resource classification: DELETE /api/v1/apps/events/subscriptions/{id} (mcp_event_revoke)`. The patch adds this classification and an explicit route-policy test. Existing test expectations are unchanged. The Hub's existing two-User router test remains the live in-memory grant test; the generic external matrix does not create a verified public webhook fixture. 4. **Receiver instructions omit the verification contract (fix required).** `docs/mcp-events.md` names Standard Webhooks and deduplication but does not tell a receiver to use constant-time HMAC verification, reject timestamps outside a replay window, or retain event IDs across retries. The existing reference receiver uses `standardwebhooks` but it is a test, not receiver documentation. Document a five-minute timestamp window and stable-ID deduplication. Rotation also permits a request already in flight to use the old key; state that limit instead of promising immediate rotation. 5. **Settings exposes draft and protocol terms (non-blocking).** `apps/web/src/routes/settings/apps/McpEventsGroup.svelte` shows `Signed updates for your assistants (draft)` and raw event names. `AppsSection.svelte` also includes `(draft)`. DESIGN §50 requires plain labels. Keep version and draft details in the technical documentation. 6. **Delivery has bounds but no fair socket allocation (fix required).** `Hub::default` gives all Users one semaphore with four permits. The delivery tick uses `try_acquire_owned` before selecting a User, and `ready_subscription` uses the first ready HashMap entry. There is no per-User socket limit or round-robin cursor. One User can occupy all four permits with slow requests. DNS is bounded to 3 s and HTTP to 5 s, so each attempt is finite, but other Users have no reserved capacity. The existing rate-limit test and the review's in-flight-selection test do not prove socket fairness. Add fair User selection and a per-User concurrency bound. 7. **Idle Events delivery polls at 10 Hz (performance finding).** `Hub::start` creates a 100 ms interval even when there are no subscriptions. The source worker also ticks every 15 s; the bill worker ticks every 30 s. These three loops schedule about 10.1 ticks/s, of which 0.1 ticks/s belong to the two producer loops. Empty producer interest sets cause no Index or Home scan. Use a Notify and a deadline for ready deliveries. For Files and Shares, use the existing change-feed signal to start scans; Task, Calendar and Bill deadlines still need scheduling. Whole-Instance idle measurements will not be presented as Events-only CPU attribution. ## Validation in progress The initial cross-User classification patch passes eight tests. Rust test compilation is in progress. Outbound, signature, queue, polling and real-server results will be added before the final verdict. ## Decisions - Keep implementation changes out of this independent review. Supply test coverage and precise code references for the feature author. - Use the requested feature SHA as the audit baseline, although this worktree starts detached. Commit only on the new review branch.
Author
Owner

Correction to preliminary findings 1 and 2: the upstream draft explicitly caches verification per (principal, URL), and requires HTTP 410/413 to stop retries for that delivery without ending the subscription. Both behaviors in the feature are compliant. The key-digest cache and whole-subscription termination recommendations are withdrawn. The final report will mark these checks cleared. Matrix classification, fair socket scheduling, receiver documentation and idle polling remain under review.

Correction to preliminary findings 1 and 2: the upstream [draft](https://github.com/modelcontextprotocol/experimental-ext-triggers-events/blob/main/docs/design-sketch-proposal.md) explicitly caches verification per (principal, URL), and requires HTTP 410/413 to stop retries for that delivery without ending the subscription. Both behaviors in the feature are compliant. The key-digest cache and whole-subscription termination recommendations are withdrawn. The final report will mark these checks cleared. Matrix classification, fair socket scheduling, receiver documentation and idle polling remain under review.
Author
Owner

MCP Events independent review (#491)

Audited feature: dadd320e24da39698112bab839edb3d2cb746dc7.
Review branch: job/mcp-events-review-491. No feature implementation is changed.
DESIGN §§21, 41, 54 and 55 set the review requirements.

Verdict: GO-with-fixes. Apply the matrix classification fix in this patch
(finding 3), add receiver verification and rotation instructions (finding 4),
and give Users fair delivery capacity (finding 6). No SSRF bypass, signing-key
disclosure, or cross-User payload leak was found. The socket fairness result
is a code-review finding, not a measured outbound denial-of-service claim.
UI wording and idle polling are non-blocking follow-up work. Preliminary
findings 1 and 2 were cleared against the upstream draft.

Findings recorded during review

  1. Endpoint verification cache: cleared after draft review.
    Hub::subscribe caches (owner, password, url), and a refresh can install
    a new key without another challenge. This matches the upstream draft's
    (principal, url) cache policy. The earlier recommendation to bind the
    cache to a key digest is withdrawn. Rotation should not multiply challenge
    requests. A new key fails verification with the old receiver key, as the
    independent reference-verifier test confirms; the client coordinates its
    own rotation.

  2. HTTP 410/413: cleared after draft review.
    finish_pending stops retries for the current item and keeps the grant.
    The upstream draft explicitly requires this behavior. The earlier
    recommendation to stop the whole grant is withdrawn. Five failed retries
    then discard the item. This is bounded but leaves no diagnostic history.
    With this documented emit-only surface there is no replay guarantee; a
    loss notice or bounded delivery-error counter would improve diagnosis.
    This diagnostic suggestion does not block the review.

  3. Cross-User contract inventory fails (fixed in review patch).
    The feature adds mcp_event_revoke but not its entry in ROUTE_ID_FIELDS.
    The unchanged classification suite failed with:
    OpenAPI path ID has no resource classification: DELETE /api/v1/apps/events/subscriptions/{id} (mcp_event_revoke).
    The patch adds this classification and an explicit route-policy test.
    Existing test expectations are unchanged. The Hub's existing two-User
    router test remains the live in-memory grant test; the generic external
    matrix does not create a verified public webhook fixture.

  4. Receiver instructions omit the verification contract (fix required).
    docs/mcp-events.md names Standard Webhooks and deduplication but does not
    tell a receiver to use constant-time HMAC verification, reject timestamps
    outside a replay window, or retain event IDs across retries. The existing
    reference receiver uses standardwebhooks but it is a test, not receiver
    documentation. Document a five-minute timestamp window and stable-ID
    deduplication. Rotation also permits a request already in flight to use
    the old key; state that limit instead of promising immediate rotation.

  5. Settings exposes draft and protocol terms (non-blocking).
    apps/web/src/routes/settings/apps/McpEventsGroup.svelte shows
    Signed updates for your assistants (draft) and raw event names.
    AppsSection.svelte also includes (draft). DESIGN §50 requires plain
    labels. Keep version and draft details in the technical documentation.

  6. Delivery has bounds but no fair socket allocation (fix required).
    Hub::default gives all Users one semaphore with four permits. The
    delivery tick uses try_acquire_owned before selecting a User, and
    ready_subscription uses the first ready HashMap entry. There is no
    per-User socket limit or round-robin cursor. One User can occupy all four
    permits with slow requests. DNS is bounded to 3 s and HTTP to 5 s, so each
    attempt is finite, but other Users have no reserved capacity. The existing
    rate-limit test and the review's in-flight-selection test do not prove
    socket fairness. Add fair User selection and a per-User concurrency bound.

  7. Idle Events delivery polls at 10 Hz (performance finding).
    Hub::start creates a 100 ms interval even when there are no subscriptions.
    The source worker also ticks every 15 s; the bill worker ticks every 30 s.
    These three loops schedule about 10.1 ticks/s, of which 0.1 ticks/s belong
    to the two producer loops. Empty producer interest sets cause no Index or
    Home scan. Use a Notify and a deadline for ready deliveries. For Files and
    Shares, use the existing change-feed signal to start scans; Task, Calendar
    and Bill deadlines still need scheduling. Whole-Instance idle measurements
    will not be presented as Events-only CPU attribution.

Security checks

Requirement Evidence at the audited feature SHA Result
HTTPS only mcp_events::callback, lines 162–180; no production exception Pass
Private, loopback, metadata, CGNAT and mapped IPv6 refusal client calls Calendar feeds::webhook_addresses; feeds/subscriptions.rs::resolve_addresses, is_public_address, is_public_ipv4 Pass; review test covers 18 literal addresses through client
DNS rebinding and mixed DNS answers The resolver rejects any disallowed answer, takes at most 65 answers, refuses more than 64, and times out after 3 s. client uses resolve_to_addrs for the original hostname Pass by code review; no live DNS-rebinding fixture
Redirects and proxy environment client uses Policy::none() and no_proxy() Pass by code review; no redirect hop is followed
Request and response bounds 3 s connect timeout, 5 s request timeout, 4 KiB streamed challenge cap; delivery reads only status and drops the response Pass; review test checks the challenge cap and no response reflection
Guard reuse New webhook entry uses the existing Calendar resolver and public policy; it excludes the admin feed allowlist Pass; no new address classifier in Events
Owner isolation identity includes User and App Password. enqueue selects the exact producer User, then applies registry field allowlists and exact filters Pass; two-User enqueue and existing Settings router tests
Resource filters at subscribe time event.valid_arguments checks shape, length and declared fields. allowed checks the broad principal and plugin, not the existence/ownership of an account, folder or item filter Other-User data still cannot enter this owner-selected queue; an inaccessible filter can produce an empty grant. Add per-resource checks if subscription acceptance must itself prove access.
Live authority principal_allowed re-reads disabled User, both surface switches, current App Password, expiry, Read access and unrestricted scope. allowed checks live plugin enablement. Worker repeats these after DNS and before sending Pass by code review; a request already sent cannot be recalled
Share access sources::files uses the User-local change feed and ordinary item reader. Worker repeats the item read before delivery of Files and Share events Pass by code review; no end-to-end revoke-during-TLS test
Signature input signature signs exact id.timestamp.body bytes with HMAC-SHA256 and sends Standard Webhooks headers Pass; independent vector and receiver tests
Replay and receiver verification Reference verifier rejects past/future timestamps beyond five minutes and a different key Tests pass; receiver documentation needs finding 4
Challenge verification Random nonce, exact constant-time comparison, bounded JSON response Pass; draft permits URL-scoped verification across key refresh
Secret handling No Debug derivation on subscriptions, no secret response, Settings uses URL origin only; fixed outbound errors Pass by code review; no secret persistence or logging found
Subscription and rate bounds 16 per User, 256 per Instance; 10 registration attempts and 60 deliveries per minute per User; verification cache at most 512 entries Pass by code review and existing rate tests
Queue and burst bounds 256-entry producer bus, 32-entry subscription queue, 4,096 seen IDs per grant, 256 KiB envelope cap Pass; review test submits 10,000 occurrences and checks queue/seen caps
Retry and loss handling Stable ID/body, new signing timestamp, five retries at 2/4/8/16/32 s, then discard Pass; 410/413 stop only the current delivery, as the draft requires
Slow receiver isolation Four shared permits, independent delivery tasks and finite network timeouts Partial; User fairness needs finding 6
User/Admin off switches RPC and capability gate use both switches; delivery repeats both checks Pass; extended real-server probe checked each off switch and hidden capability discovery

These checks use the producer-supplied User, not a recipient chosen in a
payload. Shared data needs the ordinary live item read. Scoped App Passwords
are refused rather than given a broader event grant.

The Standard Webhooks specification requires raw-body signatures and replay
protection. The review checked the upstream specification and the pinned
standardwebhooks 1.0.1 source. Its verifier has a 300 s tolerance and a
full-length XOR comparison. Reference:
Standard Webhooks specification.
cargo search confirmed reqwest 0.13.5 and standardwebhooks 1.0.1. No package
or dependency version was changed.

Producer review

Producer Exact code reference Check
Mail received, receipt and statement crates/plugins/mail/src/cache/store.rs::store_window, event loop after transaction.commit(); sync::statement_subject User comes from the sync window. Only subject/sender survive projection. No body is sent. Receipt uses the existing category; statement detection can miss other languages.
Transaction logged and Category overspent crates/plugins/money/src/routes.rs::create_transaction, lines 1088–1124 Event follows the checked Home write. Amounts are integer minor units. The existing Money read/write lock protects the projection.
Bill due crates/plugins/money/src/events.rs::start 30 s timer; no Home read with no interests; each target uses the verified User and Money read lock. Date and amount use existing parsers.
Task due and overdue crates/calternal-server/src/mcp_event_sources.rs::tasks, task_trigger User predicate in SQL, at most 10,000 rows, local time and DST test.
Calendar starting mcp_event_sources.rs::calendar; crates/plugins/calendar/src/view.rs::webhook_candidates Reuses the Calendar provider and recurrence policy. Owned cached output is capped at 20,000 day items; URL feed results are appended; source takes 10,000 candidates.
Reminder fired crates/plugins/notifications/src/store.rs, event block after transaction.commit() Only new live triggers emit; restored missed reminders stay silent.
Files added and Share changed mcp_event_sources.rs::files User-local cursor, page limit 256, ordinary live item reader for Shares. Delivery re-checks the item.

The registry lists twelve events from action declarations. No comment creation
action or store exists, so share.comment_created is omitted and documented.
The draft's endpoint cache and per-delivery 410/413 rules were checked at
the upstream design sketch.
This resolves preliminary findings 1 and 2; neither is a defect.

The timer sources deduplicate occurrences per grant. Their limits are finite,
but large scans across many filter sets still need performance testing.

Real-server round and performance

The extended tests/adversarial/mcp_events.mjs probe passed on a disposable
local Instance. It rejects malformed schema and HTTP callbacks. With valid
synthetic signing keys, it rejects five non-public callback literals before
connection. It checks both User and Admin off switches and checks that the
Events initialization capability disappears. No tested request returned a
5xx or exposed a receiver response. This was one bounded real-server round.
The probe contacted no external webhook receiver.

The perf VM returned No route to host on one check. These measurements use
a local debug build on the shared host. No release regression claim is made.
Load averages at the discovery sample were 8.13, 12.12 and 14.60.

Sample p50 / p95 (ms) CPU Mean / peak RSS (MiB)
Discovery, 30 requests, concurrency 1 518.67 / 821.95 14.82 CPU s; 90.02% 200.86 / 225.16
Discovery burst, 64 requests, concurrency 8 1126.58 / 1740.70 32.24 CPU s; 250.16% 218.79 / 248.67
No grants, 60.14 s idle Not applicable 0.61 CPU s; 1.01% 121.66 / 199.20
Release baseline, one User, 600 s idle Not applicable 0.128% 143.96 / not recorded

The last row is docs/perf/baseline.json::metrics.scenarios.idle_one_user.
It is shown for context. It is not comparable to this shorter local debug
sample. The baseline has no MCP Events workload or idle Events-only sample,
so there is no matching threshold for a regression issue. The earlier local
feature report measured discovery at 1542.50/2875.43 ms and its burst at
2781.02/5237.58 ms under higher host load. The lower values here do not prove
an implementation speedup.

The idle sample recorded 4,791 voluntary context switches in threads present
at both sample boundaries, or 79.66/s. This includes all server workers. It
is not an Events wakeup count. Source code schedules 10 delivery ticks/s,
1/15 source ticks/s and 1/30 bill ticks/s. Empty interests cause no producer
Index or Home reads. The two producer timers are acceptable at idle under
these bounds; the 10 Hz delivery timer is the first optimization target.
Files and Shares should use the existing change-feed signal when active.
Time-based Tasks, Calendar and Bills still need deadlines, not only pushes.

The queue benchmark ran once. Load averages were 15.26, 15.08 and 15.19.
Each queue stayed at or below 32 entries. The review test also proves the
4,096 seen-ID cap after a 10,000-event burst on one grant.

Queue sample Users / grants / occurrences p50 / p95 (ms) Earlier local feature sample p50 / p95 (ms)
Average, 128 bytes per field 1 / 16 / 30 0.50 / 0.67 0.67 / 1.06
Capacity burst, 4,096 bytes per field 16 / 256 / 1,024 5.65 / 7.22 6.66 / 8.91
Extended burst, 4,096 bytes per field 16 / 256 / 10,000 6.40 / 10.51 No matching sample

The combined fixture took 77.02 s and 72.73 CPU seconds (94.44% of one core).
Mean RSS was 73.11 MiB and peak RSS was 90.57 MiB. The earlier feature fixture
had mean/peak RSS of 59.93/63.23 MiB, but it did not include the 10,000-event
phase. These are different combined workloads. No unbounded queue growth was
observed. The fixture does not open sockets or measure producer Index scans.
Raw logs remain in ignored artifacts/.

Known gaps

  • No controlled live DNS-rebinding fixture or full HTTPS subscribe/refresh/
    rotation test. Resolver pinning, redirects, key replacement and authority
    checks have precise code references; address refusal and signature behavior
    have executable tests.
  • The external generic cross-User matrix still has no verified public grant
    fixture. The existing Hub router matrix and new two-User queue test cover
    active in-memory grant isolation.
  • Shared socket fairness has no production guarantee and needs finding 6.
    Existing selection tests cover a busy subscription and an exhausted User,
    not a User holding all permits.
  • No durable dead-letter history or replay. Retry attempts and all queues are
    bounded. A failure counter would help diagnosis.
  • No comment producer, as documented by the feature. No new UI is built or
    changed, so the review adds no screenshot or visual-quality claim.
  • Perf results are local debug samples. Active producer scan cost remains
    unmeasured on a large Home and on the release perf VM.

Review patch and gates

The patch adds eight defensive Rust tests, extends the two-User contract
classification, extends the real-server switch/address probe, and adds idle
and 10,000-event measurements. Production behavior is unchanged.

Changed files:

  • review-findings.md
  • crates/calternal-server/src/mcp_events.rs (test module and fixture only)
  • crates/calternal-server/src/mcp_events_review.rs
  • tests/adversarial/xuser_matrix.py
  • tests/adversarial/test_xuser_classification.py
  • tests/adversarial/mcp_events.mjs
  • bench/mcp-events.mjs

The requested fetch and merge ran once: Already up to date. No migration,
package version, lockfile, or existing test expectation was changed. Module
and non-obvious function comments were read again before this report.
The response-cap fixture was strengthened after the first gates, so the
server gates were repeated for that test change. No second real-server
adversarial round or benchmark round was run.

cargo fmt --check: exit 0, no output.

cargo clippy -p calternal-server --all-targets -- -D warnings:

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 3m 43s

cargo test -p calternal-server:

    Finished `test` profile [unoptimized + debuginfo] target(s) in 5m 04s
test result: ok. 126 passed; 0 failed; 4 ignored; 0 measured; 0 filtered out; finished in 30.01s

The normal test gate ignores the performance fixture and three fixtures with
process-global Plugin state. The latter run in the passing
live_apps_run_in_separate_processes test. The performance fixture was run
separately and passed.

python3 -m unittest discover -s tests/adversarial -p test_xuser_classification.py:

........
----------------------------------------------------------------------
Ran 8 tests in 0.114s

OK

node --check bench/mcp-events.mjs and
node --check tests/adversarial/mcp_events.mjs: exit 0, no output.

The bounded real-server round ran through node bench/mcp-events.mjs:

MCP Events local route checks passed.

The production web build passed. No application Web source was changed;
Web check/test and new visual captures are outside this review patch.
Build output is removed at the end of the job. Raw logs and the exported
patch remain under ignored artifacts/; none are committed.

Decisions

  • Keep implementation changes out of this independent review. Supply test
    coverage and precise code references for the feature author.
  • Use the requested feature SHA as the audit baseline, although this worktree
    starts detached. Commit only on the new review branch.
  • Follow the upstream draft for URL-scoped verification and per-delivery
    410/413 handling. The issue's short phrase "stop on 410 and 413" means stop
    retrying that delivery. This corrects preliminary observations, not a new
    product design choice.
  • Keep all benchmark fixtures under tests and run the existing disposable
    Instance harness. No User data, credentials or review artifacts are added
    to the repository.

Final review head: ed543dab2041c21e1ff6f300827f999250333bd4.

Local review patch: artifacts/mcp-events-review.patch in /home/kayg/Developer/calternal-wt/mcp-events-review. No push or deploy. The verdict requires the feature author to address findings 4 and 6 and apply the matrix fix in this patch. Issue remains open.

# MCP Events independent review (#491) Audited feature: `dadd320e24da39698112bab839edb3d2cb746dc7`. Review branch: `job/mcp-events-review-491`. No feature implementation is changed. DESIGN §§21, 41, 54 and 55 set the review requirements. **Verdict: GO-with-fixes.** Apply the matrix classification fix in this patch (finding 3), add receiver verification and rotation instructions (finding 4), and give Users fair delivery capacity (finding 6). No SSRF bypass, signing-key disclosure, or cross-User payload leak was found. The socket fairness result is a code-review finding, not a measured outbound denial-of-service claim. UI wording and idle polling are non-blocking follow-up work. Preliminary findings 1 and 2 were cleared against the upstream draft. ## Findings recorded during review 1. **Endpoint verification cache: cleared after draft review.** `Hub::subscribe` caches `(owner, password, url)`, and a refresh can install a new key without another challenge. This matches the upstream draft's `(principal, url)` cache policy. The earlier recommendation to bind the cache to a key digest is withdrawn. Rotation should not multiply challenge requests. A new key fails verification with the old receiver key, as the independent reference-verifier test confirms; the client coordinates its own rotation. 2. **HTTP 410/413: cleared after draft review.** `finish_pending` stops retries for the current item and keeps the grant. The upstream draft explicitly requires this behavior. The earlier recommendation to stop the whole grant is withdrawn. Five failed retries then discard the item. This is bounded but leaves no diagnostic history. With this documented emit-only surface there is no replay guarantee; a loss notice or bounded delivery-error counter would improve diagnosis. This diagnostic suggestion does not block the review. 3. **Cross-User contract inventory fails (fixed in review patch).** The feature adds `mcp_event_revoke` but not its entry in `ROUTE_ID_FIELDS`. The unchanged classification suite failed with: `OpenAPI path ID has no resource classification: DELETE /api/v1/apps/events/subscriptions/{id} (mcp_event_revoke)`. The patch adds this classification and an explicit route-policy test. Existing test expectations are unchanged. The Hub's existing two-User router test remains the live in-memory grant test; the generic external matrix does not create a verified public webhook fixture. 4. **Receiver instructions omit the verification contract (fix required).** `docs/mcp-events.md` names Standard Webhooks and deduplication but does not tell a receiver to use constant-time HMAC verification, reject timestamps outside a replay window, or retain event IDs across retries. The existing reference receiver uses `standardwebhooks` but it is a test, not receiver documentation. Document a five-minute timestamp window and stable-ID deduplication. Rotation also permits a request already in flight to use the old key; state that limit instead of promising immediate rotation. 5. **Settings exposes draft and protocol terms (non-blocking).** `apps/web/src/routes/settings/apps/McpEventsGroup.svelte` shows `Signed updates for your assistants (draft)` and raw event names. `AppsSection.svelte` also includes `(draft)`. DESIGN §50 requires plain labels. Keep version and draft details in the technical documentation. 6. **Delivery has bounds but no fair socket allocation (fix required).** `Hub::default` gives all Users one semaphore with four permits. The delivery tick uses `try_acquire_owned` before selecting a User, and `ready_subscription` uses the first ready HashMap entry. There is no per-User socket limit or round-robin cursor. One User can occupy all four permits with slow requests. DNS is bounded to 3 s and HTTP to 5 s, so each attempt is finite, but other Users have no reserved capacity. The existing rate-limit test and the review's in-flight-selection test do not prove socket fairness. Add fair User selection and a per-User concurrency bound. 7. **Idle Events delivery polls at 10 Hz (performance finding).** `Hub::start` creates a 100 ms interval even when there are no subscriptions. The source worker also ticks every 15 s; the bill worker ticks every 30 s. These three loops schedule about 10.1 ticks/s, of which 0.1 ticks/s belong to the two producer loops. Empty producer interest sets cause no Index or Home scan. Use a Notify and a deadline for ready deliveries. For Files and Shares, use the existing change-feed signal to start scans; Task, Calendar and Bill deadlines still need scheduling. Whole-Instance idle measurements will not be presented as Events-only CPU attribution. ## Security checks | Requirement | Evidence at the audited feature SHA | Result | | --- | --- | --- | | HTTPS only | `mcp_events::callback`, lines 162–180; no production exception | Pass | | Private, loopback, metadata, CGNAT and mapped IPv6 refusal | `client` calls Calendar `feeds::webhook_addresses`; `feeds/subscriptions.rs::resolve_addresses`, `is_public_address`, `is_public_ipv4` | Pass; review test covers 18 literal addresses through `client` | | DNS rebinding and mixed DNS answers | The resolver rejects any disallowed answer, takes at most 65 answers, refuses more than 64, and times out after 3 s. `client` uses `resolve_to_addrs` for the original hostname | Pass by code review; no live DNS-rebinding fixture | | Redirects and proxy environment | `client` uses `Policy::none()` and `no_proxy()` | Pass by code review; no redirect hop is followed | | Request and response bounds | 3 s connect timeout, 5 s request timeout, 4 KiB streamed challenge cap; delivery reads only status and drops the response | Pass; review test checks the challenge cap and no response reflection | | Guard reuse | New webhook entry uses the existing Calendar resolver and public policy; it excludes the admin feed allowlist | Pass; no new address classifier in Events | | Owner isolation | `identity` includes User and App Password. `enqueue` selects the exact producer User, then applies registry field allowlists and exact filters | Pass; two-User enqueue and existing Settings router tests | | Resource filters at subscribe time | `event.valid_arguments` checks shape, length and declared fields. `allowed` checks the broad principal and plugin, not the existence/ownership of an account, folder or item filter | Other-User data still cannot enter this owner-selected queue; an inaccessible filter can produce an empty grant. Add per-resource checks if subscription acceptance must itself prove access. | | Live authority | `principal_allowed` re-reads disabled User, both surface switches, current App Password, expiry, Read access and unrestricted scope. `allowed` checks live plugin enablement. Worker repeats these after DNS and before sending | Pass by code review; a request already sent cannot be recalled | | Share access | `sources::files` uses the User-local change feed and ordinary item reader. Worker repeats the item read before delivery of Files and Share events | Pass by code review; no end-to-end revoke-during-TLS test | | Signature input | `signature` signs exact `id.timestamp.body` bytes with HMAC-SHA256 and sends Standard Webhooks headers | Pass; independent vector and receiver tests | | Replay and receiver verification | Reference verifier rejects past/future timestamps beyond five minutes and a different key | Tests pass; receiver documentation needs finding 4 | | Challenge verification | Random nonce, exact constant-time comparison, bounded JSON response | Pass; draft permits URL-scoped verification across key refresh | | Secret handling | No Debug derivation on subscriptions, no secret response, Settings uses URL origin only; fixed outbound errors | Pass by code review; no secret persistence or logging found | | Subscription and rate bounds | 16 per User, 256 per Instance; 10 registration attempts and 60 deliveries per minute per User; verification cache at most 512 entries | Pass by code review and existing rate tests | | Queue and burst bounds | 256-entry producer bus, 32-entry subscription queue, 4,096 seen IDs per grant, 256 KiB envelope cap | Pass; review test submits 10,000 occurrences and checks queue/seen caps | | Retry and loss handling | Stable ID/body, new signing timestamp, five retries at 2/4/8/16/32 s, then discard | Pass; 410/413 stop only the current delivery, as the draft requires | | Slow receiver isolation | Four shared permits, independent delivery tasks and finite network timeouts | Partial; User fairness needs finding 6 | | User/Admin off switches | RPC and capability gate use both switches; delivery repeats both checks | Pass; extended real-server probe checked each off switch and hidden capability discovery | These checks use the producer-supplied User, not a recipient chosen in a payload. Shared data needs the ordinary live item read. Scoped App Passwords are refused rather than given a broader event grant. The Standard Webhooks specification requires raw-body signatures and replay protection. The review checked the upstream specification and the pinned `standardwebhooks` 1.0.1 source. Its verifier has a 300 s tolerance and a full-length XOR comparison. Reference: [Standard Webhooks specification](https://github.com/standard-webhooks/standard-webhooks/blob/main/spec/standard-webhooks.md). `cargo search` confirmed reqwest 0.13.5 and standardwebhooks 1.0.1. No package or dependency version was changed. ## Producer review | Producer | Exact code reference | Check | | --- | --- | --- | | Mail received, receipt and statement | `crates/plugins/mail/src/cache/store.rs::store_window`, event loop after `transaction.commit()`; `sync::statement_subject` | User comes from the sync window. Only subject/sender survive projection. No body is sent. Receipt uses the existing category; statement detection can miss other languages. | | Transaction logged and Category overspent | `crates/plugins/money/src/routes.rs::create_transaction`, lines 1088–1124 | Event follows the checked Home write. Amounts are integer minor units. The existing Money read/write lock protects the projection. | | Bill due | `crates/plugins/money/src/events.rs::start` | 30 s timer; no Home read with no interests; each target uses the verified User and Money read lock. Date and amount use existing parsers. | | Task due and overdue | `crates/calternal-server/src/mcp_event_sources.rs::tasks`, `task_trigger` | User predicate in SQL, at most 10,000 rows, local time and DST test. | | Calendar starting | `mcp_event_sources.rs::calendar`; `crates/plugins/calendar/src/view.rs::webhook_candidates` | Reuses the Calendar provider and recurrence policy. Owned cached output is capped at 20,000 day items; URL feed results are appended; source takes 10,000 candidates. | | Reminder fired | `crates/plugins/notifications/src/store.rs`, event block after `transaction.commit()` | Only new live triggers emit; restored missed reminders stay silent. | | Files added and Share changed | `mcp_event_sources.rs::files` | User-local cursor, page limit 256, ordinary live item reader for Shares. Delivery re-checks the item. | The registry lists twelve events from action declarations. No comment creation action or store exists, so `share.comment_created` is omitted and documented. The draft's endpoint cache and per-delivery 410/413 rules were checked at [the upstream design sketch](https://github.com/modelcontextprotocol/experimental-ext-triggers-events/blob/main/docs/design-sketch-proposal.md). This resolves preliminary findings 1 and 2; neither is a defect. The timer sources deduplicate occurrences per grant. Their limits are finite, but large scans across many filter sets still need performance testing. ## Real-server round and performance The extended `tests/adversarial/mcp_events.mjs` probe passed on a disposable local Instance. It rejects malformed schema and HTTP callbacks. With valid synthetic signing keys, it rejects five non-public callback literals before connection. It checks both User and Admin off switches and checks that the Events initialization capability disappears. No tested request returned a 5xx or exposed a receiver response. This was one bounded real-server round. The probe contacted no external webhook receiver. The perf VM returned `No route to host` on one check. These measurements use a local debug build on the shared host. No release regression claim is made. Load averages at the discovery sample were 8.13, 12.12 and 14.60. | Sample | p50 / p95 (ms) | CPU | Mean / peak RSS (MiB) | | --- | --- | --- | --- | | Discovery, 30 requests, concurrency 1 | 518.67 / 821.95 | 14.82 CPU s; 90.02% | 200.86 / 225.16 | | Discovery burst, 64 requests, concurrency 8 | 1126.58 / 1740.70 | 32.24 CPU s; 250.16% | 218.79 / 248.67 | | No grants, 60.14 s idle | Not applicable | 0.61 CPU s; 1.01% | 121.66 / 199.20 | | Release baseline, one User, 600 s idle | Not applicable | 0.128% | 143.96 / not recorded | The last row is `docs/perf/baseline.json::metrics.scenarios.idle_one_user`. It is shown for context. It is not comparable to this shorter local debug sample. The baseline has no MCP Events workload or idle Events-only sample, so there is no matching threshold for a regression issue. The earlier local feature report measured discovery at 1542.50/2875.43 ms and its burst at 2781.02/5237.58 ms under higher host load. The lower values here do not prove an implementation speedup. The idle sample recorded 4,791 voluntary context switches in threads present at both sample boundaries, or 79.66/s. This includes all server workers. It is not an Events wakeup count. Source code schedules 10 delivery ticks/s, 1/15 source ticks/s and 1/30 bill ticks/s. Empty interests cause no producer Index or Home reads. The two producer timers are acceptable at idle under these bounds; the 10 Hz delivery timer is the first optimization target. Files and Shares should use the existing change-feed signal when active. Time-based Tasks, Calendar and Bills still need deadlines, not only pushes. The queue benchmark ran once. Load averages were 15.26, 15.08 and 15.19. Each queue stayed at or below 32 entries. The review test also proves the 4,096 seen-ID cap after a 10,000-event burst on one grant. | Queue sample | Users / grants / occurrences | p50 / p95 (ms) | Earlier local feature sample p50 / p95 (ms) | | --- | --- | --- | --- | | Average, 128 bytes per field | 1 / 16 / 30 | 0.50 / 0.67 | 0.67 / 1.06 | | Capacity burst, 4,096 bytes per field | 16 / 256 / 1,024 | 5.65 / 7.22 | 6.66 / 8.91 | | Extended burst, 4,096 bytes per field | 16 / 256 / 10,000 | 6.40 / 10.51 | No matching sample | The combined fixture took 77.02 s and 72.73 CPU seconds (94.44% of one core). Mean RSS was 73.11 MiB and peak RSS was 90.57 MiB. The earlier feature fixture had mean/peak RSS of 59.93/63.23 MiB, but it did not include the 10,000-event phase. These are different combined workloads. No unbounded queue growth was observed. The fixture does not open sockets or measure producer Index scans. Raw logs remain in ignored `artifacts/`. ## Known gaps - No controlled live DNS-rebinding fixture or full HTTPS subscribe/refresh/ rotation test. Resolver pinning, redirects, key replacement and authority checks have precise code references; address refusal and signature behavior have executable tests. - The external generic cross-User matrix still has no verified public grant fixture. The existing Hub router matrix and new two-User queue test cover active in-memory grant isolation. - Shared socket fairness has no production guarantee and needs finding 6. Existing selection tests cover a busy subscription and an exhausted User, not a User holding all permits. - No durable dead-letter history or replay. Retry attempts and all queues are bounded. A failure counter would help diagnosis. - No comment producer, as documented by the feature. No new UI is built or changed, so the review adds no screenshot or visual-quality claim. - Perf results are local debug samples. Active producer scan cost remains unmeasured on a large Home and on the release perf VM. ## Review patch and gates The patch adds eight defensive Rust tests, extends the two-User contract classification, extends the real-server switch/address probe, and adds idle and 10,000-event measurements. Production behavior is unchanged. Changed files: - `review-findings.md` - `crates/calternal-server/src/mcp_events.rs` (test module and fixture only) - `crates/calternal-server/src/mcp_events_review.rs` - `tests/adversarial/xuser_matrix.py` - `tests/adversarial/test_xuser_classification.py` - `tests/adversarial/mcp_events.mjs` - `bench/mcp-events.mjs` The requested fetch and merge ran once: `Already up to date.` No migration, package version, lockfile, or existing test expectation was changed. Module and non-obvious function comments were read again before this report. The response-cap fixture was strengthened after the first gates, so the server gates were repeated for that test change. No second real-server adversarial round or benchmark round was run. `cargo fmt --check`: exit 0, no output. `cargo clippy -p calternal-server --all-targets -- -D warnings`: ``` Finished `dev` profile [unoptimized + debuginfo] target(s) in 3m 43s ``` `cargo test -p calternal-server`: ``` Finished `test` profile [unoptimized + debuginfo] target(s) in 5m 04s test result: ok. 126 passed; 0 failed; 4 ignored; 0 measured; 0 filtered out; finished in 30.01s ``` The normal test gate ignores the performance fixture and three fixtures with process-global Plugin state. The latter run in the passing `live_apps_run_in_separate_processes` test. The performance fixture was run separately and passed. `python3 -m unittest discover -s tests/adversarial -p test_xuser_classification.py`: ``` ........ ---------------------------------------------------------------------- Ran 8 tests in 0.114s OK ``` `node --check bench/mcp-events.mjs` and `node --check tests/adversarial/mcp_events.mjs`: exit 0, no output. The bounded real-server round ran through `node bench/mcp-events.mjs`: ``` MCP Events local route checks passed. ``` The production web build passed. No application Web source was changed; Web check/test and new visual captures are outside this review patch. Build output is removed at the end of the job. Raw logs and the exported patch remain under ignored `artifacts/`; none are committed. ## Decisions - Keep implementation changes out of this independent review. Supply test coverage and precise code references for the feature author. - Use the requested feature SHA as the audit baseline, although this worktree starts detached. Commit only on the new review branch. - Follow the upstream draft for URL-scoped verification and per-delivery 410/413 handling. The issue's short phrase "stop on 410 and 413" means stop retrying that delivery. This corrects preliminary observations, not a new product design choice. - Keep all benchmark fixtures under tests and run the existing disposable Instance harness. No User data, credentials or review artifacts are added to the repository. Final review head: `ed543dab2041c21e1ff6f300827f999250333bd4`. Local review patch: `artifacts/mcp-events-review.patch` in `/home/kayg/Developer/calternal-wt/mcp-events-review`. No push or deploy. The verdict requires the feature author to address findings 4 and 6 and apply the matrix fix in this patch. Issue remains open.
Author
Owner

Starting round 2 fixes on job/mcp-events-491.

Base: origin/dev at 687ff703136e71e89f8dfba139e93cd0788b25c1. Starting feature SHA: dadd320e24da39698112bab839edb3d2cb746dc7.

I am reading the independent review and porting its matrix/probe/benchmark coverage before implementing fair delivery, idle wakeups, receiver guidance and UI wording.

Starting round 2 fixes on `job/mcp-events-491`. Base: `origin/dev` at `687ff703136e71e89f8dfba139e93cd0788b25c1`. Starting feature SHA: `dadd320e24da39698112bab839edb3d2cb746dc7`. I am reading the independent review and porting its matrix/probe/benchmark coverage before implementing fair delivery, idle wakeups, receiver guidance and UI wording.
Author
Owner

Round 2 findings confirmed in the audited implementation:

  • Hub::start polls every 100 ms even with no queued deliveries. mcp_event_sources::start and the Money bill worker also wake every 15 and 30 seconds with no active interest.
  • The delivery path takes from one Instance-wide four-permit semaphore before selecting the first ready HashMap entry. There is no per-User in-flight bound, so one User can occupy all four sockets.
  • docs/mcp-events.md did not state raw-body HMAC verification, the five-minute replay window, durable event-ID deduplication, or receiver-side key rotation overlap.
  • The cross-User inventory has no mcp_event_revoke ID classification, and the Settings descriptions include “(draft)”.

Receiver guidance and visible wording are now committed as a4cd5210a. The review tests and probes are being ported and the scheduler fixes are in progress.

Round 2 findings confirmed in the audited implementation: - `Hub::start` polls every 100 ms even with no queued deliveries. `mcp_event_sources::start` and the Money bill worker also wake every 15 and 30 seconds with no active interest. - The delivery path takes from one Instance-wide four-permit semaphore before selecting the first ready `HashMap` entry. There is no per-User in-flight bound, so one User can occupy all four sockets. - `docs/mcp-events.md` did not state raw-body HMAC verification, the five-minute replay window, durable event-ID deduplication, or receiver-side key rotation overlap. - The cross-User inventory has no `mcp_event_revoke` ID classification, and the Settings descriptions include “(draft)”. Receiver guidance and visible wording are now committed as `a4cd5210a`. The review tests and probes are being ported and the scheduler fixes are in progress.
Author
Owner

Finding during Round 2 review: start_file_feed subscribed to the shared Files broadcast even when source_targets was empty, so normal file writes could wake the MCP worker without an active Files or Shares grant. The worker now waits only on source_changes in that state. When a matching grant appears, it scans the durable feed before waiting on the next signal, which also covers a write that raced with activation. This preserves the idle zero-periodic-wakeup goal and uses the existing feed signal only while relevant grants exist.

The small public Files addition is FilesState::subscribe_change_feed_wakeups(). It exposes only the existing data-free wake signal; the server still reads the durable, User-scoped change feed for event data and authorization.

Finding during Round 2 review: `start_file_feed` subscribed to the shared Files broadcast even when `source_targets` was empty, so normal file writes could wake the MCP worker without an active Files or Shares grant. The worker now waits only on `source_changes` in that state. When a matching grant appears, it scans the durable feed before waiting on the next signal, which also covers a write that raced with activation. This preserves the idle zero-periodic-wakeup goal and uses the existing feed signal only while relevant grants exist. The small public Files addition is `FilesState::subscribe_change_feed_wakeups()`. It exposes only the existing data-free wake signal; the server still reads the durable, User-scoped change feed for event data and authorization.
Author
Owner

Round 2 complete. Branch: job/mcp-events-491. Head: 48c7d4f5dbd669f24b506c1db94f718eef7a6388.

origin/dev was fetched and merged once before the final gates; it was already up to date.

Changes

  • Added UUID-ordered round-robin User selection and a one-request-per-User in-flight limit. A single User cannot occupy all four shared sockets.
  • Replaced dispatcher polling with queue, grant, completion and deadline wakeups. Scheduled workers park without a matching grant. Files and Shares use the existing Files signal and durable feed; Tasks and Calendar scan every 15 seconds while subscribed, and Bills scan every 30 seconds while subscribed because those sources have no due-time feed.
  • Added FilesState::subscribe_change_feed_wakeups(). It exposes only the existing data-free signal; event data still comes from the durable User-scoped feed.
  • Documented constant-time HMAC verification, a five-minute replay window, durable event-ID deduplication and receiver-side key rotation. Removed “(draft)” from the Settings labels.
  • Imported the independent review tests and cross-User route classification.

Files

  • apps/web/src/routes/settings/apps/AppsSection.svelte, apps/web/src/routes/settings/apps/McpEventsGroup.svelte
  • bench/mcp-events.mjs; crates/calternal-plugin/src/mcp_events.rs
  • crates/calternal-server/src/mcp_event_sources.rs, crates/calternal-server/src/mcp_events.rs, crates/calternal-server/src/mcp_events_review.rs
  • crates/plugins/files/src/lib.rs; crates/plugins/money/src/events.rs
  • docs/mcp-events.md; docs/perf/2026-10-02-mcp-events-491.md
  • tests/adversarial/mcp_events.mjs, tests/adversarial/test_xuser_classification.py, tests/adversarial/xuser_matrix.py

Gates

cargo fmt --check: exit 0; stdout and stderr were empty.

  • cargo clippy -p calternal-server --all-targets -- -D warnings:
        Finished `dev` profile [unoptimized + debuginfo] target(s) in 5m 41s
    
  • cargo clippy -p calternal-plugin --all-targets -- -D warnings:
        Finished `dev` profile [unoptimized + debuginfo] target(s) in 1m 05s
    
  • cargo clippy -p calternal-plugin-files --all-targets -- -D warnings:
        Finished `dev` profile [unoptimized + debuginfo] target(s) in 1m 56s
    
  • cargo clippy -p calternal-plugin-money --all-targets -- -D warnings:
        Finished `dev` profile [unoptimized + debuginfo] target(s) in 15.63s
    

cargo test -p calternal-server: test result: ok. 127 passed; 0 failed; 4 ignored; 0 measured; 0 filtered out; finished in 17.18s

cargo test -p calternal-plugin: test result: ok. 24 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.82s

cargo test -p calternal-plugin-files: test result: ok. 146 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 108.66s

cargo test -p calternal-plugin-money: test result: ok. 24 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 5.64s

bun run check: svelte-check found 0 errors and 0 warnings

bun run test:

 Test Files  148 passed (148)
      Tests  1011 passed (1011)
   Start at  01:54:14
   Duration  51.21s (transform 50%, environment 18%, import 16%, tests 10%, setup 5%)

Local probes and profile

Cross-User classification gate: 337 operations classified
Generated entry point classification: 951 tools classified
Admin coverage: 39 reviewed operations; contract and Rust guards agree

Two-User OpenAPI matrix: 337 operations classified; 159 operations replayed; 733 A-ID vs missing-ID comparisons across B, C, D and anonymous; 23 identifier routes classified with no local fixture factory; median absolute timing delta 0.6 ms
Job/Mail/quota ownership checks: 97 comparisons; 0 denial failures
Revoked Share timing control: identical HTTP 404 profiles; median delta 1.5 ms across 12 alternating pairs

The first matrix attempt stopped because this worktree lacked the calternal CLI binary. I built it and reran the matrix once; the run above passed. mcp_event_revoke is classified but has no local matrix fixture factory.

MCP Events local route checks passed. All twelve event types were visible.

Idle window, whole local debug server with no subscriptions: 30.05 s before / 30.10 s after; CPU 0.24 s / 0.18 s; voluntary context switches 73.34/s / 44.32/s; source-derived MCP Events periodic timer rate 10.10/s / 0.00/s. The host load differed, so this is not a release speedup claim.

After-change profile: average 30 requests, concurrency 1, p50 512.51 ms, p95 625.18 ms, CPU 98.61%, mean RSS 240.83 MiB, peak 244.46 MiB. Burst: 64 requests, concurrency 8, p50 1059.12 ms, p95 1779.91 ms, CPU 237.73%, mean RSS 293.28 MiB, peak 304.84 MiB.

Screenshots

Known gaps

  • docs/perf/baseline.json has no MCP Events workload. The local debug profile and the before/after idle sample are recorded in docs/perf/2026-10-02-mcp-events-491.md; repeat on the perf VM with the shared release build for a comparable baseline.
  • The cross-User matrix does not seed mcp_event_revoke; the route is classified, and MCP Events Settings ownership tests pass.
  • share.comment_created remains unavailable until the comment action and store exist.

Decisions not set by DESIGN §55

  • Use UUID-ordered round robin and cap each User at one active request. Keep the existing Instance-wide limit of four sockets.
  • Expose the existing data-free Files wake signal through a small public method. The durable User-scoped feed remains the source of event data.
  • Keep 15-second Task/Calendar and 30-second Bill scans while matching grants exist because those providers have no due-time feed.
  • Use a five-minute receiver replay window. Keep the prior signing key available for the five-second maximum in-flight request during rotation.
Round 2 complete. Branch: `job/mcp-events-491`. Head: `48c7d4f5dbd669f24b506c1db94f718eef7a6388`. `origin/dev` was fetched and merged once before the final gates; it was already up to date. ## Changes - Added UUID-ordered round-robin User selection and a one-request-per-User in-flight limit. A single User cannot occupy all four shared sockets. - Replaced dispatcher polling with queue, grant, completion and deadline wakeups. Scheduled workers park without a matching grant. Files and Shares use the existing Files signal and durable feed; Tasks and Calendar scan every 15 seconds while subscribed, and Bills scan every 30 seconds while subscribed because those sources have no due-time feed. - Added `FilesState::subscribe_change_feed_wakeups()`. It exposes only the existing data-free signal; event data still comes from the durable User-scoped feed. - Documented constant-time HMAC verification, a five-minute replay window, durable event-ID deduplication and receiver-side key rotation. Removed “(draft)” from the Settings labels. - Imported the independent review tests and cross-User route classification. ## Files - `apps/web/src/routes/settings/apps/AppsSection.svelte`, `apps/web/src/routes/settings/apps/McpEventsGroup.svelte` - `bench/mcp-events.mjs`; `crates/calternal-plugin/src/mcp_events.rs` - `crates/calternal-server/src/mcp_event_sources.rs`, `crates/calternal-server/src/mcp_events.rs`, `crates/calternal-server/src/mcp_events_review.rs` - `crates/plugins/files/src/lib.rs`; `crates/plugins/money/src/events.rs` - `docs/mcp-events.md`; `docs/perf/2026-10-02-mcp-events-491.md` - `tests/adversarial/mcp_events.mjs`, `tests/adversarial/test_xuser_classification.py`, `tests/adversarial/xuser_matrix.py` ## Gates `cargo fmt --check`: exit 0; stdout and stderr were empty. - `cargo clippy -p calternal-server --all-targets -- -D warnings`: ```text Finished `dev` profile [unoptimized + debuginfo] target(s) in 5m 41s ``` - `cargo clippy -p calternal-plugin --all-targets -- -D warnings`: ```text Finished `dev` profile [unoptimized + debuginfo] target(s) in 1m 05s ``` - `cargo clippy -p calternal-plugin-files --all-targets -- -D warnings`: ```text Finished `dev` profile [unoptimized + debuginfo] target(s) in 1m 56s ``` - `cargo clippy -p calternal-plugin-money --all-targets -- -D warnings`: ```text Finished `dev` profile [unoptimized + debuginfo] target(s) in 15.63s ``` `cargo test -p calternal-server`: `test result: ok. 127 passed; 0 failed; 4 ignored; 0 measured; 0 filtered out; finished in 17.18s` `cargo test -p calternal-plugin`: `test result: ok. 24 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.82s` `cargo test -p calternal-plugin-files`: `test result: ok. 146 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 108.66s` `cargo test -p calternal-plugin-money`: `test result: ok. 24 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 5.64s` `bun run check`: `svelte-check found 0 errors and 0 warnings` `bun run test`: ```text Test Files 148 passed (148) Tests 1011 passed (1011) Start at 01:54:14 Duration 51.21s (transform 50%, environment 18%, import 16%, tests 10%, setup 5%) ``` ## Local probes and profile `Cross-User classification gate: 337 operations classified` `Generated entry point classification: 951 tools classified` `Admin coverage: 39 reviewed operations; contract and Rust guards agree` `Two-User OpenAPI matrix: 337 operations classified; 159 operations replayed; 733 A-ID vs missing-ID comparisons across B, C, D and anonymous; 23 identifier routes classified with no local fixture factory; median absolute timing delta 0.6 ms` `Job/Mail/quota ownership checks: 97 comparisons; 0 denial failures` `Revoked Share timing control: identical HTTP 404 profiles; median delta 1.5 ms across 12 alternating pairs` The first matrix attempt stopped because this worktree lacked the `calternal` CLI binary. I built it and reran the matrix once; the run above passed. `mcp_event_revoke` is classified but has no local matrix fixture factory. `MCP Events local route checks passed.` All twelve event types were visible. Idle window, whole local debug server with no subscriptions: 30.05 s before / 30.10 s after; CPU 0.24 s / 0.18 s; voluntary context switches 73.34/s / 44.32/s; source-derived MCP Events periodic timer rate 10.10/s / 0.00/s. The host load differed, so this is not a release speedup claim. After-change profile: average 30 requests, concurrency 1, p50 512.51 ms, p95 625.18 ms, CPU 98.61%, mean RSS 240.83 MiB, peak 244.46 MiB. Burst: 64 requests, concurrency 8, p50 1059.12 ms, p95 1779.91 ms, CPU 237.73%, mean RSS 293.28 MiB, peak 304.84 MiB. ## Screenshots - User: [390px light](https://git.kayg.org/attachments/c3a68d10-6280-4cc1-bfc1-6498d6a109a9) · [390px dark](https://git.kayg.org/attachments/6b41f143-037f-43ba-89a7-401ef0b5f916) · [820px light](https://git.kayg.org/attachments/49226efc-622d-4296-977e-d5b34f0c3b54) · [820px dark](https://git.kayg.org/attachments/471bf206-50e6-4f64-9a84-f3a23c2e998a) · [1440px light](https://git.kayg.org/attachments/327865c0-0a0d-40ba-8816-8fa6fc15ce43) · [1440px dark](https://git.kayg.org/attachments/2222edde-3200-478a-928b-66785709bf1d) - Admin: [390px light](https://git.kayg.org/attachments/dd9621cd-c192-4982-8d88-2e51d9f6939d) · [390px dark](https://git.kayg.org/attachments/48f07802-5cb7-4b88-b7d2-aeb4abd58177) · [820px light](https://git.kayg.org/attachments/d22b692f-76b2-4896-936d-980433c5dd9f) · [820px dark](https://git.kayg.org/attachments/660c5f70-06de-4a6e-b955-fdd7536423fe) · [1440px light](https://git.kayg.org/attachments/3e0c969f-4837-4562-b5f2-8b74a7cab453) · [1440px dark](https://git.kayg.org/attachments/c6317529-ff07-40db-be09-9fff43fd1d09) ## Known gaps - `docs/perf/baseline.json` has no MCP Events workload. The local debug profile and the before/after idle sample are recorded in `docs/perf/2026-10-02-mcp-events-491.md`; repeat on the perf VM with the shared release build for a comparable baseline. - The cross-User matrix does not seed `mcp_event_revoke`; the route is classified, and MCP Events Settings ownership tests pass. - `share.comment_created` remains unavailable until the comment action and store exist. ## Decisions not set by DESIGN §55 - Use UUID-ordered round robin and cap each User at one active request. Keep the existing Instance-wide limit of four sockets. - Expose the existing data-free Files wake signal through a small public method. The durable User-scoped feed remains the source of event data. - Keep 15-second Task/Calendar and 30-second Bill scans while matching grants exist because those providers have no due-time feed. - Use a five-minute receiver replay window. Keep the prior signing key available for the five-second maximum in-flight request during rotation.
Author
Owner

Static audit evidence for DESIGN §55 MCP Events:

The MCP server generates tool definitions from the action registry at crates/calternal-server/src/mcp.rs:337-358. Its advertised capabilities enable tools only at :1229-1234. I found no events/list, subscription or signed webhook implementation.

Expected: versioned events/list entries come from the action registry, and subscriptions deliver signed webhooks with access checks before each delivery. Regression idea: subscribe, trigger an event, then revoke access and confirm later delivery stops.

Static audit evidence for DESIGN §55 MCP Events: The MCP server generates tool definitions from the action registry at crates/calternal-server/src/mcp.rs:337-358. Its advertised capabilities enable tools only at :1229-1234. I found no events/list, subscription or signed webhook implementation. Expected: versioned events/list entries come from the action registry, and subscriptions deliver signed webhooks with access checks before each delivery. Regression idea: subscribe, trigger an event, then revoke access and confirm later delivery stops.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/calternal#491
No description provided.