Skip to content

feat(mcp): support pre-registered OAuth credentials for non-DCR providers - #5614

Open
adhyaay-karnwal wants to merge 1 commit into
macro-inc:mainfrom
adhyaay-karnwal:fix/mcp-non-dcr-providers
Open

feat(mcp): support pre-registered OAuth credentials for non-DCR providers#5614
adhyaay-karnwal wants to merge 1 commit into
macro-inc:mainfrom
adhyaay-karnwal:fix/mcp-non-dcr-providers

Conversation

@adhyaay-karnwal

Copy link
Copy Markdown

Summary

Fixes #5598 — MCP servers whose authorization server doesn't support Dynamic Client Registration (e.g. HubSpot) previously couldn't be connected. The OAuth flow always attempted DCR, which has no registration_endpoint to POST to, so authorization failed before the user ever reached a consent screen.

This adds optional per-server OAuth credentials (client ID + client secret) so any non-DCR provider is connectable without vendor-specific code or hosted env vars.

What changed

Backend (mcp_client)

  • McpServerRecord gains client_id / client_secret fields; the secret is #[serde(skip)] and never leaves the service.

  • PgServerRepo stores the client id as plaintext and encrypts the client secret with AES-256-GCM at rest (the same scheme used for the OAuth grant), via a new migration adding mcp_servers.client_id / mcp_servers.client_secret.

  • OAuthService consults per-server credentials before the provider registry and DCR:

    per-server credentials → PreRegisteredProviders (Slack/GitHub) → DCR
    

    A client secret supplied without a client id is rejected with a 400.

  • add_server / update_server accept client_id / client_secret; re-adding a server preserves stored credentials, and an empty string clears them. ServerResponse exposes client_id and a has_client_secret boolean (never the secret).

Frontend (apps/web)

  • The Add server dialog now has an expandable "OAuth credentials (optional)" section with a client ID field and a masked client secret input (with reveal toggle).
  • Unconnected servers get a configure action to set/edit credentials before retrying the connection.

Generated artifacts

  • Regenerated the orval (apps/web) and openapi-ts (packages/sdk) clients from the updated OpenAPI spec, and refreshed the .sqlx offline query cache.

Testing

  • cargo test -p mcp_client — 23 passed (includes new tests: pre-registered credentials skip DCR; credential normalization/validation; response mapping).
  • cargo test -p onboarding — 6 passed.
  • cargo clippy -p mcp_client --all-features — clean.
  • Full workspace cargo check --all-features (via cargo sqlx prepare --workspace) — clean.
  • tsc type-check and biome — clean.

…ders

MCP servers whose authorization server doesn't support Dynamic Client Registration (e.g. HubSpot) couldn't be connected: the OAuth flow always attempted DCR, which has no registration_endpoint to POST to.

Let users supply an optional client_id/client_secret on the Add-server dialog (and edit them via a new configure dialog), persist the secret encrypted on the server record, and consult them before falling back to DCR. Fixes every non-DCR provider without vendor-specific code.
@adhyaay-karnwal
adhyaay-karnwal requested a review from a team as a code owner August 12, 2026 19:38
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Added support for configuring optional OAuth client ID and secret when adding MCP servers.
    • Added server configuration controls for updating credentials after setup.
    • Added show/hide controls for client secrets and status indicators for stored credentials.
    • Pre-registered credentials are used during OAuth authorization when available.
  • Bug Fixes
    • Added validation for incomplete or invalid credential combinations.
    • Preserved existing credentials during server updates when no replacement values are provided.
    • Client secrets are securely stored and never exposed in responses.

Walkthrough

MCP servers now support optional pre-registered OAuth client IDs and secrets. The database stores encrypted secrets, and APIs validate, update, clear, and report credential metadata. OAuth authorization uses stored credentials before provider or dynamic registration and preserves them during token exchange. The settings interface adds credential entry, secret visibility controls, and configuration dialogs. Tests cover validation, response mapping, persistence behavior, and OAuth client selection.

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title uses conventional commit format and describes the change, but it is 73 characters and exceeds the 72-character limit. Shorten the title to 72 characters or fewer while preserving its meaning.
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The description clearly explains the OAuth credential support, backend persistence, frontend changes, and testing related to this pull request.
Linked Issues check ✅ Passed The changes satisfy issue #5598 by supporting per-server credentials, secure secret persistence, validation, and credential use before DCR.
Out of Scope Changes check ✅ Passed The reviewed changes are directly related to adding per-server OAuth credentials for non-DCR MCP providers and supporting generated schemas and migrations.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/src/features/settings/Integrations.tsx`:
- Around line 119-131: Update handleSubmit and the Add control in Integrations
so a non-empty clientSecretValue without a clientIdValue cannot submit or call
addMutation.mutate. Return a form error or disable Add for this invalid pair,
while preserving both credential fields when clientIdValue is present and
providing feedback instead of silently discarding the secret.
- Around line 314-326: Update the Integrations save flow around handleSave to
track explicit clearing of an existing stored secret, adding a “Clear stored
secret” control near the client-secret input. When selected, include
client_secret as an empty string in updateMutation.mutate even when the client
ID is retained; otherwise preserve the current omission behavior for untouched
empty input.

In `@crates/mcp_client/src/inbound/axum_router.rs`:
- Around line 272-274: Update the API annotations for both add_server and
update_server to declare the HTTP 400 response used for OAuthRejected,
MalformedCallback, and InvalidCredentials errors, then regenerate the API
specifications so POST /mcp/servers and PUT /mcp/servers expose the validation
response.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 85633a28-2be9-4cef-997f-553bf9ca13ac

📥 Commits

Reviewing files that changed from the base of the PR and between 46a0106 and c2ad64d.

⛔ Files ignored due to path filters (14)
  • .sqlx/query-69895e1fdfef3998c26e8b62496504f96dc5d617be95ef79dd541acca264ddb4.json is excluded by !**/.sqlx/**
  • .sqlx/query-94c66b7510d0a9f791b7a00521ae9975e965670662132acfc60e18667c53370d.json is excluded by !**/.sqlx/**
  • .sqlx/query-aaa8d664de60cdcea278ee2e013ef2270b53cbc0c7b13ff2bae7617ff201ee2d.json is excluded by !**/.sqlx/**
  • .sqlx/query-d90855304ae7f4a8ca06018d4dfd88b138c1b95d46a31c63b9e93ac2198b037c.json is excluded by !**/.sqlx/**
  • apps/web/src/lib/service-clients/service-cognition/generated/schemas/addServerRequest.ts is excluded by !**/generated/**, !apps/web/src/lib/service-clients/**/generated/**
  • apps/web/src/lib/service-clients/service-cognition/generated/schemas/addServerRequestClientId.ts is excluded by !**/generated/**, !apps/web/src/lib/service-clients/**/generated/**
  • apps/web/src/lib/service-clients/service-cognition/generated/schemas/addServerRequestClientSecret.ts is excluded by !**/generated/**, !apps/web/src/lib/service-clients/**/generated/**
  • apps/web/src/lib/service-clients/service-cognition/generated/schemas/index.ts is excluded by !**/generated/**, !apps/web/src/lib/service-clients/**/generated/**
  • apps/web/src/lib/service-clients/service-cognition/generated/schemas/serverResponse.ts is excluded by !**/generated/**, !apps/web/src/lib/service-clients/**/generated/**
  • apps/web/src/lib/service-clients/service-cognition/generated/schemas/serverResponseClientId.ts is excluded by !**/generated/**, !apps/web/src/lib/service-clients/**/generated/**
  • apps/web/src/lib/service-clients/service-cognition/generated/schemas/updateServerRequest.ts is excluded by !**/generated/**, !apps/web/src/lib/service-clients/**/generated/**
  • apps/web/src/lib/service-clients/service-cognition/generated/schemas/updateServerRequestClientId.ts is excluded by !**/generated/**, !apps/web/src/lib/service-clients/**/generated/**
  • apps/web/src/lib/service-clients/service-cognition/generated/schemas/updateServerRequestClientSecret.ts is excluded by !**/generated/**, !apps/web/src/lib/service-clients/**/generated/**
  • packages/sdk/generated/cognition/types.gen.ts is excluded by !**/generated/**, !**/*.gen.ts
📒 Files selected for processing (13)
  • apps/web/src/features/settings/Integrations.tsx
  • apps/web/src/lib/service-clients/service-cognition/openapi.json
  • crates/macro_db_client/migrations/20260812191214_add_mcp_servers_oauth_credentials.down.sql
  • crates/macro_db_client/migrations/20260812191214_add_mcp_servers_oauth_credentials.sql
  • crates/mcp_client/src/domain/models/server.rs
  • crates/mcp_client/src/domain/service/persisting_credential_store/test.rs
  • crates/mcp_client/src/inbound/axum_router.rs
  • crates/mcp_client/src/inbound/axum_router/test.rs
  • crates/mcp_client/src/outbound/oauth.rs
  • crates/mcp_client/src/outbound/oauth/test.rs
  • crates/mcp_client/src/outbound/pg_server_repo.rs
  • crates/onboarding/src/domain/service/test.rs
  • packages/sdk/specs/cognition.json

Comment on lines 119 to +131
addMutation.mutate(
{ server_name: n, url: u },
{
server_name: n,
url: u,
...(clientIdValue
? {
client_id: clientIdValue,
...(clientSecretValue
? { client_secret: clientSecretValue }
: {}),
}
: {}),
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Block submission when a client secret has no client ID.

The UI displays an error for this state, but handleSubmit silently omits clientSecretValue when clientIdValue is empty. It then adds the server and starts authorization.

Disable Add for this invalid credential pair, or return a form error before the mutation. Do not discard entered secret data without feedback.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/features/settings/Integrations.tsx` around lines 119 - 131,
Update handleSubmit and the Add control in Integrations so a non-empty
clientSecretValue without a clientIdValue cannot submit or call
addMutation.mutate. Return a form error or disable Add for this invalid pair,
while preserving both credential fields when clientIdValue is present and
providing feedback instead of silently discarding the secret.

Comment on lines +314 to +326
const handleSave = () => {
const cid = clientId().trim();
const csecret = clientSecret().trim();
// Clearing the id also clears the stored secret, so we never leave a
// secret behind without an id (which the backend rejects).
const clearedId = cid === '' && (props.server.client_id ?? '') !== '';

updateMutation.mutate(
{
url: props.server.url,
client_id: cid,
...(csecret !== '' || clearedId ? { client_secret: csecret } : {}),
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Add an explicit stored-secret clear action.

clientSecret() starts empty when a secret already exists. Line 325 omits client_secret for an empty value, so Save preserves the existing secret. A user cannot remove the stored secret while retaining the client ID.

Add explicit clear state, such as a “Clear stored secret” control. Send client_secret: '' when that control is selected.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/features/settings/Integrations.tsx` around lines 314 - 326,
Update the Integrations save flow around handleSave to track explicit clearing
of an existing stored secret, adding a “Clear stored secret” control near the
client-secret input. When selected, include client_secret as an empty string in
updateMutation.mutate even when the client ID is retained; otherwise preserve
the current omission behavior for untouched empty input.

Comment on lines +272 to +274
McpHandlerErr::OAuthRejected(_)
| McpHandlerErr::MalformedCallback
| McpHandlerErr::InvalidCredentials(_) => StatusCode::BAD_REQUEST,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Document the HTTP 400 validation response.

Lines 272-274 make add_server and update_server return HTTP 400 when credential validation fails. The generated specifications do not declare HTTP 400 for POST /mcp/servers or PUT /mcp/servers.

Add the error response to both handler API annotations and regenerate the specifications. This keeps generated clients and API consumers aligned with runtime behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/mcp_client/src/inbound/axum_router.rs` around lines 272 - 274, Update
the API annotations for both add_server and update_server to declare the HTTP
400 response used for OAuthRejected, MalformedCallback, and InvalidCredentials
errors, then regenerate the API specifications so POST /mcp/servers and PUT
/mcp/servers expose the validation response.


impl PgServerRepo {
#[tracing::instrument(skip_all, err)]
#[expect(clippy::too_many_arguments, reason = "maps a DB row into a record")]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: prefer creating a CreateRecordArgs struct that will contain all db arguments to create the row.

Comment on lines +225 to +230
let client_secret = client_secret
.map(|b| {
let plaintext = self.decrypt_bytes(&b)?;
String::from_utf8(plaintext).map_err(|e| sqlx::Error::Decode(Box::new(e)))
})
.transpose()?;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please decode client_secret the same way we do for credentials

let credentials = credentials.map(|c| self.decrypt(&c)).transpose()?;

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.

fix(mcp): cannot connect MCP servers without Dynamic Client Registration (e.g. HubSpot)

2 participants