Skip to content

Personal MCP OAuth provider policy

Status

Implemented and verified through the API, persistence and provider ports. No real external OAuth account was used. The original iteration excluded personal runtime execution; subsequent runtime and user recovery delivery is recorded in the authorization specification. Administrator allowlist UI delivery is implemented and verified below.

Scope

Extend durable personal authorization with administrator-approved requested scopes and preconfigured public/confidential clients. Preserve automatic availability, administrator publication and grants, explicit user enablement and consent, shared authentication, Desktop removal and unrelated changes. Runtime execution, discovery, refresh, UI, confidential dynamic registration, client metadata documents, JWT client authentication and providers without the current S256/issuer-binding profile remain outside this iteration.

Decisions and contracts

The existing downstream MCP context owns the policy; personal_oauth_configuration/ groups its pure models and store port. Definitions remain secret-free and unchanged. An additive encrypted configuration record holds the complete policy, with connector identity and a nonsecret revision bound into the encrypted document. Missing records resolve to revision legacy, empty requested scopes and dynamic public registration. Existing normalized definition hashes must not change, and old durable preparations/contexts receive compatible legacy defaults.

PersonalOAuthSettings contains requested_scopes and an explicit client variant: dynamic_public, preconfigured_public, or preconfigured_confidential. Static clients require an administrator-configured exact HTTPS issuer_url and client_id; confidential clients additionally require client_secret and an explicit client_secret_basic or client_secret_post method. Discovery must advertise that configured issuer before using the static client. Configured client credentials identify Umbod's OAuth application, not an administrator's downstream user account; user grants and tokens remain principal-scoped. Never send an administrator-configured client secret to an issuer chosen solely by a resource's mutable metadata. Preserve exact issuer spelling and all existing destination, resource, redirect and PKCE checks.

Scopes use OAuth scope-token syntax, are case-sensitive, unique and canonicalized by sorting. Bound at 64 tokens, 256 ASCII characters per token and 4,096 characters space-joined. Request the exact administrator-configured set, without inference or expansion from metadata. AS scope advertisements are optional and may be incomplete even when supplied; they do not veto an explicit administrator request or establish consent. An explicit dynamic-registration scope response must equal the requested set; omission is accepted without interpreting it as a token grant. Empty requested sets omit scope parameters and retain the documented implicit-provider-default limitation.

Token scopes may narrow but may not exceed requested scopes. Omitted token scope is interpreted as the requested set under RFC 6749; explicit empty scope is an empty grant. Save requested/effective granted scope sets and configuration revision alongside authorization. Callback scopes are optional validation-only data: reject malformed or excessive scopes, but do not treat them as final grants. No automatic step-up requests occur.

Preconfigured clients never register dynamically, require no registration endpoint, and never fall back on failure. AS authentication-method metadata defaults to client_secret_basic when absent; dynamic-registration authentication defaults likewise. Dynamic public registration still requires none. Static clients must use their explicitly configured method, supported by metadata. Preserve code/S256 support and issuer-bound callbacks, using either an exact response issuer or a persisted issuer-specific redirect. Static client-secret expiry is provider/operator-managed in this iteration: provider rejection fails safely without retries, fallback or installation; no unknown expiry is invented.

OAuth Basic authentication UTF-8 form-encodes identifier and secret separately before colon joining and Base64 encoding. Send Basic credentials only through a typed, secret-bearing Authorization field at the transport boundary. POST authentication sends credentials only in the form body. Public clients send no secret. Never put secrets in URLs, authorization requests, diagnostics, definition JSON or projections. Store secrets only under the configured encryption root; the public administrator boundary rejects confidential writes without a nonblank root-derived configuration secret.

Administrator boundary

Extend POST /api/admin/connectors/mcp/personal-oauth with optional configuration.oauth_configuration, preserving endpoint-only creation as empty-scope dynamic public registration. Map this to CreatePersonalOAuthConnectorDefinition.oauth_configuration. Create definition and configuration atomically, without network I/O, shared credentials, health, catalogue, publication or activation.

Add authenticated GET and PUT /api/admin/connectors/mcp/{connector_id}/personal-oauth. GET projects connector ID, policy revision, requested scopes and nonsecret client fields; confidential projections expose only client_secret_configured=true, never plaintext, ciphertext or masked placeholders. PUT accepts expected_revision and complete settings. The public route maps these into ReplacePersonalOAuthConfiguration; it is full replacement, not PATCH: confidential settings must explicitly supply a nonblank replacement secret. Omission is not implicit retention. Generic personal metadata/configuration edits remain blocked. Existing authentication and cookie-CSRF controls apply.

All validation errors on these personal administrator writes must be sanitized, including unknown fields, malformed discriminators and unrelated invalid fields in bodies containing secrets. Do not reflect Pydantic input dictionaries or submitted credentials. Existing shared endpoint validation behavior remains unchanged.

Configuration replacement is optimistic and serialized. Reject stale revision or non-personal/missing definitions without mutation. A public/dynamic policy unchanged after canonicalization is a no-op; a supplied confidential secret is explicit rotation even if equal to the old value. Every effective change receives a new revision and atomically removes all personal authorizations, changes connection revisions while preserving enablement, erases all connector attempts and replaces obsolete configured secrets. Completed-attempt history may be removed during policy change. No remote token revocation is claimed. Preserve publication, grants and other connector metadata.

Durable workflow

PreparePersonalOAuth.policy snapshots the resolved revision/settings, including any configured secret, inside encrypted attempt storage. The owner service loads current configuration after identity/eligibility checks and before reservation. Reservation, publication, claim and completion recheck exact current policy against this snapshot inside their existing serialized transaction. Provider context binds policy revision, scopes, registration/client method and static client identity; exchange uses the captured preparation without rediscovery or registration replay. Old legacy preparations remain valid only for unchanged legacy configuration.

Deletion removes encrypted configuration in the existing aggregate transaction. Configuration changes and completion serialize against each other: if completion wins, configuration update removes its authorization; if update wins, the old attempt cannot install anything. Wrong-owner/state/claim protections, immediate expiry erasure, final-time expiry rechecks and one-shot exchange remain unchanged.

Acceptance

  • Given an old personal definition and old durable context, when no configuration exists, legacy behavior and hashes remain unchanged.
  • Given valid configured scopes and absent/incomplete advertisements, authorization requests only that exact set; invalid, duplicate or oversized inputs fail before mutation.
  • Given narrower, omitted, empty or excessive returned scopes, the recorded grant follows the explicit rules above; excess never installs authorization.
  • Given any static client, preparation and resumed exchange perform no registration; configured issuer mismatches fail before secret use.
  • Given each supported authentication method, only its one intended credential location is used, including reserved and Unicode Basic credentials.
  • Given any malformed administrator request containing sentinel secrets, errors, projections and logs contain none of those secrets.
  • Given persistent storage and recreated adapters, secrets decrypt correctly only under the correct connector/revision binding and remain absent from definition JSON and raw encrypted rows.
  • Given concurrent policy edits, stale expected revisions cannot overwrite the newer policy.
  • Given an active preparation/exchange or existing authorization, replacement erases old secrets and attempts, preserves user enablement, and prevents all stale completions.
  • Given injected mutation failure, configuration, connections, attempts and aggregate creation/deletion roll back together.
  • Given deletion followed by late work, neither configuration nor authorization can resurrect.
  • Given user initiation with scope/client/owner overrides, the strict empty-object request still rejects it.
  • Given successful personal authorization, shared credentials remain untouched and runtime availability stays false.

Verification

Run policy and persistence contracts through in-memory and SQLite database adapters, provider transport fixtures, service and REST boundary tests, full API/MCP regression, Ruff and shared Semgrep. Verify the running services in disposable Kind, including policy read/replace, safe secret projections, invalid-secret responses, opt-in and existing runtime guards. No real external account is available: protocol fixtures and live negative/administration checks are not an interoperability claim. Published Helm and frontend are unchanged. Operator ingress callback exemptions and query-redaction obligations remain. Secret-bearing administrator bodies and token Authorization headers must also be suppressed or redacted by proxies, tracing and error reporters.

Verified: 2,819 API/MCP tests passed, two existing parametrization skips; Ruff and git diff --check passed. Disposable Kind installation and all 29 live runtime tests passed, including confidential creation, safe policy projections and replacement, stale-write rejection, validation/CSRF secret checks, private-provider denial and collected API stdout secret sentinels. The initial Kind run incorrectly assumed its development administrator simulation rejected anonymous access; that assertion was corrected to verify the independently authenticated personal-user boundary instead. Shared Semgrep remains at the prior 52 findings with no new findings. Positive protocol flows use controlled transports and include recreated-worker completion for all supported client methods, exact Basic encoding, narrowed/omitted scopes and policy changes during an active exchange.

Administrator allowlist UI delivery

The centrally managed inventory of personal MCP definitions is the global server allowlist; it is not universal user access or a second authorization mechanism. Before offering registration, read the resolved personal-connections enabled boolean through the authenticated instance-configuration boundary. Disabled or unknown availability keeps registration disabled and provides operator/startup-setting guidance. OAuth availability is separate: disabled owner OAuth does not prohibit otherwise enabled registration. Missing, duplicated or incorrectly typed configuration entries cannot establish availability; a failed configuration read requires explicit reload. A later personal_connections_unavailable response clears transient secrets and disables registration until a fresh explicit check. The UI never chooses an identity namespace or changes the encryption root. Startup configuration may supply the stable local/dev test authority described in the configuration reference; that fallback must never apply to real-provider or production settings. Use the existing /admin/downstream-mcp-connectors workspace for both authorization modes, with one inventory, sidebar and URL selection controller and explicit Shared/Personal labels. Remove the separate personal administration page and navigation entry; its old URL is a redirect-only compatibility route. Personal selections must not load shared discovery, credentials, prompts, resources or tool bundles. Ownership is required in validated response contracts; missing or invalid authorization_mode fails closed, never defaults to shared.

The page supports registration, reading and replacing OAuth settings, explicit publication/withdrawal, and confirmed deletion through existing administrator APIs. Registration uses POST /api/admin/connectors/mcp/personal-oauth and starts unpublished, with no discovery, group grants, owner enablement or consent. OAuth policy reads and revision-checked replacements use GET/PUT /api/admin/connectors/mcp/{id}/personal-oauth. Publication uses bodyless PUT/DELETE /api/admin/connectors/mcp/{id}/publication; deletion uses the existing connector DELETE. Server name, endpoint, prefix and public path are read-only after registration because generic personal definition edits are unsupported.

Provide dynamic public, preconfigured public and preconfigured confidential client forms, explicit Basic/POST confidential authentication, and bounded canonical scopes. Use separate validated request and nonsecret response schemas. Confidential reads only display configured status; every confidential write requires a newly entered secret. Secrets remain transient, never populate a response, URL, browser storage or diagnostics, and are cleared on submission, failure, close, disposal and client-kind changes. Before replacing settings, require confirmation explaining that an effective policy change removes all users' saved authorizations and pending sign-ins without remote token revocation. Preserve publication, group grants and user enablement.

Use a typed administration port, in-memory adapter, validated HTTP route adapter and the existing composite client/transport. UI state is scoped to the component lifecycle, with explicit actions and stale-result/disposal guards. Render fixed safe errors, never raw provider bodies or schema issues. A configuration conflict requires an explicit authoritative reload; uncertain writes must not be automatically repeated. After an uncertain outcome, block further mutations until explicit reload reconciles server state. Authentication loss clears loaded administration state and secrets without automatic navigation. Unavailable deployments report that limitation rather than offering a credential fallback.

Publication permits selection by explicitly granted groups, not all users. Include a Group permissions link and explain that a connector-level group grant covers all tools available to the downstream user, including future tools after explicit rediscovery. Personal tool approval, activation and capability grants are not execution prerequisites. Owner enablement, OAuth and private discovery remain required, so publication alone is not execution readiness. Legacy administrator tool-contract APIs remain compatible metadata only, not personal access authority. No owner OAuth action, automatic discovery, permission grant or operation replay is introduced.

Administrator UI acceptance

  • Given local/dev startup with automatic personal selection and an exactly empty namespace, when configuration is derived, then registration is enabled using the stable test:umbod-local authority. Explicit opt-out and supplied namespaces are preserved; real-provider and production settings receive no fallback or generated encryption material.
  • Given a new personal server, when registration succeeds, then it remains unpublished and no provider or discovery request, group grant or owner action is issued.
  • Given any supported client kind, when valid settings are submitted, then the exact issuer/client method and canonical requested scopes are sent; invalid scopes or secret-bearing settings fail safely before I/O.
  • Given a confidential configuration, when it is read or edited, then no stored secret is returned or prefilled, and saving requires a fresh secret that is cleared after the attempt.
  • Given an existing policy, when the administrator saves an edit, then explicit invalidation confirmation precedes exactly one revision-checked replacement.
  • Given a stale revision or uncertain result, when the error is shown, then no mutation is repeated and an explicit reload is required before another write.
  • Given navigation, disposal or authentication loss during a request, when an older response arrives, then it cannot restore stale records or secrets.
  • Given an unpublished server without approved tools, when publication succeeds, then Published is shown without an execution-readiness claim; only explicitly permitted groups can select it.
  • Given withdrawal, when the user catalogue is reloaded, then new selection eligibility is revoked without implying token revocation or owner disconnect.
  • Given an outstanding connector/tool grant, when deletion is rejected, then the UI explains the need to remove grants and does not retry deletion.
  • Given saved grants for an unpublished or otherwise unassignable connector, when Group permissions is opened, then those grants remain visible for removal. Removal is staged and saved explicitly through disabled permission changes without republishing, adding grants or changing unrelated permissions. The server detail exposes its ID for matching retained grants.
  • Given shared and personal rows, when the unified workspace loads, then both appear with mode labels and either can be selected through the same connector URL parameter. Missing or malformed ownership fails closed.
  • Given a personal selection, when details load, then common read-only metadata and personal OAuth settings appear without shared configuration, description editing, sign-in, discovery, proxy instructions or capability activation controls, and without requests for those shared bundles.
  • Given the existing Add action, when Shared or Personal is chosen, then the corresponding setup form opens without stacked dialogs. Successful personal registration refreshes the one inventory and selects the new unpublished definition; cancellation creates nothing.
  • Given selection changes or a personal form closes, when an older request resolves, then it cannot restore the previous selection, policy or secrets. Confidential inputs are cleared and never enter a cached detail bundle.
  • Given either mode, when publication or deletion is confirmed, then the existing common endpoint is used without automatic grant removal. Uncertain mutations require explicit reconciliation rather than replay.
  • Given the old personal-server URL with a valid selection, when visited, then a 307 redirect preserves that selection in the unified workspace. No separate personal page or navigation remains.
  • Given generic list/detail support for both modes, when shared-only operations are attempted, then explicit ownership guards reject personal metadata/configuration updates, discovery and capability/activation operations before mutation. Overview statistics explicitly retain their shared-only projection before requesting activation counts.

Verify frontend contract/state/component tests, Bun/Svelte checks, shared Semgrep and whitespace checks. Rebuild Compose and exercise the real UI at http://localhost:3010 using isolated disposable data for administrator CRUD, publication/withdrawal, confidential secret projection and shared-workspace isolation. Inspect mobile layout, console/page errors and saved screenshots. Controlled browser fault injection may cover conflict/uncertainty/authentication-loss branches but must be reported separately from running-service verification. No backend or Helm changes are planned; if that scope changes, run their mandatory runtime gates.

Verified unified administrator workspace: 683 frontend tests passed across the existing 76 test files; Svelte reported zero errors with the one existing overview warning. Shared Semgrep remained at 71 findings and whitespace checks passed. The separate page, page-owned state and navigation entry were removed; the compatibility loader redirects to the unified workspace with selection preserved. Superseded personal-only list/publication/deletion client methods were removed, leaving common resource operations in the generic client.

The rebuilt Compose UI was checked in a real browser with both modes present. Disposable shared registration used a controlled loopback FastMCP fixture and displayed its discovered catalogue and existing shared controls. Disposable personal registration stayed unpublished, selected the new definition in the same sidebar, and did not expose shared credentials, discovery or activation controls. Personal policy replacement, explicit publication, withdrawal and deletion were exercised. Selection changes discarded unsaved confidential input. Registration cancellation created nothing, and concurrently rendered policy/registration forms had unique IDs and correctly scoped accessible labels. Desktop and 390-pixel mobile layouts, including expanded technical details, had no horizontal overflow. Verified screenshots: /tmp/umbod-unified-admin-desktop.png, /tmp/umbod-unified-admin-mobile.png and /tmp/umbod-unified-admin-shared-desktop.png.

A controlled browser fault discarded a successful registration response after the disposable server was committed. Exactly one POST occurred; the uncertainty message and mutation lock survived closing the dialog, disabled Add and hid competing workspace mutations. Explicit full reload reconciled the committed server without replay. The fixture was then deleted. Review also verified independent registration/policy lock aggregation and the combined-form label regression. Normal browser console/page checks were clear; captured requests contained no failed responses and no shared catalogue requests for the real personal server. All three disposable records and both controlled fixture containers were removed. Before/after hashes confirmed the real server definition and OAuth policy were unchanged, and its enabled, saved-authorization and runtime-available states remained true. No real provider tool was invoked. Backend APIs, owner Connections, credentials, grants, roots, private inventories and Helm were not changed by this frontend consolidation.

Historical verification of the separate administrator page before workspace consolidation: 631 frontend tests passed; Svelte checks reported zero errors and one existing administration accessibility warning. Shared Semgrep completed with the unchanged 66 findings, and whitespace checks passed. Independent reviews identified native-validation secret retention, modal focus handling and a long-label mobile overflow; all were corrected with regression coverage. The rebuilt Compose UI was exercised at localhost:3010 with isolated disposable data: registration stayed unpublished, explicit publication worked without approved tools, confidential POST-client settings were saved and read back without a secret, shared-workspace direct selection excluded the personal server, and outstanding grants blocked deletion with safe guidance. After withdrawal, saved grants were explicitly removed through Group permissions without republishing; deletion then succeeded. Native invalid submissions cleared secrets without writes; modal keyboard isolation, Escape cancellation and opener/fallback focus were checked, together with desktop/mobile layouts and verified screenshots. Controlled fetch faults separately verified conflict blocking, a lost response after a real committed policy update, manual reconciliation without replay, and state clearing on 401 without navigation. Browser console/page errors and service-stdout credential sentinels were clear. No provider discovery or shared execution endpoint was called for the personal fixture. Test data was removed and the default Compose configuration restored. These checks used local administrator simulation and do not establish external-provider account interoperability or authenticated user-catalogue interoperability.

Registration-availability correction verification: 652 frontend tests passed, Svelte checks reported zero errors and one existing warning, and shared Semgrep retained 66 findings. Independent review found no blocking issue. The default Compose configuration was reproduced with resolved personal selection disabled; the existing registration endpoint returned personal_connections_unavailable with 404. After rebuilding, the real browser showed disabled registration and startup-setting guidance before any write. Controlled configuration responses verified unknown/unavailable/missing-entry rejection and enabled registration independently of disabled OAuth. A controlled stale-enabled projection against the actually disabled backend produced one explicit 404, cleared the submitted diagnostic secret, and disabled further submission without replay. Neither diagnostic request created an inventory record; the secret sentinel was absent from API stdout. Desktop/mobile screenshots were verified and no browser console/page errors were observed. No deployment setting, namespace or encryption root was changed.

Local-default follow-up verification: ordinary local/dev automatic startup now derives test:umbod-local for an exactly empty namespace, while explicit settings and real-provider/production behavior are preserved. The full backend suite passed 3,503 tests with three skips, the frontend suite passed 652 tests, Ruff passed, and Svelte checks retained zero errors and one existing warning. The disposable Kind gate passed all three in-cluster checks and all 33 running-service tests, then removed its owned cluster. Independent review found no blockers. The default Compose API and MCP both reported selection enabled with the same derived authority; raw environment mode remained auto and the supplied namespace remained empty. Real browser registration with a nonempty requested scope succeeded without a configuration overlay or provider request, remained unpublished, and was explicitly deleted afterward while preserving an existing server. An earlier UI attempt encountered an externally stopped local stack; inventory was reconciled before another explicit submission. Browser console/page checks were clear. Shared Semgrep reported 71 workspace findings and none in the changed configuration files. No encryption key was generated or replaced; user OAuth and confidential credentials still require the documented encryption and identity prerequisites.

Local development Test User

Local simulated authentication must use the configured Test User for personal connections without requiring an Umbod sign-in. Eligibility is restricted to local/dev runtime, development simulation, enabled personal selection and a valid test namespace. The default user verifier may verify an empty token only within that policy; explicitly supplied user verifiers remain token-required and never inherit the administrator verifier. Canonical simulated ownership requires the configured subject and rejects mismatched, blank, machine or issuer-bearing identities. Production, real-provider JWT and unauthenticated configurations retain strict verification.

Authenticated single_test_user MCP personal boundaries project the same configured simulated identity and membership as REST. No MCP access-token context still means no personal principal. Original machine identities remain rejected, simulated non-administrator membership is not elevated, and mismatched group-claim configuration must not invent grants. Global MCP token claims and shared/native authorization are unchanged. Publication, explicit connector/tool permissions, user enablement, provider consent and discovery remain independent; local identity is not downstream authorization. Changing the configured simulated subject or namespace changes ownership and must never transfer existing grants automatically.

Verified: the full backend suite passed 3,544 tests with three skips; frontend tests passed 652, Ruff passed and Svelte checks retained zero errors and one existing warning. The disposable Kind gate passed all three Helm test pods and all 33 runtime tests, including local/dev behavior and a production tokenless rejection assertion, then removed its owned cluster. Independent security review found no blockers. Default Compose returned a tokenless personal catalogue with status 200. A temporary published server remained absent until an explicit grant for the configured Test User group; the browser then enabled it without an Umbod login and preserved enablement across full navigation. No provider sign-in or discovery occurred. Temporary selection, grant, definition and empty fixture group were removed without changing existing servers. Browser console/page checks were clear and the screenshot was verified. Shared Semgrep retained 71 workspace findings, with none in the changed identity/middleware modules. No frontend credentials, encryption root or production authentication settings were created or changed.

Mandatory server root preflight

Operator-backed API and MCP startup require a nonblank UMBOD_ROOT_SECRET in every profile, including local/dev and configurations with personal OAuth disabled. The shared server configuration loader must reject an absent, empty or whitespace-only root before application composition. A root-only settings projection resolves the same environment/dotenv precedence before unrelated settings validation, so malformed ports or flags cannot hide this diagnosis. Full operator settings are loaded once afterward and the actual root is rechecked before derivation. The supported Umbod CLI must perform that check before invoking Uvicorn, including reload/worker parent socket creation; service factories must repeat the check before persistence and metrics startup. The error names UMBOD_ROOT_SECRET, tells the operator to supply a persistent value and never includes secret contents. No key is generated or substituted.

Docker Compose must reject missing or empty roots before creating services. Whitespace roots remain rejected by server preflight. Generic configuration construction and local structural inspection remain separate from server readiness; explicit typed embedding owns its supplied security configuration. Externally prebound sockets and an external Uvicorn reload/worker supervisor are outside the application's pre-bind guarantee. The root must remain stable and consistent between API and MCP. This changes startup requirements, not publication, permissions, Test User identity or downstream consent.

Verified: the full backend suite passed 3,656 tests with three skips and Ruff passed. The final disposable Kind run passed all three Helm test pods and all 33 runtime tests, including telemetry/secret-log checks, then deleted its owned cluster. Twenty-four isolated image probes trapped every socket bind and confirmed clear, traceback-free rejection for missing, empty and whitespace roots across both services, normal/reload launches and valid/invalid port settings; no bind was attempted. Compose rejected missing and empty roots before service creation and accepted a supplied test value. Independent review confirmed diagnostic priority, source precedence and secret-free errors. Shared Semgrep retained 71 workspace findings. Default local API and MCP were restarted with the existing configured root and both became healthy; no root was created, changed or disclosed.

Authorized local legacy-record recovery

The first root-secret configuration exposed two pre-existing records encrypted with LOCAL_DOWNSTREAM_MCP_CREDENTIAL_SECRET: the dynamic-public policy and the configured Test User's unauthorized connection state for server f9909875-a0d0-4cae-95e1-66acd0a94dba. Read-only probes confirmed current-key failure, legacy-local-key compatibility and valid connector/revision/owner bindings. Publication and the explicit umbod:admin grant were intact. The policy returned personal_oauth_configuration_unavailable while owner inventory failed decrypting the connection record; neither was an absent route or missing grant.

After explicit operator authorization, maintenance verified the local/dev test namespace, dynamic-public client and absence of stored owner authorization. It held a SQLite write reservation, created and integrity-checked a consistent full backup with mode 0600, and atomically re-encrypted exactly those two document columns under the existing root-derived key. Exact plaintext equality and target-key model validation were checked before commit; IDs, revisions, enablement, publication and grants were unchanged. No root or provider credentials were created, rotated or substituted, and no runtime legacy-key retry was added. The backup is retained inside the data volume at /app/data/umbod.sqlite3.before-local-key-recovery-20261022T123542Z-0da92deb62be492d8e218e9b413bcd42.sqlite3.

After restart, both policy and owner inventory returned 200. Browser verification showed the published policy editor and enabled test connection without errors; no provider sign-in or discovery was attempted. This was an explicitly authorized one-time local data repair, not an automatic migration or proof of legacy-data provenance. Legacy confidential credentials or tokens require separate security assessment, not this secret-free recovery procedure.

Local OAuth initiation compatibility and setup clarity

The default local callback is http://localhost:18010/api/oauth/downstream-mcp/callback. It is an inbound callback, not an outbound provider destination. Callback validation must be separate from the unchanged HTTPS/public-network destination rules. An immutable policy derived only from trusted local/dev simulation with test identity isolation may permit exact HTTP loopback callback authorities; production, real-provider and ineligible configurations remain HTTPS-only. Credentials, control characters, query/fragment tricks, encoded or alternate hosts and invalid ports remain rejected. Preparation, exchange and supported refresh must enforce the same policy and preserve the exact stored redirect URI. Dynamic registration supplies the stable application name Umbod and declares application_type=native for an already-validated local HTTP callback, otherwise web. This describes callback topology, not client confidentiality. A returned application type must match when present; older responses may omit it. No additional operator choice is introduced.

An omitted authorization_response_iss_parameter_supported advertisement is not proof of issuer-response support. Following explicit operator approval to support issuer-less callbacks, preparation uses the generic callback only when metadata advertises true; it otherwise uses an issuer-specific callback for an omitted or explicitly false advertisement. Null or mistyped advertisements remain rejected. The issuer-specific redirect is the trusted configured callback base followed by /issuer/ and the lowercase SHA-256 hex digest of the exact discovered issuer's UTF-8 bytes. It is finalized before registration and authorization, persisted encrypted with its expected issuer, and reused exactly during token exchange. Preconfigured applications must register this exact redirect with their provider when required; no client policy is silently changed.

A successful issuer-specific callback may omit iss only when its actual matched route agrees with the persisted binding. Any supplied issuer must be nonempty and exactly match; legacy/generic callbacks still require it. For newly prepared typed bindings, wrong-route, cross-issuer and generic-route downgrade callbacks must not consume the pending attempt. The provider independently rechecks the binding before any token POST. State, PKCE, resource, client, redirect, policy revision, expiry and one-shot exchange bindings remain mandatory. Old encrypted records remain legacy/generic rather than being silently promoted. They preserve the existing opaque-context comparison before token POST, so a mismatching legacy callback may consume that old pending attempt without exchanging a token; new generic and issuer-bound records have pre-claim mismatch protection. Initiation success is not end-to-end provider interoperability.

The setup choices identify Umbod's application, not a user's login method. The visible labels are Automatic registration, Use an existing client ID, and Use an existing client ID and secret; internal modes and required fields are unchanged. Known initiation 422 codes receive fixed, validated explanatory copy without reflecting provider data. Uncertain outcomes and reconciliation gates remain conservative because an unsupported response can occur after remote client registration. No automatic retry, discovery, refresh, credential fallback or tool replay is introduced.

Before issuer-specific callback compatibility: 3,830 backend tests passed with three skips, 663 frontend tests passed, Ruff passed, and Svelte retained zero errors and one existing warning. That disposable Kind run passed all three Helm test pods and all 33 runtime tests, including telemetry/secret checks, then removed its owned cluster. Independent security review found no production blockers or new production metric violations; the newly flagged test parameter bundles were corrected. Shared Semgrep retained 71 findings. Rebuilt Compose browser checks confirmed the plain setup labels on desktop/mobile, no mobile horizontal overflow, and fixed 422 copy with conservative locking followed by explicit status reconciliation; the 422 browser case used a controlled response override, not a provider failure. Console/page checks were clear and the mobile screenshot was verified.

Real-provider checks separately read Wispr Flow's protected-resource and authorization-server metadata, confirmed its advertised registration endpoint and public-client capabilities, and observed no issuer-support advertisement. A controlled native-loopback registration returned 201. The complete Umbod initiation endpoint then returned 201 with a durable pending attempt and HTTPS authorization link against Wispr Flow; that diagnostic attempt was explicitly cancelled afterward. No provider account login, consent, code exchange, token installation or tool discovery occurred during those checks. A subsequent operator-run Wispr Flow login reached the generic callback with code and state but no iss, confirming the original strict implementation's compatibility failure before exchange. The pending attempt then caused secondary 409 conflicts until expiry. This led to explicit approval of the issuer-specific alternative above; it is not evidence of a successful completed login with the new implementation.

Issuer-specific callback verification

The completed compatibility slice passed 3,882 backend tests with three skips, Ruff and diff whitespace checks. Shared Semgrep retained the prior 71 findings without new callback-model warnings. Independent review found no new blockers after documenting the legacy pending-attempt limitation and replacing incidental nullable issuer values with explicit missing/present variants.

The final disposable Kind image gate passed three Helm test pods and 35 running-service tests, including two new production-profile cases for omitted and false issuer-support advertisements. Controlled HTTPS provider callbacks without iss completed publicly without an Umbod token; generic-route downgrade, incorrect issuer route and supplied issuer mismatch preserved pending attempts and made zero token exchanges. Correct completion exchanged once, replay failed and explicit refresh worked. Telemetry and secret checks passed, and the owned cluster was removed. These controlled-provider assertions are not real-provider login evidence.

Rebuilt Compose services were healthy. One real Wispr Flow initiation returned 201 with a pending attempt, the expected authorization server and the exact issuer-specific local redirect; the diagnostic attempt was explicitly cancelled. Browser verification confirmed the enabled connection and available Sign in action, plus revised recovery guidance on a controlled invalid callback, with clear console/page checks and a verified screenshot at /tmp/umbod-issuer-bound-callback-error.png. No real user code, token exchange or provider account consent was replayed. At that verification point, completed Wispr Flow login remained unverified. The operator subsequently confirmed that the fresh issuer-bound authorization flow completed successfully; local authorization-health reads also confirmed a valid installed grant.

Server-level personal tool access correction

The operator explicitly replaced the earlier per-tool intersection model. A published allowlisted personal server, current connector-level group permission, owner enablement and valid owner authorization permit discovery and execution of all tools exposed to that owner. New tools become available after explicit rediscovery without administrator reapproval. Shared/native authorization is not changed.

Discovery must retain complete validated descriptors in an encrypted owner/server-scoped version-2 inventory, never a global user-derived catalogue. Authorization/configuration revisions, expiry, private inventory revision and schema fingerprints fence stale work. Capture the inventory revision before network I/O and commit with compare-and-swap after revalidating the current target; failed, cancelled or overlapping discovery must not destroy or overwrite a newer snapshot. Legacy match-only caches require explicit rediscovery, not reconstruction from global contracts. Legacy administrator tool-policy changes must not invalidate the new private snapshots.

Invocation still requires current owner/server admission, normal action confirmations, validated arguments/output and fresh same-session fingerprint preflight before one tool call. Personal namespace reservations prevent native/shared fallback even when the private inventory is missing or unavailable. Reuse pure SDK inventory mapping between shared and personal adapters without sharing credentials, sessions or global catalogue writes. Keep the personal transport's DNS pinning, verified TLS, bounds, no proxies/cookies/redirects/retries and no implicit refresh or execution.

The concrete reported failure was local: the real Wispr Flow owner authorization was valid, but its administrator tool policy was unconfigured with zero approved tools. The old discovery service rejected that condition before contacting the provider. The corrected flow does not require creating that policy. The UI describes whole-server trust and distinguishes a discovery failure from an OAuth-attempt conflict.

Verified rebuilt Compose discovery through the actual browser against Wispr Flow: the explicit Discover tools action completed and the connection displayed Tools available. A read-only encrypted-cache check confirmed 17 version-2 descriptors under the configured owner and zero global tool-catalogue rows. The existing grant, server configuration, enablement and group permission were preserved; no real provider tool was invoked. Desktop/mobile connection and administrator guidance were checked, with no horizontal overflow and clear console/page checks. Verified screenshots are /tmp/umbod-server-level-connections-desktop.png and /tmp/umbod-server-level-connections-mobile.png.

Final backend regression passed 3,997 tests with three skips; Ruff passed. Frontend regression passed 672 tests, and Svelte reported zero errors with one existing warning. Shared Semgrep retained 71 findings. Independent reviews verified private owner metadata delivery across sequential/concurrent SDK requests, gateway/codemode search and manifests, anonymous/no-inventory behavior, namespace collisions and current admission. Legacy metadata replacement now preserves tool-grant rows, activation records and private inventories; its compatible revoked_grants field remains empty. Unused match-only and private-name enumeration interfaces were removed.

The first Kind run passed 36 of 37 runtime tests and all three Helm tests, but correctly failed a new stale-schema assertion: its fixture changed only a description, which is intentionally excluded from the schema fingerprint. An installed-SDK reproduction confirmed registration replaced the description correctly. The test fixture was corrected to change and restore an actual input-schema constraint; independent in-memory mapping confirmed different changed fingerprints and an identical restored fingerprint. No production fingerprint check or denial assertion was weakened.

A subsequent Kind run exposed two obsolete initial-phase checks after the metadata-only correction: a test still expected activation/grant deletion, and its fixture silently retained the grant, preventing deletion and causing a later public-name conflict. The test now asserts preservation, and fixture teardown explicitly removes only its own retained capability grants before connector cleanup, asserting successful deletion. The final clean disposable Kind run passed all three Helm tests and all 37 runtime tests (32 initial plus five production-profile tests), including current server-group revocation, owner isolation, newly added tools, stale-schema denial/reconciliation, issuer-bound callbacks, explicit refresh, rejection and no-replay cases. Telemetry and all configured secret-log checks passed; the owned cluster was deleted. Evidence: /tmp/umbod-server-level-kind-verified.log.

Shared and personal tool-format alignment

Shared and personal discovery use the existing DiscoveredTool model. Both MCP materializers consume one descriptor projection, preserving downstream application metadata, title, description, schemas, annotations, icons, execution hints and framework tag interpretation. Public-name mangling and trusted invocation identity remain separate. Personal descriptors do not inject or overwrite downstream connector_id or operation_name metadata. An absent output schema remains distinct from a present empty schema.

Successful personal results preserve content, optional structured content and optional object-shaped _meta, matching shared proxy behavior. Metadata remains untrusted response data: it cannot select identity, permissions, credentials or execution behavior. Present malformed metadata is rejected; absent and empty metadata are accepted. Existing finite-JSON, token-reflection, byte-budget and structured-output validation continue before forwarding. Failures remain sanitized and metadata-free. Response-metadata preservation does not relax outbound request-metadata restrictions or change codemode's structured-content result contract.

Credentials, transport/session handling, owner-private encrypted inventories, authoritative admission, schema safeguards, OAuth and refresh stay separate and unchanged. No private descriptors or results enter a global catalogue or result cache.

Verified alignment with extensions to existing pipeline, failure-sanitization and result-contract tests rather than a new test suite: eight additional logical cases, twelve collected cases under existing persistence parametrization. Full backend regression passed 4,009 tests with three skips; Ruff passed and shared Semgrep remained at 71 findings. Independent review confirmed descriptor equivalence with the installed SDK proxy, typed content and metadata preservation, separate trusted identity and unchanged error sanitization. The rebuilt Compose stack retained the existing enabled/authorized/available connection without provider calls. Disposable Kind passed all 37 runtime tests and three Helm tests, telemetry and secret-log checks, with owned-cluster cleanup confirmed. Evidence: /tmp/umbod-format-alignment-kind.log. No real provider tool was invoked.

References

  • RFC 6749 sections 2.3.1, 3.3 and 5.1: client authentication and scope semantics.
  • RFC 8414 section 2: optional/incomplete scope advertisements and authentication defaults.
  • RFC 7591 sections 2 and 3.2.1: dynamic registration metadata and response validation.
  • RFC 9700 section 4.4.2: authorization-server mix-up defenses, including distinct redirect URIs.
  • RFC 9207: authorization response issuer identification.
  • MCP 2025-11-25 authorization: pre-registration, protected-resource discovery, S256 and resource indicators.