fix: harden org-scoped MCP config chat gating, updates, and visibility (#28065)

Hardens the org-scoped MCP server config surface from #27942 with fixes
and regression pins that are independent of the core cutover:

- Keep chats sendable after a selected MCP server is disabled (persisted
selections are exempt from message-time rejection).
- Keep cached MCP selections usable after a background refetch error
(gate the composer on missing data, not `isSuccess`).
- Merge PATCH updates onto the current row so unset fields are not
clobbered.
- Give auditors the full management view of MCP configs their audit logs
reference (site and org auditor roles).
- Regression pins: frozen OAuth2 callback path, cross-org concealment
for item routes, OAuth callback token binding, and disconnect responses
indistinguishable from nonexistent config IDs.
- Storybook: semantic textbox queries in MCP loading stories; docs note
that the MCP settings page needs deployment access.

## Stack context

Part of the MCP org-separation stack (CODAGT-711 org scope -> apidocs ->
hardening -> CODAGT-717 audit -> CODAGT-712 ACLs -> CODAGT-806 token
RBAC). Split out of #27942 to keep the core cutover reviewable; each
change here builds on the org-scoped routes and chat gating introduced
below it.

> Mux (AI agent) authored this PR on Mike's behalf.

<!-- mux-attribution: model=claude-fable-5 thinking=high -->
This commit is contained in:
Michael Suchacz
2026-08-19 18:50:31 +00:00
committed by GitHub
parent dd43574990
commit f7a0de7a11
4 changed files with 157 additions and 12 deletions
+25 -6
View File
@@ -2146,16 +2146,30 @@ func TestMCPServerOAuth2PKCE(t *testing.T) {
require.NotEmpty(t, query.Get("code_challenge"),
"connect redirect must include a code_challenge")
// A verifier cookie must be set.
var verifierCookie *http.Cookie
// The callback path is frozen because it is registered as a
// redirect URI with external authorization servers.
frozenCallbackPath := "/api/experimental/mcp/servers/" + created.ID.String() + "/oauth2/callback"
redirectURI, err := url.Parse(query.Get("redirect_uri"))
require.NoError(t, err)
require.Equal(t, frozenCallbackPath, redirectURI.Path,
"outbound redirect_uri must use the frozen callback path")
var stateCookie, verifierCookie *http.Cookie
for _, c := range res.Cookies() {
if c.Name == "mcp_oauth2_verifier_"+created.ID.String() {
switch c.Name {
case "mcp_oauth2_state_" + created.ID.String():
stateCookie = c
case "mcp_oauth2_verifier_" + created.ID.String():
verifierCookie = c
break
}
}
require.NotNil(t, stateCookie, "response must set a state cookie")
require.Equal(t, frozenCallbackPath, stateCookie.Path,
"state cookie must be scoped to the frozen callback path")
require.NotNil(t, verifierCookie, "response must set a PKCE verifier cookie")
require.NotEmpty(t, verifierCookie.Value)
require.Equal(t, frozenCallbackPath, verifierCookie.Path,
"verifier cookie must be scoped to the frozen callback path")
// Verify the code_challenge matches SHA256(verifier).
h := sha256.Sum256([]byte(verifierCookie.Value))
@@ -2260,12 +2274,17 @@ func TestMCPServerOAuth2PKCE(t *testing.T) {
"token exchange must send the PKCE code_verifier")
// Verify the verifier cookie is cleared in the response.
var clearedVerifier *http.Cookie
for _, c := range res.Cookies() {
if c.Name == "mcp_oauth2_verifier_"+created.ID.String() {
require.Equal(t, -1, c.MaxAge,
"verifier cookie must be cleared after callback")
clearedVerifier = c
}
}
require.NotNil(t, clearedVerifier, "callback must clear the verifier cookie")
require.Equal(t, -1, clearedVerifier.MaxAge,
"verifier cookie must be cleared after callback")
require.Equal(t, callbackURL.Path, clearedVerifier.Path,
"cleared verifier cookie must be scoped to the frozen callback path")
})
t.Run("CallbackWithoutVerifierStillWorks", func(t *testing.T) {
+2 -4
View File
@@ -1159,8 +1159,7 @@ func OrgMemberPermissions(org OrgSettings) OrgRolePermissions {
ResourceOrganization.Type: {policy.ActionRead},
// Can read available roles.
ResourceAssignOrgRole.Type: {policy.ActionRead},
// TODO(mafredri): Remove once CODAGT-712 replaces this grant with
// per-config ACL evaluation.
// TODO(mafredri): remove once CODAGT-712 adds per-config ACL evaluation.
ResourceMCPServerConfig.Type: {policy.ActionRead},
}
@@ -1239,8 +1238,7 @@ func OrgServiceAccountPermissions(org OrgSettings) OrgRolePermissions {
ResourceOrganization.Type: {policy.ActionRead},
// Can read available roles.
ResourceAssignOrgRole.Type: {policy.ActionRead},
// TODO(mafredri): Remove once CODAGT-712 replaces this grant with
// per-config ACL evaluation.
// TODO(mafredri): remove once CODAGT-712 adds per-config ACL evaluation.
ResourceMCPServerConfig.Type: {policy.ActionRead},
}