Skip to content

portal: seerrsense, api and coolify-mcp under mcp-servers.tf in automatic (DCR) mode #1363

Closed

Description

@mashkovd

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

  1. 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, like alice/projects: no auth_credentials, no client_secret. The live server is currently manual. Check whether the provider can switch mode in place. If not, recreate it, re-add it with scripts/portal-membership-add.sh, and re-apply seerrsense's docs/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 with scope = "seerr:read seerr:request offline_access", modelled on tg.
  2. api: api.mctl.ai already advertises DCR (/oauth/register, scope mctl, auth none). 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 as seerrsense applies if it is live as manual today.
  3. coolify (https://coolify.mctl.ai/mcp): new resource, automatic (DCR). coolify-mcp is already CIMD with a DCR fallback and grants coolify when no scope is named (src/lib/oauth.ts:641). Then:
    • an admin completes the upstream login once;
    • scripts/portal-membership-add.sh coolify adds every tool disabled;
    • coolify-mcp has destructive tools (stop_all_apps, deletes, bulk env updates), so the enable decision goes in a new docs/portal-allowlist.json in mctlhq/mctl-coolify-mcp.
  4. tg stays 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.
  5. 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.
  6. Fix the stale comment in mcp-servers.tf: "The three above are auth_mode: manual" now sits above a single manual server.

After apply

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

  1. changed the title [-]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[/+] on Sep 24, 2026
  2. mashkovd commented on Sep 24, 2026

    @mashkovd
    MemberAuthor

    Why coolify is registered by DCR, although coolify-mcp's primary client registration is CIMD (mctl-coolify-mcp eeb60ebf, "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 required client_secret on create, an optional scope). It cannot present a Client ID Metadata Document URL as its client_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:

    • a manual server's tool catalogue is captured once and never re-synced;
    • the scope pin that manual mode buys is unnecessary, because coolify-mcp grants its only scope, 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.

  3. changed the title [-]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[/+] on Sep 24, 2026
  4. mashkovd commented on Sep 24, 2026

    @mashkovd
    MemberAuthor

    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.

  5. mashkovd commented on Sep 24, 2026

    @mashkovd
    MemberAuthor

    Additional step for seerrsense: set SEERRSENSE_DCR_REDIRECT_URIS: https://mcp.mctl.ai/servers-callback in platform-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.

  6. mashkovd commented on Sep 24, 2026

    @mashkovd
    MemberAuthor

    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_endpoint is absent from metadata and POST /register → 404. The seerrsense steps here are now unblocked:

    1. Set SEERRSENSE_DCR_REDIRECT_URIS: https://mcp.mctl.ai/servers-callback in platform-gitops/services/labs/seerrsense/values.yaml. Verify that registration_endpoint appears and that a registration with another redirect gets 400.
    2. Switch the seerrsense portal upstream to automatic (DCR) and sign in once.
    3. Acceptance: Ratatouille (TMDB 2062) is requested through the portal.
  7. mctl-agents commented on Sep 24, 2026

    @mctl-agents
    Contributor

    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. Review requirements.md, design.md and tasks.md, then approve: signal this issue's DevLoopWorkflow (mctl-api POST /api/v1/agents/dev-loop/dev-loop-mctlhq-mctl-gitops-1363/approve, or python -m orchestrator.temporal.cli approve dev-loop-mctlhq-mctl-gitops-1363 --approver <your-identity>) — the workflow flips .status.yaml to accepted via the mctl-agents-approve operation and runs the Tier 2 implementer. If no DevLoopWorkflow is running for this issue (pre-Temporal proposal), run the mctl-agents-approve operation directly with service=mctl-gitops slug=issue-1363-portal-seerrsense-api-and-coolify-mcp-un.

    The approver is recorded in .status.yaml and in the gitops commit, and the implementer refuses a proposal whose control.requires_human_approval carries no named approver — so prefer the mctl-api endpoint, which takes the approver from the authenticated caller rather than from what the caller types.

  8. added 3 commits that reference this issue on Sep 24, 2026
  9. mashkovd commented on Sep 24, 2026

    @mashkovd
    MemberAuthor

    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-server now carries registration_endpoint: https://seerrsense.mctl.ai/register.
    • POST /register with a foreign redirect answers 400 invalid_redirect_uri ("redirect_uris must all be on the allowlist").
    • The portal session is unaffected: seerrsense_whoami returns connected, still on the manual cloudflare-portal-mcp client.

    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 the coolify resource and the detector fixes. It is not applied yet.

  10. mashkovd commented on Sep 24, 2026

    @mashkovd
    MemberAuthor

    Measured while signing coolify in (2026-09-24 ~17:30 UTC): the portal's DCR admin login uses the dashboard callback, not servers-callback.

    "Authenticate server" on the coolify resource in automatic mode sent this to coolify:

    GET https://coolify.mctl.ai/authorize?client_id=mcp_client_SoVb…&redirect_uri=https://dash.cloudflare.com/6a09f637d20e1f66a8e9d45ebe778058/one/access-controls/ai-controls/mcp-server/oauth-callback/coolify&scope=coolify&resource=https://coolify.mctl.ai/mcp
    

    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> (with coolify as the id). Whether it also lists https://mcp.mctl.ai/servers-callback for end-user sign-ins is not visible: coolify-mcp logged nothing.

    Consequence for seerrsense (tasks 0/2/11). SEERRSENSE_DCR_REDIRECT_URIS is currently only https://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 400 invalid_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/seerrsense
    It 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 to api's DCR restriction check in task 2. The rule in mctlhq/.github#137 needs the same correction.

    Also: coolify is 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.

  11. mashkovd commented on Sep 25, 2026

    @mashkovd
    MemberAuthor

    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.tf resource comment. "Authenticate server" in the dashboard then let Cloudflare register at the portal-restricted /register.

    Current state: ready and connected, no manual registration, catalogue synced on its own. seerrsense_whoami answers through the portal.

    The server was adopted into tofu in #1400. Apply 36200526533 reported 1 imported, 0 changed, and seerrsense is in DCR_SERVERS.

    Not done yet: the Ratatouille acceptance request, which creates a real request in Seerr and is left to the owner.

    Remaining:

  12. mashkovd commented on Sep 26, 2026

    @mashkovd
    MemberAuthor

    All six portal upstreams are in automatic (DCR) mode and in mcp-servers.tf, as of 2026-09-26.

    Server Mode In OpenTofu Live (read back after the last apply)
    projects DCR ✓ ready / connected, 8 tools (re-authenticated 08:02 after its admin session went stale)
    alice DCR ✓ ready / connected, 12
    coolify DCR ✓ ready / connected, 45
    seerrsense DCR ✓ #1400 ready / connected, 5
    api DCR ✓ #1402 (import) ready / connected, 92, the same 15 tools still disabled on the portal
    tg DCR ✓ #1403 + #1404 (forget, then re-import) ready / connected, 36

    auth_config_summary is null on all six.

    How api and tg moved. Both used the seerrsense sequence recorded in mcp-servers.tf:

    1. PUT bearer + dummy, which clears the manual registration;
    2. PUT {auth_type: oauth, name, description};
    3. "Authenticate server" in the dashboard.

    This needed each upstream to admit the portal's exact callbacks first:

    A portal call succeeded on each afterwards (api_mctl_whoami, tg_get_my_notification_preferences). The catalogues matched allowlists/catalogue.json name for name, in order.

    A trap for the next mode change. Removing auth_credentials from an existing resource cannot be applied. Provider 5.24 sends the dropped write-only attribute as an explicit null, and the API refuses the PUT with 400 7001 "Expected string, received null". That was the failed tg step of the #1402 apply; nothing was written. The way through is forget + import: removed { lifecycle { destroy = false } } (#1403), then import (#1404). The comment in mcp-servers.tf now says so.

    The nightly checks were adjusted to match.

    Still 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 tg forget/import, with no API write) and step 6 (the stale comment is rewritten).

  13. mashkovd commented on Sep 26, 2026

    @mashkovd
    MemberAuthor

    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) returns That 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, before requestMediaSafely), so reaching the "already available" refusal means the portal token now carries seerr: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.

  14. mashkovd commented on Sep 26, 2026

    @mashkovd
    MemberAuthor

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions