feat(mcp): OpenAPI contract + MCP SSE pod, de-prefixed slug - #65
Open
github-actions[bot] wants to merge 3 commits into
Open
feat(mcp): OpenAPI contract + MCP SSE pod, de-prefixed slug#65github-actions[bot] wants to merge 3 commits into
github-actions[bot] wants to merge 3 commits into
Conversation
Brings FuzeKeys to the platform's 4-pod standard (backend, frontend, MCP,
A2A) and gives it the OpenAPI contract it had never had.
contracts/openapi.yaml — DERIVED, not hand-written
--------------------------------------------------
scripts/export-openapi.py exports the document straight out of the FastAPI
application, so "the contract describes the real endpoints" is a property of
the build rather than a promise in a review. 70 operations across 65 paths.
`--check` re-derives and compares the (method, path) set against the committed
file, which is the failure that matters downstream: the MCP gateway turns every
operation in the contract into a tool, so an operation missing from the
contract is an invisible tool and one that is only in the contract is a tool
that 404s.
The backend already serves the document over HTTP: FastAPI publishes
/openapi.json and /docs, verified against the running app.
mcp/tools.overrides.yaml — the part the HTTP method gets wrong
--------------------------------------------------------------
Two FuzeKeys-specific consequences drive the classification, and neither is
visible from the verb:
* Disclosing a secret is not a read. GET /api/credentials/account/{id}/
credentials delegates to request_account_credentials and returns DECRYPTED
credentials; it is declared mutates:true so a client that auto-approves
reads can never reach it. (The gateway forbids calling a GET irreversible —
if a GET really changed state the spec would be what needs fixing — so
mutates:true is the strongest correct signal.)
* This API reaches out of the product. signup / apikey / google-signup create
a real account or a real API key on somebody else's system; FuzeKeys has no
operation that undoes them. Marked irreversible.
broker/redeem is single-use (redemption_count check in broker/service.py)
and releases the secret, so it both burns the grant and discloses — neither
half is undoable. broker/revoke is final for that grant id.
grant and mint-token stay reversible: revoke is exactly their undo.
Verified by booting the real @fuzefront/mcp-gateway against this spec:
70 tools enumerate, scripts/smoke.mjs passes (handshake, no read-only tool on
an unsafe method, every irreversible tool is a write, unauthenticated call
fails closed).
Pods
----
* MCP SSE — templates/mcp-gateway.yaml, the shared gateway image configured
with this product's spec + in-cluster backend URL. Holds no credential of its
own; forwards the caller's Authorization header, which on a credential vault
is the whole point. Enabled in values-contabo.yaml, matching FuzeService.
* A2A — templates/a2a.yaml, the pre-built ghcr.io/izzywdev/fuzeagent-a2a image
with exactly the env surface that image declares. Rendered values.json parses
with FuzeAgent's own a2a.config.load_config and validates against the frozen
values-interface.schema.json. Left DISABLED: the family contract specifies one
shared A2A server with tenants[] entries rather than a pod per product, the
keys-broker role has no `description` so card projection fails by design, and
the OIDC issuer and JWS signing key do not exist for FuzeKeys yet. Those are
reported, not invented.
Slug
----
registration/manifest.json: slug fuzekeys -> keys, name FuzeKeys -> Keys, and
nothing else. slug is immutable once registered, so the live migration (and the
Permit grant question) is owned by a separate workstream.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GaPa3JgrVNtWrGvqQEAEqv
…ing one FuzeInfra owns Argo Application and AppProject objects — they are the destination and security boundary for the shared cluster, and a product repo that ships its own competes with it. FuzeMarket PR #61 made that move for FuzeMarket a day ago. deploy/argocd/README.md is the handoff: repoURL, path, targetRevision, the values files to layer, destination namespace, sync options (including WHY ServerSideApply is needed — the chart now mounts a ~110 KB OpenAPI document into a ConfigMap), the full workload inventory with the values key that gates each one, and the Secrets the release expects to already exist. It also records, without changing them, that this repo STILL carries in-repo Application manifests under deploy/argocd/applications/. Deleting those is a deploy-affecting change owned by the workstream that did FuzeMarket #61, so they are flagged rather than removed here. One thing worth stating explicitly, because the failure is silent: the FuzeKeys chart deploys the backend, so the MCP gateway pod has a real upstream. A sibling in this family ships a chart that deploys only its MFE, which leaves its MCP pod able to enumerate tools and unable to execute any of them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GaPa3JgrVNtWrGvqQEAEqv
The A2A Deployment/Service I added is deleted, and the values block with it.
It could not have worked, and it is worth being exact about why rather than
just deferring it.
agent-templates/a2a/card_generator.py line 29:
IN_CLUSTER_URL = "http://a2a-shared.fuzeagent.svc.cluster.local:8080/rpc"
`_interface()` returns that CONSTANT as the callable endpoint of every
non-external Agent Card. It is not values-driven and no env var overrides it;
the only branch is `external: true`, which swaps in a public tunnel URL. So a
per-product pod would boot, pass its health check, and publish a card
advertising the SHARED server's address. Every caller would dial
a2a-shared.fuzeagent and the per-product pod would never receive a request — a
pod that looks healthy and is functionally dead, which is worse than no pod,
because nothing goes red.
FuzeAgent's docs/a2a/enable-your-pod.md says the same in prose: one shared
server, onboarding is data, "never a new pod". I had flagged that as a design
tension and shipped the pod disabled; the constant makes it not a tension but a
fact, so the templates go rather than sit there waiting for someone to enable
them.
Also correcting the premise: A2A is not an "LLM to REST/OpenAPI" bridge. It
reads no OpenAPI and proxies no REST. It is a JSON-RPC 2.0 agent-delegation
server whose Agent Card is PROJECTED from .fuze/manifest.json plus
agent-templates/roles/<role>/role.json. The OpenAPI-to-tools job is the MCP
gateway's, and that one IS genuinely per-product and stays.
docs/a2a-tenant-registration.md replaces the templates with the real handoff:
the exact `tenants[]` entry to add to FuzeAgent's a2a-shared values, and the one
precondition this repo still fails — agent-templates/roles/keys-broker/role.json
has `summary` but no `description`, and the projection fails rather than
shipping a placeholder. (FuzeKeys is ahead of its siblings on the other one: its
`providesTo` is already populated, where an empty list would mean deny-all.)
Also verified, since a Service that selects two workloads load-balances API
traffic into the wrong pod and nothing goes red: every Service this chart
renders selects exactly one workload (backend, frontend, mcp).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GaPa3JgrVNtWrGvqQEAEqv
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this is
FuzeKeys had no OpenAPI contract, which made an MCP pod impossible — the gateway derives its tools from a spec. This adds the contract, wires the MCP SSE pod into the chart, and de-prefixes the registration slug.
The API I found, and the spec I wrote
70 operations across 65 paths, and the backend already publishes them: FastAPI serves
/openapi.jsonand/docs(verified against the running app), andGET /healthalready existed.contracts/openapi.yamlis derived, not hand-written.scripts/export-openapi.pyexports it straight out of the FastAPI application, so "the contract describes the real endpoints" is a property of the build rather than a promise in a review.--checkre-derives and compares the(method, path)set against the committed file — the failure that actually matters downstream, because the gateway turns every operation in the contract into a tool: one missing from the contract is an invisible tool, one only in the contract is a tool that 404s.Two findings, reported not fixed:
accountsroutes — FastAPI warns about it at startup. FastAPI's auto-generated ids also run to 76 characters while the MCP gateway truncates tool names at 64, so two operations could collide into one tool. The contract therefore assigns its own deterministicMETHOD + pathids (unique by construction, all ≤ 61 chars). The application keeps FastAPI's. That divergence is narrow and deliberate:operationIdis metadata, not API shape, and fixing it in the app would mean editing routers another workstream is currently rewriting.scripts/export-openapi.py --checkis not wired into CI. It would go red inside the security-migration PR that is rewritingauth.py, which is not this PR's call to make.Irreversible operations, and why
The classification is on effect, not verb.
post_integrations_signup,post_google_signup_*post_integrations_apikeypost_broker_redeemredemption_countcheck inbroker/service.py), so this both burns the grant and discloses the secret. Neither half is undoable.post_broker_revokepost_chat_signuppost_infrastructure_mobile_send_commanddelete_identities_*,delete_llm_scraper_*get_credentials_account_by_account_id_credentialsis a GET, and it is declaredmutates: true. It delegates torequest_account_credentialsand returns decrypted credentials. Disclosing a secret is not a read — a secret that has been handed out cannot be un-handed-out. The gateway forbids calling a GET irreversible (invariant 3: if a GET really changed state, the spec is what needs fixing), somutates: trueis the strongest correct signal: it removes the operation from the read-only set, so a client that auto-approves reads can never reach it.Deliberately not marked:
post_broker_grantandpost_broker_mint_token—revokeis exactly their undo, which is what "reversible" means.Pod state — honestly
backend.enabled: true). Serves/healthand/openapi.json. ✅templates/mcp-gateway.yaml, shared@fuzefront/mcp-gatewayimage pointed atfuzekeys-backend:8002. Enabled invalues-contabo.yaml, matching FuzeService. ✅docs/a2a-tenant-registration.md. ❌ by designThe backend is genuinely deployed, so the MCP pod has a real upstream to call. Worth stating plainly: a sibling in this family ships a chart that deploys only its MFE, which leaves its MCP pod able to enumerate tools and unable to execute a single one. That is not the shape here —
backend.enabled: truein bothvalues.yamlandvalues-contabo.yaml, andbackend/Dockerfileexists and is built bybuild-and-push.ymlon push tomaster.Verified: booted the real
@fuzefront/mcp-gatewayfrom the exact byteshelm templaterenders into the ConfigMap — 70 tools enumerate, andscripts/smoke.mjspasses over the real SSE transport (handshake, no read-only tool on an unsafe method, every irreversible tool is a write, unauthenticated call fails closed with zero upstream requests). Every override key was also checked to resolve to a realoperationId— an override for a tool that does not exist is silently ignored by the gateway.Why there is no A2A pod
I wrote one, shipped it disabled, and then deleted it. The reason is not a policy preference — it is in the image's source.
agent-templates/a2a/card_generator.pyline 29:_interface()returns that constant as the callable endpoint of every non-external Agent Card. It is not values-driven and no environment variable overrides it; the only branch isexternal: true, which swaps in a public tunnel URL. So a per-product pod would boot, pass its health check, and publish a card advertising the shared server's address. Every caller dialsa2a-shared.fuzeagent; the per-product pod never receives a request. A pod that looks healthy and is functionally dead is worse than no pod, because nothing goes red.I also had the premise wrong and want it on the record: A2A is not an "LLM → REST/OpenAPI" bridge. It reads no OpenAPI and proxies no REST. It is a JSON-RPC 2.0 agent-delegation server whose Agent Card is projected from
.fuze/manifest.json+agent-templates/roles/<role>/role.json. The OpenAPI-to-tools job is the MCP gateway's — and that one is genuinely per-product, and stays.docs/a2a-tenant-registration.mdis the real deliverable: the exacttenants[]entry to add to FuzeAgent'sa2a-sharedvalues, and the one precondition FuzeKeys still fails —agent-templates/roles/keys-broker/role.jsonhassummarybut nodescription, and the projection fails rather than shipping a placeholder (card-projection.md §3). FuzeKeys is ahead of its siblings on the other precondition: itsprovidesTois already populated with 18 entries, where an empty list would mean deny-all.Slug
registration/manifest.json:slugfuzekeys→keys,nameFuzeKeys→Keys. Nothing else.routing.pathandintegration.remoteEntrystill containfuzekeysbecause those are real nginx serving paths, not the registration slug.keysis 4 characters, so it clears the contract'sSlugpattern (which requires ≥3 — short de-prefixed slugs likebido not). Validated rather than assumed.slugis immutable once registered, so the live migration, the delete-after-verify ordering and the Permit grant question are owned by a separate workstream — nothing here calls a registry API.Argo CD — a FuzeInfra delegation
No Application manifest is added.
deploy/argocd/README.mdcarries the adoption spec instead: repoURL, path, targetRevision, values layering, destination namespace, sync options (including whyServerSideApplyis now required — the chart mounts a ~110 KB spec into a ConfigMap), the workload inventory with the values key gating each pod, and the Secrets the release expects.It also records, without changing them, that this repo still carries in-repo Application manifests under
deploy/argocd/applications/. Deleting those is a deploy-affecting change owned by the workstream that did FuzeMarket #61.Assumptions flagged
ghcr.io/izzywdev/fuze-mcp-gateway:0.1.0is assumed published from FuzeFrontpackages/mcp-gateway. I could not confirm the tag — an anonymous GHCR manifest probe returns 403 for a private package, indistinguishable from "does not exist". If it is not there, the MCP pod sits in ImagePullBackOff and nothing else in the release is affected; flipmcp.enabled: falseuntil it is.build-and-push.ymltriggers onbackend/**,frontend/**,pii-tokenizer/**andChart.yaml. This PR touches none of those, so no image rebuild is expected or needed — the MCP inputs are chart-side, so Argo picks them up without one.Verified vs not
Verified:
helm lint(default andvalues-contabo.yaml) — 0 failed.helm templatewith both. Every rendered Service selects exactly one workload (backend, frontend, mcp) — a selector collision would load-balance API traffic into nginx with nothing going red. Gateway boots from the rendered ConfigMap bytes: 70 tools, smoke passes, no override references a missing operation. Manifest validates againstpackages/onboarding-kit/manifest.schema.json; policy passesvalidate-policy.mjs --slug keys. FastAPI's/openapi.jsonroute confirmed on the running app. All three Dockerfiles referenced bybuild-and-push.ymlexist.NOT verified: no cluster operations of any kind — nothing was applied, and no pod was ever scheduled. I did not run the backend test suite (
backend/tests): the full dependency set is heavy and I changed no backend code, only addedcontracts/,mcp/,scripts/,docs/and chart templates. The GHCR tag above.Related
.fuze/manifest.jsonstill hasmcp.enabled: false. Per the gateway README that flag should flip only once the pod has run against the real spec in the cluster — the local smoke run is necessary, not sufficient.