diff --git a/src/ax/mcp/oauth/discovery.ts b/src/ax/mcp/oauth/discovery.ts index fd6cf99c..166c2538 100644 --- a/src/ax/mcp/oauth/discovery.ts +++ b/src/ax/mcp/oauth/discovery.ts @@ -1,5 +1,11 @@ import { fetchJSON, stripTrailingSlash } from '../util/http.js'; +/** + * Extracts the resource_metadata URL from a WWW-Authenticate header value. + * Supports both quoted and unquoted parameter formats. + * @param www - The WWW-Authenticate header value to parse + * @returns The extracted resource_metadata URL, or null if not found + */ export function parseWWWAuthenticateForResourceMetadata( www: string | null ): string | null { @@ -10,6 +16,16 @@ export function parseWWWAuthenticateForResourceMetadata( return match ? match[1] : null; } +/** + * Discovers the protected resource identifier and its authorization servers. + * First attempts to use the resource_metadata URL from the WWW-Authenticate header. + * If not available, falls back to trying well-known endpoints with and without path components. + * Validates that the discovered resource matches the requested URL and that authorization servers are advertised. + * @param requestedUrl - The URL of the protected resource being accessed + * @param wwwAuthenticate - The WWW-Authenticate header value from the response + * @returns An object containing the resource identifier and array of issuer URLs + * @throws {Error} If resource metadata cannot be discovered or validation fails + */ export async function discoverResourceAndAS( requestedUrl: string, wwwAuthenticate: string | null @@ -82,6 +98,14 @@ export async function discoverResourceAndAS( ); } +/** + * Discovers authorization server metadata by attempting multiple well-known endpoint patterns. + * Tries OAuth authorization server and OpenID configuration endpoints with various path combinations. + * Validates that the metadata includes required endpoints and PKCE S256 support. + * @param issuer - The authorization server issuer URL + * @returns The authorization server metadata object + * @throws {Error} If metadata cannot be discovered or validation fails + */ export async function discoverASMetadata(issuer: string): Promise { const u = new URL(issuer); const path = u.pathname.replace(/^\/+/, ''); diff --git a/src/ax/mcp/transports/sseTransport.ts b/src/ax/mcp/transports/sseTransport.ts index 774d2794..2607ed9c 100644 --- a/src/ax/mcp/transports/sseTransport.ts +++ b/src/ax/mcp/transports/sseTransport.ts @@ -7,6 +7,10 @@ import type { import type { AxMCPStreamableHTTPTransportOptions } from './options.js'; import { OAuthHelper } from '../oauth/oauthHelper.js'; +/** + * HTTP Server-Sent Events (SSE) transport implementation for MCP communication. + * Establishes a persistent SSE connection for receiving messages and uses HTTP POST for sending. + */ export class AxMCPHTTPSSETransport implements AxMCPTransport { private endpoint: string | null = null; private sseUrl: string; @@ -28,6 +32,13 @@ export class AxMCPHTTPSSETransport implements AxMCPTransport { ) => void; private endpointReady?: { resolve: () => void; promise: Promise }; + /** + * Creates a new SSE transport instance. + * Initializes custom headers and OAuth helper from the provided options. + * + * @param sseUrl - The URL to establish the SSE connection + * @param options - Optional configuration including headers and OAuth settings + */ constructor(sseUrl: string, options?: AxMCPStreamableHTTPTransportOptions) { this.sseUrl = sseUrl; this.customHeaders = { ...(options?.headers ?? {}) }; @@ -36,10 +47,25 @@ export class AxMCPHTTPSSETransport implements AxMCPTransport { this.oauthHelper = new OAuthHelper(options?.oauth); } + /** + * Merges custom headers with the provided base headers. + * + * @param base - The base headers to merge with custom headers + * @returns The merged headers object + */ private buildHeaders(base: Record): Record { return { ...this.customHeaders, ...base }; } + /** + * Establishes an SSE connection using the Fetch API. + * Handles OAuth authentication by retrying with a bearer token if a 401 response is received. + * Waits for the endpoint URI to be received via SSE before resolving. + * + * @param headers - The headers to use for the SSE connection request + * @throws {Error} If a 401 response is received and OAuth authentication fails + * @throws {Error} If the SSE connection cannot be established + */ private async openSSEWithFetch( headers: Record ): Promise { @@ -72,6 +98,12 @@ export class AxMCPHTTPSSETransport implements AxMCPTransport { await ready; } + /** + * Creates a promise that resolves when the endpoint URI is received. + * Returns the existing promise if already created. + * + * @returns A promise that resolves when the endpoint is ready + */ private createEndpointReady(): Promise { if (!this.endpointReady) { let resolver!: () => void; @@ -83,6 +115,16 @@ export class AxMCPHTTPSSETransport implements AxMCPTransport { return this.endpointReady.promise; } + /** + * Processes the SSE stream from the response body. + * Parses SSE events and handles 'endpoint' events to set the endpoint URI. + * Routes JSON-RPC messages to pending request handlers or the message handler. + * Continues reading until the stream is closed. + * + * @param response - The fetch response containing the SSE stream + * @throws {Error} If the response body is not available + * @throws {Error} If the endpoint URI is missing in the SSE event data + */ private async consumeSSEStream(response: Response): Promise { if (!response.body) throw new Error('No response body available for SSE stream'); @@ -150,11 +192,25 @@ export class AxMCPHTTPSSETransport implements AxMCPTransport { } } + /** + * Establishes the SSE connection and waits for the endpoint URI to be received. + */ async connect(): Promise { const headers = this.buildHeaders({ Accept: 'text/event-stream' }); await this.openSSEWithFetch(headers); } + /** + * Sends a JSON-RPC request to the endpoint and returns the response. + * Handles OAuth authentication by retrying with a bearer token if a 401 response is received. + * If the response is not immediate JSON, waits for the response to arrive via SSE. + * + * @param message - The JSON-RPC request to send + * @returns The JSON-RPC response received from the server + * @throws {Error} If the endpoint is not initialized + * @throws {Error} If a 401 response is received and OAuth authentication fails + * @throws {Error} If the HTTP request fails with a non-OK status + */ async send( message: Readonly> ): Promise> { @@ -211,6 +267,16 @@ export class AxMCPHTTPSSETransport implements AxMCPTransport { return pending; } + /** + * Sends a JSON-RPC notification to the endpoint without expecting a response. + * Handles OAuth authentication by retrying with a bearer token if a 401 response is received. + * Expects a 202 status code for successful notifications. + * + * @param message - The JSON-RPC notification to send + * @throws {Error} If the endpoint is not initialized + * @throws {Error} If a 401 response is received and OAuth authentication fails + * @throws {Error} If the HTTP request fails with a non-OK status + */ async sendNotification( message: Readonly ): Promise { @@ -251,6 +317,9 @@ export class AxMCPHTTPSSETransport implements AxMCPTransport { console.warn(`Unexpected status for notification: ${res.status}`); } + /** + * Closes the SSE connection and aborts any ongoing requests. + */ close(): void { if (this.eventSource) { this.eventSource.close(); diff --git a/src/docs/src/components/TypeDropdown.tsx b/src/docs/src/components/TypeDropdown.tsx index e8805ef9..ae84e3fe 100644 --- a/src/docs/src/components/TypeDropdown.tsx +++ b/src/docs/src/components/TypeDropdown.tsx @@ -12,6 +12,10 @@ interface FieldTypeOption { requiresLanguage?: boolean; } +/** + * Available field type options with their metadata and requirements. + * Each type includes display information and optional constraints for options or language specification. + */ const FIELD_TYPES: FieldTypeOption[] = [ { value: 'string', @@ -86,6 +90,11 @@ interface TypeDropdownProps { isInputField?: boolean; } +/** + * Dropdown component for selecting field types with optional modifiers. + * Displays available field types filtered by input/output context, allows toggling optional and array modifiers, + * handles click-outside and keyboard events for closing, and formats the selected type with appropriate syntax. + */ export default function TypeDropdown({ visible, position, @@ -99,6 +108,9 @@ export default function TypeDropdown({ const dropdownRef = useRef(null); useEffect(() => { + /** + * Closes the dropdown when a click occurs outside the dropdown element. + */ function handleClickOutside(event: MouseEvent) { if ( dropdownRef.current && @@ -116,6 +128,9 @@ export default function TypeDropdown({ }, [visible, onClose]); useEffect(() => { + /** + * Closes the dropdown when the Escape key is pressed. + */ function handleKeyDown(event: KeyboardEvent) { if (!visible) return; @@ -130,6 +145,11 @@ export default function TypeDropdown({ if (!visible) return null; + /** + * Processes the selected field type and formats it with modifiers. + * Adds special formatting for types requiring options, appends array notation if selected, + * prefixes with colon, and resets the modifier state after selection. + */ const handleTypeSelect = (type: FieldTypeOption) => { let insertText = type.value;