Repository navigation
portal: seerrsense, api and coolify-mcp under mcp-servers.tf in automatic (DCR) mode #1363
Description
Problem
Through the portal, seerrsense_request_media answers this token is not granted the seerr:request scope while search works (mctlhq/seerrsense#73).
This is the failure tg had on 2026-09-12, as described in infrastructure/cloudflare/portal/README.md ("Why the scope needs describing at all"). The portal is its own OAuth client against each upstream and receives only the scopes it names. The seerrsense upstream was created through the dashboard with no scope in its registration, so seerrsense's authorize endpoint grants its read-only default and the portal's token can never file a request. A refresh keeps the original grant, so the token never widens by itself.
More generally, two live upstreams are not in mcp-servers.tf at all, and one org MCP server is not on the portal.
Inventory (2026-09-24)
scopes_supported below is read live from each upstream's /.well-known/oauth-authorization-server.
| Server | Live on portal | In mcp-servers.tf |
scopes_supported |
Registration |
|---|---|---|---|---|
tg |
✓ | ✓ imported | 5 telegram/account scopes | manual, scope pinned |
projects |
✓ | ✓ | (Access) | DCR |
alice |
✓ | ✓ | (own AS) | DCR |
seerrsense |
✓ | ✗ | seerr:read seerr:request offline_access |
dashboard-made, manual; scope unknown (write-only) |
api |
✓ | ✗ | mctl |
dashboard-made; scope unknown (write-only) |
coolify (mctlhq/mctl-coolify-mcp, coolify.mctl.ai) |
✗ not a member | ✗ | coolify |
offers DCR + CIMD |
The mctl-mcp repository is a public directory entry, not a server, and is out of scope.
Registration rule (mctlhq/.github#137)
The portal runs every upstream in automatic (DCR) mode. Manual mode is used only while an upstream cannot do DCR. Manual freezes the tool catalogue at the first login and needs hand-pasted credentials. Each server restricts DCR to https://mcp.mctl.ai/servers-callback and gives a DCR client that names no scope its full scopes_supported, so the portal's grant is full without a pinned scope string.
Change
seerrsense: waits for auth: DCR fallback restricted to the Cloudflare portal callback, with the full scope set for DCR clients seerrsense#74 (portal-restricted DCR, full default for DCR clients). Once that is released, add the resource in automatic mode, likealice/projects: noauth_credentials, noclient_secret. The live server is currently manual. Check whether the provider can switch mode in place. If not, recreate it, re-add it withscripts/portal-membership-add.sh, and re-apply seerrsense'sdocs/portal-allowlist.json.
Interim, only if the fix is needed before feat(workflows): pass GITHUB_TOKEN to mctl-agents service-agent runs #74 ships: import it as manual withscope = "seerr:read seerr:request offline_access", modelled ontg.api: api.mctl.ai already advertises DCR (/oauth/register, scopemctl, authnone). Check that its DCR is restricted to the portal callback (MCP servers: CIMD primary, DCR fallback restricted to the portal callback, full scope for DCR clients .github#137), then add it in automatic mode. The same mode-switch caveat asseerrsenseapplies if it is live as manual today.coolify(https://coolify.mctl.ai/mcp): new resource, automatic (DCR). coolify-mcp is already CIMD with a DCR fallback and grantscoolifywhen no scope is named (src/lib/oauth.ts:641). Then:- an admin completes the upstream login once;
scripts/portal-membership-add.sh coolifyadds every tool disabled;- coolify-mcp has destructive tools (
stop_all_apps, deletes, bulk env updates), so the enable decision goes in a newdocs/portal-allowlist.jsonin mctlhq/mctl-coolify-mcp.
tgstays manual with its pinned scope until mctl-telegram conforms to feat(labs): Hook B — labs-trading-data MCP service + openclaw wiring #137. Its DCR default currently drops unnamed scopes. After that it moves to automatic as well.- The plan must not replace a live server unexpectedly. Any recreate in steps 1–2 is deliberate and done with the re-add steps above.
- Fix the stale comment in
mcp-servers.tf: "The three above areauth_mode: manual" now sits above a single manual server.
After apply
- Sign
seerrsenseandapiin once from the dashboard after the switch to automatic. A new registration means a new grant. - Acceptance for seerrsense: request Ratatouille (2007, TMDB 2062) through the portal.
request_mediafiles it instead of refusing. - Acceptance for coolify: the server leaves
waiting, its catalogue is populated, and a read-only tool (for exampleget_version) answers through the portal once enabled. - Record the results here and on request_media refused through the Cloudflare portal: an authorize request with no scope is granted seerr:read only seerrsense#73.
Context
The server-side alternative, making seerrsense's no-scope default grant seerr:request, was proposed (seerrsense#72, DevLoop proposal for #73) and dropped. The portal-side fix keeps seerrsense least-privilege by default and matches how tg was fixed. The DevLoop execution for #73 is left unapproved. Ending it is tracked in mctlhq/mctl-api#392.
🤖 Generated with Claude Code
Activity
[-]portal: adopt the seerrsense and api upstreams into mcp-servers.tf and pin their OAuth scope[/-][+]portal: bring seerrsense, api and coolify-mcp under mcp-servers.tf with their full scopes_supported[/+]mashkovd commentedon Sep 24, 2026
Why
coolifyis registered by DCR, although coolify-mcp's primary client registration is CIMD (mctl-coolify-mcpeeb60ebf, "Client ID Metadata Documents, DCR kept as fallback"):The Cloudflare MCP portal, acting as a client toward an upstream, has only two modes: automatic (DCR) and manual (a pre-registered
client_id, a requiredclient_secreton create, an optionalscope). It cannot present a Client ID Metadata Document URL as itsclient_id. See the MCP portals docs.CIMD on coolify-mcp therefore serves direct clients, such as claude.ai and Claude Code. The portal takes the DCR fallback. DCR is also preferable to manual here:
coolify, when none is named (src/lib/oauth.ts:641).If the DCR fallback is ever removed from coolify-mcp, this server has to move to manual mode with a pre-registered client.
[-]portal: bring seerrsense, api and coolify-mcp under mcp-servers.tf with their full scopes_supported[/-][+]portal: seerrsense, api and coolify-mcp under mcp-servers.tf in automatic (DCR) mode[/+]mashkovd commentedon Sep 24, 2026
Plan revised: every upstream moves to automatic (DCR) mode under the org rule mctlhq/.github#137, not to manual mode with a pinned scope. seerrsense first needs DCR (mctlhq/seerrsense#74). api and coolify already offer DCR. tg stays manual until mctl-telegram conforms.
mashkovd commentedon Sep 24, 2026
Additional step for
seerrsense: setSEERRSENSE_DCR_REDIRECT_URIS: https://mcp.mctl.ai/servers-callbackinplatform-gitops/services/labs/seerrsense/values.yaml. seerrsense#74 ships with DCR off by default (spec amended in #1364). It must be live before the upstream switches to automatic mode, or the portal's registration gets 404.mashkovd commentedon Sep 24, 2026
seerrsense 1.11.0 is live (2026-09-24 16:18 UTC, ArgoCD Healthy). It carries the portal-restricted DCR fallback (seerrsense#74 → #75). As shipped, DCR is off:
registration_endpointis absent from metadata andPOST /register→ 404. The seerrsense steps here are now unblocked:SEERRSENSE_DCR_REDIRECT_URIS: https://mcp.mctl.ai/servers-callbackinplatform-gitops/services/labs/seerrsense/values.yaml. Verify thatregistration_endpointappears and that a registration with another redirect gets 400.seerrsenseportal upstream to automatic (DCR) and sign in once.mctl-agents commentedon Sep 24, 2026
mctl-agents issue-investigator has analyzed this issue and created a proposal:
https://github.com/mctlhq/mctl-gitops/tree/main/platform-gitops/agents-state/mctl-gitops/proposals/issue-1363-portal-seerrsense-api-and-coolify-mcp-un/
Status:
proposed— pending human approval. Reviewrequirements.md,design.mdandtasks.md, then approve: signal this issue's DevLoopWorkflow (mctl-apiPOST /api/v1/agents/dev-loop/dev-loop-mctlhq-mctl-gitops-1363/approve, orpython -m orchestrator.temporal.cli approve dev-loop-mctlhq-mctl-gitops-1363 --approver <your-identity>) — the workflow flips.status.yamltoacceptedvia themctl-agents-approveoperation and runs the Tier 2 implementer. If no DevLoopWorkflow is running for this issue (pre-Temporal proposal), run themctl-agents-approveoperation directly withservice=mctl-gitops slug=issue-1363-portal-seerrsense-api-and-coolify-mcp-un.The approver is recorded in
.status.yamland in the gitops commit, and the implementer refuses a proposal whosecontrol.requires_human_approvalcarries no named approver — so prefer the mctl-api endpoint, which takes the approver from the authenticated caller rather than from what the caller types.chore(agents): investigate #1363 2026-09-24
docs(agents): #1363 spec gates the manual fallback on the probe, not on
Merge pull request #1365 from mctlhq/docs/portal-1363-dcr-values-step
mashkovd commentedon Sep 24, 2026
Task 0 is done. #1367 merged (
27b3bff3), and ArgoCD is Synced/Healthy at 17:14 UTC on 1.11.0. Live checks:/.well-known/oauth-authorization-servernow carriesregistration_endpoint: https://seerrsense.mctl.ai/register.POST /registerwith a foreign redirect answers 400invalid_redirect_uri("redirect_uris must all be on the allowlist").seerrsense_whoamireturns connected, still on the manualcloudflare-portal-mcpclient.The positive probe (a portal-callback registration, then a no-scope authorize granting
seerr:read seerr:request) belongs to task 2 and is left to the operator, because it writes a registration row in production.Also done: #1366 merged (
2ad0217a) with thecoolifyresource and the detector fixes. It is not applied yet.mashkovd commentedon Sep 24, 2026
Measured while signing
coolifyin (2026-09-24 ~17:30 UTC): the portal's DCR admin login uses the dashboard callback, notservers-callback."Authenticate server" on the
coolifyresource in automatic mode sent this to coolify:coolify accepted it, so the DCR registration Cloudflare made includes
https://dash.cloudflare.com/6a09f637d20e1f66a8e9d45ebe778058/one/access-controls/ai-controls/mcp-server/oauth-callback/<server_id>(withcoolifyas the id). Whether it also listshttps://mcp.mctl.ai/servers-callbackfor end-user sign-ins is not visible: coolify-mcp logged nothing.Consequence for seerrsense (tasks 0/2/11).
SEERRSENSE_DCR_REDIRECT_URISis currently onlyhttps://mcp.mctl.ai/servers-callback, and seerrsense requires every registered redirect to be on the list. A portal registration carrying the dashboard callback would get 400invalid_redirect_uri, and the switch to automatic would fail. The allowlist has to also carry the exact dashboard callback for this server:https://dash.cloudflare.com/6a09f637d20e1f66a8e9d45ebe778058/one/access-controls/ai-controls/mcp-server/oauth-callback/seerrsenseIt is exact-match (
isAllowedRedirectUri), account- and server-scoped, so it does not widen the gate to anyone but this account's dashboard. The same applies toapi's DCR restriction check in task 2. The rule in mctlhq/.github#137 needs the same correction.Also:
coolifyis now Ready with 45 tools and 3 prompts (owner login, Google,mashkoffdmitry@gmail.com). Portal membership (portal-membership-add.sh coolify) is still pending, because it needs a Cloudflare API token.mashkovd commentedon Sep 25, 2026
seerrsense is on DCR (task 1 done, 2026-09-25).
The server was moved to automatic mode through the API; the sequence is recorded in the
mcp-servers.tfresource comment. "Authenticate server" in the dashboard then let Cloudflare register at the portal-restricted/register.Current state:
readyandconnected, no manual registration, catalogue synced on its own.seerrsense_whoamianswers through the portal.The server was adopted into tofu in #1400. Apply 36200526533 reported
1 imported, 0 changed, andseerrsenseis inDCR_SERVERS.Not done yet: the Ratatouille acceptance request, which creates a real request in Seerr and is left to the owner.
Remaining:
OAUTH_ALLOWED_IMPLICIT_HOSTS, which the recorded design (chore(argocd): freeze admins-openclaw to manual sync during upstream sync #585 proposal) keeps free of the portal. It needs an exact portal-callback allowlist for DCR, as in seerrsense#74.narrowGrantalready returns the full entitlement when no scope is named.mashkovd commentedon Sep 26, 2026
All six portal upstreams are in automatic (DCR) mode and in
mcp-servers.tf, as of 2026-09-26.projectsstale)alicecoolifyseerrsenseapitgauth_config_summaryis null on all six.How
apiandtgmoved. Both used the seerrsense sequence recorded inmcp-servers.tf:PUTbearer + dummy, which clears the manual registration;PUT {auth_type: oauth, name, description};This needed each upstream to admit the portal's exact callbacks first:
api: feat(oauth): persist dynamic client registrations for the Cloudflare portal (DCR) mctl-api#400 (4.54.0), registrations persisted in Postgres, and the callbacks inOAUTH_ALLOWED_REDIRECT_URIS(feat(oauth): allow the portal's DCR callbacks on mctl-api and mctl-telegram #1401).tg: feat(oauth): exact-match DCR redirect allowlist for the MCP portal mctl-telegram#691 (0.69.0), a pinned client derived fromOAUTH_DCR_REDIRECT_URIS(feat(oauth): allow the portal's DCR callbacks on mctl-api and mctl-telegram #1401).A portal call succeeded on each afterwards (
api_mctl_whoami,tg_get_my_notification_preferences). The catalogues matchedallowlists/catalogue.jsonname for name, in order.A trap for the next mode change. Removing
auth_credentialsfrom an existing resource cannot be applied. Provider 5.24 sends the dropped write-only attribute as an explicitnull, and the API refuses the PUT with400 7001 "Expected string, received null". That was the failedtgstep of the #1402 apply; nothing was written. The way through is forget + import:removed { lifecycle { destroy = false } }(#1403), thenimport(#1404). The comment inmcp-servers.tfnow says so.The nightly checks were adjusted to match.
portal-auth-credentials-drift.py: every server is onDCR_SERVERS, and a DCR server whose live summary is non-empty now reports as drift.validate-portal-allowlists.py:UNMANAGEDis empty.prepare_send_message, until 2026-12-31.cloudflare-driftis green on every root: https://github.com/mctlhq/mctl-gitops/actions/runs/36228068498Still open here. The seerrsense acceptance: request Ratatouille (TMDB 2062) through the portal. It files a real Seerr request, so it waits for the owner's go-ahead. The rest of the checklist above is done: steps 1–4, step 5 (the only recreate was the deliberate
tgforget/import, with no API write) and step 6 (the stale comment is rewritten).mashkovd commentedon Sep 26, 2026
seerrsense acceptance through the portal, 2026-09-26: the scope failure is gone.
seerrsense_get_media(movie, TMDB 2062) returns Ratatouille (2007),status: AVAILABLE.seerrsense_request_media(movie, TMDB 2062) returnsThat title is already available on your Seerr, so nothing was requested.Before the DCR move this call answered
this token is not granted the seerr:request scope. That check runs first in the handler (src/mcp/server.ts:346, beforerequestMediaSafely), so reaching the "already available" refusal means the portal token now carriesseerr:request, the DCR default from mctlhq/seerrsense#74. No request was filed, because the title is already in the library. The refusal comes from Seerr's own guard, not from the portal.mashkovd commentedon Sep 26, 2026
Closing. All six portal upstreams are in automatic (DCR) mode and managed in mcp-servers.tf, the nightly drift run is green, and the seerrsense acceptance passed (the seerr:request scope is granted through the portal). The remaining follow-up, closing projects' output schemas before the 2026-10-15 waiver expiry, is tracked in mctlhq/projects-mcp#28.