-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Description
Summary
Tool handlers that call upstream APIs with delegated OAuth tokens have no way to signal that the token has expired and needs refresh. When an upstream API returns 401, the tool handler can only throw an error, which the SDK wraps in a JSON-RPC error response with HTTP 200. The client never sees HTTP 401 and therefore never triggers token refresh.
Use Case
Consider an MCP server that wraps Gmail's API:
- User authenticates via OAuth, server receives access token
- User calls
gmail_search_messagestool - Server makes request to Gmail API with the access token
- Gmail API returns 401 (token expired)
- Tool handler throws an error
- Problem: SDK catches this and returns JSON-RPC error with HTTP 200
- Client receives the error but doesn't know it's an auth error that requires token refresh
The MCP client SDK only triggers token refresh when it sees HTTP 401 responses. Since tool errors are always wrapped in HTTP 200 JSON-RPC responses, the client never refreshes.
Proposed Solution
Similar to #1151 (scope challenges with 403), tool handlers should be able to signal auth errors that result in HTTP 401:
server.registerTool(
'gmail_search',
{ ... },
async (args, context) => {
const response = await callGmailAPI(args);
if (response.status === 401) {
// Signal that upstream auth failed - should result in HTTP 401
throw new TokenExpiredError();
// or: context.signalTokenExpired();
}
return { content: [...] };
}
);This would allow the SDK to return HTTP 401 instead of wrapping in a JSON-RPC error, triggering the client's token refresh flow.
Related
- Server SDK support for scope challenges on tool calls #1151 - Server SDK support for scope challenges on tool calls (same pattern for 403)
- Support upscoping on insufficient_scope 403 like in Python SDK #1039 - Support upscoping on insufficient_scope 403 like in Python SDK