Skip to content

feat(mcp): overlay tools: list_overlays, get_overlay_exposure, configure_overlay - #11

Merged
chris-cpz merged 2 commits into
mainfrom
feat/overlay-tools
Sep 27, 2026
Merged

chris-cpz merged 2 commits into
mainfrom
feat/overlay-tools

Conversation

@chris-cpz

@chris-cpz chris-cpz commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Overlay strategies are live on the platform, but an MCP client could not see or configure them. This adds three tools. All three call the cpz gateway (https://api-ai.cpz-lab.com/cpz) with the caller's own X-CPZ-Key / X-CPZ-Secret and need the strategies scope. OAuth tokens are unsealed to that same pair, and the gateway authenticates and scopes it the same way rest-api does. This server already calls the same host for the Simons proxy.

Tool Platform call Kind
list_overlays GET /cpz/overlays read-only
get_overlay_exposure GET /cpz/overlay/exposure?strategy_id=<uuid> read-only
configure_overlay PUT /cpz/overlays/{uuid} destructive, idempotent write

The write route came from CPZ-Lab/cpzai#1736 (save_overlay_config_as, merged and deployed). The first commit on this branch shipped the two reads only, with the gap written up; the second adds configure_overlay and moves list_overlays onto the new config route.

list_overlays

  • One read, no market data. Each overlay comes back with its full policy (objective, hedge_ratio, tolerance_band, hedge_instruments, benchmark_symbol, rebalance_trigger, notes) and its targets. configured is true when an overlay has a policy and at least one target.
  • The result is {data, count, truncated}. The platform returns at most 200 overlays, ordered by title. When truncated is true the result adds a note saying it is not the complete list.
  • A 200 with no overlays array or no boolean truncated is refused as invalid_upstream_response.

get_overlay_exposure

  • It uses the same route and the same 30 s budget as cpz-py client.overlays.exposure().
  • The result leads with the verdict, then gives the platform document under data: {complete, hedge_ratio_status: measured | not_measured | withheld, hedge_ratio_reason, withheld?, data}.
  • complete: false adds a withheld block. It lists the missing prices and history and tells the model not to size or place a hedge from the result.
  • Whenever complete is false, hedge.current_ratio, drift, outside_band and suggested_order are forced to null. The platform already withholds them. If a drifted upstream sends numbers anyway, they are removed and the event is logged. A ratio computed from part of a book can never reach a model as a number.
  • not_measured is distinct from withheld. It covers objectives with no ratio (fx, duration, tail, custom, vol_target) and a target book with nothing to hedge. None of these cases is reported as zero.
  • Errors come through with their real status: 404, 409 not_an_overlay / overlay_not_configured, 502, and 503 market_data_unconfigured. The 409s get guidance pointing at configure_overlay and list_overlays.
  • A 200 with no boolean complete, no hedge block or no targets list is refused as invalid_upstream_response.

configure_overlay

  • It sends PUT /overlays/{id} with {role, policy, targets}, or {role: "alpha"} alone to remove the configuration.
  • role: "overlay" replaces the whole policy and the whole target list. The description says so, and tells the model to read list_overlays first. It also says that becoming an overlay turns short selling on.
  • Annotations: readOnlyHint: false, destructiveHint: true, idempotentHint: true. Compact mode advertises it by name, and call_tool refuses it (not_dispatchable).
  • Client-side refusals happen before any request. The policy and target objects are strict, so a misspelt field (for example hedge_ration) is refused rather than dropped and silently replaced by the database default. It refuses:
    • an unknown objective or rebalance trigger;
    • hedge_ratio outside 0 to 5, or tolerance_band outside (0, 1];
    • a beta objective without benchmark_symbol;
    • a missing policy, or no targets;
    • a self-target, or the same target twice (broker and account key normalised as the database trigger does);
    • a weight outside (0, 10], or an environment other than paper or live;
    • policy or targets passed with role alpha.
  • Only the fields given are sent, so any defaults are the database's own. Symbols are sent upper-case.
  • Ownership, account membership and cycles can only be checked by the platform. Its 400 invalid_overlay_config message is passed through verbatim in message and in the text content. 404 not_found_or_not_owned and 409 duplicate_target come through as errors.
  • Never reports a failed save as a save:
    • A 5xx or a timeout carries operation_outcome: "unknown", with guidance to check before saving again. The PUT is not retried.
    • A 200 whose body has no saved object or no overlay key is refused as invalid_upstream_response, also with outcome unknown.
    • A 200 with overlay: null and read_back_error is reported as saved, with a note to confirm it with list_overlays.
  • Failures are logged with request id, strategy id, role, status and error code.

Plumbing

  • callRestApi gains api: 'gateway', with base CPZ_GATEWAY_BASE_URL (default https://api-ai.cpz-lab.com/cpz). It sends the same headers, timeout, retry policy and request id. Retries stay GET-only.
  • All three tools are added to TOOL_SCOPES (strategies) and the strategies category. OUTPUT_SCHEMAS covers the two reads, whose envelopes this server builds; configure_overlay declares none, like the other writes that proxy to their own handlers.
  • The guide resources (tool-usage, permissions), README and tool-catalog.json are updated. The catalogue goes from 31 to 34 tools, and compact from 15 to 16 (it gains configure_overlay). A data key still sees 10.

Tests

  • npm test: 225 passed across 10 files. The baseline on origin/main was 163 passed.
    • tests/overlay-tools.test.ts has 61 tests. tests/api-client.test.ts gains one gateway routing test. The pinned counts are updated.
  • npm run build and tsc --noEmit are clean.
  • HTTP is mocked throughout. The tests cover:
    • the exact gateway URLs, methods, credential and request-id headers, and PUT bodies;
    • exposure complete, incomplete, not-measured and upstream-drift cases, plus the 404/409/503/403 errors;
    • the list: success, empty, truncated, unreadable 200s, and 502/403;
    • configure success, alpha removal, only-given-fields, read-back failure reported as saved, the verbatim 400 message, 404/409/403, a 502 not retried with outcome unknown, and an unreadable 200 not reported as a save;
    • 9 cross-field refusals and 15 schema refusals, all before HTTP;
    • scope filtering, and compact-mode search, dispatch and the write refusal.
  • Mutation checks: removing the client-side validation, the unreadable-200 guard or the list shape guard fails 11 tests. Removing the exposure withholding, the not-configured branch or the strategy_role guard in the first commit failed 3. The code was restored each time.

Not verified

  • No call was made against production. There was no credential, by instruction. The route shapes come from origin/feat/overlay-config-api (supabase/functions/_shared/overlay-config.ts, the cpz gateway route and migration 20260927180000). The coordinator reports cpzai#1736 merged, migrated and deployed.
  • Not deployed. It ships through buildspec/ECS.

Follow-ups

  • After deploy, export the catalogue to the platform docs: npm run export:catalog -- --output /path/to/cpzai/src/lib/mcp-tool-catalog.json.
  • The GitHub remote reports this repository moved to CPZ-Lab/cpzai-mcp-server. Local remotes still point at the old name.

🤖 Generated with Claude Code

chris-cpz and others added 2 commits September 27, 2026 10:39
…rlays

Overlay strategies are live on the platform, but an agent could not see
them: nothing on this server read the exposure resolver, and list_strategies
has no role filter.

get_overlay_exposure reads GET /cpz/overlay/exposure on the cpz gateway with
the caller's own X-CPZ-Key/X-CPZ-Secret (strategies scope), the same route
cpz-py's client.overlays.exposure() uses, with its 30 second budget. The
result leads with the verdict (complete, hedge_ratio_status, reason) and, on
an incomplete book, a withheld block naming the missing prices and history.
The ratio, drift, band flag and suggested order are forced to null whenever
complete is false, and an upstream that sends numbers anyway is logged, so a
ratio computed from part of a book can never reach a model as a number.

list_overlays scans /v1/strategies in full (oldest first, 100 a page, refused
past 2000 rather than listed from a partial scan), keeps strategy_role
overlay, and reads each page's configuration from the exposure resolver,
which is the only API-credential route that carries targets and policy. An
unconfigured overlay (409 overlay_not_configured) is a state; any other read
failure makes the page an error with every entry attached, because an
unread configuration is unknown, not empty. hedge_instruments,
rebalance_trigger and notes are named as unavailable.

There is no configure_overlay. save_overlay_config is SECURITY INVOKER and
raises unless auth.uid() is set; this server only ever holds an API
credential. The gateway's rpc/ passthrough calls PostgREST as service_role
(auth.uid() is null) and injects a user_id argument the function does not
take, and rest-api has no overlay resource. Writing strategy_role alone
through PATCH /strategies would create the half-configured overlay the
atomic RPC exists to prevent. The write tool waits for a user-scoped route.

callRestApi gains api: 'gateway' (CPZ_GATEWAY_BASE_URL, default
https://api-ai.cpz-lab.com/cpz), sending the same credential headers. Both
tools map to the strategies scope and the strategies category; they are
read-only, so compact mode defers them behind search_tools and call_tool.
Catalogue 31 -> 33 tools; compact stays at 15.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… route

CPZ-Lab/cpzai#1736 closed the platform gap the first commit described: the
cpz gateway now serves GET /overlays, GET and PUT /overlays/{id} for an API
credential (strategies scope), writing through save_overlay_config_as with
the key owner's id.

configure_overlay PUTs {role, policy, targets} to /overlays/{id}. It is a
destructive, idempotent write, advertised by name in compact mode and never
dispatchable through call_tool. Policy and target objects are strict, so a
misspelt field is refused instead of silently replaced by a database
default. Before any request it refuses: unknown objective or trigger,
hedge_ratio outside 0..5, tolerance_band outside (0, 1], beta without a
benchmark, no targets, a self-target, the same target twice (broker and
account key normalised as the database does), weight outside (0, 10],
environment other than paper or live, and policy or targets with role
alpha. Only the fields given are sent, so defaults stay the database's.
The platform's 400 message comes through verbatim. A 200 whose read-back
failed is reported as saved with a note to confirm; a 5xx, a timeout or an
unreadable 200 is never reported as a save and carries operation_outcome
unknown. The PUT is not retried.

list_overlays is now one GET /overlays with no market data: full policy
(hedge_instruments, rebalance_trigger and notes included) and targets. The
strategies scan, the per-overlay exposure fan-out and the unavailable
fields note are gone. A truncated list says it is not the complete list; a
200 without an overlays array or a truncated flag is refused.

get_overlay_exposure is unchanged except that its 409 guidance now points at
configure_overlay and list_overlays. Catalogue 33 -> 34 tools; compact
15 -> 16.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chris-cpz chris-cpz changed the title feat(mcp): read overlay strategies: get_overlay_exposure and list_overlays feat(mcp): overlay tools: list_overlays, get_overlay_exposure, configure_overlay Sep 27, 2026
@chris-cpz
chris-cpz merged commit acbd53d into main Sep 27, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant