• v0.1.6 7c766ea752

    v0.1.6
    All checks were successful
    CI / cargo (push) Successful in 35s
    CI / docker (push) Successful in 20s
    Stable

    jlxq0 released this 2026-08-29 00:20:02 +00:00 | 8 commits to main since this release

    Adds upcoming_birthdays: contacts whose birthday falls within the next N days,
    across one or all address books, in one call.

    BDAY was parsed nowhere in the service before this, so the question could only
    be answered by paging list_contacts four times against 366 cards over a limit
    of 100 and parsing vCards client-side, which is 5.39 MB of vCard for a
    one-line question
    . ContactSummary also gains a birthday field.

    Accepts every ISO form vCard 3.0 and 4.0 permit and returns nothing on anything
    else, because a card with a free-text BDAY is a card and must not fail a query
    over the whole book. unparseable_birthdays in the response is the only signal
    that an answer is incomplete.

    Accepted in the field, both arms

    days=30   birthdays=3   contacts_scanned=374   unparseable=0   ref=2026-08-29
    days=14   birthdays=0   contacts_scanned=374   unparseable=0   ref=2026-08-29
    

    Expected 3 and 0, predicted before the code existed from a raw
    addressbook-query REPORT rather than from the tool: 45 birthdays across 374
    cards, three inside 30 days at offsets 15, 22 and 30.

    Confirmed independently by the pod's own audit log rather than only by the
    caller: method=upcoming_birthdays outcome=ok result_count=3 latency_ms=431 and
    result_count=0 latency_ms=292. 431 ms to scan 374 cards is the measure of
    what the tool replaces.

    The 14-day arm is a control only because the 30-day arm answers differently
    over the same cards on the same day. Alone it returns an empty list from a
    correct implementation, from a query matching nothing, and from one erroring
    into an empty vector alike.

    What the address book cannot pin

    45 birthdays, none today, none on a leap day, none unparseable. Fixtures pin the
    date rules and each dies under its own mutation: the leap-day choice (29
    February occurs on 1 March in a common year, so a birthday is never reported as
    past while still ahead), the inclusive window edge, the wrap into next year, and
    the birth-year floor.

    That last one is from cross-engine review, which found that a reference_date
    before the birth year treated the month-day as already recurring: 1984-03-17
    asked from 1983-03-16 returned 1 day instead of 367.

    Downloads
  • v0.1.5 c2d913da37

    v0.1.5
    All checks were successful
    CI / cargo (push) Successful in 38s
    CI / docker (push) Successful in 21s
    Stable

    jlxq0 released this 2026-08-28 01:55:05 +00:00 | 14 commits to main since this release

    Stops the initialize rate limiter livelocking the fleet. Every new MCP session
    was refused for hours on 2026-08-28 and the server recorded nothing about it.

    • A refused request still spent a token: check() charged the per-bearer bucket
      and then tested the per-subject bucket, so retries consumed every refill
      without reaching the two tokens one connection needs. It now charges exactly
      one bucket.
    • Burst 8 to 32. MAX_SESSIONS, the actual flood defence, is untouched at 256.
    • The refill is no longer anchored to SESSION_KEEP_ALIVE: one token per minute
      rather than one per thirty, because the old anchor tied recovery to session
      lifetime and made one connection cost an hour of fleet silence.
    • Adds carddav_mcp_initialize_rejected_total{bucket} and a warn line. Nothing
      recorded a rejection before, which is why the outage had to be reconstructed
      from what succeeded and from the silence after it.

    Acceptance, and what it does not show

    Pod carddav-mcp-7bf7b8ff4d-nw6kl, sha256:85da0deb…, up 02:07:22Z from a
    zero baseline. Three connections from the three agent sessions that hold a
    carddav mount, inside four minutes:

    refused MCP initialize    0
    create new session        6
    client initialized        3      2:1, no orphans
    authenticated requests   23
    distinct token_hash       1
    xff_entries / hops       23 of 23 at 2 / 2 / resolved
    

    This window would have passed on v0.1.4 as well. Six charges against the old
    burst of eight, and a pod replacement resets the buckets either way, so it
    evidences the rollout rather than the fix. The charge-order path never executed:
    zero rejections means the changed code did not run.

    The fix is evidenced by mutation, not in the field. Restoring the old order
    turns a_refused_initialize_spends_nothing red with left: Err(Bearer), right: Err(Subject) — the rejected request having spent its own token. Reverting the
    burst or the replenish period each kills only its own test.

    The field case cannot be constructed with three mounts: distinguishing the
    images needs five connections in one window, or a drained bucket observed
    recovering.

    v0.1.4's trusted-proxy-hop fix survives the upgrade unchanged, which is the
    regression nobody thinks to check.

    Downloads
  • v0.1.4 6d534e8a42

    v0.1.4
    All checks were successful
    CI / cargo (push) Successful in 42s
    CI / docker (push) Successful in 21s
    Stable

    jlxq0 released this 2026-08-27 04:06:18 +00:00 | 27 commits to main since this release

    Corrects one sentence in this tag's annotation. The tag itself is not re-cut:
    an image already points at the ref, and moving it would briefly make it
    ambiguous which image the tag names.

    The annotation says the bypass residual costs "a stolen bearer plus LAN
    access". It costs a stolen bearer plus code running inside the cluster.

    Measured 2026-08-27: from a machine on the house LAN, 203.24.209.5 and its v6
    address time out after 8 s on ports 80 and 443, while
    https://carddav.kampong.social/ answers 401 from that same machine as a
    control. From a pod inside the cluster, both gateway addresses answer 401.
    MetalLB is the reason: the fondue pool holding 203.24.209.5/32 is a
    BGPAdvertisement peered across sgp, lax and zrh, and the L2 pool is a
    different address on home-lan, so no BGP session carries the route to the
    wifi.

    Everything else in the annotation stands. The corrected wording, the manifest
    parentRef where the fact can actually be broken, and the reason a hop count
    of 2 would be worse than 1 if it ever were, are in src/config.rs and
    AGENTS.md as of PR 16.

    Verified on this release: pod carddav-mcp-5cf76bb494-7dg5g running
    sha256:dd915b10117a66eb0d6fad3fcae1b6f99ac6fb1d0d77c405e2d03542cc580858,
    reporting xff_entries=2 trusted_proxy_hops=2 client_ip_resolved=true on 7 of
    7 authenticated requests.

    Downloads
  • v0.1.2 e5bd505b2c

    carddav-mcp v0.1.2
    All checks were successful
    CI / docker (push) Successful in 36s
    CI / cargo (push) Successful in 54s
    Stable

    jlxq0 released this 2026-08-19 07:35:37 +00:00 | 47 commits to main since this release

    Security and open-source readiness release.

    Highlights:

    • bounds public MCP request bodies and identity-keyed in-memory state;
    • coalesces and rate-limits unknown-key JWKS refreshes;
    • restores OAuth token anti-caching headers and requires HTTPS for public dependencies;
    • adds Claude Desktop and Cowork callbacks, self-hosting docs, Compose/Kubernetes examples, and contributor/security policies;
    • ships a scanned Linux/AMD64 non-root image with SLSA provenance and SPDX SBOM attestations.

    Container:
    forge.oddie.app/jlxq0/carddav-mcp:v0.1.2@sha256:5ea11aa76eaf4a97a18a4185d96bfb1e51ff8b9e7270a4942968d58022a236ae

    Attached CycloneDX SBOM SHA-256:
    56e4f72e8ebe6a28b6d938aff78de35c375803a7a79f4036319185c612c05b63

    The v0.1.1 source tag was superseded before container publication because its release workflow used a Buildx-only flag with standalone buildctl.

    Downloads