feat: revoke MCP server OAuth grants at the provider on disconnect (#27300)

Closes
[CODAGT-805](https://linear.app/codercom/issue/CODAGT-805/revoke-oauth-grants-at-the-source-for-mcp-servers).

The experimental MCP server OAuth2 disconnect endpoint previously
deleted only the stored token row, leaving the grant active at the OAuth
provider. This PR adds provider-side token revocation while keeping
local disconnect independent of provider availability.

## Changes

- Add `mcp_server_configs.oauth2_revocation_url` in migration `000547`.
The value can be configured manually, discovered from RFC 8414 metadata,
and managed through the MCP server settings UI. Non-admin responses
redact it with the other OAuth2 fields.
- Revoke the refresh token first through the RFC 7009 endpoint, then
fall back to the access token only for `unsupported_token_type`. Public
clients send `client_id`; confidential clients use
`client_secret_basic`.
- Delete the local token transactionally before best-effort provider
revocation. Callers without a token receive the same response for hidden
and nonexistent config IDs, and provider failures return a generic
warning without exposing provider response bodies.
- Require HTTPS revocation endpoints except for HTTP loopback URLs.
Redirects must preserve the POST and remain on the configured origin.
Redirect errors omit provider-controlled paths and query strings so
reflected token material cannot enter logs.
- Treat `200 OK` and `204 No Content` as completed revocations. `202
Accepted` remains a failure because it does not confirm completion.
- Prevent an in-flight refresh from recreating a token deleted by
disconnect. Refresh persistence now uses an optimistic update keyed by
token ID and `updated_at`; only the OAuth callback can create a token
row. Refresh conflicts reload the current row or clear in-memory auth
when disconnect deleted it.
- Return `{token_revoked, token_revocation_error}` from disconnect,
while retaining SDK compatibility with the legacy `204` response. The UI
surfaces provider revocation failures as warning toasts.
- Document revocation endpoint discovery, HTTPS requirements, and
best-effort disconnect behavior.

No token or no configured revocation URL returns `token_revoked: false`
without an error, so disconnect remains idempotent.

> Updated by Mux, an AI coding agent, on Mike's behalf.
This commit is contained in:
Michael Suchacz
2026-07-20 00:14:03 +02:00
committed by GitHub
parent 33fcc9de68
commit 9f4ddea571
31 changed files with 1810 additions and 200 deletions
+7
View File
@@ -7599,6 +7599,13 @@ func (q *querier) UpdateMCPServerConfig(ctx context.Context, arg database.Update
return q.db.UpdateMCPServerConfig(ctx, arg)
}
func (q *querier) UpdateMCPServerUserTokenFromRefresh(ctx context.Context, arg database.UpdateMCPServerUserTokenFromRefreshParams) (database.MCPServerUserToken, error) {
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceDeploymentConfig); err != nil {
return database.MCPServerUserToken{}, err
}
return q.db.UpdateMCPServerUserTokenFromRefresh(ctx, arg)
}
func (q *querier) UpdateMemberRoles(ctx context.Context, arg database.UpdateMemberRolesParams) (database.OrganizationMember, error) {
// Authorized fetch will check that the actor has read access to the org member since the org member is returned.
member, err := database.ExpectOne(q.OrganizationMembers(ctx, database.OrganizationMembersParams{
+11
View File
@@ -1937,6 +1937,17 @@ func (s *MethodTestSuite) TestChats() {
dbm.EXPECT().UpdateMCPServerConfig(gomock.Any(), arg).Return(config, nil).AnyTimes()
check.Args(arg).Asserts(rbac.ResourceDeploymentConfig, policy.ActionUpdate).Returns(config)
}))
s.Run("UpdateMCPServerUserTokenFromRefresh", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
token := testutil.Fake(s.T(), faker, database.MCPServerUserToken{})
arg := database.UpdateMCPServerUserTokenFromRefreshParams{
ID: token.ID,
UpdatedAt: token.UpdatedAt,
AccessToken: "refreshed-access-token",
TokenType: "bearer",
}
dbm.EXPECT().UpdateMCPServerUserTokenFromRefresh(gomock.Any(), arg).Return(token, nil).AnyTimes()
check.Args(arg).Asserts(rbac.ResourceDeploymentConfig, policy.ActionUpdate).Returns(token)
}))
s.Run("UpsertMCPServerUserToken", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
arg := database.UpsertMCPServerUserTokenParams{
MCPServerConfigID: uuid.New(),
+1
View File
@@ -361,6 +361,7 @@ func MCPServerConfig(t testing.TB, db database.Store, seed database.MCPServerCon
OAuth2ClientSecretKeyID: seed.OAuth2ClientSecretKeyID,
OAuth2AuthURL: seed.OAuth2AuthURL,
OAuth2TokenURL: seed.OAuth2TokenURL,
OAuth2RevocationURL: seed.OAuth2RevocationURL,
OAuth2Scopes: seed.OAuth2Scopes,
APIKeyHeader: seed.APIKeyHeader,
APIKeyValue: seed.APIKeyValue,
+8
View File
@@ -5393,6 +5393,14 @@ func (m queryMetricsStore) UpdateMCPServerConfig(ctx context.Context, arg databa
return r0, r1
}
func (m queryMetricsStore) UpdateMCPServerUserTokenFromRefresh(ctx context.Context, arg database.UpdateMCPServerUserTokenFromRefreshParams) (database.MCPServerUserToken, error) {
start := time.Now()
r0, r1 := m.s.UpdateMCPServerUserTokenFromRefresh(ctx, arg)
m.queryLatencies.WithLabelValues("UpdateMCPServerUserTokenFromRefresh").Observe(time.Since(start).Seconds())
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "UpdateMCPServerUserTokenFromRefresh").Inc()
return r0, r1
}
func (m queryMetricsStore) UpdateMemberRoles(ctx context.Context, arg database.UpdateMemberRolesParams) (database.OrganizationMember, error) {
start := time.Now()
r0, r1 := m.s.UpdateMemberRoles(ctx, arg)
+15
View File
@@ -10159,6 +10159,21 @@ func (mr *MockStoreMockRecorder) UpdateMCPServerConfig(ctx, arg any) *gomock.Cal
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateMCPServerConfig", reflect.TypeOf((*MockStore)(nil).UpdateMCPServerConfig), ctx, arg)
}
// UpdateMCPServerUserTokenFromRefresh mocks base method.
func (m *MockStore) UpdateMCPServerUserTokenFromRefresh(ctx context.Context, arg database.UpdateMCPServerUserTokenFromRefreshParams) (database.MCPServerUserToken, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateMCPServerUserTokenFromRefresh", ctx, arg)
ret0, _ := ret[0].(database.MCPServerUserToken)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateMCPServerUserTokenFromRefresh indicates an expected call of UpdateMCPServerUserTokenFromRefresh.
func (mr *MockStoreMockRecorder) UpdateMCPServerUserTokenFromRefresh(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateMCPServerUserTokenFromRefresh", reflect.TypeOf((*MockStore)(nil).UpdateMCPServerUserTokenFromRefresh), ctx, arg)
}
// UpdateMemberRoles mocks base method.
func (m *MockStore) UpdateMemberRoles(ctx context.Context, arg database.UpdateMemberRolesParams) (database.OrganizationMember, error) {
m.ctrl.T.Helper()
+1
View File
@@ -2496,6 +2496,7 @@ CREATE TABLE mcp_server_configs (
model_intent boolean DEFAULT false NOT NULL,
allow_in_plan_mode boolean DEFAULT false NOT NULL,
forward_coder_headers boolean DEFAULT false NOT NULL,
oauth2_revocation_url text DEFAULT ''::text NOT NULL,
CONSTRAINT mcp_server_configs_auth_type_check CHECK ((auth_type = ANY (ARRAY['none'::text, 'oauth2'::text, 'api_key'::text, 'custom_headers'::text, 'user_oidc'::text]))),
CONSTRAINT mcp_server_configs_availability_check CHECK ((availability = ANY (ARRAY['force_on'::text, 'default_on'::text, 'default_off'::text]))),
CONSTRAINT mcp_server_configs_transport_check CHECK ((transport = ANY (ARRAY['streamable_http'::text, 'sse'::text])))
@@ -0,0 +1,2 @@
ALTER TABLE mcp_server_configs
DROP COLUMN oauth2_revocation_url;
@@ -0,0 +1,2 @@
ALTER TABLE mcp_server_configs
ADD COLUMN oauth2_revocation_url text NOT NULL DEFAULT '';
+1
View File
@@ -5437,6 +5437,7 @@ type MCPServerConfig struct {
ModelIntent bool `db:"model_intent" json:"model_intent"`
AllowInPlanMode bool `db:"allow_in_plan_mode" json:"allow_in_plan_mode"`
ForwardCoderHeaders bool `db:"forward_coder_headers" json:"forward_coder_headers"`
OAuth2RevocationURL string `db:"oauth2_revocation_url" json:"oauth2_revocation_url"`
}
type MCPServerUserToken struct {
+3
View File
@@ -1428,6 +1428,9 @@ type sqlcQuerier interface {
UpdateInactiveUsersToDormant(ctx context.Context, arg UpdateInactiveUsersToDormantParams) ([]UpdateInactiveUsersToDormantRow, error)
UpdateInboxNotificationReadStatus(ctx context.Context, arg UpdateInboxNotificationReadStatusParams) error
UpdateMCPServerConfig(ctx context.Context, arg UpdateMCPServerConfigParams) (MCPServerConfig, error)
// Refresh persistence must not recreate a token deleted by disconnect.
// The optimistic lock also prevents stale refreshes from replacing newer tokens.
UpdateMCPServerUserTokenFromRefresh(ctx context.Context, arg UpdateMCPServerUserTokenFromRefreshParams) (MCPServerUserToken, error)
UpdateMemberRoles(ctx context.Context, arg UpdateMemberRolesParams) (OrganizationMember, error)
UpdateMemoryResourceMonitor(ctx context.Context, arg UpdateMemoryResourceMonitorParams) error
UpdateNotificationTemplateMethodByID(ctx context.Context, arg UpdateNotificationTemplateMethodByIDParams) (NotificationTemplate, error)
+103 -28
View File
@@ -16693,7 +16693,7 @@ func (q *sqlQuerier) DeleteMCPServerUserToken(ctx context.Context, arg DeleteMCP
const getEnabledMCPServerConfigs = `-- name: GetEnabledMCPServerConfigs :many
SELECT
id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at, model_intent, allow_in_plan_mode, forward_coder_headers
id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at, model_intent, allow_in_plan_mode, forward_coder_headers, oauth2_revocation_url
FROM
mcp_server_configs
WHERE
@@ -16742,6 +16742,7 @@ func (q *sqlQuerier) GetEnabledMCPServerConfigs(ctx context.Context) ([]MCPServe
&i.ModelIntent,
&i.AllowInPlanMode,
&i.ForwardCoderHeaders,
&i.OAuth2RevocationURL,
); err != nil {
return nil, err
}
@@ -16758,7 +16759,7 @@ func (q *sqlQuerier) GetEnabledMCPServerConfigs(ctx context.Context) ([]MCPServe
const getForcedMCPServerConfigs = `-- name: GetForcedMCPServerConfigs :many
SELECT
id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at, model_intent, allow_in_plan_mode, forward_coder_headers
id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at, model_intent, allow_in_plan_mode, forward_coder_headers, oauth2_revocation_url
FROM
mcp_server_configs
WHERE
@@ -16808,6 +16809,7 @@ func (q *sqlQuerier) GetForcedMCPServerConfigs(ctx context.Context) ([]MCPServer
&i.ModelIntent,
&i.AllowInPlanMode,
&i.ForwardCoderHeaders,
&i.OAuth2RevocationURL,
); err != nil {
return nil, err
}
@@ -16824,7 +16826,7 @@ func (q *sqlQuerier) GetForcedMCPServerConfigs(ctx context.Context) ([]MCPServer
const getMCPServerConfigByID = `-- name: GetMCPServerConfigByID :one
SELECT
id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at, model_intent, allow_in_plan_mode, forward_coder_headers
id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at, model_intent, allow_in_plan_mode, forward_coder_headers, oauth2_revocation_url
FROM
mcp_server_configs
WHERE
@@ -16865,13 +16867,14 @@ func (q *sqlQuerier) GetMCPServerConfigByID(ctx context.Context, id uuid.UUID) (
&i.ModelIntent,
&i.AllowInPlanMode,
&i.ForwardCoderHeaders,
&i.OAuth2RevocationURL,
)
return i, err
}
const getMCPServerConfigBySlug = `-- name: GetMCPServerConfigBySlug :one
SELECT
id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at, model_intent, allow_in_plan_mode, forward_coder_headers
id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at, model_intent, allow_in_plan_mode, forward_coder_headers, oauth2_revocation_url
FROM
mcp_server_configs
WHERE
@@ -16912,13 +16915,14 @@ func (q *sqlQuerier) GetMCPServerConfigBySlug(ctx context.Context, slug string)
&i.ModelIntent,
&i.AllowInPlanMode,
&i.ForwardCoderHeaders,
&i.OAuth2RevocationURL,
)
return i, err
}
const getMCPServerConfigs = `-- name: GetMCPServerConfigs :many
SELECT
id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at, model_intent, allow_in_plan_mode, forward_coder_headers
id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at, model_intent, allow_in_plan_mode, forward_coder_headers, oauth2_revocation_url
FROM
mcp_server_configs
ORDER BY
@@ -16965,6 +16969,7 @@ func (q *sqlQuerier) GetMCPServerConfigs(ctx context.Context) ([]MCPServerConfig
&i.ModelIntent,
&i.AllowInPlanMode,
&i.ForwardCoderHeaders,
&i.OAuth2RevocationURL,
); err != nil {
return nil, err
}
@@ -16981,7 +16986,7 @@ func (q *sqlQuerier) GetMCPServerConfigs(ctx context.Context) ([]MCPServerConfig
const getMCPServerConfigsByIDs = `-- name: GetMCPServerConfigsByIDs :many
SELECT
id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at, model_intent, allow_in_plan_mode, forward_coder_headers
id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at, model_intent, allow_in_plan_mode, forward_coder_headers, oauth2_revocation_url
FROM
mcp_server_configs
WHERE
@@ -17030,6 +17035,7 @@ func (q *sqlQuerier) GetMCPServerConfigsByIDs(ctx context.Context, ids []uuid.UU
&i.ModelIntent,
&i.AllowInPlanMode,
&i.ForwardCoderHeaders,
&i.OAuth2RevocationURL,
); err != nil {
return nil, err
}
@@ -17138,6 +17144,7 @@ INSERT INTO mcp_server_configs (
oauth2_client_secret_key_id,
oauth2_auth_url,
oauth2_token_url,
oauth2_revocation_url,
oauth2_scopes,
api_key_header,
api_key_value,
@@ -17172,18 +17179,19 @@ INSERT INTO mcp_server_configs (
$16::text,
$17::text,
$18::text,
$19::text[],
$19::text,
$20::text[],
$21::text,
$22::boolean,
$21::text[],
$22::text,
$23::boolean,
$24::boolean,
$25::boolean,
$26::uuid,
$27::uuid
$26::boolean,
$27::uuid,
$28::uuid
)
RETURNING
id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at, model_intent, allow_in_plan_mode, forward_coder_headers
id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at, model_intent, allow_in_plan_mode, forward_coder_headers, oauth2_revocation_url
`
type InsertMCPServerConfigParams struct {
@@ -17199,6 +17207,7 @@ type InsertMCPServerConfigParams struct {
OAuth2ClientSecretKeyID sql.NullString `db:"oauth2_client_secret_key_id" json:"oauth2_client_secret_key_id"`
OAuth2AuthURL string `db:"oauth2_auth_url" json:"oauth2_auth_url"`
OAuth2TokenURL string `db:"oauth2_token_url" json:"oauth2_token_url"`
OAuth2RevocationURL string `db:"oauth2_revocation_url" json:"oauth2_revocation_url"`
OAuth2Scopes string `db:"oauth2_scopes" json:"oauth2_scopes"`
APIKeyHeader string `db:"api_key_header" json:"api_key_header"`
APIKeyValue string `db:"api_key_value" json:"api_key_value"`
@@ -17230,6 +17239,7 @@ func (q *sqlQuerier) InsertMCPServerConfig(ctx context.Context, arg InsertMCPSer
arg.OAuth2ClientSecretKeyID,
arg.OAuth2AuthURL,
arg.OAuth2TokenURL,
arg.OAuth2RevocationURL,
arg.OAuth2Scopes,
arg.APIKeyHeader,
arg.APIKeyValue,
@@ -17278,6 +17288,7 @@ func (q *sqlQuerier) InsertMCPServerConfig(ctx context.Context, arg InsertMCPSer
&i.ModelIntent,
&i.AllowInPlanMode,
&i.ForwardCoderHeaders,
&i.OAuth2RevocationURL,
)
return i, err
}
@@ -17346,25 +17357,26 @@ SET
oauth2_client_secret_key_id = $10::text,
oauth2_auth_url = $11::text,
oauth2_token_url = $12::text,
oauth2_scopes = $13::text,
api_key_header = $14::text,
api_key_value = $15::text,
api_key_value_key_id = $16::text,
custom_headers = $17::text,
custom_headers_key_id = $18::text,
tool_allow_list = $19::text[],
tool_deny_list = $20::text[],
availability = $21::text,
enabled = $22::boolean,
model_intent = $23::boolean,
allow_in_plan_mode = $24::boolean,
forward_coder_headers = $25::boolean,
updated_by = $26::uuid,
oauth2_revocation_url = $13::text,
oauth2_scopes = $14::text,
api_key_header = $15::text,
api_key_value = $16::text,
api_key_value_key_id = $17::text,
custom_headers = $18::text,
custom_headers_key_id = $19::text,
tool_allow_list = $20::text[],
tool_deny_list = $21::text[],
availability = $22::text,
enabled = $23::boolean,
model_intent = $24::boolean,
allow_in_plan_mode = $25::boolean,
forward_coder_headers = $26::boolean,
updated_by = $27::uuid,
updated_at = NOW()
WHERE
id = $27::uuid
id = $28::uuid
RETURNING
id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at, model_intent, allow_in_plan_mode, forward_coder_headers
id, display_name, slug, description, icon_url, transport, url, auth_type, oauth2_client_id, oauth2_client_secret, oauth2_client_secret_key_id, oauth2_auth_url, oauth2_token_url, oauth2_scopes, api_key_header, api_key_value, api_key_value_key_id, custom_headers, custom_headers_key_id, tool_allow_list, tool_deny_list, availability, enabled, created_by, updated_by, created_at, updated_at, model_intent, allow_in_plan_mode, forward_coder_headers, oauth2_revocation_url
`
type UpdateMCPServerConfigParams struct {
@@ -17380,6 +17392,7 @@ type UpdateMCPServerConfigParams struct {
OAuth2ClientSecretKeyID sql.NullString `db:"oauth2_client_secret_key_id" json:"oauth2_client_secret_key_id"`
OAuth2AuthURL string `db:"oauth2_auth_url" json:"oauth2_auth_url"`
OAuth2TokenURL string `db:"oauth2_token_url" json:"oauth2_token_url"`
OAuth2RevocationURL string `db:"oauth2_revocation_url" json:"oauth2_revocation_url"`
OAuth2Scopes string `db:"oauth2_scopes" json:"oauth2_scopes"`
APIKeyHeader string `db:"api_key_header" json:"api_key_header"`
APIKeyValue string `db:"api_key_value" json:"api_key_value"`
@@ -17411,6 +17424,7 @@ func (q *sqlQuerier) UpdateMCPServerConfig(ctx context.Context, arg UpdateMCPSer
arg.OAuth2ClientSecretKeyID,
arg.OAuth2AuthURL,
arg.OAuth2TokenURL,
arg.OAuth2RevocationURL,
arg.OAuth2Scopes,
arg.APIKeyHeader,
arg.APIKeyValue,
@@ -17459,6 +17473,67 @@ func (q *sqlQuerier) UpdateMCPServerConfig(ctx context.Context, arg UpdateMCPSer
&i.ModelIntent,
&i.AllowInPlanMode,
&i.ForwardCoderHeaders,
&i.OAuth2RevocationURL,
)
return i, err
}
const updateMCPServerUserTokenFromRefresh = `-- name: UpdateMCPServerUserTokenFromRefresh :one
UPDATE mcp_server_user_tokens
SET
access_token = $1::text,
access_token_key_id = $2::text,
refresh_token = $3::text,
refresh_token_key_id = $4::text,
token_type = $5::text,
expiry = $6::timestamptz,
oauth_refresh_failure_reason = '',
updated_at = NOW()
WHERE
id = $7::uuid
AND updated_at = $8::timestamptz
RETURNING
id, mcp_server_config_id, user_id, access_token, access_token_key_id, refresh_token, refresh_token_key_id, token_type, expiry, created_at, updated_at, oauth_refresh_failure_reason
`
type UpdateMCPServerUserTokenFromRefreshParams struct {
AccessToken string `db:"access_token" json:"access_token"`
AccessTokenKeyID sql.NullString `db:"access_token_key_id" json:"access_token_key_id"`
RefreshToken string `db:"refresh_token" json:"refresh_token"`
RefreshTokenKeyID sql.NullString `db:"refresh_token_key_id" json:"refresh_token_key_id"`
TokenType string `db:"token_type" json:"token_type"`
Expiry sql.NullTime `db:"expiry" json:"expiry"`
ID uuid.UUID `db:"id" json:"id"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}
// Refresh persistence must not recreate a token deleted by disconnect.
// The optimistic lock also prevents stale refreshes from replacing newer tokens.
func (q *sqlQuerier) UpdateMCPServerUserTokenFromRefresh(ctx context.Context, arg UpdateMCPServerUserTokenFromRefreshParams) (MCPServerUserToken, error) {
row := q.db.QueryRowContext(ctx, updateMCPServerUserTokenFromRefresh,
arg.AccessToken,
arg.AccessTokenKeyID,
arg.RefreshToken,
arg.RefreshTokenKeyID,
arg.TokenType,
arg.Expiry,
arg.ID,
arg.UpdatedAt,
)
var i MCPServerUserToken
err := row.Scan(
&i.ID,
&i.MCPServerConfigID,
&i.UserID,
&i.AccessToken,
&i.AccessTokenKeyID,
&i.RefreshToken,
&i.RefreshTokenKeyID,
&i.TokenType,
&i.Expiry,
&i.CreatedAt,
&i.UpdatedAt,
&i.OauthRefreshFailureReason,
)
return i, err
}
@@ -67,6 +67,7 @@ INSERT INTO mcp_server_configs (
oauth2_client_secret_key_id,
oauth2_auth_url,
oauth2_token_url,
oauth2_revocation_url,
oauth2_scopes,
api_key_header,
api_key_value,
@@ -95,6 +96,7 @@ INSERT INTO mcp_server_configs (
sqlc.narg('oauth2_client_secret_key_id')::text,
@oauth2_auth_url::text,
@oauth2_token_url::text,
@oauth2_revocation_url::text,
@oauth2_scopes::text,
@api_key_header::text,
@api_key_value::text,
@@ -130,6 +132,7 @@ SET
oauth2_client_secret_key_id = sqlc.narg('oauth2_client_secret_key_id')::text,
oauth2_auth_url = @oauth2_auth_url::text,
oauth2_token_url = @oauth2_token_url::text,
oauth2_revocation_url = @oauth2_revocation_url::text,
oauth2_scopes = @oauth2_scopes::text,
api_key_header = @api_key_header::text,
api_key_value = @api_key_value::text,
@@ -207,6 +210,25 @@ ON CONFLICT (mcp_server_config_id, user_id) DO UPDATE SET
RETURNING
*;
-- name: UpdateMCPServerUserTokenFromRefresh :one
-- Refresh persistence must not recreate a token deleted by disconnect.
-- The optimistic lock also prevents stale refreshes from replacing newer tokens.
UPDATE mcp_server_user_tokens
SET
access_token = @access_token::text,
access_token_key_id = sqlc.narg('access_token_key_id')::text,
refresh_token = @refresh_token::text,
refresh_token_key_id = sqlc.narg('refresh_token_key_id')::text,
token_type = @token_type::text,
expiry = sqlc.narg('expiry')::timestamptz,
oauth_refresh_failure_reason = '',
updated_at = NOW()
WHERE
id = @id::uuid
AND updated_at = @updated_at::timestamptz
RETURNING
*;
-- name: MarkMCPServerUserTokenRefreshFailure :one
-- Records a permanent refresh failure (e.g. revoked grant) and clears
-- the dead token material so it is never attached to a request again.
+1
View File
@@ -288,6 +288,7 @@ sql:
oauth2_client_secret_key_id: OAuth2ClientSecretKeyID
oauth2_auth_url: OAuth2AuthURL
oauth2_token_url: OAuth2TokenURL
oauth2_revocation_url: OAuth2RevocationURL
oauth2_scopes: OAuth2Scopes
api_key_header: APIKeyHeader
api_key_value: APIKeyValue