Skip to content

Add mcp-oauth-console: OAuth sign-in for MCP servers - #77

Open
Microbiosis wants to merge 4 commits into
MiniMax-AI:mainfrom
Microbiosis:add-mcp-oauth-console
Open

Microbiosis wants to merge 4 commits into
MiniMax-AI:mainfrom
Microbiosis:add-mcp-oauth-console

Conversation

@Microbiosis

@Microbiosis Microbiosis commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Adds mcp-oauth-console: sign in to MCP servers that require OAuth.

This branch also answers the review on #77 — the reply there lists the Must fix items.

What it does

Runs an OAuth sign-in for a server, keeps the token inside the Node process, writes the Authorization header into the MiniMax Code MCP configuration, and can refresh the token before it expires.

The page shows two independent facts per server: what the server demands (probed from the endpoint) and what this machine holds (the local credential). A server can require OAuth and still be satisfied by a local token, and one can need no OAuth and still have no token — a single status field hides one of the two.

Notes for the reviewer

  • Device code (RFC 8628) is the recommended way in. No redirect, so nothing depends on the browser reaching the Mini App. Authorization code + PKCE is the fallback: from MiniMax Code 3.1.2 the Host authenticates every request to the loopback port, so the callback page answers 401 and the authorization code has to be copied back from the address bar.
  • A 401 alone is not OAuth. Only a WWW-Authenticate: Bearer resource_metadata="…" challenge (RFC 9728) counts, so the page distinguishes requires OAuth / requires a Bearer credential / requires a credential, scheme undeclared / not required / undetermined.
  • The probe matches the declared transport: a real initialize call for streamable-http, a GET for sse.
  • Background refresh is an explicit switch, off by default. A write to mcp.json is surgical, atomic, backed up, and bound to the path the user confirmed.

Checks

npm run check: plugins/microbiosis/mcp-oauth-console: OK, repository: OK. CI green on 55be487.

Files read and written, network hosts, spawned processes, state, credentials, Agent turns and the tested environment are in the package README: README.md.

Package tests: node --test "tests/**/*.test.mjs" from the package directory.

@Microbiosis
Microbiosis force-pushed the add-mcp-oauth-console branch from 0fb4272 to f543139 Compare October 26, 2026 00:59

@MyPrototypeWhat MyPrototypeWhat left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at f543139.

Thanks for this. The dual status model, device-code preference when the Host owns the loopback port, keeping tokens off the page, and the surgical Authorization patch are all solid. npm run check reports the package OK on this head, and GitHub Actions is green. A few things need to change before merge.

Must fix

  1. miniapp/node/errors.mjs is imported but not in the package. Both the entry and the domain service import it (server.mjs:16 → describeServiceError / isKnownServiceError; oauth-service.mjs:21 → serviceError), and the client comments point at it too (app.js:36). The file is not among the 15 paths in this PR (confirmed with git ls-tree on f543139 and a direct import() of oauth-service.mjs, which fails with Cannot find module '.../errors.mjs'). scripts/validate.mjs only checks manifests, so it still prints OK. As shipped, start() cannot load. Please add the missing module (catalog of codes → HTTP status + user-facing message, plus serviceError / isKnownServiceError), or fold that catalog into an existing Node file and drop the dead imports.

  2. Credential paths must not follow .. in the server name. loadCredential / saveCredential build the path as join(credentialDir, \${serverName}.json`) (oauth-service.mjs:570, :593). path.joinresolves.., so a name like ../..//xlands outside this plugin'scontext.dataDir(and can touch another plugin's directory under the same parent). The authorize / device / apply routes takeserverName from the request body (server.mjs:212, :227, :262), including names that are not yet in the Host MCP config, so this is reachable from the page's "add server" flow. Please restrict names the same way other packages do (for example ^[A-Za-z0-9._-]+$, reject ./.., and/or resolve the path and refuse anything that is not still under credentialDir`).

  3. Host MCP config write: accepted, with conditions. We accept writing the Host MCP config. The README discloses which file is edited and how, and the backup / atomic / serialized write approach matches repo docs/security.md ("Writing outside dataDir"). Before merge, please meet these conditions:

    a. Only write the file the user confirmed, and realpath it. Discovery currently tries MINIMAX_MCP_CONFIG, parents of context.dataDir, cwd, the Node executable's directory, then the default Host location (resolveMcpConfigPath, oauth-service.mjs:154-176). The env var and default-location fallback are not a published contract, and paths are not realpath'd, so an unrelated mcp.json under cwd or next to the executable can win. Write only the resolved default file or one the user confirms in the UI, realpath'd before read and write.

    b. Hold the lock across the whole read-modify-write. The read / modify / verify in patchMcpAuthorization around :987 is not fully under the write lock, so another process (or a second apply) editing this file at the same time can lose entries. Take the lock before reading, re-read inside it, and release only after the write is verified.

    c. Background refresh must be opt-in or confirmed each time. Every 60s, refreshExpiring (server.mjs:300-309) can write credentials and patch the Host MCP config with no second confirm; the first apply asks the user, the timer does not. Make background refresh an explicit opt-in, or ask for confirmation on each write.

    d. Write credential files with mode 0o600. Tokens are stored in cleartext under the plugin's credential dir (save path around oauth-service.mjs:593). Set the mode explicitly on create and keep it on rewrite, so a looser umask cannot leave the grant world-readable. This applies on macOS/Linux; on Windows the mode is effectively ignored and nothing extra is needed.

    e. Back up mcp.json before every write, including background refresh writes. The plugin already backs up before writing (backupConfig into context.dataDir, oauth-service.mjs:509-511, README :73-74); please keep that. refreshServer (:919) goes through the same patchMcpAuthorization path, so refresh writes are backed up today too (:1004, :1045). Two gaps remain: backupConfig (:190-199) swallows errors and returns false, and the write still goes ahead, so a failed backup is silent; and every write overwrites the single mcp.json.bak, so a quiet refresh replaces the backup taken before the user's last confirmed apply. Please abort the write if the backup fails, and keep the pre-apply backup from being clobbered by refresh writes (for example a separate or timestamped backup).

  4. Outbound fetches must share one URL guard, including private networks. assertTargetUrl (oauth-service.mjs:316-333) runs on the MCP endpoint at authorize / device start, but the resource_metadata URL from WWW-Authenticate (:377) and the issuer / .well-known / token / device / registration endpoints taken from that metadata are fetched without the same check. The allowlist also lets RFC1918 and IPv4-mapped forms through (10/8, 192.168/16, 172.16/12, [::ffff:127.0.0.1]). Warm-up and /api/discover probe server.url from the Host config without assertTargetUrl either (server.mjs:186, :319). A hostile MCP can point metadata at an internal address (SSRF). Please run every fetch target through one checker that rejects private / link-local / metadata addresses (and, if you ever follow redirects, re-check after each hop).

  5. Callback page renders the error param as raw HTML. handleCallback takes error straight from the callback URL and concatenates it into the result message (`授权被拒绝:${error}`, oauth-service.mjs:847); renderCallbackPage then inserts that message unescaped into <p>${detail}</p> (server.mjs:416). Anyone who can get the user to open a crafted callback URL (or a hostile authorization server redirecting with a chosen error) can inject markup and script into a page served from the plugin's origin. Please HTML-escape detail (and title) before interpolating it (&, <, >, ", ').

Optional

  1. PKCE complete path treats a missing state as success. In completeAuthorization (oauth-service.mjs:798), if (state && state !== session.state) only fails when a state is present and wrong; a pasted callback URL with no state still exchanges the code. Prefer requiring an exact match whenever the session stored one.

  2. JSON key lookup is first-match substring of the key spelling. findMemberObjectSpan / findMemberStringSpan (oauth-service.mjs:111-142) use indexOf(JSON.stringify(key)), so a server name that appears as a key elsewhere in the file (or a nested "Authorization" outside the intended entry) can patch the wrong span — confirmed by running it. Scoping the search to mcpServers first, or parsing and re-serializing only that entry while accepting format churn, would be safer.

  3. Creating a brand-new server entry rewrites the whole file (:990-1007), which conflicts with the README claim that other bytes stay unchanged.

  4. Apply confirm is page-only. The confirm step lives in the page (app.js:838-881); POST /api/apply has no confirm token. Anything that can call that route can write the Host MCP config without the dialog.

  5. Browser open on non-Windows / non-macOS. openInBrowser (oauth-service.mjs:706-719) uses explorer.exe or open. On Linux neither is reliable; the page already shows the URL to copy, so a short README / UI note for "open failed — use the link" is enough.

  6. Manifest / catalog copy still describes only authorization-code + PKCE. .minimax-plugin/plugin.json:6 and the root README table row omit the device-code path that this head recommends. Aligning those one-liners with the package README would avoid underselling the preferred flow.

  7. GRANT_TTL_MS is defined but unused. Either wire it or drop it so the lifetime story matches the code.

  8. No plugin tests. There is no tests/ tree. Even a few unit cases around assertTargetUrl, safe server-name → credential path, and the Authorization patch (replace / insert headers / create entry) would have caught item 2 above and would help the next review.

Copy link
Copy Markdown
Collaborator

Update: please disregard condition d (0o600 on credential files); it's no longer required. Conditions a, b, c and e still apply. The other Must fix items still apply.

@Microbiosis
Microbiosis force-pushed the add-mcp-oauth-console branch from f543139 to 2674da1 Compare October 26, 2026 12:59
Microbiosis added a commit to Microbiosis/MiniMax-Code-MiniApps that referenced this pull request Oct 26, 2026
…tighten the guards

Must fix
- package miniapp/node/errors.mjs, which the entry imports but the package
  did not ship, so the Mini App could not start at all
- keep every error code the service can raise in one catalog; an unregistered
  code now fails fast instead of reaching the user
- restrict a server name to a safe set, so it can no longer walk out of the
  credential directory
- locate mcp.json only in the Host data-dir chain and the documented default
  location; the working directory and the Node executable's directory are no
  longer searched, and the resolved path is realpath'ed and shown in the page
- bind a write to the exact path the user confirmed
- run the whole read-modify-write under one lock and re-read inside it
- refuse a write when the backup failed, and keep the confirmed snapshot apart
  from the one a background refresh writes
- make the automatic refresh an explicit switch, off by default
- validate every outbound URL, including the IPv4-mapped, IPv4-compatible and
  NAT64 IPv6 forms; URLs a remote server handed back are always strict
- escape the callback page, and require the PKCE state to match exactly
- limit the JSON lookup to mcpServers, and insert a new entry textually

Optional
- bind a confirmed write to a one-time token
- report a browser that could not be opened, instead of claiming it opened
- drop the unused grant TTL
- add unit tests for the URL guard, the credential path, the surgical patch
  and the error catalog
@Microbiosis

Microbiosis commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor Author

All five Must fix items and the eight Optional ones are addressed on 86acdcb (rebased onto upstream/main 1ff1253). npm run check: mcp-oauth-console: OK, repository: OK. CI green.

Must fix

  1. miniapp/node/errors.mjs now ships — catalog of code → status + message, plus serviceError / isKnownServiceError / describeServiceError. tests/entry-loads.test.mjs imports the entry, the service and the catalog, so a module that is imported but not shipped fails the tests. The same file asserts every thrown code is registered and that no file throws a raw code.
  2. Server names cannot leave the credential directory — assertServerName allows only ^[A-Za-z0-9._-]{1,64}$ and rejects . / ..; credentialPath resolves and proves the path is still inside. tests/credential-path.test.mjs.
  3. Host config write.
    • (a) resolveMcpConfigPath uses only the data-dir chain and ~/.minimax/mcp.json; MINIMAX_MCP_CONFIG, cwd and the Node executable's directory are gone. The path is realpathed, shown in the page, and every write carries the confirmed expectedPath.
    • (b) The read-modify-verify-write runs under one lock and re-reads inside it.
    • (c) Background refresh is an explicit switch, off by default (<dataDir>/settings.json). The manual button always runs.
    • (d) Withdrawn per your follow-up — not implemented.
    • (e) A failed backup aborts the write. A confirmed write backs up to mcp.json.applied.bak, other writes to mcp.json.bak, so a refresh cannot replace the snapshot the user was shown.
  4. One URL guard for every outbound request — the probe, resource_metadata, .well-known, registration, token, device and the authorization endpoint. Private, link-local and metadata ranges are refused; IPv4-mapped, IPv4-compatible and NAT64 are resolved to IPv4 first. URLs a remote server returned are always strict; the user's own endpoint may be private when the switch is on. Redirects are followed by hand and every hop is re-checked, max 5 hops. tests/redirect-guard.test.mjs.
  5. The callback page escapes title and detail.

Package tests: node --test "tests/**/*.test.mjs" from the package directory.

Still 1.0.0 since it is not merged yet — say if you prefer 1.1.0. I did not touch the "Autofix with [code]smith" button.

Run the authorization-code + PKCE flow for MCP servers that require OAuth,
keep the token inside the Node process, write the Authorization header into
the MCP configuration, and refresh it before it expires.

The page separates two facts that are easy to conflate: what the server
demands, probed with a handshake that matches the declared transport and
only read as OAuth when the server sends a Bearer resource_metadata
challenge, and what this machine holds, the local credential.

Credentials live under context.dataDir. The MCP configuration is patched
surgically, written atomically, and backed up before every write.
From MiniMax Code 3.1.2 the Host authenticates every request to the
assigned loopback port, so the browser redirect of an authorization-code
flow answers 401 and the callback handler never runs. The device
authorization grant (RFC 8628) has no redirect at all, so it is not
affected; the page now offers it as the recommended method and keeps the
PKCE method with the callback URL copied back by hand.

Also rebase onto upstream main, document the Agent turns and the two sign-in
methods, and tag the row action buttons with data-action so they can be
targeted unambiguously.
…tighten the guards

Must fix
- package miniapp/node/errors.mjs, which the entry imports but the package
  did not ship, so the Mini App could not start at all
- keep every error code the service can raise in one catalog; an unregistered
  code now fails fast instead of reaching the user
- restrict a server name to a safe set, so it can no longer walk out of the
  credential directory
- locate mcp.json only in the Host data-dir chain and the documented default
  location; the working directory and the Node executable's directory are no
  longer searched, and the resolved path is realpath'ed and shown in the page
- bind a write to the exact path the user confirmed
- run the whole read-modify-write under one lock and re-read inside it
- refuse a write when the backup failed, and keep the confirmed snapshot apart
  from the one a background refresh writes
- make the automatic refresh an explicit switch, off by default
- validate every outbound URL, including the IPv4-mapped, IPv4-compatible and
  NAT64 IPv6 forms; URLs a remote server handed back are always strict
- escape the callback page, and require the PKCE state to match exactly
- limit the JSON lookup to mcpServers, and insert a new entry textually

Optional
- bind a confirmed write to a one-time token
- report a browser that could not be opened, instead of claiming it opened
- drop the unused grant TTL
- add unit tests for the URL guard, the credential path, the surgical patch
  and the error catalog
- follow redirects by hand and check every hop again with the same rule, so a
  302 cannot walk past the URL guard
- resolve IPv4-mapped, IPv4-compatible and NAT64 IPv6 addresses back to their
  IPv4 address before the private-range check
- let the configured MCP endpoint use a private address when the user opted
  into it, in the authorize and device paths as well as the probe
- check the authorization endpoint too, so the browser is never sent to an
  address a hostile server chose
- map an unregistered error code to the generic one before it reaches the
  page, and route the remaining raw throws through the catalog
- add tests for the redirect guard and for the error contract
@Microbiosis
Microbiosis force-pushed the add-mcp-oauth-console branch from 86acdcb to 55be487 Compare October 26, 2026 16:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants