feat: introduce minimal AuthProvider interface with OAuthClientProvider adapter (#1710)
This commit is contained in:
parent
13a0d345c0
commit
e563e63bd2
3 changed files with 1102 additions and 293 deletions
|
|
@ -8,7 +8,7 @@
|
|||
*/
|
||||
|
||||
//#region imports
|
||||
import type { Prompt, Resource, Tool } from '@modelcontextprotocol/client';
|
||||
import type { AuthProvider, Prompt, Resource, Tool } from '@modelcontextprotocol/client';
|
||||
import {
|
||||
applyMiddlewares,
|
||||
Client,
|
||||
|
|
@ -107,6 +107,16 @@ async function serverInstructions_basic(client: Client) {
|
|||
// Authentication
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Example: Minimal AuthProvider for bearer auth with externally-managed tokens. */
|
||||
async function auth_tokenProvider(getStoredToken: () => Promise<string>) {
|
||||
//#region auth_tokenProvider
|
||||
const authProvider: AuthProvider = { token: async () => getStoredToken() };
|
||||
|
||||
const transport = new StreamableHTTPClientTransport(new URL('http://localhost:3000/mcp'), { authProvider });
|
||||
//#endregion auth_tokenProvider
|
||||
return transport;
|
||||
}
|
||||
|
||||
/** Example: Client credentials auth for service-to-service communication. */
|
||||
async function auth_clientCredentials() {
|
||||
//#region auth_clientCredentials
|
||||
|
|
@ -540,6 +550,7 @@ void connect_stdio;
|
|||
void connect_sseFallback;
|
||||
void disconnect_streamableHttp;
|
||||
void serverInstructions_basic;
|
||||
void auth_tokenProvider;
|
||||
void auth_clientCredentials;
|
||||
void auth_privateKeyJwt;
|
||||
void auth_crossAppAccess;
|
||||
|
|
|
|||
114
examples/client/src/dualModeAuth.ts
Normal file
114
examples/client/src/dualModeAuth.ts
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
#!/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://localhost: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;
|
||||
}
|
||||
55
examples/client/src/simpleTokenProvider.ts
Normal file
55
examples/client/src/simpleTokenProvider.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Example demonstrating the minimal AuthProvider for bearer token authentication.
|
||||
*
|
||||
* AuthProvider is the base interface for all client auth. For simple cases where
|
||||
* tokens are managed externally — pre-configured API tokens, gateway/proxy patterns,
|
||||
* or tokens obtained through a separate auth flow — implement only `token()`.
|
||||
*
|
||||
* For OAuth flows (client_credentials, private_key_jwt, etc.), use the built-in
|
||||
* providers which implement both `token()` and `onUnauthorized()`.
|
||||
*
|
||||
* Environment variables:
|
||||
* MCP_SERVER_URL - Server URL (default: http://localhost:3000/mcp)
|
||||
* MCP_TOKEN - Bearer token to use for authentication (required)
|
||||
*/
|
||||
|
||||
import type { AuthProvider } from '@modelcontextprotocol/client';
|
||||
import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
|
||||
|
||||
const DEFAULT_SERVER_URL = process.env.MCP_SERVER_URL || 'http://localhost:3000/mcp';
|
||||
|
||||
async function main() {
|
||||
const token = process.env.MCP_TOKEN;
|
||||
if (!token) {
|
||||
console.error('MCP_TOKEN environment variable is required');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// AuthProvider with just token() — the simplest possible auth.
|
||||
// token() is called before every request, so it can handle refresh internally.
|
||||
// With no onUnauthorized(), a 401 throws UnauthorizedError immediately.
|
||||
const authProvider: AuthProvider = {
|
||||
token: async () => token
|
||||
};
|
||||
|
||||
const client = new Client({ name: 'auth-provider-example', version: '1.0.0' }, { capabilities: {} });
|
||||
|
||||
const transport = new StreamableHTTPClientTransport(new URL(DEFAULT_SERVER_URL), { authProvider });
|
||||
|
||||
await client.connect(transport);
|
||||
console.log('Connected successfully.');
|
||||
|
||||
const tools = await client.listTools();
|
||||
console.log('Available tools:', tools.tools.map(t => t.name).join(', ') || '(none)');
|
||||
|
||||
await transport.close();
|
||||
}
|
||||
|
||||
try {
|
||||
await main();
|
||||
} catch (error) {
|
||||
console.error('Error running client:', error);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
Loading…
Reference in a new issue