Skip to content

Commit 3823536

Browse files
jblzclaude
andcommitted
feat(client): add config toggle to opt out of OAuth revoke on Disconnect
The Inspector is a testing tool, so users may legitimately want to exercise the inverse scenario: how does an authorization server behave when a client disconnects without revoking? Mirror the existing pattern used for MCP_REQUEST_TIMEOUT_RESET_ON_PROGRESS — boolean ConfigItem, default to the spec-compliant value (`true`), expose via the Settings panel (auto-rendered for any ConfigItem). The local clear() still runs when revocation is disabled, so users still get a clean slate in the Inspector even when opting out of the remote revoke. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 7416af0 commit 3823536

5 files changed

Lines changed: 66 additions & 4 deletions

File tree

client/src/lib/configurationTypes.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,12 @@ export type InspectorConfig = {
4545
* Default Time-to-Live (TTL) in milliseconds for newly created tasks.
4646
*/
4747
MCP_TASK_TTL: ConfigItem;
48+
49+
/**
50+
* Whether to send an RFC 7009 token revocation request to the authorization server
51+
* on Disconnect (when the server advertises a `revocation_endpoint`). Default `true`
52+
* (spec-compliant). Disable to test server behavior when a client disconnects
53+
* without revoking, or to suppress the network call during offline testing.
54+
*/
55+
MCP_OAUTH_REVOKE_ON_DISCONNECT: ConfigItem;
4856
};

client/src/lib/constants.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,11 @@ export const DEFAULT_INSPECTOR_CONFIG: InspectorConfig = {
9292
value: 60000,
9393
is_session_item: false,
9494
},
95+
MCP_OAUTH_REVOKE_ON_DISCONNECT: {
96+
label: "Revoke OAuth Tokens on Disconnect",
97+
description:
98+
"When disconnecting, send an RFC 7009 token revocation request to the authorization server before clearing local state. Disable to test how a server behaves when a client disconnects without revoking.",
99+
value: true,
100+
is_session_item: false,
101+
},
95102
} as const;

client/src/lib/hooks/__tests__/useConnection.test.tsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1836,5 +1836,42 @@ describe("useConnection", () => {
18361836

18371837
expect(result.current.connectionStatus).toBe("disconnected");
18381838
});
1839+
1840+
test("skips revokeTokens when MCP_OAUTH_REVOKE_ON_DISCONNECT is false", async () => {
1841+
const { InspectorOAuthClientProvider } = jest.requireMock("../../auth");
1842+
const providerCtor = InspectorOAuthClientProvider as jest.Mock;
1843+
providerCtor.mockClear();
1844+
1845+
const propsWithRevokeDisabled: Parameters<typeof useConnection>[0] = {
1846+
...defaultProps,
1847+
config: {
1848+
...DEFAULT_INSPECTOR_CONFIG,
1849+
MCP_OAUTH_REVOKE_ON_DISCONNECT: {
1850+
...DEFAULT_INSPECTOR_CONFIG.MCP_OAUTH_REVOKE_ON_DISCONNECT,
1851+
value: false,
1852+
},
1853+
},
1854+
};
1855+
1856+
const { result } = renderHook(() =>
1857+
useConnection(propsWithRevokeDisabled),
1858+
);
1859+
await act(async () => {
1860+
await result.current.connect();
1861+
});
1862+
1863+
await act(async () => {
1864+
await result.current.disconnect();
1865+
});
1866+
1867+
expect(mockRevokeTokens).not.toHaveBeenCalled();
1868+
// Local clear still runs so the user gets a fresh slate even when
1869+
// remote revocation is opted out.
1870+
const disconnectInstance = providerCtor.mock.results[
1871+
providerCtor.mock.results.length - 1
1872+
].value as { clear: jest.Mock };
1873+
expect(disconnectInstance.clear).toHaveBeenCalledTimes(1);
1874+
expect(result.current.connectionStatus).toBe("disconnected");
1875+
});
18391876
});
18401877
});

client/src/lib/hooks/useConnection.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ import {
7171
getMCPServerRequestMaxTotalTimeout,
7272
resetRequestTimeoutOnProgress,
7373
getMCPProxyAuthToken,
74+
revokeOAuthTokensOnDisconnect,
7475
} from "@/utils/configUtils";
7576
import { getMCPServerRequestTimeout } from "@/utils/configUtils";
7677
import { InspectorConfig } from "../configurationTypes";
@@ -1183,10 +1184,13 @@ export function useConnection({
11831184
).terminateSession();
11841185
await mcpClient?.close();
11851186
// RFC 7009: revoke tokens at the AS before wiping local state, so the
1186-
// server doesn't keep a still-valid token around as a tombstone.
1187-
const fetchFn =
1188-
connectionType === "proxy" ? createProxyFetch(config) : undefined;
1189-
await revokeTokens({ serverUrl: sseUrl, fetchFn });
1187+
// server doesn't keep a still-valid token around as a tombstone. Users
1188+
// testing the inverse scenario can opt out via the config toggle.
1189+
if (revokeOAuthTokensOnDisconnect(config)) {
1190+
const fetchFn =
1191+
connectionType === "proxy" ? createProxyFetch(config) : undefined;
1192+
await revokeTokens({ serverUrl: sseUrl, fetchFn });
1193+
}
11901194
const authProvider = new InspectorOAuthClientProvider(sseUrl);
11911195
authProvider.clear();
11921196
setMcpClient(null);

client/src/utils/configUtils.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,12 @@ export const getMCPTaskTtl = (config: InspectorConfig): number => {
5858
return config.MCP_TASK_TTL.value as number;
5959
};
6060

61+
export const revokeOAuthTokensOnDisconnect = (
62+
config: InspectorConfig,
63+
): boolean => {
64+
return config.MCP_OAUTH_REVOKE_ON_DISCONNECT.value as boolean;
65+
};
66+
6167
export const getInitialTransportType = ():
6268
| "stdio"
6369
| "sse"

0 commit comments

Comments
 (0)