Skip to content

Polyglot MCP fleet + image-host server + Edison catalog contract (edison-jwt verify) - #12

Open
Miyamura80 wants to merge 13 commits into
mainfrom
claude/mcp-repo-refactor-strategy-jc75pq
Open

Polyglot MCP fleet + image-host server + Edison catalog contract (edison-jwt verify)#12
Miyamura80 wants to merge 13 commits into
mainfrom
claude/mcp-repo-refactor-strategy-jc75pq

Conversation

@Miyamura80

@Miyamura80 Miyamura80 commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Summary

Refactors this repo in place into a polyglot MCP fleet monorepo and ships the first Edison-hosted server (image-host), the Edison catalog contract (how a fleet server advertises itself to the marketplace), and the edison-jwt verify half of the per-user JWT auth model. Pairs with edison-watch/edison-watch#<first-party MCP integration PR> (issuer + catalog sync + edison_hosted badge).

Changes

Fleet scaffold

  • servers/ + shared/{auth,catalog,ui} added additively around the existing Python app; strategy doc docs/mcp_commodity_fleet_strategy.md; README reframed around the commodity-fleet direction.

image-host (Cloudflare Worker + R2)servers/image-host/

  • upload_image / delete_image tools: base64 in, public non-expiring URL out (R2 via binding, no S3 keys); zero-config origin-derived URLs when PUBLIC_BASE_URL is unset; content-type allowlist + size cap + unguessable keys; public /i/<key> read path, guarded /mcp write path.
  • workerd integration test tier.

Edison catalog contractshared/catalog/

  • schema.json (entry shape) + aggregate.py validator (stdlib, no deps); make catalog_check wired into make ci.
  • servers/image-host/catalog-entry.json + co-located icon: the worked example. edison-watch's sync mirrors these source entries one-way into the marketplace catalog.

Auth: pluggable open | bearer | edison-jwtservers/image-host/src/

  • edison-jwt verify implemented: fetch Edison's JWKS (cached, kid-aware refetch with a rate-limit cooldown), verify RS256 via Workers WebCrypto, enforce iss/aud/exp, require kid; checkAuth is now async and fails closed on incomplete config.
  • Catalog auth enum gains edison-jwt (requires edison_hosted: true).

Testing

  • Unit tests pass: 45 tests green via bun test test/unit (incl. RS256 mint↔verify, expiry/aud/iss/kid, JWKS fetch + cooldown), tsc --noEmit clean.
  • make catalog_check green; ruff clean on shared/catalog/.
  • Note: full make ci / make test run in CI (sandbox lacks a working uv).

Rollout note

Not live on flip. image-host stays on auth: "token" (bearer) and the current dev Worker URL. Switching to edison-jwt is a coordinated deploy (Edison issuer key + Worker EDISON_JWKS_URL/ISSUER/AUDIENCE + catalog auth flip), done per-environment after the issuer is live. For release, image-host also moves to an Edison-owned Cloudflare account/domain.

Related Issues

Closes #

🤖 Generated with Claude Code

https://claude.ai/code/session_01GVK1ngvGgmfoGYxrjtAF5g


Generated by Claude Code


Summary by cubic

Refactors this repo into a polyglot MCP fleet and ships the first TypeScript server, image-host, plus the Edison catalog contract and edison-jwt verification. Adds shared auth/catalog scaffolding, CI for TS servers, and hardens auth/jwt/catalog and image validation.

  • New Features

    • Fleet scaffold: servers/ and shared/{auth,catalog,ui} added around the existing Python app; TS server CI; strategy doc.
    • image-host (Cloudflare Worker + R2): upload_image/delete_image; base64 in → public non-expiring URL; /i/<key> serves images; origin-derived URLs when PUBLIC_BASE_URL is unset; size/type checks; unguessable keys.
    • Auth modes: open | bearer | edison-jwt; edison-jwt verify via JWKS (RS256) with cache, cooldown, and in‑flight fetch dedup; enforce iss/aud/exp; require kid; fails closed on bad/missing config.
    • Catalog contract: shared/catalog/schema.json + aggregate.py; make catalog_check in CI; servers/image-host/catalog-entry.json + icon; catalog enum includes edison-jwt (requires edison_hosted: true). Tests/CI: offline unit and workerd integration tiers; path‑scoped Actions; 52 unit tests; typecheck clean.
  • Bug Fixes

    • JWT verify: catch WebCrypto import/verify errors; require RSA n/e; JWKS fetch/parse failures return 503; kid required and must match; refetch cooldown advances only on successful fetch; coalesce concurrent JWKS fetches per URL.
    • Auth/images: empty or whitespace AUTH_TOKEN now fails closed; PNG/GIF signature checks hardened; reject unsupported declared content_type values, including an explicit empty string.
    • Catalog: validator parity with schema — rejects non-object entries and unknown fields; enforces id regex (uses fullmatch to reject trailing-newline ids), non-empty string tags, valid icon name <id>.svg, non-empty URL hosts, and correct headers/template_fields.env shapes.

Written for commit d2b552f. Summary will update on new commits.

Review in cubic

claude added 10 commits July 24, 2026 11:00
Captures the decision to move from a single Gmail MCP to a polyglot,
per-server-deploy fleet: TS/Cloudflare-Workers default (Python/FastMCP for
Gmail + heavy cases), shared React MCP-UI lib, Edison-minted per-user JWT auth
(pluggable, v1 bearer), edison_hosted catalog flag, image hosting as wave 1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GVK1ngvGgmfoGYxrjtAF5g
Add a Direction section noting the transition from a single Gmail MCP to a
fleet of first-party, open-source streamable-HTTP servers (TS/Cloudflare
Workers default; Python/FastMCP for Gmail + heavy cases), linking the new
strategy doc.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GVK1ngvGgmfoGYxrjtAF5g
Resolve the repo-shape open question: this repo is refactored in place into the
polyglot monorepo (not greenfield). Grow additively — add servers/ (TS Workers)
+ shared/{ui,auth,catalog} around the existing Python/Gmail app, whose deploy is
untouched; Gmail becomes a co-tenant, optional servers/gmail/ relocation later.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GVK1ngvGgmfoGYxrjtAF5g
… server

Add the additive monorepo skeleton and the first TypeScript-on-Cloudflare-Workers
first-party server, per docs/mcp_commodity_fleet_strategy.md.

- servers/image-host: streamable-HTTP MCP (McpAgent + R2 binding). upload_image
  takes base64, validates by magic bytes (png/jpeg/webp/gif; svg rejected),
  stores under an unguessable key, and returns a public non-expiring URL served
  by the worker at /i/<key>. delete_image companion. Pure logic (validation,
  key-gen, auth) is dependency-free and unit-tested offline via `bun test`
  (28 tests); typechecks clean against the real SDKs.
- Pluggable auth (open | bearer | edison-jwt); v1 ships bearer, edison-jwt is a
  loud 501 stub so the drop-in seam is real but never silently open.
- shared/{auth,catalog,ui}: fleet contracts (placeholders + auth reference).
- .github/workflows/servers_ts.yaml: path-scoped bun typecheck + test, matrix
  over servers. .gitignore: TS toolchain artifacts.

The existing Python/Gmail app and its deploy are untouched.
…hardening

- delete_image: head-check before delete so `deleted` is truthful (R2 delete is
  a no-op on a missing key and can't report it).
- upload_image: fail loudly when PUBLIC_BASE_URL is unset instead of returning a
  non-embeddable relative URL (the tool's whole promise is an absolute URL).
- upload_image: reject oversized payloads before base64 decode (base64PayloadTooLarge)
  to avoid the ~3x peak allocation on a large blob.
- serveImage: guard decodeURIComponent (malformed %-encoding -> 404, not 500)
  and set X-Content-Type-Options: nosniff on served bytes.
- parsePositiveInt: use Number + isInteger so junk like "10MB" is rejected, not
  silently truncated to 10.
- resolveAuthMode: return AuthMode | string so an unknown mode is passed through
  and rejected (fail closed) rather than cast into the AuthMode type; fix the
  misleading "read-only-ish" open-mode comment (open still exposes mutating tools).
- Tests: +4 (base64PayloadTooLarge, unknown-mode passthrough). 32/32 pass; tsc clean.
Harden image-host to production bar with a second test tier and the ctx.props
follow-up the review deferred.

- Zero-config URLs: the fetch handler injects the connected origin via ctx.props
  so upload_image returns an absolute, embeddable URL even when PUBLIC_BASE_URL
  is unset. Explicit PUBLIC_BASE_URL still wins (override for custom domains);
  fail-loud remains only if neither resolves.
- Integration tier: @cloudflare/vitest-pool-workers runs the real Worker in
  workerd with real R2 + Durable Object bindings. Drives the full MCP
  streamable-HTTP handshake over SELF.fetch and asserts the wire format, the
  origin-derived URL, R2 round-trip + serving, nosniff, the /i/%ZZ 404 guard,
  and the bearer auth gate (8 tests). Reads only the first SSE result event so
  tool calls return in ms instead of waiting on the idle stream close.
- Test layout split into tiers: test/unit (bun test, offline, 32 tests) and
  test/integration (vitest + workerd). bunfig scopes bare `bun test` to unit;
  wrangler config drives the pool; ajv pre-bundled via deps.optimizer so its
  JSON require resolves under workerd.
- CI (servers_ts.yaml) now runs unit + typecheck + integration.

All green: 32 unit, 8 integration, tsc clean.
…entry)

Promote shared/catalog from placeholder to a real contract: each server
declares one servers/<id>/catalog-entry.json (connector spec) plus its
co-located icon; shared/catalog/aggregate.py validates every entry against
shared/catalog/schema.json and builds the combined dist/catalog.json.

- servers/image-host/catalog-entry.json + image-host.svg: the worked example.
- make catalog_check (wired into make ci) fails on any invalid entry.

edison-watch's scripts/sync_fleet_connectors.py mirrors these source entries
one-way into the marketplace catalog. See docs/mcp_commodity_fleet_strategy.md
§7 and edison-watch first_party_mcp_integration.md §(1)-(2).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GVK1ngvGgmfoGYxrjtAF5g
Replace the edison-jwt 501 stub with stateless verification: fetch Edison's
JWKS (cached, kid-aware refetch), verify the RS256 signature via Workers
WebCrypto, and enforce iss/aud/exp before admitting a call — subject = the
token's `sub` for per-user attribution. RS256 only (pins alg; no downgrade).

- src/jwt.ts: verifyJwtWithJwks (pure, unit-tested) + verifyEdisonJwt (JWKS
  fetch/cache). checkAuth is now async and fails closed on incomplete config.
- test/unit/jwt.test.ts: real RS256 keypair, covers valid/expired/wrong-iss/
  wrong-aud/bad-sig/wrong-alg/kid-miss/malformed. 43 unit tests green.

AUTH_MODE stays bearer; the flip to edison-jwt is a coordinated deploy once
Edison's issuer is live (wrangler.jsonc documents the three config vars).

Contract mirrors edison-watch src/mcp_jwt.py.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GVK1ngvGgmfoGYxrjtAF5g
Add "edison-jwt" to the catalog auth enum so a first-party server can declare
that Edison mints + injects a per-user JWT (no user-supplied token). aggregate.py
enforces the invariant that edison-jwt implies edison_hosted: true.

No entry uses it yet — image-host stays on "token" (bearer) until the
coordinated flip. This just makes the flip pure data.

Verify + inject sides: image-host/src/jwt.ts and edison-watch src/mcp_jwt*.py.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GVK1ngvGgmfoGYxrjtAF5g
Thermo-nuclear review hardening on the verify side:
- Rate-limit JWKS refetches (JWKS_MIN_REFETCH_MS): an attacker-chosen `kid` was
  fully unauthenticated and forced one upstream /.well-known/jwks.json GET per
  crafted request. Now, past the cooldown we serve the cached set (a kid miss
  then 401s) instead of hammering Edison.
- Require `kid` and match it exactly (Edison always sets it), dropping the
  "try every RSA key" fallback for kid-less tokens.

Adds tests for the missing-kid rejection and the verifyEdisonJwt fetch/cooldown
path (previously uncovered). 45 unit tests green, tsc clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GVK1ngvGgmfoGYxrjtAF5g
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Polyglot MCP fleet scaffold with image-host Worker, catalog contract, and Edison JWT verify

✨ Enhancement 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Add servers/ + shared/ scaffold for a polyglot, per-server-deploy MCP fleet.
• Ship image-host Worker MCP: base64 upload/delete, R2 storage, public /i/ URLs.
• Define Edison catalog + JWT auth contracts with CI validation and edison-jwt verification.
Diagram

graph TD
  caller{{"Edison proxy/client"}} --> worker["image-host Worker"] --> auth["Auth + JWT verify"] --> jwks{{"Edison JWKS"}}
  worker --> mcp["MCP Durable Object"] --> r2[(R2Bucket)]
  entry[/"catalog-entry.json"/] --> contract["Catalog contract + CI check"]

  subgraph Legend
    direction LR
    _ext{{External}} ~~~ _svc["Service/Module"] ~~~ _db[(Database)] ~~~ _file[/"File"/]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Use a standard JWT/JWKS library (e.g., jose) for RS256 verification
  • ➕ Less custom cryptography/parsing code to maintain
  • ➕ Broader coverage of JWT edge cases and JWK formats
  • ➕ Easier to extend (e.g., additional claim validation, clock skew options)
  • ➖ Bigger dependency/bundle size for Workers
  • ➖ Potential workerd/vitest bundling friction (CJS/JSON imports)
  • ➖ Harder to keep the unit-test tier dependency-free
2. Gate hosted endpoints with Cloudflare Access / Zero Trust instead of per-user JWTs
  • ➕ Operationally simple for Edison-owned deployments
  • ➕ Avoids implementing JWT verification at each server
  • ➕ Leverages existing edge auth controls
  • ➖ Does not naturally provide per-user attribution via sub
  • ➖ Less portable to non-Cloudflare/self-hosted environments
  • ➖ Harder to unify auth semantics across a polyglot fleet
3. Validate catalog entries with a JSON Schema validator dependency in Python
  • ➕ Closer adherence to the JSON Schema spec
  • ➕ Less duplicated validation logic (schema drives checks)
  • ➖ Adds CI/runtime dependency (contrary to stdlib-only goal)
  • ➖ Schema validators can be slower/less controllable for bespoke constraints
  • ➖ Still needs extra checks for repo-specific invariants (icon co-location, id==dir)

Recommendation: The PR’s approach is reasonable for a first fleet server: it keeps the Worker runtime lean (no heavyweight crypto deps), explicitly fails closed on auth misconfiguration, and provides strong test coverage (unit + workerd integration). If JWT requirements expand (multiple algs, richer claim rules, more JWK variants), revisit adopting a hardened JWT library; otherwise keep the current minimal verifier and promote it into shared/auth once a second TS server needs it.

Files changed (32) +3180 / -1

Enhancement (4) +764 / -0
auth.tsImplement pluggable auth (open, bearer, edison-jwt) +129/-0

Implement pluggable auth (open, bearer, edison-jwt)

• Adds auth mode resolution, constant-time bearer comparison, and async auth checking including Edison JWT verification with fail-closed behavior on misconfiguration.

servers/image-host/src/auth.ts

images.tsAdd image validation, base64 decode, and unguessable key generation +200/-0

Add image validation, base64 decode, and unguessable key generation

• Implements dependency-free helpers to decode base64/data URLs, enforce size caps, sniff allowed image types via magic bytes, reject SVG, and generate sanitized, random object keys.

servers/image-host/src/images.ts

index.tsAdd image-host Worker MCP server with R2-backed tools and routing +215/-0

Add image-host Worker MCP server with R2-backed tools and routing

• Implements the MCP tool surface (upload_image/delete_image), routes GET /i/<key> for public reads with immutable caching and nosniff, gates POST /mcp with auth, and supports origin-derived URL generation when PUBLIC_BASE_URL is unset.

servers/image-host/src/index.ts

jwt.tsAdd RS256 JWT verification with JWKS caching and cooldown refetch +220/-0

Add RS256 JWT verification with JWKS caching and cooldown refetch

• Implements Edison JWT verification using Workers WebCrypto, enforces alg/kid/iss/aud/exp/nbf/sub, and fetches JWKS with isolate-level TTL caching plus a rate-limited kid-miss refetch strategy.

servers/image-host/src/jwt.ts

Tests (5) +572 / -0
env.d.tsType cloudflare:test env bindings for integration tests +7/-0

Type cloudflare:test env bindings for integration tests

• Augments cloudflare:test’s ProvidedEnv typing with the worker Env interface for better editor and test type safety.

servers/image-host/test/integration/env.d.ts

worker.spec.tsAdd workerd integration tests for routing, auth, MCP, and R2 serving +168/-0

Add workerd integration tests for routing, auth, MCP, and R2 serving

• Runs the real Worker in workerd to validate /health and /i routing behavior, bearer auth gating on /mcp, MCP session establishment, tool calls, R2 round-trip, and security headers like nosniff.

servers/image-host/test/integration/worker.spec.ts

auth.test.tsAdd unit tests for auth mode resolution and bearer handling +91/-0

Add unit tests for auth mode resolution and bearer handling

• Covers resolveAuthMode defaults/unknown handling, bearer extraction, constant-time equality behavior, and fail-closed behavior for edison-jwt misconfiguration.

servers/image-host/test/unit/auth.test.ts

images.test.tsAdd unit tests for image validation and key generation +151/-0

Add unit tests for image validation and key generation

• Covers format sniffing, base64/data URL decoding, size guards, SVG rejection, prefix sanitization, slug generation, and key uniqueness/format constraints.

servers/image-host/test/unit/images.test.ts

jwt.test.tsAdd unit tests for RS256 verification and JWKS fetch cooldown +155/-0

Add unit tests for RS256 verification and JWKS fetch cooldown

• Validates signature verification, issuer/audience/expiry/kid enforcement, alg pinning, and ensures JWKS refetch is rate-limited on attacker-driven kid misses.

servers/image-host/test/unit/jwt.test.ts

Documentation (9) +494 / -0
README.mdDocument repo direction toward an MCP fleet monorepo +14/-0

Document repo direction toward an MCP fleet monorepo

• Adds a Direction section describing the transition to a polyglot fleet of streamable-HTTP MCP servers and links to the strategy document.

README.md

mcp_commodity_fleet_strategy.mdAdd MCP commodity fleet refactor strategy document +202/-0

Add MCP commodity fleet refactor strategy document

• Introduces a detailed strategy covering monorepo topology, runtime choices (Workers vs FastMCP), shared UI/auth/catalog contracts, and rollout sequencing (bearer first, edison-jwt later).

docs/mcp_commodity_fleet_strategy.md

README.mdIntroduce servers/ fleet overview and first server listing +27/-0

Introduce servers/ fleet overview and first server listing

• Documents the purpose of 'servers/', the cross-fleet auth and catalog contracts, and enumerates 'image-host' as the first TS/Workers server.

servers/README.md

README.mdDocument image-host server behavior, endpoints, and operation +122/-0

Document image-host server behavior, endpoints, and operation

• Describes the upload/delete tools, format/size constraints, public read endpoint, auth modes, development commands, and deployment checklist.

servers/image-host/README.md

image-host.svgAdd image-host catalog icon asset +5/-0

Add image-host catalog icon asset

• Adds an SVG icon co-located with the server catalog entry for marketplace display.

servers/image-host/image-host.svg

README.mdIntroduce shared/ contracts overview for the fleet +18/-0

Introduce shared/ contracts overview for the fleet

• Documents the shared cross-fleet areas (auth, catalog, UI) and their intended role across TS and Python servers.

shared/README.md

README.mdDocument the fleet auth contract and modes +34/-0

Document the fleet auth contract and modes

• Defines the intended meaning of open/bearer/edison-jwt across the fleet and the long-term plan to host shared TS/Py implementations.

shared/auth/README.md

README.mdDocument the Edison catalog contract and sync model +47/-0

Document the Edison catalog contract and sync model

• Explains the per-server catalog-entry.json pattern, schema/validator responsibilities, the one-way sync into edison-watch, and the CI check surface.

shared/catalog/README.md

README.mdAdd placeholder documentation for shared MCP-UI component library +25/-0

Add placeholder documentation for shared MCP-UI component library

• Describes the plan for a shared React MCP-UI library consumable by both TS/ext-apps and Python/FastMCP runtimes, noting it is not yet implemented.

shared/ui/README.md

Other (14) +1350 / -1
servers_ts.yamlAdd CI workflow for TypeScript fleet servers +50/-0

Add CI workflow for TypeScript fleet servers

• Introduces a GitHub Actions workflow that runs per-server (matrix) bun install, unit tests, typecheck, and workerd-based integration tests for TS servers under 'servers/'/'shared/' changes.

.github/workflows/servers_ts.yaml

.gitignoreIgnore TS fleet build and local dev artifacts +7/-0

Ignore TS fleet build and local dev artifacts

• Adds ignore rules for node_modules, Wrangler state, local .dev.vars, per-server dist outputs, and TS build info files across 'servers/**'.

.gitignore

MakefileWire catalog entry validation into CI +6/-1

Wire catalog entry validation into CI

• Adds a 'catalog_check' target that runs 'shared/catalog/aggregate.py --check' and includes it in the 'ci' target.

Makefile

.dev.vars.exampleAdd local dev env var template for Wrangler +12/-0

Add local dev env var template for Wrangler

• Provides a copyable '.dev.vars' example for 'wrangler dev', including AUTH_TOKEN and PUBLIC_BASE_URL plus optional overrides.

servers/image-host/.dev.vars.example

.gitignoreIgnore image-host local artifacts +5/-0

Ignore image-host local artifacts

• Ignores node_modules, Wrangler state, local dev vars, dist, and tsbuildinfo within the server directory.

servers/image-host/.gitignore

bun.lockLock bun dependencies for image-host +837/-0

Lock bun dependencies for image-host

• Adds a bun lockfile pinning runtime and dev dependencies used by the Worker, unit tests, and workerd integration tests.

servers/image-host/bun.lock

bunfig.tomlConfigure bun test root to unit tests only +5/-0

Configure bun test root to unit tests only

• Restricts bare 'bun test' to the offline unit test tier, keeping integration tests on vitest/workerd.

servers/image-host/bunfig.toml

catalog-entry.jsonAdd Edison marketplace catalog entry for image-host +29/-0

Add Edison marketplace catalog entry for image-host

• Defines the connector listing metadata (id, name, tags, URL, icon) and v1 token-based auth template fields for Edison marketplace sync.

servers/image-host/catalog-entry.json

package.jsonAdd image-host package scripts and dependencies +30/-0

Add image-host package scripts and dependencies

• Defines bun scripts for unit/integration tests and typechecking, plus dependencies on MCP SDK, agents runtime, zod, vitest pool for workers, and wrangler.

servers/image-host/package.json

tsconfig.jsonAdd strict TS config for image-host (excluding integration tests) +18/-0

Add strict TS config for image-host (excluding integration tests)

• Configures strict typechecking for src and unit tests with Workers/Bun types, while excluding integration tests that run under vitest/workerd.

servers/image-host/tsconfig.json

vitest.config.tsConfigure vitest/workerd integration test pool +42/-0

Configure vitest/workerd integration test pool

• Sets up @cloudflare/vitest-pool-workers with bundling workarounds for ajv, longer timeouts for SSE sessions, and bindings/config loading from wrangler.jsonc.

servers/image-host/vitest.config.ts

wrangler.jsoncAdd Wrangler config with Durable Object + R2 bindings +27/-0

Add Wrangler config with Durable Object + R2 bindings

• Defines Worker entrypoint, compatibility settings, DO migrations/binding, R2 bucket binding, and runtime vars for auth mode, key prefix, and public base URL resolution.

servers/image-host/wrangler.jsonc

aggregate.pyAdd stdlib validator and aggregator for fleet catalog entries +161/-0

Add stdlib validator and aggregator for fleet catalog entries

• Discovers servers/*/catalog-entry.json files, validates required fields and fleet invariants (id/icon/auth rules), and optionally emits a deterministic combined dist/catalog.json artifact; supports --check for CI.

shared/catalog/aggregate.py

schema.jsonAdd JSON Schema for fleet catalog entries (incl. edison-jwt mode) +121/-0

Add JSON Schema for fleet catalog entries (incl. edison-jwt mode)

• Defines the catalog entry JSON schema, including auth modes (none/token/oauth/edison-jwt), token template field requirements, icon/id conventions, and the edison_hosted flag constraint.

shared/catalog/schema.json

@qodo-code-review

qodo-code-review Bot commented Aug 3, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (1) 📜 Skill insights (1)

Context used
✅ Compliance rules (platform): 31 rules

Grey Divider


Action required

1. JWKS cooldown returns wrong cache ✓ Resolved 🐞 Bug ☼ Reliability
Description
ensureJwks() can return jwksCache during the refetch cooldown without verifying the cached JWKS was
fetched from the requested URL, and it updates lastFetchAt before the fetch succeeds. A JWKS URL
change or transient fetch failure (especially on a cold isolate with no cache) can cause valid
tokens to be verified against a stale JWKS or yield 503s for up to JWKS_MIN_REFETCH_MS.
Code

servers/image-host/src/jwt.ts[R198-201]

+  if (now - lastFetchAt < JWKS_MIN_REFETCH_MS) return jwksCache ? jwksCache.jwks : null;
+  lastFetchAt = now;
+
+  const jwks = await fetchJwks(url);
Evidence
The cooldown early-return ignores the cache URL and lastFetchAt is set before the network call, so
failures or config changes can incorrectly reuse stale JWKS or block retries; verifyEdisonJwt
converts a null JWKS into a 503.

servers/image-host/src/jwt.ts[52-56]
servers/image-host/src/jwt.ts[190-205]
servers/image-host/src/jwt.ts[207-219]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`ensureJwks()` has two related bugs:
1) During the cooldown window it returns `jwksCache.jwks` even if `jwksCache.url !== url`.
2) It sets `lastFetchAt` before the fetch succeeds, which can suppress retries after a failed fetch.

This can lead to authentication flakiness (bounded outages / stale verification set) in `edison-jwt` mode.

### Issue Context
- `jwksCache` is module-scoped and includes the `url` it was fetched from.
- `verifyEdisonJwt()` returns 503 when `ensureJwks()` returns null.

### Fix Focus Areas
- servers/image-host/src/jwt.ts[52-56]
- servers/image-host/src/jwt.ts[190-205]
- servers/image-host/src/jwt.ts[207-219]

### Implementation notes
- In the cooldown early-return path, only return the cached JWKS if `jwksCache?.url === url`.
- Only advance `lastFetchAt` after a successful fetch (or consider a separate `inFlight` promise to dedupe concurrent fetches without suppressing retries on failure).
- Add a unit test for (a) URL change with existing cache and (b) transient fetch failure with no cache to ensure the next request can retry immediately (or at least not get stuck returning null for the entire cooldown).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Version in package.json ✓ Resolved 📜 Skill insight ≡ Correctness
Description
servers/image-host/package.json introduces a hardcoded version field, creating an additional
version source outside pyproject.toml. This violates the requirement that versioning be
single-sourced only in pyproject.toml.
Code

servers/image-host/package.json[R2-4]

+  "name": "@edison/mcp-image-host",
+  "version": "0.1.0",
+  "private": true,
Evidence
pyproject.toml already defines the project version, but the new package.json adds another
version string (0.1.0), violating the single-source requirement.

pyproject.toml[1-4]
servers/image-host/package.json[1-4]
Skill: push-release

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The repo must single-source its version in `pyproject.toml` only, but `servers/image-host/package.json` adds a separate `version` field.

## Issue Context
The compliance rule explicitly disallows version strings outside `pyproject.toml` (e.g., `__version__`, `version = ...`, or other version declarations).

## Fix Focus Areas
- servers/image-host/package.json[1-6]
- pyproject.toml[1-4]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. No Python version guard 📘 Rule violation ≡ Correctness
Description
shared/catalog/aggregate.py is an executable entry point but does not enforce the repo’s
configured minimum Python version via a sys.version_info guard. Running it under an older
interpreter could proceed (or fail later) instead of exiting immediately with a clear error.
Code

shared/catalog/aggregate.py[R160-161]

+if __name__ == "__main__":
+    sys.exit(main())
Evidence
The project declares requires-python = ">= 3.12" in pyproject.toml, but the new entry point
directly executes main() without any interpreter version check, violating the requirement to exit
immediately on unsupported Python versions.

Rule 2208949: Enforce minimum Python version from project configuration
pyproject.toml[74-76]
shared/catalog/aggregate.py[160-161]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`shared/catalog/aggregate.py` is a runtime entry point and should enforce the project’s minimum Python version (`>=3.12`) with an early `sys.version_info` guard that exits non-zero with a clear message.

## Issue Context
The project declares `requires-python = ">= 3.12"` in `pyproject.toml`, and the compliance rule requires runtime enforcement in entry points.

## Fix Focus Areas
- shared/catalog/aggregate.py[30-50]
- shared/catalog/aggregate.py[160-161]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Uncaught WebCrypto verify errors ✓ Resolved 🐞 Bug ☼ Reliability
Description
verifySignature() does not catch crypto.subtle.importKey()/verify() failures, so a
malformed/unsupported RSA JWK can throw and bypass the intended JwtResult/AuthResult error handling.
In edison-jwt mode this can surface as an internal error instead of a controlled 401/503 response.
Code

servers/image-host/src/jwt.ts[R121-123]

+  const algo = { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" };
+  const key = await crypto.subtle.importKey("jwk", jwk, algo, false, ["verify"]);
+  return crypto.subtle.verify(algo, key, signature, new TextEncoder().encode(signingInput));
Evidence
verifySignature calls importKey/verify without handling rejections, and verifyJwtWithJwks/checkAuth
do not guard against thrown errors from that call chain.

servers/image-host/src/jwt.ts[116-124]
servers/image-host/src/jwt.ts[153-171]
servers/image-host/src/auth.ts[103-124]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`verifySignature()` awaits WebCrypto operations without try/catch. If `importKey` or `verify` rejects, the promise rejection can escape `verifyJwtWithJwks()`/`verifyEdisonJwt()` and thus escape `checkAuth()`'s normal `{ ok: false, status, message }` path.

### Issue Context
- Candidate keys are selected only by `kty === "RSA"` and `kid` equality; other required JWK fields are not validated before `importKey`.
- `checkAuth()` awaits `verifyEdisonJwt()` without a surrounding try/catch.

### Fix Focus Areas
- servers/image-host/src/jwt.ts[116-124]
- servers/image-host/src/jwt.ts[153-171]
- servers/image-host/src/auth.ts[103-124]

### Implementation notes
- Wrap `importKey` + `verify` in `try/catch` and return `false` (or a structured failure) on exceptions.
- Optionally filter candidates to RSA keys that include `n` and `e` before attempting `importKey`.
- Consider distinguishing between "token invalid" vs "JWKS unusable" (e.g., 401 vs 503), but at minimum avoid unhandled exceptions.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Catalog check misses schema ✓ Resolved 🐞 Bug ≡ Correctness
Description
shared/catalog/aggregate.py is used as the CI validator (make catalog_check) but it does not enforce
several constraints declared in shared/catalog/schema.json (e.g., additionalProperties:false,
id/icon regex patterns, and tag item types/min lengths). As a result, schema-invalid
servers/*/catalog-entry.json files can pass CI validation and violate the documented contract.
Code

shared/catalog/aggregate.py[R11-14]

+1. discovers every `servers/*/catalog-entry.json`,
+2. validates each against the constraints in `shared/catalog/schema.json`
+   (enforced here in stdlib Python so CI needs no extra dependency), and
+3. emits the combined `shared/catalog/dist/catalog.json` build artifact.
Evidence
aggregate.py claims to validate against schema.json, and is invoked from CI, but its actual checks
omit multiple schema constraints such as additionalProperties:false and regex/type rules.

Makefile[301-307]
shared/catalog/aggregate.py[5-14]
shared/catalog/aggregate.py[56-111]
shared/catalog/schema.json[6-8]
shared/catalog/schema.json[19-52]
shared/catalog/schema.json[98-103]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`aggregate.py --check` is presented as validating entries against `shared/catalog/schema.json`, but it currently checks only a subset of schema rules. This undermines the intended contract gate in CI.

### Issue Context
- `schema.json` sets `additionalProperties: false`, regex patterns for `id`/`icon`, and minimum lengths/types for multiple fields.
- `aggregate.py` only validates a few conditions (e.g., tags is a non-empty list, icon endswith .svg) and does not reject unknown fields.
- `Makefile` wires this into `ci`.

### Fix Focus Areas
- shared/catalog/aggregate.py[5-14]
- shared/catalog/aggregate.py[56-111]
- shared/catalog/schema.json[6-8]
- shared/catalog/schema.json[19-52]
- shared/catalog/schema.json[98-103]

### Implementation notes
- Either:
 - Implement the missing schema checks in stdlib Python (e.g., reject unknown keys, enforce regex patterns via `re`, validate `tags` items are non-empty strings, validate `headers` and `template_fields` shapes), or
 - Introduce a JSON Schema validator dependency and run real schema validation in CI.
- Add a small test/fixture set of intentionally-invalid entries to ensure `--check` fails for each missing constraint and stays in sync with `schema.json`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (1)
6. any and as casts 📜 Skill insight ≡ Correctness
Description
New TypeScript code introduces any and several unchecked casts (including double casts) that
weaken type safety at key parsing/mocking boundaries. These should be replaced with explicit types
and/or runtime validation (e.g., Zod parsing) to avoid masking shape errors.
Code

servers/image-host/test/integration/worker.spec.ts[23]

+function extractMessage(buffer: string): Record<string, any> | null {
Evidence
The new integration test returns Record<string, any>, and core JWT parsing uses as T on
JSON.parse() output; unit tests also use as unknown as typeof fetch to coerce a stub. These
patterns match the rule’s prohibited loose typing/casting and can be replaced by explicit types/type
guards or Zod validation.

servers/image-host/test/integration/worker.spec.ts[23-23]
servers/image-host/src/jwt.ts[77-79]
servers/image-host/test/unit/jwt.test.ts[134-140]
Skill: thermo-nuclear-code-quality-review

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
New TS code uses `any` and unchecked type assertions (`as T`, `as unknown as ...`), which can hide real shape mismatches coming from `JSON.parse()` / `res.json()` and from test stubs.

## Issue Context
The compliance rule requires avoiding `any`/`unknown`/casts when explicit types or clearer boundaries can be defined.

## Fix Focus Areas
- servers/image-host/test/integration/worker.spec.ts[21-33]
- servers/image-host/src/jwt.ts[77-79]
- servers/image-host/test/unit/jwt.test.ts[134-140]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment on lines +160 to +161
if __name__ == "__main__":
sys.exit(main())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

1. No python version guard 📘 Rule violation ≡ Correctness

shared/catalog/aggregate.py is an executable entry point but does not enforce the repo’s
configured minimum Python version via a sys.version_info guard. Running it under an older
interpreter could proceed (or fail later) instead of exiting immediately with a clear error.
Agent Prompt
## Issue description
`shared/catalog/aggregate.py` is a runtime entry point and should enforce the project’s minimum Python version (`>=3.12`) with an early `sys.version_info` guard that exits non-zero with a clear message.

## Issue Context
The project declares `requires-python = ">= 3.12"` in `pyproject.toml`, and the compliance rule requires runtime enforcement in entry points.

## Fix Focus Areas
- shared/catalog/aggregate.py[30-50]
- shared/catalog/aggregate.py[160-161]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread servers/image-host/package.json

// Streamable-HTTP replies are SSE (`data: {json}\n\n`). Pull the first JSON-RPC
// message carrying a result/error out of the buffered stream so far.
function extractMessage(buffer: string): Record<string, any> | null {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

3. any and as casts 📜 Skill insight ≡ Correctness

New TypeScript code introduces any and several unchecked casts (including double casts) that
weaken type safety at key parsing/mocking boundaries. These should be replaced with explicit types
and/or runtime validation (e.g., Zod parsing) to avoid masking shape errors.
Agent Prompt
## Issue description
New TS code uses `any` and unchecked type assertions (`as T`, `as unknown as ...`), which can hide real shape mismatches coming from `JSON.parse()` / `res.json()` and from test stubs.

## Issue Context
The compliance rule requires avoiding `any`/`unknown`/casts when explicit types or clearer boundaries can be defined.

## Fix Focus Areas
- servers/image-host/test/integration/worker.spec.ts[21-33]
- servers/image-host/src/jwt.ts[77-79]
- servers/image-host/test/unit/jwt.test.ts[134-140]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread servers/image-host/src/jwt.ts Outdated
Comment thread servers/image-host/src/jwt.ts Outdated
Comment thread shared/catalog/aggregate.py

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

18 issues found across 32 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="shared/auth/README.md">

<violation number="1" location="shared/auth/README.md:26">
P3: This status line is now stale and inaccurate: it claims edison-jwt is "stubbed to fail loudly with 501," but the verify layer in servers/image-host/src/jwt.ts (RS256 + JWKS + iss/aud/exp/kid) is implemented and wired into the edison-jwt case in auth.ts, per the PR itself. Update the status to reflect that edison-jwt verify is implemented for image-host, and keep the note about TS/Python ports being promoted later.</violation>
</file>

<file name="servers/image-host/README.md">

<violation number="1" location="servers/image-host/README.md:49">
P3: The authentication documentation describes `edison-jwt` as an unimplemented stub returning 501, but this checkout contains the implemented verification path: `checkAuth` calls `verifyEdisonJwt`, which returns authentication/configuration statuses such as 401, 500, and 503. This stale description can cause operators to avoid or incorrectly diagnose the supported mode; the `.dev.vars.example` comment also repeats that it is not implemented. Updating the docs to describe JWKS-backed verification would align the deployment contract with the code.</violation>

<violation number="2" location="servers/image-host/README.md:103">
P3: The deployment checklist incorrectly says uploads fail until `PUBLIC_BASE_URL` is configured. The implementation intentionally falls back to the connected request origin when that variable is empty, and the integration test asserts this origin-derived URL behavior. This contradiction makes the documented two-deploy requirement and the `wrangler.jsonc` “fails loudly” comments misleading; the checklist should say the variable is optional when the request origin is the public URL.</violation>

<violation number="3" location="servers/image-host/README.md:119">
P3: The Layout block shows tests flat under test/ (images.test.ts, auth.test.ts), but they actually live under test/unit/ (plus test/integration/ and a jwt.test.ts). This contradicts the Develop section above it and is stale. Update the tree to match the real layout.</violation>
</file>

<file name="servers/image-host/vitest.config.ts">

<violation number="1" location="servers/image-host/vitest.config.ts:33">
P2: The integration tier runs under compatibilityDate 2025-04-17, but production (wrangler.jsonc) uses 2026-01-01 with nodejs_compat. Test runtime and prod runtime have diverged, so the workerd tier can't catch regressions introduced by compat/flag behavior after 2025-04-17. The comment frames this as a pool constraint — consider bumping @cloudflare/vitest-pool-workers to a version whose bundled workerd supports a date matching production, and note any features that rely on newer nodejs_compat semantics. If the pin must stay, add a CI guard so the mismatch is visible rather than silent.</violation>
</file>

<file name="servers/image-host/tsconfig.json">

<violation number="1" location="servers/image-host/tsconfig.json:7">
P2: Including "bun" in the global types for the same compilation that type-checks production `src/` lets Node/Bun-only globals (process, Bun.*, etc.) type-check cleanly inside code that actually runs in workerd, and lets the overlapping Request/Response/crypto/WebSocket globals from @types/bun shadow the Cloudflare workers-types ones. That can mask a real runtime failure at deploy time, since the deployed target is workerd, not Bun/Node. The `bun` types are only needed for the `bun:test` unit tests, so consider giving the unit tests their own tsconfig (or an `extends` split that keeps `src/` on workers-types alone) rather than polluting the worker source compilation.</violation>
</file>

<file name="servers/image-host/package.json">

<violation number="1" location="servers/image-host/package.json:24">
P3: @vitest/runner and @vitest/snapshot are already transitive dependencies of vitest (vitest → @vitest/runner, @vitest/snapshot). Listing them as direct devDependencies is redundant and risks accidental version drift if they fall out of sync with the vitest install. Recommend removing them and letting vitest pin its own internals.</violation>
</file>

<file name="docs/mcp_commodity_fleet_strategy.md">

<violation number="1" location="docs/mcp_commodity_fleet_strategy.md:3">
P2: The Status header says "Draft / decisions locked, implementation not started", but this PR actually delivers the Wave 1 image-host server, the catalog contract (schema.json + aggregate.py + catalog-entry.json), and the edison-jwt verify layer. That reads as stale and will mislead a reader into thinking this strategy is purely aspirational. Consider updating the status to reflect that image-host + the catalog contract + edison-jwt verify have landed (deploy/rollout still pending) and that only the issuer + catalog sync remain open.</violation>

<violation number="2" location="docs/mcp_commodity_fleet_strategy.md:190">
P3: The JWT-signing "open question" is already answered by the delivered code: jwt.ts verifies RS256 via JWKS and deliberately rejects a shared-secret path, and that behavior is tested. Listing "shared HS256 secret vs RS256/EdDSA + JWKS" as an open question (and the §6 diagram's "(or shared HS256 secret)" fallback) is stale and contradicts the implementation. Recommend closing this question in §10 and dropping the HS256 mention in §6.</violation>
</file>

<file name="servers/image-host/src/auth.ts">

<violation number="1" location="servers/image-host/src/auth.ts:118">
P2: JWKS/network or key-parsing failures can reject `checkAuth` instead of returning an auth result, turning an Edison-authenticated request into an unstructured Worker error. Converting verifier exceptions to a fail-closed 503 would preserve the auth gate and give callers a stable response during issuer outages.</violation>

<violation number="2" location="servers/image-host/src/auth.ts:118">
P3: The implementation and published auth contract now disagree: selecting `edison-jwt` can authenticate requests, while both auth READMEs promise 501 until the issuer exists. Either retain the 501 stub or update the rollout/status documentation in this change so operators do not get contradictory deployment guidance.</violation>
</file>

<file name="servers/image-host/src/index.ts">

<violation number="1" location="servers/image-host/src/index.ts:60">
P3: Returned upload URLs can be malformed when PUBLIC_BASE_URL is set to a non-absolute value because the base is not validated. Validating http/https absolute URLs here would preserve the tool’s absolute URL guarantee and surface misconfiguration early.</violation>

<violation number="2" location="servers/image-host/src/index.ts:107">
P2: Upload limit configuration can be silently ignored, so operators may believe a stricter cap is active when uploads still allow the 10 MiB default. Consider failing closed when MAX_UPLOAD_BYTES is present but not a positive integer instead of defaulting.</violation>
</file>

<file name=".github/workflows/servers_ts.yaml">

<violation number="1" location=".github/workflows/servers_ts.yaml:41">
P2: This CI job can silently continue with a freshly resolved dependency tree when the lockfile is stale or incompatible, so a green result may not be reproducible locally or on deployment. Removing the fallback and failing on lockfile drift would make the committed `servers/image-host/bun.lock` an effective CI contract.</violation>
</file>

<file name="shared/README.md">

<violation number="1" location="shared/README.md:9">
P3: The status table marks [`catalog/`](./catalog) as "placeholder", but this PR ships the catalog contract: `shared/catalog/schema.json`, the `aggregate.py` validator, and a worked `servers/image-host/catalog-entry.json` example. The row should read implemented/active, not placeholder, so a reader of this README isn't misled about delivered working code.</violation>

<violation number="2" location="shared/README.md:14">
P3: The closing paragraph says the `catalog` generator "get[s] promoted here out of `servers/image-host/`" once a second server lands, but the catalog generator already lives in `shared/catalog/aggregate.py` in this PR, not in `servers/image-host/`. That sentence is only accurate for the auth verify layer today; as written it conflates the two and misstates where the catalog generator currently resides.</violation>
</file>

<file name="servers/image-host/test/integration/worker.spec.ts">

<violation number="1" location="servers/image-host/test/integration/worker.spec.ts:69">
P2: The post-initialize MCP requests omit the required `MCP-Protocol-Version` header. As a result, this integration client does not actually speak the negotiated 2025-06-18 Streamable HTTP protocol and can fail or silently rely on compatibility fallback with a stricter server. Reading the initialize response and adding the negotiated version to both `notifications/initialized` and `tools/call` requests would make the handshake test conformant.</violation>

<violation number="2" location="servers/image-host/test/integration/worker.spec.ts:83">
P3: callTool silently converts a JSON-RPC `error` message into `{}`, so if the server ever rejects a tool call via the protocol-level error channel (instead of a result-level `isError`), the helper returns an empty object and assertions like `result.isError`/`result.structuredContent` fail with a confusing undefined. Consider surfacing `msg.error` (e.g. throw it) so failures are diagnosable.</violation>
</file>

Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.

Re-trigger cubic

Comment thread servers/image-host/src/auth.ts Outdated
Comment thread shared/catalog/aggregate.py Outdated
Comment thread shared/catalog/schema.json
miniflare: {
// The pool's bundled workerd supports compat dates up to 2025-04-17;
// pin the test runtime there (production uses wrangler.jsonc's date).
compatibilityDate: "2025-04-17",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The integration tier runs under compatibilityDate 2025-04-17, but production (wrangler.jsonc) uses 2026-01-01 with nodejs_compat. Test runtime and prod runtime have diverged, so the workerd tier can't catch regressions introduced by compat/flag behavior after 2025-04-17. The comment frames this as a pool constraint — consider bumping @cloudflare/vitest-pool-workers to a version whose bundled workerd supports a date matching production, and note any features that rely on newer nodejs_compat semantics. If the pin must stay, add a CI guard so the mismatch is visible rather than silent.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At servers/image-host/vitest.config.ts, line 33:

<comment>The integration tier runs under compatibilityDate 2025-04-17, but production (wrangler.jsonc) uses 2026-01-01 with nodejs_compat. Test runtime and prod runtime have diverged, so the workerd tier can't catch regressions introduced by compat/flag behavior after 2025-04-17. The comment frames this as a pool constraint — consider bumping @cloudflare/vitest-pool-workers to a version whose bundled workerd supports a date matching production, and note any features that rely on newer nodejs_compat semantics. If the pin must stay, add a CI guard so the mismatch is visible rather than silent.</comment>

<file context>
@@ -0,0 +1,42 @@
+        miniflare: {
+          // The pool's bundled workerd supports compat dates up to 2025-04-17;
+          // pin the test runtime there (production uses wrangler.jsonc's date).
+          compatibilityDate: "2025-04-17",
+          // wrangler.jsonc leaves PUBLIC_BASE_URL empty on purpose so the
+          // integration tests exercise the request-origin fallback; provide the
</file context>

"module": "es2022",
"moduleResolution": "bundler",
"lib": ["es2022"],
"types": ["@cloudflare/workers-types", "bun"],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Including "bun" in the global types for the same compilation that type-checks production src/ lets Node/Bun-only globals (process, Bun.*, etc.) type-check cleanly inside code that actually runs in workerd, and lets the overlapping Request/Response/crypto/WebSocket globals from @types/bun shadow the Cloudflare workers-types ones. That can mask a real runtime failure at deploy time, since the deployed target is workerd, not Bun/Node. The bun types are only needed for the bun:test unit tests, so consider giving the unit tests their own tsconfig (or an extends split that keeps src/ on workers-types alone) rather than polluting the worker source compilation.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At servers/image-host/tsconfig.json, line 7:

<comment>Including "bun" in the global types for the same compilation that type-checks production `src/` lets Node/Bun-only globals (process, Bun.*, etc.) type-check cleanly inside code that actually runs in workerd, and lets the overlapping Request/Response/crypto/WebSocket globals from @types/bun shadow the Cloudflare workers-types ones. That can mask a real runtime failure at deploy time, since the deployed target is workerd, not Bun/Node. The `bun` types are only needed for the `bun:test` unit tests, so consider giving the unit tests their own tsconfig (or an `extends` split that keeps `src/` on workers-types alone) rather than polluting the worker source compilation.</comment>

<file context>
@@ -0,0 +1,18 @@
+    "module": "es2022",
+    "moduleResolution": "bundler",
+    "lib": ["es2022"],
+    "types": ["@cloudflare/workers-types", "bun"],
+    "strict": true,
+    "skipLibCheck": true,
</file context>

const { value, done } = await reader.read();
if (value) buffer += decoder.decode(value, { stream: true });
const msg = extractMessage(buffer);
if (msg) return (msg.result ?? {}) as Record<string, any>;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: callTool silently converts a JSON-RPC error message into {}, so if the server ever rejects a tool call via the protocol-level error channel (instead of a result-level isError), the helper returns an empty object and assertions like result.isError/result.structuredContent fail with a confusing undefined. Consider surfacing msg.error (e.g. throw it) so failures are diagnosable.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At servers/image-host/test/integration/worker.spec.ts, line 83:

<comment>callTool silently converts a JSON-RPC `error` message into `{}`, so if the server ever rejects a tool call via the protocol-level error channel (instead of a result-level `isError`), the helper returns an empty object and assertions like `result.isError`/`result.structuredContent` fail with a confusing undefined. Consider surfacing `msg.error` (e.g. throw it) so failures are diagnosable.</comment>

<file context>
@@ -0,0 +1,168 @@
+      const { value, done } = await reader.read();
+      if (value) buffer += decoder.decode(value, { stream: true });
+      const msg = extractMessage(buffer);
+      if (msg) return (msg.result ?? {}) as Record<string, any>;
+      if (done) break;
+    }
</file context>

}
const presented = extractBearer(request.headers.get("authorization"));
if (!presented) return { ok: false, status: 401, message: "missing bearer token" };
const result = await verifyEdisonJwt(presented, {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The implementation and published auth contract now disagree: selecting edison-jwt can authenticate requests, while both auth READMEs promise 501 until the issuer exists. Either retain the 501 stub or update the rollout/status documentation in this change so operators do not get contradictory deployment guidance.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At servers/image-host/src/auth.ts, line 118:

<comment>The implementation and published auth contract now disagree: selecting `edison-jwt` can authenticate requests, while both auth READMEs promise 501 until the issuer exists. Either retain the 501 stub or update the rollout/status documentation in this change so operators do not get contradictory deployment guidance.</comment>

<file context>
@@ -0,0 +1,129 @@
+      }
+      const presented = extractBearer(request.headers.get("authorization"));
+      if (!presented) return { ok: false, status: 401, message: "missing bearer token" };
+      const result = await verifyEdisonJwt(presented, {
+        jwksUrl: EDISON_JWKS_URL,
+        issuer: EDISON_JWT_ISSUER,
</file context>

/** Absolute base for returned URLs, e.g. `https://image-host.acme.workers.dev`. */
function resolvePublicBase(env: Env): string | null {
const raw = env.PUBLIC_BASE_URL?.trim();
return raw ? raw.replace(/\/+$/, "") : null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Returned upload URLs can be malformed when PUBLIC_BASE_URL is set to a non-absolute value because the base is not validated. Validating http/https absolute URLs here would preserve the tool’s absolute URL guarantee and surface misconfiguration early.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At servers/image-host/src/index.ts, line 60:

<comment>Returned upload URLs can be malformed when PUBLIC_BASE_URL is set to a non-absolute value because the base is not validated. Validating http/https absolute URLs here would preserve the tool’s absolute URL guarantee and surface misconfiguration early.</comment>

<file context>
@@ -0,0 +1,215 @@
+/** Absolute base for returned URLs, e.g. `https://image-host.acme.workers.dev`. */
+function resolvePublicBase(env: Env): string | null {
+  const raw = env.PUBLIC_BASE_URL?.trim();
+  return raw ? raw.replace(/\/+$/, "") : null;
+}
+
</file context>

```bash
wrangler deploy
```
4. **Set `PUBLIC_BASE_URL`** to the deployed worker URL (or a custom domain) in

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The deployment checklist incorrectly says uploads fail until PUBLIC_BASE_URL is configured. The implementation intentionally falls back to the connected request origin when that variable is empty, and the integration test asserts this origin-derived URL behavior. This contradiction makes the documented two-deploy requirement and the wrangler.jsonc “fails loudly” comments misleading; the checklist should say the variable is optional when the request origin is the public URL.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At servers/image-host/README.md, line 103:

<comment>The deployment checklist incorrectly says uploads fail until `PUBLIC_BASE_URL` is configured. The implementation intentionally falls back to the connected request origin when that variable is empty, and the integration test asserts this origin-derived URL behavior. This contradiction makes the documented two-deploy requirement and the `wrangler.jsonc` “fails loudly” comments misleading; the checklist should say the variable is optional when the request origin is the public URL.</comment>

<file context>
@@ -0,0 +1,122 @@
+   ```bash
+   wrangler deploy
+   ```
+4. **Set `PUBLIC_BASE_URL`** to the deployed worker URL (or a custom domain) in
+   `wrangler.jsonc` `vars`, then `wrangler deploy` again so returned URLs are
+   absolute. **Required:** until it's set, `upload_image` fails loudly rather
</file context>

Pluggable via [`src/auth.ts`](./src/auth.ts): `open` | `bearer` | `edison-jwt`.
v1 ships **`bearer`** — set `AUTH_TOKEN` and clients send
`Authorization: Bearer <token>`. `edison-jwt` (Edison mints a per-user JWT and
injects it, no consent screen) is stubbed as an explicit drop-in and returns

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The authentication documentation describes edison-jwt as an unimplemented stub returning 501, but this checkout contains the implemented verification path: checkAuth calls verifyEdisonJwt, which returns authentication/configuration statuses such as 401, 500, and 503. This stale description can cause operators to avoid or incorrectly diagnose the supported mode; the .dev.vars.example comment also repeats that it is not implemented. Updating the docs to describe JWKS-backed verification would align the deployment contract with the code.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At servers/image-host/README.md, line 49:

<comment>The authentication documentation describes `edison-jwt` as an unimplemented stub returning 501, but this checkout contains the implemented verification path: `checkAuth` calls `verifyEdisonJwt`, which returns authentication/configuration statuses such as 401, 500, and 503. This stale description can cause operators to avoid or incorrectly diagnose the supported mode; the `.dev.vars.example` comment also repeats that it is not implemented. Updating the docs to describe JWKS-backed verification would align the deployment contract with the code.</comment>

<file context>
@@ -0,0 +1,122 @@
+Pluggable via [`src/auth.ts`](./src/auth.ts): `open` | `bearer` | `edison-jwt`.
+v1 ships **`bearer`** — set `AUTH_TOKEN` and clients send
+`Authorization: Bearer <token>`. `edison-jwt` (Edison mints a per-user JWT and
+injects it, no consent screen) is stubbed as an explicit drop-in and returns
+`501` until the Edison issuer exists. With no token configured the server
+defaults to `open` (self-host friendly). See
</file context>

Address reviewer findings on PR #12 and clear the em-dash lint gate.

Security / correctness (image-host):
- jwt.ts: wrap WebCrypto importKey/verify in try/catch (malformed JWK -> 401,
  never an uncaught 500); require n/e on candidate keys; fetchJwks catches
  network/parse failures -> null (503, not 500); ensureJwks only falls back to
  a same-url cached JWKS and advances the refetch cooldown after a *successful*
  fetch so a blip can retry next request.
- auth.ts: a defined-but-empty AUTH_TOKEN no longer collapses to `open` - it
  stays `bearer` and fails closed (500 misconfig); bearer guard trims so a
  whitespace-only token is also a misconfig. Refresh the stale "edison-jwt is
  stubbed" header comment (it is implemented).
- images.ts: match the FULL 8-byte PNG and 6-byte GIF signatures (not just the
  ascii prefix); reject a declared content_type we can't normalize instead of
  silently serving under the sniffed type.

Catalog contract:
- aggregate.py: reject a non-object top-level entry, an empty url host
  (`https:///mcp`), and an icon with path separators or one that isn't
  `<id>.svg`.
- schema.json: add an allOf so auth `edison-jwt` requires `edison_hosted: true`,
  mirroring aggregate.py.

Tests + docs:
- new unit coverage for the empty-token, full-signature, and bad-declared-type
  paths (49 unit tests pass).
- refresh .dev.vars.example and shared/catalog/README to reflect edison-jwt
  being implemented end to end.
- replace U+2014 em dashes with hyphens across tracked markdown/source to pass
  the repo's lint-em-dash CI gate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GVK1ngvGgmfoGYxrjtAF5g

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 21 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread servers/image-host/src/jwt.ts
Comment thread servers/image-host/src/images.ts
Second round of PR #12 review fixes.

jwt.ts (cubic P2): coalesce a concurrent cold/stale-cache JWKS burst into a
single upstream GET via a URL-keyed in-flight promise. The refetch cooldown only
throttles sequential requests, so without dedup every request in a cold-isolate
burst opened its own fetch; a failed fetch still doesn't advance the cooldown, so
the next request retries immediately. Adds concurrency + failed-retry tests.

images.ts (cubic P3): treat an explicitly-supplied empty content_type ("") as a
declaration (`!== undefined`), rejecting it like any other unsupported value
instead of silently serving under the sniffed type.

aggregate.py (qodo): bring the stdlib validator into real parity with
schema.json - reject unknown fields (additionalProperties:false), enforce the id
regex, require tags to be non-empty strings, and validate the headers /
template_fields.env object shapes. The doc comment's "validates against
schema.json" claim now holds.

package.json (qodo): drop the redundant `version` field - the package is
private and versioning is single-sourced in pyproject.toml.

52 unit tests pass; catalog --check still validates the image-host entry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GVK1ngvGgmfoGYxrjtAF5g

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 6 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread shared/catalog/aggregate.py Outdated
Python's `$` also matches just before a final newline, so `ID_RE.match` would
accept an id like 'image-host\n' that schema.json (and downstream consumers)
reject. `fullmatch` anchors the whole string, keeping the stdlib validator in
lockstep with the schema.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GVK1ngvGgmfoGYxrjtAF5g
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants