typescript-sdk/examples/oauth/dualModeAuth.ts
Felix Weinberger 1823aae891
Implement MCP 2026-07-28 (#2286)
Co-authored-by: Matt <77928207+mattzcarey@users.noreply.github.com>
Co-authored-by: Konstantin Konstantinov <KKonstantinov@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-26 14:01:10 +01:00

114 lines
4.7 KiB
JavaScript

#!/usr/bin/env node
/**
* Two auth patterns through the same `authProvider` option.
*
* The transport accepts either a minimal `AuthProvider` (just `token()` +
* optional `onUnauthorized()`) or a full `OAuthClientProvider`, adapting
* the latter automatically. This means your connect/call code is identical
* regardless of which pattern fits your deployment.
*
* HOST-MANAGED — token lives in an enclosing app
* The app fetches and stores tokens; the MCP client just reads them.
* On 401, there is nothing to refresh — signal the UI and throw so the
* user can re-authenticate through the host's flow.
*
* USER-CONFIGURED — OAuth credentials supplied directly
* Pass a built-in or custom OAuthClientProvider. The transport handles
* the full OAuth flow: token refresh on 401, or redirect for interactive
* authorization.
*/
import type { AuthProvider } from '@modelcontextprotocol/client';
import { Client, ClientCredentialsProvider, StreamableHTTPClientTransport, UnauthorizedError } from '@modelcontextprotocol/client';
// --- Stubs for host-app integration points ---------------------------------
/** Whatever the host app uses to store session state (e.g., cookies, keychain, in-memory). */
interface HostSessionStore {
getMcpToken(): string | undefined;
}
/** Whatever the host app uses to surface UI prompts. */
interface HostUi {
showReauthPrompt(message: string): void;
}
// --- MODE A: Host-managed auth ---------------------------------------------
function createHostManagedTransport(serverUrl: URL, session: HostSessionStore, ui: HostUi): StreamableHTTPClientTransport {
const authProvider: AuthProvider = {
// Called before every request — just read whatever the host has.
token: async () => session.getMcpToken(),
// Called on 401 — don't refresh (the host owns the token), signal the UI and bail.
// The transport will retry once after this returns, so we throw to stop it:
// the user needs to act before a retry makes sense.
onUnauthorized: async () => {
ui.showReauthPrompt('MCP connection lost — click to reconnect');
throw new UnauthorizedError('Host token rejected — user action required');
}
};
return new StreamableHTTPClientTransport(serverUrl, { authProvider });
}
// --- MODE B: User-configured OAuth -----------------------------------------
function createUserConfiguredTransport(serverUrl: URL, clientId: string, clientSecret: string): StreamableHTTPClientTransport {
// Built-in OAuth provider — the transport adapts it to AuthProvider internally.
// On 401, adaptOAuthProvider synthesizes onUnauthorized → handleOAuthUnauthorized,
// which runs token refresh (or redirect for interactive flows).
const authProvider = new ClientCredentialsProvider({ clientId, clientSecret });
return new StreamableHTTPClientTransport(serverUrl, { authProvider });
}
// --- Same caller code for both modes ---------------------------------------
async function connectAndList(transport: StreamableHTTPClientTransport): Promise<void> {
const client = new Client({ name: 'dual-mode-example', version: '1.0.0' }, { capabilities: {} });
await client.connect(transport);
const tools = await client.listTools();
console.log('Tools:', tools.tools.map(t => t.name).join(', ') || '(none)');
await transport.close();
}
// --- Driver ----------------------------------------------------------------
async function main() {
const serverUrl = new URL(process.env.MCP_SERVER_URL || 'http://127.0.0.1:3000/mcp');
const mode = process.argv[2] || 'host';
let transport: StreamableHTTPClientTransport;
if (mode === 'host') {
// Simulate a host app with a session-stored token and a UI hook.
const session: HostSessionStore = { getMcpToken: () => process.env.MCP_TOKEN };
const ui: HostUi = { showReauthPrompt: msg => console.error(`[UI] ${msg}`) };
transport = createHostManagedTransport(serverUrl, session, ui);
} else if (mode === 'oauth') {
const clientId = process.env.OAUTH_CLIENT_ID;
const clientSecret = process.env.OAUTH_CLIENT_SECRET;
if (!clientId || !clientSecret) {
console.error('OAUTH_CLIENT_ID and OAUTH_CLIENT_SECRET required for oauth mode');
process.exit(1);
}
transport = createUserConfiguredTransport(serverUrl, clientId, clientSecret);
} else {
console.error(`Unknown mode: ${mode}. Use 'host' or 'oauth'.`);
process.exit(1);
}
// Same connect/list code regardless of mode — the transport abstracts the difference.
await connectAndList(transport);
}
try {
await main();
} catch (error) {
console.error('Error:', error);
process.exitCode = 1;
}