diff --git a/.changeset/clear-toolset-user-session-issuer.md b/.changeset/clear-toolset-user-session-issuer.md
new file mode 100644
index 0000000000..331b78c2e1
--- /dev/null
+++ b/.changeset/clear-toolset-user-session-issuer.md
@@ -0,0 +1,5 @@
+---
+"server": minor
+---
+
+Add `toolsets.clearUserSessionIssuer` endpoint that unlinks any user_session_issuer attached to a toolset.
diff --git a/.changeset/remove-user-session-issuer-button.md b/.changeset/remove-user-session-issuer-button.md
new file mode 100644
index 0000000000..75e7ed4aa0
--- /dev/null
+++ b/.changeset/remove-user-session-issuer-button.md
@@ -0,0 +1,5 @@
+---
+"dashboard": patch
+---
+
+Add admin-only "Remove user session issuer" button on the MCP authentication tab that unlinks the toolset's user_session_issuer via `toolsets.clearUserSessionIssuer`.
diff --git a/.speakeasy/out.openapi.yaml b/.speakeasy/out.openapi.yaml
index 9f6a20fda0..ca99bf8182 100644
--- a/.speakeasy/out.openapi.yaml
+++ b/.speakeasy/out.openapi.yaml
@@ -23037,6 +23037,115 @@ paths:
x-speakeasy-name-override: checkMCPSlugAvailability
x-speakeasy-react-hook:
name: CheckMCPSlugAvailability
+ /rpc/toolsets.clearUserSessionIssuer:
+ post:
+ description: Unlink the user_session_issuer from a toolset. No-op if the toolset has no user_session_issuer linked.
+ operationId: clearToolsetUserSessionIssuer
+ parameters:
+ - allowEmptyValue: true
+ description: The slug of the toolset to unlink
+ in: query
+ name: slug
+ required: true
+ schema:
+ description: A short url-friendly label that uniquely identifies a resource.
+ maxLength: 40
+ pattern: ^[a-z0-9_-]{1,128}$
+ type: string
+ - allowEmptyValue: true
+ description: Session header
+ in: header
+ name: Gram-Session
+ schema:
+ description: Session header
+ type: string
+ - allowEmptyValue: true
+ description: API Key header
+ in: header
+ name: Gram-Key
+ schema:
+ description: API Key header
+ type: string
+ - allowEmptyValue: true
+ description: project header
+ in: header
+ name: Gram-Project
+ schema:
+ description: project header
+ type: string
+ responses:
+ "200":
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Toolset'
+ description: OK response.
+ "400":
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ description: 'bad_request: request is invalid'
+ "401":
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ description: 'unauthorized: unauthorized access'
+ "403":
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ description: 'forbidden: permission denied'
+ "404":
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ description: 'not_found: resource not found'
+ "409":
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ description: 'conflict: resource already exists'
+ "415":
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ description: 'unsupported_media: unsupported media type'
+ "422":
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ description: 'invalid: request contains one or more invalidation fields'
+ "500":
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ description: 'unexpected: an unexpected error occurred'
+ "502":
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ description: 'gateway_error: an unexpected error occurred'
+ security:
+ - project_slug_header_Gram-Project: []
+ session_header_Gram-Session: []
+ - apikey_header_Gram-Key: []
+ project_slug_header_Gram-Project: []
+ - {}
+ summary: clearUserSessionIssuer toolsets
+ tags:
+ - toolsets
+ x-speakeasy-name-override: clearUserSessionIssuer
+ x-speakeasy-react-hook:
+ name: ClearToolsetUserSessionIssuer
/rpc/toolsets.clone:
post:
description: Clone an existing toolset with a new name
diff --git a/.speakeasy/workflow.lock b/.speakeasy/workflow.lock
index ff601ca9f8..7c40e90e90 100644
--- a/.speakeasy/workflow.lock
+++ b/.speakeasy/workflow.lock
@@ -2,8 +2,8 @@ speakeasyVersion: 1.761.5
sources:
Gram-Internal:
sourceNamespace: gram-api-description
- sourceRevisionDigest: sha256:4f5d0ec1263cc5417617a7cee7786490592d78f7f02371310ef59fe4a224f01f
- sourceBlobDigest: sha256:16a89de83ab7c177db0d5f4bf86f6f3fbcad0a7b69c026c8bb9dc71377230dd2
+ sourceRevisionDigest: sha256:bfd22e8d0ed4339094ead9f6495eb2f7fdeb0517a7e4b083c828ded8ff22e6bf
+ sourceBlobDigest: sha256:60b1aa90d8bcb74cd2e5774774f484821daeb278ed1b0d58c25c2182715b1c06
tags:
- latest
- 0.0.1
@@ -11,10 +11,10 @@ targets:
gram-internal:
source: Gram-Internal
sourceNamespace: gram-api-description
- sourceRevisionDigest: sha256:4f5d0ec1263cc5417617a7cee7786490592d78f7f02371310ef59fe4a224f01f
- sourceBlobDigest: sha256:16a89de83ab7c177db0d5f4bf86f6f3fbcad0a7b69c026c8bb9dc71377230dd2
+ sourceRevisionDigest: sha256:bfd22e8d0ed4339094ead9f6495eb2f7fdeb0517a7e4b083c828ded8ff22e6bf
+ sourceBlobDigest: sha256:60b1aa90d8bcb74cd2e5774774f484821daeb278ed1b0d58c25c2182715b1c06
codeSamplesNamespace: gram-api-description-typescript-code-samples
- codeSamplesRevisionDigest: sha256:901f3d0135976214725cabf726813f14f2148dc9cf0938525f6216c163ced2bc
+ codeSamplesRevisionDigest: sha256:c6f49aecc7afa6086cc9a66bfe070e06208e7ae737183c6b9de68f18d178057f
workflow:
workflowVersion: 1.0.0
speakeasyVersion: pinned
diff --git a/cli/internal/api/toolsets.go b/cli/internal/api/toolsets.go
index 71675dc4a5..cab070e029 100644
--- a/cli/internal/api/toolsets.go
+++ b/cli/internal/api/toolsets.go
@@ -43,6 +43,7 @@ func NewToolsetsClient(options *ToolsetsClientOptions) *ToolsetsClient {
h.AddOAuthProxyServer(),
h.UpdateOAuthProxyServer(),
h.SetUserSessionIssuer(),
+ h.ClearUserSessionIssuer(),
)
return &ToolsetsClient{client: client}
diff --git a/client/dashboard/src/pages/mcp/MCPEnvironmentSettings.tsx b/client/dashboard/src/pages/mcp/MCPEnvironmentSettings.tsx
index 0f3740f540..374eb73e74 100644
--- a/client/dashboard/src/pages/mcp/MCPEnvironmentSettings.tsx
+++ b/client/dashboard/src/pages/mcp/MCPEnvironmentSettings.tsx
@@ -7,7 +7,7 @@ import {
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
-import { useSession } from "@/contexts/Auth";
+import { useIsAdmin, useSession } from "@/contexts/Auth";
import { useTelemetry } from "@/contexts/Telemetry";
import { useMissingRequiredEnvVars } from "@/hooks/useMissingEnvironmentVariables";
import { ONBOARD_EXTERNAL_MCP_TO_USER_SESSIONS_FLAG } from "@/lib/externalMcpUserSessions";
@@ -18,6 +18,7 @@ import {
invalidateAllGetMcpMetadata,
invalidateAllListEnvironments,
invalidateAllToolset,
+ useClearToolsetUserSessionIssuerMutation,
useCreateEnvironmentMutation,
useGetMcpMetadata,
useListEnvironments,
@@ -902,10 +903,23 @@ function OAuthSection({ toolset }: OAuthSectionProps) {
] = useState(false);
const telemetry = useTelemetry();
+ const isAdmin = useIsAdmin();
+ const queryClient = useQueryClient();
const { data: environmentsData } = useListEnvironments();
const environments = environmentsData?.environments ?? [];
+ const clearUserSessionIssuerMutation =
+ useClearToolsetUserSessionIssuerMutation({
+ onSuccess: () => {
+ invalidateAllToolset(queryClient);
+ toast.success("Removed user session issuer from toolset");
+ },
+ onError: (err) => {
+ toast.error(`Failed to remove user session issuer: ${err.message}`);
+ },
+ });
+
const loginSecured = !!toolset.userSessionIssuerSlug;
const isOAuthConnected = !!(
toolset?.oauthProxyServer || toolset?.externalOauthServer
@@ -973,6 +987,21 @@ function OAuthSection({ toolset }: OAuthSectionProps) {
Login Secured
)}
+ {userSessionIssuerWired && isAdmin && (
+
+ )}
{showWireUserSessionIssuer && (