v2 ESlint fixes (#1403)

Co-authored-by: Matt <77928207+mattzcarey@users.noreply.github.com>
This commit is contained in:
Konstantin Konstantinov 2026-01-20 14:21:53 +02:00 • committed by GitHub
commit 9fc7e28f48
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 1672 additions and 1346 deletions

View file

@ -277,17 +277,21 @@ function applyClientAuthentication(
const { client_id, client_secret } = clientInformation;
switch (method) {
case 'client_secret_basic':
case 'client_secret_basic': {
applyBasicAuth(client_id, client_secret, headers);
return;
case 'client_secret_post':
}
case 'client_secret_post': {
applyPostAuth(client_id, client_secret, params);
return;
case 'none':
}
case 'none': {
applyPublicAuth(client_id, params);
return;
default:
}
default: {
throw new Error(`Unsupported client authentication method: ${method}`);
}
}
}
@ -611,7 +615,7 @@ function extractFieldFromWwwAuth(response: Response, fieldName: string): string
return null;
}
const pattern = new RegExp(`${fieldName}=(?:"([^"]+)"|([^\\s,]+))`);
const pattern = new RegExp(String.raw`${fieldName}=(?:"([^"]+)"|([^\s,]+))`);
const match = wwwAuthHeader.match(pattern);
if (match) {
@ -689,13 +693,9 @@ async function fetchWithCorsRetry(url: URL, headers?: Record<string, string>, fe
return await fetchFn(url, { headers });
} catch (error) {
if (error instanceof TypeError) {
if (headers) {
// CORS errors come back as TypeError, retry without headers
return fetchWithCorsRetry(url, undefined, fetchFn);
} else {
// We're getting CORS errors on retry too, return undefined
return undefined;
}
// CORS errors come back as TypeError, retry without headers
// We're getting CORS errors on retry too, return undefined
return headers ? fetchWithCorsRetry(url, undefined, fetchFn) : undefined;
}
throw error;
}
@ -827,17 +827,20 @@ export function buildDiscoveryUrls(authorizationServerUrl: string | URL): { url:
const urlsToTry: { url: URL; type: 'oauth' | 'oidc' }[] = [];
if (!hasPath) {
// Root path: https://example.com/.well-known/oauth-authorization-server
urlsToTry.push({
url: new URL('/.well-known/oauth-authorization-server', url.origin),
type: 'oauth'
});
urlsToTry.push(
// Root path: https://example.com/.well-known/oauth-authorization-server
// OIDC: https://example.com/.well-known/openid-configuration
urlsToTry.push({
url: new URL(`/.well-known/openid-configuration`, url.origin),
type: 'oidc'
});
{
url: new URL('/.well-known/oauth-authorization-server', url.origin),
type: 'oauth'
},
// OIDC: https://example.com/.well-known/openid-configuration
{
url: new URL(`/.well-known/openid-configuration`, url.origin),
type: 'oidc'
}
);
return urlsToTry;
}
@ -848,25 +851,26 @@ export function buildDiscoveryUrls(authorizationServerUrl: string | URL): { url:
pathname = pathname.slice(0, -1);
}
// 1. OAuth metadata at the given URL
// Insert well-known before the path: https://example.com/.well-known/oauth-authorization-server/tenant1
urlsToTry.push({
url: new URL(`/.well-known/oauth-authorization-server${pathname}`, url.origin),
type: 'oauth'
});
urlsToTry.push(
// 1. OAuth metadata at the given URL
// Insert well-known before the path: https://example.com/.well-known/oauth-authorization-server/tenant1
{
url: new URL(`/.well-known/oauth-authorization-server${pathname}`, url.origin),
type: 'oauth'
},
// 2. OIDC metadata endpoints
// RFC 8414 style: Insert /.well-known/openid-configuration before the path
{
url: new URL(`/.well-known/openid-configuration${pathname}`, url.origin),
type: 'oidc'
},
// OIDC Discovery 1.0 style: Append /.well-known/openid-configuration after the path
// 2. OIDC metadata endpoints
// RFC 8414 style: Insert /.well-known/openid-configuration before the path
urlsToTry.push({
url: new URL(`/.well-known/openid-configuration${pathname}`, url.origin),
type: 'oidc'
});
// OIDC Discovery 1.0 style: Append /.well-known/openid-configuration after the path
urlsToTry.push({
url: new URL(`${pathname}/.well-known/openid-configuration`, url.origin),
type: 'oidc'
});
{
url: new URL(`${pathname}/.well-known/openid-configuration`, url.origin),
type: 'oidc'
}
);
return urlsToTry;
}
@ -929,11 +933,9 @@ export async function discoverAuthorizationServerMetadata(
}
// Parse and validate based on type
if (type === 'oauth') {
return OAuthMetadataSchema.parse(await response.json());
} else {
return OpenIdProviderDiscoveryMetadataSchema.parse(await response.json());
}
return type === 'oauth'
? OAuthMetadataSchema.parse(await response.json())
: OpenIdProviderDiscoveryMetadataSchema.parse(await response.json());
}
return undefined;

View file

@ -0,0 +1,398 @@
/**
* OAuth provider extensions for specialized authentication flows.
*
* This module provides ready-to-use OAuthClientProvider implementations
* for common machine-to-machine authentication scenarios.
*/
import type { OAuthClientInformation, OAuthClientMetadata, OAuthTokens } from '@modelcontextprotocol/core';
import type { CryptoKey, JWK } from 'jose';
import type { AddClientAuthentication, OAuthClientProvider } from './auth.js';
/**
* Helper to produce a private_key_jwt client authentication function.
*
* Usage:
* const addClientAuth = createPrivateKeyJwtAuth({ issuer, subject, privateKey, alg, audience? });
* // pass addClientAuth as provider.addClientAuthentication implementation
*/
export function createPrivateKeyJwtAuth(options: {
issuer: string;
subject: string;
privateKey: string | Uint8Array | Record<string, unknown>;
alg: string;
audience?: string | URL;
lifetimeSeconds?: number;
claims?: Record<string, unknown>;
}): AddClientAuthentication {
return async (_headers, params, url, metadata) => {
// Lazy import to avoid heavy dependency unless used
if (globalThis.crypto === undefined) {
throw new TypeError(
'crypto is not available, please ensure you add have Web Crypto API support for older Node.js versions (see https://github.com/modelcontextprotocol/typescript-sdk#nodejs-web-crypto-globalthiscrypto-compatibility)'
);
}
const jose = await import('jose');
const audience = String(options.audience ?? metadata?.issuer ?? url);
const lifetimeSeconds = options.lifetimeSeconds ?? 300;
const now = Math.floor(Date.now() / 1000);
const jti = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
const baseClaims = {
iss: options.issuer,
sub: options.subject,
aud: audience,
exp: now + lifetimeSeconds,
iat: now,
jti
};
const claims = options.claims ? { ...baseClaims, ...options.claims } : baseClaims;
// Import key for the requested algorithm
const alg = options.alg;
let key: unknown;
if (typeof options.privateKey === 'string') {
if (alg.startsWith('RS') || alg.startsWith('ES') || alg.startsWith('PS')) {
key = await jose.importPKCS8(options.privateKey, alg);
} else if (alg.startsWith('HS')) {
key = new TextEncoder().encode(options.privateKey);
} else {
throw new Error(`Unsupported algorithm ${alg}`);
}
} else if (options.privateKey instanceof Uint8Array) {
// Assume PKCS#8 DER in Uint8Array for asymmetric algorithms
key = alg.startsWith('HS') ? options.privateKey : await jose.importPKCS8(new TextDecoder().decode(options.privateKey), alg);
} else {
// Treat as JWK
key = await jose.importJWK(options.privateKey as JWK, alg);
}
// Sign JWT
const assertion = await new jose.SignJWT(claims)
.setProtectedHeader({ alg, typ: 'JWT' })
.setIssuer(options.issuer)
.setSubject(options.subject)
.setAudience(audience)
.setIssuedAt(now)
.setExpirationTime(now + lifetimeSeconds)
.setJti(jti)
.sign(key as unknown as Uint8Array | CryptoKey);
params.set('client_assertion', assertion);
params.set('client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');
};
}
/**
* Options for creating a ClientCredentialsProvider.
*/
export interface ClientCredentialsProviderOptions {
/**
* The client_id for this OAuth client.
*/
clientId: string;
/**
* The client_secret for client_secret_basic authentication.
*/
clientSecret: string;
/**
* Optional client name for metadata.
*/
clientName?: string;
}
/**
* OAuth provider for client_credentials grant with client_secret_basic authentication.
*
* This provider is designed for machine-to-machine authentication where
* the client authenticates using a client_id and client_secret.
*
* @example
* const provider = new ClientCredentialsProvider({
* clientId: 'my-client',
* clientSecret: 'my-secret'
* });
*
* const transport = new StreamableHTTPClientTransport(serverUrl, {
* authProvider: provider
* });
*/
export class ClientCredentialsProvider implements OAuthClientProvider {
private _tokens?: OAuthTokens;
private _clientInfo: OAuthClientInformation;
private _clientMetadata: OAuthClientMetadata;
constructor(options: ClientCredentialsProviderOptions) {
this._clientInfo = {
client_id: options.clientId,
client_secret: options.clientSecret
};
this._clientMetadata = {
client_name: options.clientName ?? 'client-credentials-client',
redirect_uris: [],
grant_types: ['client_credentials'],
token_endpoint_auth_method: 'client_secret_basic'
};
}
get redirectUrl(): undefined {
return undefined;
}
get clientMetadata(): OAuthClientMetadata {
return this._clientMetadata;
}
clientInformation(): OAuthClientInformation {
return this._clientInfo;
}
saveClientInformation(info: OAuthClientInformation): void {
this._clientInfo = info;
}
tokens(): OAuthTokens | undefined {
return this._tokens;
}
saveTokens(tokens: OAuthTokens): void {
this._tokens = tokens;
}
redirectToAuthorization(): void {
throw new Error('redirectToAuthorization is not used for client_credentials flow');
}
saveCodeVerifier(): void {
// Not used for client_credentials
}
codeVerifier(): string {
throw new Error('codeVerifier is not used for client_credentials flow');
}
prepareTokenRequest(scope?: string): URLSearchParams {
const params = new URLSearchParams({ grant_type: 'client_credentials' });
if (scope) params.set('scope', scope);
return params;
}
}
/**
* Options for creating a PrivateKeyJwtProvider.
*/
export interface PrivateKeyJwtProviderOptions {
/**
* The client_id for this OAuth client.
*/
clientId: string;
/**
* The private key for signing JWT assertions.
* Can be a PEM string, Uint8Array, or JWK object.
*/
privateKey: string | Uint8Array | Record<string, unknown>;
/**
* The algorithm to use for signing (e.g., 'RS256', 'ES256').
*/
algorithm: string;
/**
* Optional client name for metadata.
*/
clientName?: string;
/**
* Optional JWT lifetime in seconds (default: 300).
*/
jwtLifetimeSeconds?: number;
}
/**
* OAuth provider for client_credentials grant with private_key_jwt authentication.
*
* This provider is designed for machine-to-machine authentication where
* the client authenticates using a signed JWT assertion (RFC 7523 Section 2.2).
*
* @example
* const provider = new PrivateKeyJwtProvider({
* clientId: 'my-client',
* privateKey: pemEncodedPrivateKey,
* algorithm: 'RS256'
* });
*
* const transport = new StreamableHTTPClientTransport(serverUrl, {
* authProvider: provider
* });
*/
export class PrivateKeyJwtProvider implements OAuthClientProvider {
private _tokens?: OAuthTokens;
private _clientInfo: OAuthClientInformation;
private _clientMetadata: OAuthClientMetadata;
addClientAuthentication: AddClientAuthentication;
constructor(options: PrivateKeyJwtProviderOptions) {
this._clientInfo = {
client_id: options.clientId
};
this._clientMetadata = {
client_name: options.clientName ?? 'private-key-jwt-client',
redirect_uris: [],
grant_types: ['client_credentials'],
token_endpoint_auth_method: 'private_key_jwt'
};
this.addClientAuthentication = createPrivateKeyJwtAuth({
issuer: options.clientId,
subject: options.clientId,
privateKey: options.privateKey,
alg: options.algorithm,
lifetimeSeconds: options.jwtLifetimeSeconds
});
}
get redirectUrl(): undefined {
return undefined;
}
get clientMetadata(): OAuthClientMetadata {
return this._clientMetadata;
}
clientInformation(): OAuthClientInformation {
return this._clientInfo;
}
saveClientInformation(info: OAuthClientInformation): void {
this._clientInfo = info;
}
tokens(): OAuthTokens | undefined {
return this._tokens;
}
saveTokens(tokens: OAuthTokens): void {
this._tokens = tokens;
}
redirectToAuthorization(): void {
throw new Error('redirectToAuthorization is not used for client_credentials flow');
}
saveCodeVerifier(): void {
// Not used for client_credentials
}
codeVerifier(): string {
throw new Error('codeVerifier is not used for client_credentials flow');
}
prepareTokenRequest(scope?: string): URLSearchParams {
const params = new URLSearchParams({ grant_type: 'client_credentials' });
if (scope) params.set('scope', scope);
return params;
}
}
/**
* Options for creating a StaticPrivateKeyJwtProvider.
*/
export interface StaticPrivateKeyJwtProviderOptions {
/**
* The client_id for this OAuth client.
*/
clientId: string;
/**
* A pre-built JWT client assertion to use for authentication.
*
* This token should already contain the appropriate claims
* (iss, sub, aud, exp, etc.) and be signed by the client's key.
*/
jwtBearerAssertion: string;
/**
* Optional client name for metadata.
*/
clientName?: string;
}
/**
* OAuth provider for client_credentials grant with a static private_key_jwt assertion.
*
* This provider mirrors {@link PrivateKeyJwtProvider} but instead of constructing and
* signing a JWT on each request, it accepts a pre-built JWT assertion string and
* uses it directly for authentication.
*/
export class StaticPrivateKeyJwtProvider implements OAuthClientProvider {
private _tokens?: OAuthTokens;
private _clientInfo: OAuthClientInformation;
private _clientMetadata: OAuthClientMetadata;
addClientAuthentication: AddClientAuthentication;
constructor(options: StaticPrivateKeyJwtProviderOptions) {
this._clientInfo = {
client_id: options.clientId
};
this._clientMetadata = {
client_name: options.clientName ?? 'static-private-key-jwt-client',
redirect_uris: [],
grant_types: ['client_credentials'],
token_endpoint_auth_method: 'private_key_jwt'
};
const assertion = options.jwtBearerAssertion;
this.addClientAuthentication = async (_headers, params) => {
params.set('client_assertion', assertion);
params.set('client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');
};
}
get redirectUrl(): undefined {
return undefined;
}
get clientMetadata(): OAuthClientMetadata {
return this._clientMetadata;
}
clientInformation(): OAuthClientInformation {
return this._clientInfo;
}
saveClientInformation(info: OAuthClientInformation): void {
this._clientInfo = info;
}
tokens(): OAuthTokens | undefined {
return this._tokens;
}
saveTokens(tokens: OAuthTokens): void {
this._tokens = tokens;
}
redirectToAuthorization(): void {
throw new Error('redirectToAuthorization is not used for client_credentials flow');
}
saveCodeVerifier(): void {
// Not used for client_credentials
}
codeVerifier(): string {
throw new Error('codeVerifier is not used for client_credentials flow');
}
prepareTokenRequest(scope?: string): URLSearchParams {
const params = new URLSearchParams({ grant_type: 'client_credentials' });
if (scope) params.set('scope', scope);
return params;
}
}

View file

@ -358,7 +358,7 @@ export class Client<
}
if (typeof methodValue !== 'string') {
throw new Error('Schema method literal must be a string');
throw new TypeError('Schema method literal must be a string');
}
const method = methodValue;
if (method === 'elicitation/create') {
@ -413,13 +413,17 @@ export class Client<
const validatedResult = validationResult.data;
const requestedSchema = params.mode === 'form' ? (params.requestedSchema as JsonSchemaType) : undefined;
if (params.mode === 'form' && validatedResult.action === 'accept' && validatedResult.content && requestedSchema) {
if (this._capabilities.elicitation?.form?.applyDefaults) {
try {
applyElicitationDefaults(requestedSchema, validatedResult.content);
} catch {
// gracefully ignore errors in default application
}
if (
params.mode === 'form' &&
validatedResult.action === 'accept' &&
validatedResult.content &&
requestedSchema &&
this._capabilities.elicitation?.form?.applyDefaults
) {
try {
applyElicitationDefaults(requestedSchema, validatedResult.content);
} catch {
// gracefully ignore errors in default application
}
}
@ -563,24 +567,26 @@ export class Client<
protected assertCapabilityForMethod(method: RequestT['method']): void {
switch (method as ClientRequest['method']) {
case 'logging/setLevel':
case 'logging/setLevel': {
if (!this._serverCapabilities?.logging) {
throw new Error(`Server does not support logging (required for ${method})`);
}
break;
}
case 'prompts/get':
case 'prompts/list':
case 'prompts/list': {
if (!this._serverCapabilities?.prompts) {
throw new Error(`Server does not support prompts (required for ${method})`);
}
break;
}
case 'resources/list':
case 'resources/templates/list':
case 'resources/read':
case 'resources/subscribe':
case 'resources/unsubscribe':
case 'resources/unsubscribe': {
if (!this._serverCapabilities?.resources) {
throw new Error(`Server does not support resources (required for ${method})`);
}
@ -590,49 +596,58 @@ export class Client<
}
break;
}
case 'tools/call':
case 'tools/list':
case 'tools/list': {
if (!this._serverCapabilities?.tools) {
throw new Error(`Server does not support tools (required for ${method})`);
}
break;
}
case 'completion/complete':
case 'completion/complete': {
if (!this._serverCapabilities?.completions) {
throw new Error(`Server does not support completions (required for ${method})`);
}
break;
}
case 'initialize':
case 'initialize': {
// No specific capability required for initialize
break;
}
case 'ping':
case 'ping': {
// No specific capability required for ping
break;
}
}
}
protected assertNotificationCapability(method: NotificationT['method']): void {
switch (method as ClientNotification['method']) {
case 'notifications/roots/list_changed':
case 'notifications/roots/list_changed': {
if (!this._capabilities.roots?.listChanged) {
throw new Error(`Client does not support roots list changed notifications (required for ${method})`);
}
break;
}
case 'notifications/initialized':
case 'notifications/initialized': {
// No specific capability required for initialized
break;
}
case 'notifications/cancelled':
case 'notifications/cancelled': {
// Cancellation notifications are always allowed
break;
}
case 'notifications/progress':
case 'notifications/progress': {
// Progress notifications are always allowed
break;
}
}
}
@ -644,36 +659,41 @@ export class Client<
}
switch (method) {
case 'sampling/createMessage':
case 'sampling/createMessage': {
if (!this._capabilities.sampling) {
throw new Error(`Client does not support sampling capability (required for ${method})`);
}
break;
}
case 'elicitation/create':
case 'elicitation/create': {
if (!this._capabilities.elicitation) {
throw new Error(`Client does not support elicitation capability (required for ${method})`);
}
break;
}
case 'roots/list':
case 'roots/list': {
if (!this._capabilities.roots) {
throw new Error(`Client does not support roots capability (required for ${method})`);
}
break;
}
case 'tasks/get':
case 'tasks/list':
case 'tasks/result':
case 'tasks/cancel':
case 'tasks/cancel': {
if (!this._capabilities.tasks) {
throw new Error(`Client does not support tasks capability (required for ${method})`);
}
break;
}
case 'ping':
case 'ping': {
// No specific capability required for ping
break;
}
}
}
@ -866,7 +886,7 @@ export class Client<
// Validate callback
if (typeof options.onChanged !== 'function') {
throw new Error(`Invalid ${listType} listChanged options: onChanged must be a function`);
throw new TypeError(`Invalid ${listType} listChanged options: onChanged must be a function`);
}
const { autoRefresh, debounceMs } = parseResult.data;
@ -881,9 +901,9 @@ export class Client<
try {
const items = await fetcher();
onChanged(null, items);
} catch (e) {
const error = e instanceof Error ? e : new Error(String(e));
onChanged(error, null);
} catch (error) {
const newError = error instanceof Error ? error : new Error(String(error));
onChanged(newError, null);
}
};

View file

@ -167,16 +167,12 @@ export const withLogging = (options: LoggingOptions = {}): Middleware => {
// Add headers to message if requested
if (includeRequestHeaders && requestHeaders) {
const reqHeaders = Array.from(requestHeaders.entries())
.map(([key, value]) => `${key}: ${value}`)
.join(', ');
const reqHeaders = [...requestHeaders.entries()].map(([key, value]) => `${key}: ${value}`).join(', ');
message += `\n Request Headers: {${reqHeaders}}`;
}
if (includeResponseHeaders && responseHeaders) {
const resHeaders = Array.from(responseHeaders.entries())
.map(([key, value]) => `${key}: ${value}`)
.join(', ');
const resHeaders = [...responseHeaders.entries()].map(([key, value]) => `${key}: ${value}`).join(', ');
message += `\n Response Headers: {${resHeaders}}`;
}
@ -255,7 +251,11 @@ export const withLogging = (options: LoggingOptions = {}): Middleware => {
*/
export const applyMiddlewares = (...middleware: Middleware[]): Middleware => {
return next => {
return middleware.reduce((handler, mw) => mw(handler), next);
let handler = next;
for (const mw of middleware) {
handler = mw(handler);
}
return handler;
};
};

View file

@ -219,7 +219,7 @@ export class StdioClientTransport implements Transport {
// ignore
}
await Promise.race([closePromise, new Promise(resolve => setTimeout(resolve, 2_000).unref())]);
await Promise.race([closePromise, new Promise(resolve => setTimeout(resolve, 2000).unref())]);
if (processToClose.exitCode === null) {
try {
@ -228,7 +228,7 @@ export class StdioClientTransport implements Transport {
// ignore
}
await Promise.race([closePromise, new Promise(resolve => setTimeout(resolve, 2_000).unref())]);
await Promise.race([closePromise, new Promise(resolve => setTimeout(resolve, 2000).unref())]);
}
if (processToClose.exitCode === null) {

View file

@ -17,7 +17,7 @@ import { auth, extractWWWAuthenticateParams, UnauthorizedError } from './auth.js
// Default reconnection options for StreamableHTTP connections
const DEFAULT_STREAMABLE_HTTP_RECONNECTION_OPTIONS: StreamableHTTPReconnectionOptions = {
initialReconnectionDelay: 1000,
maxReconnectionDelay: 30000,
maxReconnectionDelay: 30_000,
reconnectionDelayGrowFactor: 1.5,
maxRetries: 2
};
@ -468,8 +468,8 @@ export class StreamableHTTPClientTransport implements Transport {
if (resumptionToken) {
// If we have at last event ID, we need to reconnect the SSE stream
this._startOrAuthSse({ resumptionToken, replayMessageId: isJSONRPCRequest(message) ? message.id : undefined }).catch(err =>
this.onerror?.(err)
this._startOrAuthSse({ resumptionToken, replayMessageId: isJSONRPCRequest(message) ? message.id : undefined }).catch(
error => this.onerror?.(error)
);
return;
}
@ -573,7 +573,7 @@ export class StreamableHTTPClientTransport implements Transport {
// if it's supported by the server
if (isInitializedNotification(message)) {
// Start without a lastEventId since this is a fresh connection
this._startOrAuthSse({ resumptionToken: undefined }).catch(err => this.onerror?.(err));
this._startOrAuthSse({ resumptionToken: undefined }).catch(error => this.onerror?.(error));
}
return;
}
@ -581,7 +581,7 @@ export class StreamableHTTPClientTransport implements Transport {
// Get original message(s) for detecting request IDs
const messages = Array.isArray(message) ? message : [message];
const hasRequests = messages.filter(msg => 'method' in msg && 'id' in msg && msg.id !== undefined).length > 0;
const hasRequests = messages.some(msg => 'method' in msg && 'id' in msg && msg.id !== undefined);
// Check the response type
const contentType = response.headers.get('content-type');

View file

@ -1,5 +1,5 @@
export * from './client/auth.js';
export * from './client/auth-extensions.js';
export * from './client/authExtensions.js';
export * from './client/client.js';
export * from './client/middleware.js';
export * from './client/sse.js';