v2 ESlint fixes (#1403)
Co-authored-by: Matt <77928207+mattzcarey@users.noreply.github.com>
This commit is contained in:
parent
9296459ac0
commit
9fc7e28f48
7 changed files with 1672 additions and 1346 deletions
|
|
@ -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;
|
||||
|
|
|
|||
398
packages/client/src/client/authExtensions.ts
Normal file
398
packages/client/src/client/authExtensions.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
Loading…
Reference in a new issue