@frontmcp/testing 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/package.json +4 -4
  2. package/src/auth/mock-api-server.d.ts +99 -0
  3. package/src/auth/mock-api-server.js +200 -0
  4. package/src/auth/mock-api-server.js.map +1 -0
  5. package/src/auth/mock-oauth-server.d.ts +85 -0
  6. package/src/auth/mock-oauth-server.js +253 -0
  7. package/src/auth/mock-oauth-server.js.map +1 -0
  8. package/src/client/mcp-test-client.builder.d.ts +43 -1
  9. package/src/client/mcp-test-client.builder.js +52 -0
  10. package/src/client/mcp-test-client.builder.js.map +1 -1
  11. package/src/client/mcp-test-client.js +22 -14
  12. package/src/client/mcp-test-client.js.map +1 -1
  13. package/src/client/mcp-test-client.types.d.ts +67 -6
  14. package/src/client/mcp-test-client.types.js +9 -0
  15. package/src/client/mcp-test-client.types.js.map +1 -1
  16. package/src/example-tools/index.d.ts +19 -0
  17. package/src/example-tools/index.js +40 -0
  18. package/src/example-tools/index.js.map +1 -0
  19. package/src/example-tools/tool-configs.d.ts +170 -0
  20. package/src/example-tools/tool-configs.js +222 -0
  21. package/src/example-tools/tool-configs.js.map +1 -0
  22. package/src/expect.d.ts +6 -5
  23. package/src/expect.js.map +1 -1
  24. package/src/fixtures/fixture-types.d.ts +19 -0
  25. package/src/fixtures/fixture-types.js.map +1 -1
  26. package/src/fixtures/test-fixture.d.ts +3 -1
  27. package/src/fixtures/test-fixture.js +35 -4
  28. package/src/fixtures/test-fixture.js.map +1 -1
  29. package/src/index.d.ts +7 -0
  30. package/src/index.js +40 -1
  31. package/src/index.js.map +1 -1
  32. package/src/matchers/matcher-types.js.map +1 -1
  33. package/src/matchers/mcp-matchers.d.ts +7 -0
  34. package/src/matchers/mcp-matchers.js +8 -4
  35. package/src/matchers/mcp-matchers.js.map +1 -1
  36. package/src/platform/index.d.ts +28 -0
  37. package/src/platform/index.js +47 -0
  38. package/src/platform/index.js.map +1 -0
  39. package/src/platform/platform-client-info.d.ts +97 -0
  40. package/src/platform/platform-client-info.js +155 -0
  41. package/src/platform/platform-client-info.js.map +1 -0
  42. package/src/platform/platform-types.d.ts +72 -0
  43. package/src/platform/platform-types.js +110 -0
  44. package/src/platform/platform-types.js.map +1 -0
  45. package/src/server/test-server.d.ts +4 -0
  46. package/src/server/test-server.js +58 -3
  47. package/src/server/test-server.js.map +1 -1
  48. package/src/transport/streamable-http.transport.js +6 -0
  49. package/src/transport/streamable-http.transport.js.map +1 -1
  50. package/src/transport/transport.interface.d.ts +3 -0
  51. package/src/transport/transport.interface.js.map +1 -1
  52. package/src/ui/ui-assertions.d.ts +59 -0
  53. package/src/ui/ui-assertions.js +152 -0
  54. package/src/ui/ui-assertions.js.map +1 -1
  55. package/src/ui/ui-matchers.d.ts +8 -0
  56. package/src/ui/ui-matchers.js +218 -0
  57. package/src/ui/ui-matchers.js.map +1 -1
@@ -30,6 +30,7 @@ class StreamableHttpTransport {
30
30
  publicMode: config.publicMode ?? false,
31
31
  debug: config.debug ?? false,
32
32
  interceptors: config.interceptors,
33
+ clientInfo: config.clientInfo,
33
34
  };
34
35
  this.authToken = config.auth?.token;
35
36
  this.interceptors = config.interceptors;
@@ -337,6 +338,11 @@ class StreamableHttpTransport {
337
338
  'Content-Type': 'application/json',
338
339
  Accept: 'application/json, text/event-stream',
339
340
  };
341
+ // Add User-Agent header based on clientInfo for platform detection
342
+ // Server uses this to detect the AI platform before MCP initialize
343
+ if (this.config.clientInfo) {
344
+ headers['User-Agent'] = `${this.config.clientInfo.name}/${this.config.clientInfo.version}`;
345
+ }
340
346
  // Only add Authorization header if we have a token AND not in public mode
341
347
  // Public mode explicitly skips auth headers for CI/CD and public docs testing
342
348
  if (this.authToken && !this.publicMode) {
@@ -1 +1 @@
1
- {"version":3,"file":"streamable-http.transport.js","sourceRoot":"","sources":["../../../src/transport/streamable-http.transport.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @file streamable-http.transport.ts\n * @description StreamableHTTP transport implementation for MCP Test Client\n */\n\nimport type {\n McpTransport,\n TransportConfig,\n TransportState,\n JsonRpcRequest,\n JsonRpcResponse,\n} from './transport.interface';\nimport type { InterceptorChain } from '../interceptor';\n\nconst DEFAULT_TIMEOUT = 30000;\n\n/**\n * StreamableHTTP transport for MCP communication\n *\n * This transport uses HTTP POST requests for all communication,\n * following the MCP StreamableHTTP specification.\n */\nexport class StreamableHttpTransport implements McpTransport {\n private readonly config: Required<Omit<TransportConfig, 'interceptors'>> & { interceptors?: InterceptorChain };\n private state: TransportState = 'disconnected';\n private sessionId: string | undefined;\n private authToken: string | undefined;\n private connectionCount = 0;\n private reconnectCount = 0;\n private lastRequestHeaders: Record<string, string> = {};\n private interceptors?: InterceptorChain;\n private readonly publicMode: boolean;\n\n constructor(config: TransportConfig) {\n this.config = {\n baseUrl: config.baseUrl.replace(/\\/$/, ''), // Remove trailing slash\n timeout: config.timeout ?? DEFAULT_TIMEOUT,\n auth: config.auth ?? {},\n publicMode: config.publicMode ?? false,\n debug: config.debug ?? false,\n interceptors: config.interceptors,\n };\n\n this.authToken = config.auth?.token;\n this.interceptors = config.interceptors;\n this.publicMode = config.publicMode ?? false;\n }\n\n async connect(): Promise<void> {\n this.state = 'connecting';\n this.connectionCount++;\n\n try {\n // Public mode: Skip all authentication - connect without any token\n if (this.publicMode) {\n this.log('Public mode: connecting without authentication');\n this.state = 'connected';\n return;\n }\n\n // If no auth token provided, request anonymous token from FrontMCP SDK\n if (!this.authToken) {\n await this.requestAnonymousToken();\n }\n\n // StreamableHTTP doesn't require an explicit connection step\n // The session is established on the first request\n this.state = 'connected';\n this.log('Connected to StreamableHTTP transport');\n } catch (error) {\n this.state = 'error';\n throw error;\n }\n }\n\n /**\n * Request an anonymous token from the FrontMCP OAuth endpoint\n * This allows the test client to authenticate without user interaction\n */\n private async requestAnonymousToken(): Promise<void> {\n const clientId = crypto.randomUUID();\n const tokenUrl = `${this.config.baseUrl}/oauth/token`;\n\n this.log(`Requesting anonymous token from ${tokenUrl}`);\n\n try {\n const response = await fetch(tokenUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n grant_type: 'anonymous',\n client_id: clientId,\n resource: this.config.baseUrl,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n this.log(`Failed to get anonymous token: ${response.status} ${errorText}`);\n // Continue without token - server may allow unauthenticated access\n return;\n }\n\n const tokenResponse = await response.json();\n if (tokenResponse.access_token) {\n this.authToken = tokenResponse.access_token;\n this.log('Anonymous token acquired successfully');\n }\n } catch (error) {\n this.log(`Error requesting anonymous token: ${error}`);\n // Continue without token - server may allow unauthenticated access\n }\n }\n\n async request<T = unknown>(message: JsonRpcRequest): Promise<JsonRpcResponse & { result?: T }> {\n this.ensureConnected();\n\n const startTime = Date.now();\n\n // Process through interceptors if available\n if (this.interceptors) {\n const interceptResult = await this.interceptors.processRequest(message, {\n timestamp: new Date(),\n transport: 'streamable-http',\n sessionId: this.sessionId,\n });\n\n switch (interceptResult.type) {\n case 'mock': {\n // Return mock response directly, run through response interceptors\n const mockResponse = await this.interceptors.processResponse(\n message,\n interceptResult.response,\n Date.now() - startTime,\n );\n return mockResponse as JsonRpcResponse & { result?: T };\n }\n\n case 'error':\n throw interceptResult.error;\n\n case 'continue':\n // Use possibly modified request\n message = interceptResult.request;\n break;\n }\n }\n\n const headers = this.buildHeaders();\n this.lastRequestHeaders = headers;\n\n const url = `${this.config.baseUrl}/`;\n this.log(`POST ${url}`, message);\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);\n\n try {\n const response = await fetch(url, {\n method: 'POST',\n headers,\n body: JSON.stringify(message),\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n // Check for session ID in response headers\n const newSessionId = response.headers.get('mcp-session-id');\n if (newSessionId) {\n this.sessionId = newSessionId;\n }\n\n let jsonResponse: JsonRpcResponse;\n\n if (!response.ok) {\n // Handle HTTP errors\n const errorText = await response.text();\n this.log(`HTTP Error ${response.status}: ${errorText}`);\n\n jsonResponse = {\n jsonrpc: '2.0',\n id: message.id ?? null,\n error: {\n code: -32000,\n message: `HTTP ${response.status}: ${response.statusText}`,\n data: errorText,\n },\n };\n } else {\n // Parse response - may be JSON or SSE\n const contentType = response.headers.get('content-type') ?? '';\n const text = await response.text();\n this.log('Response:', text);\n\n // Handle empty response (for notifications)\n if (!text.trim()) {\n jsonResponse = {\n jsonrpc: '2.0',\n id: message.id ?? null,\n result: undefined,\n };\n } else if (contentType.includes('text/event-stream')) {\n // Parse SSE response - extract data and session ID from event stream\n const { response: sseResponse, sseSessionId } = this.parseSSEResponseWithSession(text, message.id);\n jsonResponse = sseResponse;\n // Store session ID from SSE id field (format: sessionId:messageId)\n if (sseSessionId && !this.sessionId) {\n this.sessionId = sseSessionId;\n this.log('Session ID from SSE:', this.sessionId);\n }\n } else {\n jsonResponse = JSON.parse(text) as JsonRpcResponse;\n }\n }\n\n // Process response through interceptors\n if (this.interceptors) {\n jsonResponse = await this.interceptors.processResponse(message, jsonResponse, Date.now() - startTime);\n }\n\n return jsonResponse as JsonRpcResponse & { result?: T };\n } catch (error) {\n clearTimeout(timeoutId);\n\n if (error instanceof Error && error.name === 'AbortError') {\n return {\n jsonrpc: '2.0',\n id: message.id ?? null,\n error: {\n code: -32000,\n message: `Request timeout after ${this.config.timeout}ms`,\n },\n };\n }\n\n throw error;\n }\n }\n\n async notify(message: JsonRpcRequest): Promise<void> {\n this.ensureConnected();\n\n const headers = this.buildHeaders();\n this.lastRequestHeaders = headers;\n\n const url = `${this.config.baseUrl}/`;\n this.log(`POST ${url} (notification)`, message);\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);\n\n try {\n const response = await fetch(url, {\n method: 'POST',\n headers,\n body: JSON.stringify(message),\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n // Update session ID if present\n const newSessionId = response.headers.get('mcp-session-id');\n if (newSessionId) {\n this.sessionId = newSessionId;\n }\n\n if (!response.ok) {\n const errorText = await response.text();\n this.log(`HTTP Error ${response.status} on notification: ${errorText}`);\n }\n } catch (error) {\n clearTimeout(timeoutId);\n\n if (error instanceof Error && error.name !== 'AbortError') {\n throw error;\n }\n }\n }\n\n async sendRaw(data: string): Promise<JsonRpcResponse> {\n this.ensureConnected();\n\n const headers = this.buildHeaders();\n this.lastRequestHeaders = headers;\n\n const url = `${this.config.baseUrl}/`;\n this.log(`POST ${url} (raw)`, data);\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);\n\n try {\n const response = await fetch(url, {\n method: 'POST',\n headers,\n body: data,\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n const text = await response.text();\n\n if (!text.trim()) {\n return {\n jsonrpc: '2.0',\n id: null,\n error: {\n code: -32700,\n message: 'Parse error',\n },\n };\n }\n\n return JSON.parse(text) as JsonRpcResponse;\n } catch (error) {\n clearTimeout(timeoutId);\n\n return {\n jsonrpc: '2.0',\n id: null,\n error: {\n code: -32700,\n message: 'Parse error',\n data: error instanceof Error ? error.message : 'Unknown error',\n },\n };\n }\n }\n\n async close(): Promise<void> {\n this.state = 'disconnected';\n this.sessionId = undefined;\n this.log('StreamableHTTP transport closed');\n }\n\n isConnected(): boolean {\n return this.state === 'connected';\n }\n\n getState(): TransportState {\n return this.state;\n }\n\n getSessionId(): string | undefined {\n return this.sessionId;\n }\n\n setAuthToken(token: string): void {\n this.authToken = token;\n }\n\n setTimeout(ms: number): void {\n this.config.timeout = ms;\n }\n\n setInterceptors(interceptors: InterceptorChain): void {\n this.interceptors = interceptors;\n }\n\n getInterceptors(): InterceptorChain | undefined {\n return this.interceptors;\n }\n\n getConnectionCount(): number {\n return this.connectionCount;\n }\n\n getReconnectCount(): number {\n return this.reconnectCount;\n }\n\n getLastRequestHeaders(): Record<string, string> {\n return { ...this.lastRequestHeaders };\n }\n\n async simulateDisconnect(): Promise<void> {\n this.state = 'disconnected';\n this.sessionId = undefined;\n }\n\n async waitForReconnect(timeoutMs: number): Promise<void> {\n const deadline = Date.now() + timeoutMs;\n\n // Auto-reconnect\n this.reconnectCount++;\n await this.connect();\n\n while (Date.now() < deadline) {\n if (this.state === 'connected') {\n return;\n }\n await new Promise((r) => setTimeout(r, 50));\n }\n\n throw new Error('Timeout waiting for reconnection');\n }\n\n // ═══════════════════════════════════════════════════════════════════\n // PRIVATE HELPERS\n // ═══════════════════════════════════════════════════════════════════\n\n private buildHeaders(): Record<string, string> {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n Accept: 'application/json, text/event-stream',\n };\n\n // Only add Authorization header if we have a token AND not in public mode\n // Public mode explicitly skips auth headers for CI/CD and public docs testing\n if (this.authToken && !this.publicMode) {\n headers['Authorization'] = `Bearer ${this.authToken}`;\n }\n\n // Always send session ID if we have one (even in public mode - server creates sessions)\n if (this.sessionId) {\n headers['mcp-session-id'] = this.sessionId;\n }\n\n // Add custom headers from config (allow override even in public mode)\n if (this.config.auth.headers) {\n Object.assign(headers, this.config.auth.headers);\n }\n\n return headers;\n }\n\n private ensureConnected(): void {\n if (this.state !== 'connected') {\n throw new Error('Transport not connected. Call connect() first.');\n }\n }\n\n private log(message: string, data?: unknown): void {\n if (this.config.debug) {\n console.log(`[StreamableHTTP] ${message}`, data ?? '');\n }\n }\n\n /**\n * Parse SSE (Server-Sent Events) response format with session ID extraction\n * SSE format is:\n * event: message\n * id: sessionId:messageId\n * data: {\"jsonrpc\":\"2.0\",...}\n *\n * The id field contains the session ID followed by a colon and the message ID.\n *\n * @param text - The raw SSE response text\n * @param requestId - The original request ID\n * @returns Object with parsed JSON-RPC response and session ID (if found)\n */\n private parseSSEResponseWithSession(\n text: string,\n requestId: string | number | undefined,\n ): { response: JsonRpcResponse; sseSessionId?: string } {\n const lines = text.split('\\n');\n const dataLines: string[] = [];\n let sseSessionId: string | undefined;\n\n // Collect all data lines and extract session ID from id: field\n for (const line of lines) {\n if (line.startsWith('data: ')) {\n dataLines.push(line.slice(6)); // Remove 'data: ' prefix\n } else if (line === 'data:') {\n // Empty data line represents a newline in the data\n dataLines.push('');\n } else if (line.startsWith('id: ')) {\n // Extract session ID from id field (format: sessionId:messageId)\n const idValue = line.slice(4);\n const colonIndex = idValue.lastIndexOf(':');\n if (colonIndex > 0) {\n sseSessionId = idValue.substring(0, colonIndex);\n } else {\n // No colon, use the whole id as session ID\n sseSessionId = idValue;\n }\n }\n }\n\n if (dataLines.length > 0) {\n const jsonData = dataLines.join('\\n');\n try {\n return {\n response: JSON.parse(jsonData) as JsonRpcResponse,\n sseSessionId,\n };\n } catch {\n this.log('Failed to parse SSE data as JSON:', jsonData);\n }\n }\n\n // Fallback: return error response\n return {\n response: {\n jsonrpc: '2.0',\n id: requestId ?? null,\n error: {\n code: -32700,\n message: 'Failed to parse SSE response',\n data: text,\n },\n },\n sseSessionId,\n };\n }\n}\n"]}
1
+ {"version":3,"file":"streamable-http.transport.js","sourceRoot":"","sources":["../../../src/transport/streamable-http.transport.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @file streamable-http.transport.ts\n * @description StreamableHTTP transport implementation for MCP Test Client\n */\n\nimport type {\n McpTransport,\n TransportConfig,\n TransportState,\n JsonRpcRequest,\n JsonRpcResponse,\n} from './transport.interface';\nimport type { InterceptorChain } from '../interceptor';\nimport type { ClientInfo } from '../client/mcp-test-client.types';\n\nconst DEFAULT_TIMEOUT = 30000;\n\n/**\n * StreamableHTTP transport for MCP communication\n *\n * This transport uses HTTP POST requests for all communication,\n * following the MCP StreamableHTTP specification.\n */\nexport class StreamableHttpTransport implements McpTransport {\n private readonly config: Required<Omit<TransportConfig, 'interceptors' | 'clientInfo'>> & {\n interceptors?: InterceptorChain;\n clientInfo?: ClientInfo;\n };\n private state: TransportState = 'disconnected';\n private sessionId: string | undefined;\n private authToken: string | undefined;\n private connectionCount = 0;\n private reconnectCount = 0;\n private lastRequestHeaders: Record<string, string> = {};\n private interceptors?: InterceptorChain;\n private readonly publicMode: boolean;\n\n constructor(config: TransportConfig) {\n this.config = {\n baseUrl: config.baseUrl.replace(/\\/$/, ''), // Remove trailing slash\n timeout: config.timeout ?? DEFAULT_TIMEOUT,\n auth: config.auth ?? {},\n publicMode: config.publicMode ?? false,\n debug: config.debug ?? false,\n interceptors: config.interceptors,\n clientInfo: config.clientInfo,\n };\n\n this.authToken = config.auth?.token;\n this.interceptors = config.interceptors;\n this.publicMode = config.publicMode ?? false;\n }\n\n async connect(): Promise<void> {\n this.state = 'connecting';\n this.connectionCount++;\n\n try {\n // Public mode: Skip all authentication - connect without any token\n if (this.publicMode) {\n this.log('Public mode: connecting without authentication');\n this.state = 'connected';\n return;\n }\n\n // If no auth token provided, request anonymous token from FrontMCP SDK\n if (!this.authToken) {\n await this.requestAnonymousToken();\n }\n\n // StreamableHTTP doesn't require an explicit connection step\n // The session is established on the first request\n this.state = 'connected';\n this.log('Connected to StreamableHTTP transport');\n } catch (error) {\n this.state = 'error';\n throw error;\n }\n }\n\n /**\n * Request an anonymous token from the FrontMCP OAuth endpoint\n * This allows the test client to authenticate without user interaction\n */\n private async requestAnonymousToken(): Promise<void> {\n const clientId = crypto.randomUUID();\n const tokenUrl = `${this.config.baseUrl}/oauth/token`;\n\n this.log(`Requesting anonymous token from ${tokenUrl}`);\n\n try {\n const response = await fetch(tokenUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n grant_type: 'anonymous',\n client_id: clientId,\n resource: this.config.baseUrl,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n this.log(`Failed to get anonymous token: ${response.status} ${errorText}`);\n // Continue without token - server may allow unauthenticated access\n return;\n }\n\n const tokenResponse = await response.json();\n if (tokenResponse.access_token) {\n this.authToken = tokenResponse.access_token;\n this.log('Anonymous token acquired successfully');\n }\n } catch (error) {\n this.log(`Error requesting anonymous token: ${error}`);\n // Continue without token - server may allow unauthenticated access\n }\n }\n\n async request<T = unknown>(message: JsonRpcRequest): Promise<JsonRpcResponse & { result?: T }> {\n this.ensureConnected();\n\n const startTime = Date.now();\n\n // Process through interceptors if available\n if (this.interceptors) {\n const interceptResult = await this.interceptors.processRequest(message, {\n timestamp: new Date(),\n transport: 'streamable-http',\n sessionId: this.sessionId,\n });\n\n switch (interceptResult.type) {\n case 'mock': {\n // Return mock response directly, run through response interceptors\n const mockResponse = await this.interceptors.processResponse(\n message,\n interceptResult.response,\n Date.now() - startTime,\n );\n return mockResponse as JsonRpcResponse & { result?: T };\n }\n\n case 'error':\n throw interceptResult.error;\n\n case 'continue':\n // Use possibly modified request\n message = interceptResult.request;\n break;\n }\n }\n\n const headers = this.buildHeaders();\n this.lastRequestHeaders = headers;\n\n const url = `${this.config.baseUrl}/`;\n this.log(`POST ${url}`, message);\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);\n\n try {\n const response = await fetch(url, {\n method: 'POST',\n headers,\n body: JSON.stringify(message),\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n // Check for session ID in response headers\n const newSessionId = response.headers.get('mcp-session-id');\n if (newSessionId) {\n this.sessionId = newSessionId;\n }\n\n let jsonResponse: JsonRpcResponse;\n\n if (!response.ok) {\n // Handle HTTP errors\n const errorText = await response.text();\n this.log(`HTTP Error ${response.status}: ${errorText}`);\n\n jsonResponse = {\n jsonrpc: '2.0',\n id: message.id ?? null,\n error: {\n code: -32000,\n message: `HTTP ${response.status}: ${response.statusText}`,\n data: errorText,\n },\n };\n } else {\n // Parse response - may be JSON or SSE\n const contentType = response.headers.get('content-type') ?? '';\n const text = await response.text();\n this.log('Response:', text);\n\n // Handle empty response (for notifications)\n if (!text.trim()) {\n jsonResponse = {\n jsonrpc: '2.0',\n id: message.id ?? null,\n result: undefined,\n };\n } else if (contentType.includes('text/event-stream')) {\n // Parse SSE response - extract data and session ID from event stream\n const { response: sseResponse, sseSessionId } = this.parseSSEResponseWithSession(text, message.id);\n jsonResponse = sseResponse;\n // Store session ID from SSE id field (format: sessionId:messageId)\n if (sseSessionId && !this.sessionId) {\n this.sessionId = sseSessionId;\n this.log('Session ID from SSE:', this.sessionId);\n }\n } else {\n jsonResponse = JSON.parse(text) as JsonRpcResponse;\n }\n }\n\n // Process response through interceptors\n if (this.interceptors) {\n jsonResponse = await this.interceptors.processResponse(message, jsonResponse, Date.now() - startTime);\n }\n\n return jsonResponse as JsonRpcResponse & { result?: T };\n } catch (error) {\n clearTimeout(timeoutId);\n\n if (error instanceof Error && error.name === 'AbortError') {\n return {\n jsonrpc: '2.0',\n id: message.id ?? null,\n error: {\n code: -32000,\n message: `Request timeout after ${this.config.timeout}ms`,\n },\n };\n }\n\n throw error;\n }\n }\n\n async notify(message: JsonRpcRequest): Promise<void> {\n this.ensureConnected();\n\n const headers = this.buildHeaders();\n this.lastRequestHeaders = headers;\n\n const url = `${this.config.baseUrl}/`;\n this.log(`POST ${url} (notification)`, message);\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);\n\n try {\n const response = await fetch(url, {\n method: 'POST',\n headers,\n body: JSON.stringify(message),\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n // Update session ID if present\n const newSessionId = response.headers.get('mcp-session-id');\n if (newSessionId) {\n this.sessionId = newSessionId;\n }\n\n if (!response.ok) {\n const errorText = await response.text();\n this.log(`HTTP Error ${response.status} on notification: ${errorText}`);\n }\n } catch (error) {\n clearTimeout(timeoutId);\n\n if (error instanceof Error && error.name !== 'AbortError') {\n throw error;\n }\n }\n }\n\n async sendRaw(data: string): Promise<JsonRpcResponse> {\n this.ensureConnected();\n\n const headers = this.buildHeaders();\n this.lastRequestHeaders = headers;\n\n const url = `${this.config.baseUrl}/`;\n this.log(`POST ${url} (raw)`, data);\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);\n\n try {\n const response = await fetch(url, {\n method: 'POST',\n headers,\n body: data,\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n const text = await response.text();\n\n if (!text.trim()) {\n return {\n jsonrpc: '2.0',\n id: null,\n error: {\n code: -32700,\n message: 'Parse error',\n },\n };\n }\n\n return JSON.parse(text) as JsonRpcResponse;\n } catch (error) {\n clearTimeout(timeoutId);\n\n return {\n jsonrpc: '2.0',\n id: null,\n error: {\n code: -32700,\n message: 'Parse error',\n data: error instanceof Error ? error.message : 'Unknown error',\n },\n };\n }\n }\n\n async close(): Promise<void> {\n this.state = 'disconnected';\n this.sessionId = undefined;\n this.log('StreamableHTTP transport closed');\n }\n\n isConnected(): boolean {\n return this.state === 'connected';\n }\n\n getState(): TransportState {\n return this.state;\n }\n\n getSessionId(): string | undefined {\n return this.sessionId;\n }\n\n setAuthToken(token: string): void {\n this.authToken = token;\n }\n\n setTimeout(ms: number): void {\n this.config.timeout = ms;\n }\n\n setInterceptors(interceptors: InterceptorChain): void {\n this.interceptors = interceptors;\n }\n\n getInterceptors(): InterceptorChain | undefined {\n return this.interceptors;\n }\n\n getConnectionCount(): number {\n return this.connectionCount;\n }\n\n getReconnectCount(): number {\n return this.reconnectCount;\n }\n\n getLastRequestHeaders(): Record<string, string> {\n return { ...this.lastRequestHeaders };\n }\n\n async simulateDisconnect(): Promise<void> {\n this.state = 'disconnected';\n this.sessionId = undefined;\n }\n\n async waitForReconnect(timeoutMs: number): Promise<void> {\n const deadline = Date.now() + timeoutMs;\n\n // Auto-reconnect\n this.reconnectCount++;\n await this.connect();\n\n while (Date.now() < deadline) {\n if (this.state === 'connected') {\n return;\n }\n await new Promise((r) => setTimeout(r, 50));\n }\n\n throw new Error('Timeout waiting for reconnection');\n }\n\n // ═══════════════════════════════════════════════════════════════════\n // PRIVATE HELPERS\n // ═══════════════════════════════════════════════════════════════════\n\n private buildHeaders(): Record<string, string> {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n Accept: 'application/json, text/event-stream',\n };\n\n // Add User-Agent header based on clientInfo for platform detection\n // Server uses this to detect the AI platform before MCP initialize\n if (this.config.clientInfo) {\n headers['User-Agent'] = `${this.config.clientInfo.name}/${this.config.clientInfo.version}`;\n }\n\n // Only add Authorization header if we have a token AND not in public mode\n // Public mode explicitly skips auth headers for CI/CD and public docs testing\n if (this.authToken && !this.publicMode) {\n headers['Authorization'] = `Bearer ${this.authToken}`;\n }\n\n // Always send session ID if we have one (even in public mode - server creates sessions)\n if (this.sessionId) {\n headers['mcp-session-id'] = this.sessionId;\n }\n\n // Add custom headers from config (allow override even in public mode)\n if (this.config.auth.headers) {\n Object.assign(headers, this.config.auth.headers);\n }\n\n return headers;\n }\n\n private ensureConnected(): void {\n if (this.state !== 'connected') {\n throw new Error('Transport not connected. Call connect() first.');\n }\n }\n\n private log(message: string, data?: unknown): void {\n if (this.config.debug) {\n console.log(`[StreamableHTTP] ${message}`, data ?? '');\n }\n }\n\n /**\n * Parse SSE (Server-Sent Events) response format with session ID extraction\n * SSE format is:\n * event: message\n * id: sessionId:messageId\n * data: {\"jsonrpc\":\"2.0\",...}\n *\n * The id field contains the session ID followed by a colon and the message ID.\n *\n * @param text - The raw SSE response text\n * @param requestId - The original request ID\n * @returns Object with parsed JSON-RPC response and session ID (if found)\n */\n private parseSSEResponseWithSession(\n text: string,\n requestId: string | number | undefined,\n ): { response: JsonRpcResponse; sseSessionId?: string } {\n const lines = text.split('\\n');\n const dataLines: string[] = [];\n let sseSessionId: string | undefined;\n\n // Collect all data lines and extract session ID from id: field\n for (const line of lines) {\n if (line.startsWith('data: ')) {\n dataLines.push(line.slice(6)); // Remove 'data: ' prefix\n } else if (line === 'data:') {\n // Empty data line represents a newline in the data\n dataLines.push('');\n } else if (line.startsWith('id: ')) {\n // Extract session ID from id field (format: sessionId:messageId)\n const idValue = line.slice(4);\n const colonIndex = idValue.lastIndexOf(':');\n if (colonIndex > 0) {\n sseSessionId = idValue.substring(0, colonIndex);\n } else {\n // No colon, use the whole id as session ID\n sseSessionId = idValue;\n }\n }\n }\n\n if (dataLines.length > 0) {\n const jsonData = dataLines.join('\\n');\n try {\n return {\n response: JSON.parse(jsonData) as JsonRpcResponse,\n sseSessionId,\n };\n } catch {\n this.log('Failed to parse SSE data as JSON:', jsonData);\n }\n }\n\n // Fallback: return error response\n return {\n response: {\n jsonrpc: '2.0',\n id: requestId ?? null,\n error: {\n code: -32700,\n message: 'Failed to parse SSE response',\n data: text,\n },\n },\n sseSessionId,\n };\n }\n}\n"]}
@@ -2,6 +2,7 @@
2
2
  * @file transport.interface.ts
3
3
  * @description Interface for MCP transport implementations
4
4
  */
5
+ import type { ClientInfo } from '../client/mcp-test-client.types';
5
6
  export interface JsonRpcRequest {
6
7
  jsonrpc: '2.0';
7
8
  id?: string | number;
@@ -121,4 +122,6 @@ export interface TransportConfig {
121
122
  debug?: boolean;
122
123
  /** Interceptor chain for request/response interception */
123
124
  interceptors?: import('../interceptor').InterceptorChain;
125
+ /** Client info for User-Agent header (enables platform detection on server) */
126
+ clientInfo?: ClientInfo;
124
127
  }
@@ -1 +1 @@
1
- {"version":3,"file":"transport.interface.js","sourceRoot":"","sources":["../../../src/transport/transport.interface.ts"],"names":[],"mappings":";AAAA;;;GAGG","sourcesContent":["/**\n * @file transport.interface.ts\n * @description Interface for MCP transport implementations\n */\n\n// Simplified JSON-RPC types for transport layer\nexport interface JsonRpcRequest {\n jsonrpc: '2.0';\n id?: string | number;\n method: string;\n params?: Record<string, unknown>;\n}\n\nexport interface JsonRpcResponse {\n jsonrpc: '2.0';\n id: string | number | null;\n result?: unknown;\n error?: {\n code: number;\n message: string;\n data?: unknown;\n };\n}\n\nexport type TransportState = 'disconnected' | 'connecting' | 'connected' | 'error';\n\n/**\n * Interface that all MCP transports must implement\n */\nexport interface McpTransport {\n /**\n * Connect to the MCP server\n */\n connect(): Promise<void>;\n\n /**\n * Send a JSON-RPC request and wait for response\n */\n request<T = unknown>(message: JsonRpcRequest): Promise<JsonRpcResponse & { result?: T }>;\n\n /**\n * Send a notification (no response expected)\n */\n notify(message: JsonRpcRequest): Promise<void>;\n\n /**\n * Send raw string data (for error testing)\n */\n sendRaw(data: string): Promise<JsonRpcResponse>;\n\n /**\n * Close the connection\n */\n close(): Promise<void>;\n\n /**\n * Check if transport is connected\n */\n isConnected(): boolean;\n\n /**\n * Get current transport state\n */\n getState(): TransportState;\n\n /**\n * Get the session ID (if applicable)\n */\n getSessionId(): string | undefined;\n\n /**\n * Set the authentication token\n */\n setAuthToken(token: string): void;\n\n /**\n * Set the request timeout\n */\n setTimeout(ms: number): void;\n\n // Optional methods for testing\n\n /**\n * Get the message endpoint URL (SSE transport)\n */\n getMessageEndpoint?(): string | undefined;\n\n /**\n * Get number of connections made\n */\n getConnectionCount?(): number;\n\n /**\n * Get number of reconnections\n */\n getReconnectCount?(): number;\n\n /**\n * Get the headers from the last request\n */\n getLastRequestHeaders?(): Record<string, string>;\n\n /**\n * Simulate a disconnect (for testing reconnection)\n */\n simulateDisconnect?(): Promise<void>;\n\n /**\n * Wait for reconnection to complete\n */\n waitForReconnect?(timeoutMs: number): Promise<void>;\n\n /**\n * Set interceptor chain for request/response interception\n */\n setInterceptors?(interceptors: import('../interceptor').InterceptorChain): void;\n\n /**\n * Get the current interceptor chain\n */\n getInterceptors?(): import('../interceptor').InterceptorChain | undefined;\n}\n\n/**\n * Configuration for transport implementations\n */\nexport interface TransportConfig {\n /** Base URL of the MCP server */\n baseUrl: string;\n /** Request timeout in milliseconds */\n timeout?: number;\n /** Authentication configuration */\n auth?: {\n token?: string;\n headers?: Record<string, string>;\n };\n /**\n * Enable public mode - skip authentication entirely.\n * When true, no Authorization header is sent and anonymous token is not requested.\n * Use this for testing public/unauthenticated endpoints in CI/CD pipelines.\n */\n publicMode?: boolean;\n /** Enable debug logging */\n debug?: boolean;\n /** Interceptor chain for request/response interception */\n interceptors?: import('../interceptor').InterceptorChain;\n}\n"]}
1
+ {"version":3,"file":"transport.interface.js","sourceRoot":"","sources":["../../../src/transport/transport.interface.ts"],"names":[],"mappings":";AAAA;;;GAGG","sourcesContent":["/**\n * @file transport.interface.ts\n * @description Interface for MCP transport implementations\n */\n\nimport type { ClientInfo } from '../client/mcp-test-client.types';\n\n// Simplified JSON-RPC types for transport layer\nexport interface JsonRpcRequest {\n jsonrpc: '2.0';\n id?: string | number;\n method: string;\n params?: Record<string, unknown>;\n}\n\nexport interface JsonRpcResponse {\n jsonrpc: '2.0';\n id: string | number | null;\n result?: unknown;\n error?: {\n code: number;\n message: string;\n data?: unknown;\n };\n}\n\nexport type TransportState = 'disconnected' | 'connecting' | 'connected' | 'error';\n\n/**\n * Interface that all MCP transports must implement\n */\nexport interface McpTransport {\n /**\n * Connect to the MCP server\n */\n connect(): Promise<void>;\n\n /**\n * Send a JSON-RPC request and wait for response\n */\n request<T = unknown>(message: JsonRpcRequest): Promise<JsonRpcResponse & { result?: T }>;\n\n /**\n * Send a notification (no response expected)\n */\n notify(message: JsonRpcRequest): Promise<void>;\n\n /**\n * Send raw string data (for error testing)\n */\n sendRaw(data: string): Promise<JsonRpcResponse>;\n\n /**\n * Close the connection\n */\n close(): Promise<void>;\n\n /**\n * Check if transport is connected\n */\n isConnected(): boolean;\n\n /**\n * Get current transport state\n */\n getState(): TransportState;\n\n /**\n * Get the session ID (if applicable)\n */\n getSessionId(): string | undefined;\n\n /**\n * Set the authentication token\n */\n setAuthToken(token: string): void;\n\n /**\n * Set the request timeout\n */\n setTimeout(ms: number): void;\n\n // Optional methods for testing\n\n /**\n * Get the message endpoint URL (SSE transport)\n */\n getMessageEndpoint?(): string | undefined;\n\n /**\n * Get number of connections made\n */\n getConnectionCount?(): number;\n\n /**\n * Get number of reconnections\n */\n getReconnectCount?(): number;\n\n /**\n * Get the headers from the last request\n */\n getLastRequestHeaders?(): Record<string, string>;\n\n /**\n * Simulate a disconnect (for testing reconnection)\n */\n simulateDisconnect?(): Promise<void>;\n\n /**\n * Wait for reconnection to complete\n */\n waitForReconnect?(timeoutMs: number): Promise<void>;\n\n /**\n * Set interceptor chain for request/response interception\n */\n setInterceptors?(interceptors: import('../interceptor').InterceptorChain): void;\n\n /**\n * Get the current interceptor chain\n */\n getInterceptors?(): import('../interceptor').InterceptorChain | undefined;\n}\n\n/**\n * Configuration for transport implementations\n */\nexport interface TransportConfig {\n /** Base URL of the MCP server */\n baseUrl: string;\n /** Request timeout in milliseconds */\n timeout?: number;\n /** Authentication configuration */\n auth?: {\n token?: string;\n headers?: Record<string, string>;\n };\n /**\n * Enable public mode - skip authentication entirely.\n * When true, no Authorization header is sent and anonymous token is not requested.\n * Use this for testing public/unauthenticated endpoints in CI/CD pipelines.\n */\n publicMode?: boolean;\n /** Enable debug logging */\n debug?: boolean;\n /** Interceptor chain for request/response interception */\n interceptors?: import('../interceptor').InterceptorChain;\n /** Client info for User-Agent header (enables platform detection on server) */\n clientInfo?: ClientInfo;\n}\n"]}
@@ -22,6 +22,7 @@
22
22
  * ```
23
23
  */
24
24
  import type { ToolResultWrapper } from '../client/mcp-test-client.types';
25
+ import type { TestPlatformType } from '../platform/platform-types';
25
26
  /**
26
27
  * UI-specific assertion helpers.
27
28
  * Use these for imperative-style assertions with detailed error messages.
@@ -91,4 +92,62 @@ export declare const UIAssertions: {
91
92
  * @throws Error if any validation fails
92
93
  */
93
94
  assertValidUI(result: ToolResultWrapper, boundKeys?: string[]): string;
95
+ /**
96
+ * Assert tool result has correct meta keys for OpenAI platform.
97
+ * Verifies openai/* keys are present and ui/*, frontmcp/* keys are absent.
98
+ * @param result - The tool result wrapper
99
+ * @throws Error if meta keys don't match OpenAI expectations
100
+ */
101
+ assertOpenAIMeta(result: ToolResultWrapper): void;
102
+ /**
103
+ * Assert tool result has correct meta keys for ext-apps platform (SEP-1865).
104
+ * Verifies ui/* keys are present and openai/*, frontmcp/* keys are absent.
105
+ * @param result - The tool result wrapper
106
+ * @throws Error if meta keys don't match ext-apps expectations
107
+ */
108
+ assertExtAppsMeta(result: ToolResultWrapper): void;
109
+ /**
110
+ * Assert tool result has correct meta keys for FrontMCP platforms (Claude, Cursor, etc.).
111
+ * Verifies frontmcp/* + ui/* keys are present and openai/* keys are absent.
112
+ * @param result - The tool result wrapper
113
+ * @throws Error if meta keys don't match FrontMCP expectations
114
+ */
115
+ assertFrontmcpMeta(result: ToolResultWrapper): void;
116
+ /**
117
+ * Assert tool result has correct meta keys for a specific platform.
118
+ * @param result - The tool result wrapper
119
+ * @param platform - The platform type to check for
120
+ * @throws Error if meta keys don't match platform expectations
121
+ */
122
+ assertPlatformMeta(result: ToolResultWrapper, platform: TestPlatformType): void;
123
+ /**
124
+ * Assert that no cross-namespace pollution exists in meta.
125
+ * @param result - The tool result wrapper
126
+ * @param expectedNamespace - The namespace that SHOULD be present
127
+ * @throws Error if other namespaces are found
128
+ */
129
+ assertNoMixedNamespaces(result: ToolResultWrapper, expectedNamespace: string): void;
130
+ /**
131
+ * Assert that _meta has the correct MIME type for a platform.
132
+ * @param result - The tool result wrapper
133
+ * @param platform - The platform type to check for
134
+ * @throws Error if MIME type doesn't match platform expectations
135
+ */
136
+ assertPlatformMimeType(result: ToolResultWrapper, platform: TestPlatformType): void;
137
+ /**
138
+ * Assert that _meta has HTML in the correct platform-specific key.
139
+ * @param result - The tool result wrapper
140
+ * @param platform - The platform type to check for
141
+ * @returns The HTML string
142
+ * @throws Error if HTML is missing or in wrong key
143
+ */
144
+ assertPlatformHtml(result: ToolResultWrapper, platform: TestPlatformType): string;
145
+ /**
146
+ * Comprehensive platform meta validation.
147
+ * @param result - The tool result wrapper
148
+ * @param platform - The platform type to validate for
149
+ * @returns The platform-specific HTML string
150
+ * @throws Error if any platform-specific validation fails
151
+ */
152
+ assertValidPlatformMeta(result: ToolResultWrapper, platform: TestPlatformType): string;
94
153
  };
@@ -24,6 +24,7 @@
24
24
  */
25
25
  Object.defineProperty(exports, "__esModule", { value: true });
26
26
  exports.UIAssertions = void 0;
27
+ const platform_types_1 = require("../platform/platform-types");
27
28
  // Type-only reference: Metadata keys used below align with UIMetadata from @frontmcp/ui/adapters
28
29
  // This is an optional peer dependency, so we don't import it directly
29
30
  // ═══════════════════════════════════════════════════════════════════
@@ -211,5 +212,156 @@ exports.UIAssertions = {
211
212
  }
212
213
  return html;
213
214
  },
215
+ // ═══════════════════════════════════════════════════════════════════
216
+ // PLATFORM META ASSERTIONS
217
+ // ═══════════════════════════════════════════════════════════════════
218
+ /**
219
+ * Assert tool result has correct meta keys for OpenAI platform.
220
+ * Verifies openai/* keys are present and ui/*, frontmcp/* keys are absent.
221
+ * @param result - The tool result wrapper
222
+ * @throws Error if meta keys don't match OpenAI expectations
223
+ */
224
+ assertOpenAIMeta(result) {
225
+ exports.UIAssertions.assertPlatformMeta(result, 'openai');
226
+ },
227
+ /**
228
+ * Assert tool result has correct meta keys for ext-apps platform (SEP-1865).
229
+ * Verifies ui/* keys are present and openai/*, frontmcp/* keys are absent.
230
+ * @param result - The tool result wrapper
231
+ * @throws Error if meta keys don't match ext-apps expectations
232
+ */
233
+ assertExtAppsMeta(result) {
234
+ exports.UIAssertions.assertPlatformMeta(result, 'ext-apps');
235
+ },
236
+ /**
237
+ * Assert tool result has correct meta keys for FrontMCP platforms (Claude, Cursor, etc.).
238
+ * Verifies frontmcp/* + ui/* keys are present and openai/* keys are absent.
239
+ * @param result - The tool result wrapper
240
+ * @throws Error if meta keys don't match FrontMCP expectations
241
+ */
242
+ assertFrontmcpMeta(result) {
243
+ exports.UIAssertions.assertPlatformMeta(result, 'claude');
244
+ },
245
+ /**
246
+ * Assert tool result has correct meta keys for a specific platform.
247
+ * @param result - The tool result wrapper
248
+ * @param platform - The platform type to check for
249
+ * @throws Error if meta keys don't match platform expectations
250
+ */
251
+ assertPlatformMeta(result, platform) {
252
+ const meta = result.raw._meta;
253
+ if (!meta) {
254
+ throw new Error(`Expected tool result to have _meta with platform meta for "${platform}"`);
255
+ }
256
+ const expectedPrefixes = (0, platform_types_1.getToolCallMetaPrefixes)(platform);
257
+ const forbiddenPrefixes = (0, platform_types_1.getForbiddenMetaPrefixes)(platform);
258
+ const metaKeys = Object.keys(meta);
259
+ // Check for expected prefixes
260
+ const hasExpectedPrefix = metaKeys.some((key) => expectedPrefixes.some((prefix) => key.startsWith(prefix)));
261
+ if (!hasExpectedPrefix) {
262
+ throw new Error(`Expected _meta to have keys with prefixes [${expectedPrefixes.join(', ')}] for platform "${platform}", ` +
263
+ `but found: [${metaKeys.join(', ')}]`);
264
+ }
265
+ // Check for forbidden prefixes
266
+ const forbiddenKeys = metaKeys.filter((key) => forbiddenPrefixes.some((prefix) => key.startsWith(prefix)));
267
+ if (forbiddenKeys.length > 0) {
268
+ throw new Error(`Expected _meta NOT to have keys [${forbiddenKeys.join(', ')}] for platform "${platform}" ` +
269
+ `(forbidden prefixes: [${forbiddenPrefixes.join(', ')}])`);
270
+ }
271
+ },
272
+ /**
273
+ * Assert that no cross-namespace pollution exists in meta.
274
+ * @param result - The tool result wrapper
275
+ * @param expectedNamespace - The namespace that SHOULD be present
276
+ * @throws Error if other namespaces are found
277
+ */
278
+ assertNoMixedNamespaces(result, expectedNamespace) {
279
+ const meta = result.raw._meta;
280
+ if (!meta) {
281
+ throw new Error(`Expected tool result to have _meta with namespace "${expectedNamespace}"`);
282
+ }
283
+ const metaKeys = Object.keys(meta);
284
+ const wrongKeys = metaKeys.filter((key) => !key.startsWith(expectedNamespace));
285
+ if (wrongKeys.length > 0) {
286
+ throw new Error(`Expected _meta to ONLY have keys with namespace "${expectedNamespace}", ` +
287
+ `but found: [${wrongKeys.join(', ')}]`);
288
+ }
289
+ },
290
+ /**
291
+ * Assert that _meta has the correct MIME type for a platform.
292
+ * @param result - The tool result wrapper
293
+ * @param platform - The platform type to check for
294
+ * @throws Error if MIME type doesn't match platform expectations
295
+ */
296
+ assertPlatformMimeType(result, platform) {
297
+ const meta = result.raw._meta;
298
+ const expectedMimeType = (0, platform_types_1.getPlatformMimeType)(platform);
299
+ if (!meta) {
300
+ throw new Error(`Expected tool result to have _meta with MIME type for platform "${platform}"`);
301
+ }
302
+ // Determine which key to check based on platform
303
+ let mimeTypeKey;
304
+ switch (platform) {
305
+ case 'openai':
306
+ mimeTypeKey = 'openai/mimeType';
307
+ break;
308
+ case 'ext-apps':
309
+ mimeTypeKey = 'ui/mimeType';
310
+ break;
311
+ default:
312
+ mimeTypeKey = 'frontmcp/mimeType';
313
+ }
314
+ const actualMimeType = meta[mimeTypeKey];
315
+ if (actualMimeType !== expectedMimeType) {
316
+ throw new Error(`Expected _meta["${mimeTypeKey}"] to be "${expectedMimeType}" for platform "${platform}", ` +
317
+ `but got "${actualMimeType}"`);
318
+ }
319
+ },
320
+ /**
321
+ * Assert that _meta has HTML in the correct platform-specific key.
322
+ * @param result - The tool result wrapper
323
+ * @param platform - The platform type to check for
324
+ * @returns The HTML string
325
+ * @throws Error if HTML is missing or in wrong key
326
+ */
327
+ assertPlatformHtml(result, platform) {
328
+ const meta = result.raw._meta;
329
+ if (!meta) {
330
+ throw new Error(`Expected tool result to have _meta with platform HTML for "${platform}"`);
331
+ }
332
+ // Determine which key to check based on platform
333
+ let htmlKey;
334
+ switch (platform) {
335
+ case 'openai':
336
+ htmlKey = 'openai/html';
337
+ break;
338
+ case 'ext-apps':
339
+ htmlKey = 'ui/html';
340
+ break;
341
+ default:
342
+ htmlKey = 'frontmcp/html';
343
+ }
344
+ const html = meta[htmlKey];
345
+ if (typeof html !== 'string' || html.length === 0) {
346
+ throw new Error(`Expected _meta["${htmlKey}"] to contain HTML for platform "${platform}", ` +
347
+ `but ${html === undefined ? 'key not found' : `got ${typeof html}`}`);
348
+ }
349
+ return html;
350
+ },
351
+ /**
352
+ * Comprehensive platform meta validation.
353
+ * @param result - The tool result wrapper
354
+ * @param platform - The platform type to validate for
355
+ * @returns The platform-specific HTML string
356
+ * @throws Error if any platform-specific validation fails
357
+ */
358
+ assertValidPlatformMeta(result, platform) {
359
+ // 1. Check correct namespace keys
360
+ exports.UIAssertions.assertPlatformMeta(result, platform);
361
+ // 2. Check MIME type
362
+ exports.UIAssertions.assertPlatformMimeType(result, platform);
363
+ // 3. Get and return HTML
364
+ return exports.UIAssertions.assertPlatformHtml(result, platform);
365
+ },
214
366
  };
215
367
  //# sourceMappingURL=ui-assertions.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ui-assertions.js","sourceRoot":"","sources":["../../../src/ui/ui-assertions.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @file ui-assertions.ts\n * @description UI-specific assertion helpers for testing tool UI responses\n *\n * The metadata keys used in these assertions align with the UIMetadata interface\n * from @frontmcp/ui/adapters. Key fields include:\n * - `ui/html`: Inline rendered HTML (universal)\n * - `ui/mimeType`: MIME type for the HTML content\n * - `openai/outputTemplate`: Resource URI for widget template (OpenAI)\n * - `openai/widgetAccessible`: Whether widget can invoke tools (OpenAI)\n *\n * @see {@link https://docs.agentfront.dev/docs/servers/tools#tool-ui | Tool UI Documentation}\n *\n * @example\n * ```typescript\n * import { UIAssertions } from '@frontmcp/testing';\n *\n * const result = await client.tools.call('my-tool', {});\n * const html = UIAssertions.assertRenderedUI(result);\n * UIAssertions.assertXssSafe(html);\n * UIAssertions.assertDataBinding(html, result.json(), ['location', 'temperature']);\n * ```\n */\n\nimport type { ToolResultWrapper } from '../client/mcp-test-client.types';\n\n// Type-only reference: Metadata keys used below align with UIMetadata from @frontmcp/ui/adapters\n// This is an optional peer dependency, so we don't import it directly\n\n// ═══════════════════════════════════════════════════════════════════\n// HELPER FUNCTIONS\n// ═══════════════════════════════════════════════════════════════════\n\n/**\n * Escape special regex metacharacters in a string.\n * This prevents user-provided tag/class names from being interpreted as regex patterns.\n */\nfunction escapeRegex(str: string): string {\n return str.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\n// ═══════════════════════════════════════════════════════════════════\n// UI ASSERTIONS\n// ═══════════════════════════════════════════════════════════════════\n\n/**\n * UI-specific assertion helpers.\n * Use these for imperative-style assertions with detailed error messages.\n */\nexport const UIAssertions = {\n /**\n * Assert tool result has valid rendered UI HTML.\n * @param result - The tool result wrapper\n * @returns The rendered HTML string\n * @throws Error if no UI HTML found or if mdx-fallback detected\n */\n assertRenderedUI(result: ToolResultWrapper): string {\n const meta = result.raw._meta as Record<string, unknown> | undefined;\n\n if (!meta) {\n throw new Error('Expected tool result to have _meta, but _meta is undefined');\n }\n\n const html = meta['ui/html'];\n\n if (!html) {\n throw new Error('Expected tool result to have ui/html in _meta, but it is missing');\n }\n\n if (typeof html !== 'string') {\n throw new Error(`Expected ui/html to be a string, but got ${typeof html}`);\n }\n\n if (html.includes('mdx-fallback')) {\n throw new Error(\n 'Got mdx-fallback instead of rendered HTML - MDX/React rendering failed. ' +\n 'Check that @mdx-js/mdx is installed and the template syntax is valid.',\n );\n }\n\n return html;\n },\n\n /**\n * Assert HTML contains all expected bound values from tool output.\n * @param html - The rendered HTML string\n * @param output - The tool output object\n * @param keys - Array of keys whose values should appear in the HTML\n * @throws Error if any expected value is missing from the HTML\n */\n assertDataBinding(html: string, output: Record<string, unknown>, keys: string[]): void {\n const missingKeys: string[] = [];\n\n for (const key of keys) {\n const value = output[key];\n if (value === undefined || value === null) {\n continue; // Skip undefined/null values\n }\n\n const stringValue = String(value);\n if (!html.includes(stringValue)) {\n missingKeys.push(`${key}=\"${stringValue}\"`);\n }\n }\n\n if (missingKeys.length > 0) {\n throw new Error(\n `Expected HTML to contain bound values for: ${missingKeys.join(', ')}. ` + 'Data binding may have failed.',\n );\n }\n },\n\n /**\n * Assert HTML is XSS-safe (no scripts, event handlers, or javascript: URIs).\n * @param html - The rendered HTML string\n * @throws Error if potential XSS vulnerabilities are detected\n */\n assertXssSafe(html: string): void {\n const vulnerabilities: string[] = [];\n\n if (/<script[\\s>]/i.test(html)) {\n vulnerabilities.push('<script> tag detected');\n }\n\n if (/\\son\\w+\\s*=/i.test(html)) {\n vulnerabilities.push('inline event handler detected (onclick, onerror, etc.)');\n }\n\n if (/javascript:/i.test(html)) {\n vulnerabilities.push('javascript: URI detected');\n }\n\n if (vulnerabilities.length > 0) {\n throw new Error(`Potential XSS vulnerabilities found: ${vulnerabilities.join('; ')}`);\n }\n },\n\n /**\n * Assert HTML has proper structure (not escaped raw content).\n * @param html - The rendered HTML string\n * @throws Error if HTML appears to be raw/unrendered content\n */\n assertProperHtmlStructure(html: string): void {\n // Check for escaped HTML entities that suggest content wasn't rendered\n if (html.includes('&lt;') && html.includes('&gt;')) {\n throw new Error(\n 'HTML contains escaped HTML entities (&lt;, &gt;) - content was likely not rendered. ' +\n 'Check that the template is being processed correctly.',\n );\n }\n\n // Check that there's at least one HTML tag\n if (!/<[a-z]/i.test(html)) {\n throw new Error('HTML contains no HTML tags - content may be plain text or rendering failed.');\n }\n },\n\n /**\n * Assert HTML contains a specific element.\n * @param html - The rendered HTML string\n * @param tag - The HTML tag name to look for\n * @throws Error if the element is not found\n */\n assertContainsElement(html: string, tag: string): void {\n // Escape regex metacharacters to prevent user input from breaking the regex\n const regex = new RegExp(`<${escapeRegex(tag)}[\\\\s>]`, 'i');\n if (!regex.test(html)) {\n throw new Error(`Expected HTML to contain <${tag}> element`);\n }\n },\n\n /**\n * Assert HTML contains a specific CSS class.\n * @param html - The rendered HTML string\n * @param className - The CSS class name to look for\n * @throws Error if the class is not found\n */\n assertHasCssClass(html: string, className: string): void {\n // Escape regex metacharacters to prevent user input from breaking the regex\n const classRegex = new RegExp(`class(?:Name)?\\\\s*=\\\\s*[\"'][^\"']*\\\\b${escapeRegex(className)}\\\\b[^\"']*[\"']`, 'i');\n if (!classRegex.test(html)) {\n throw new Error(`Expected HTML to have CSS class \"${className}\"`);\n }\n },\n\n /**\n * Assert HTML does NOT contain specific content.\n * Useful for verifying custom components were rendered, not left as raw tags.\n * @param html - The rendered HTML string\n * @param content - The content that should NOT appear\n * @throws Error if the content is found\n */\n assertNotContainsRaw(html: string, content: string): void {\n if (html.includes(content)) {\n throw new Error(\n `HTML contains raw content \"${content}\" - this component may not have been rendered. ` +\n 'Check that all custom components are properly passed to the renderer.',\n );\n }\n },\n\n /**\n * Assert that widget metadata is present in the result.\n * Checks for ui/html, openai/outputTemplate, or ui/mimeType.\n * @param result - The tool result wrapper\n * @throws Error if widget metadata is missing\n */\n assertWidgetMetadata(result: ToolResultWrapper): void {\n const meta = result.raw._meta as Record<string, unknown> | undefined;\n\n if (!meta) {\n throw new Error('Expected tool result to have _meta with widget metadata');\n }\n\n // Check for any widget-related metadata fields (aligned with toHaveWidgetMetadata matcher)\n const hasUiHtml = Boolean(meta['ui/html']);\n const hasOutputTemplate = Boolean(meta['openai/outputTemplate']);\n const hasMimeType = Boolean(meta['ui/mimeType']);\n\n if (!hasUiHtml && !hasOutputTemplate && !hasMimeType) {\n throw new Error('Expected _meta to have widget metadata (ui/html, openai/outputTemplate, or ui/mimeType)');\n }\n },\n\n /**\n * Comprehensive UI validation that runs all checks.\n * @param result - The tool result wrapper\n * @param boundKeys - Optional array of output keys to check for data binding\n * @returns The rendered HTML string\n * @throws Error if any validation fails\n */\n assertValidUI(result: ToolResultWrapper, boundKeys?: string[]): string {\n // 1. Get and validate HTML exists\n const html = UIAssertions.assertRenderedUI(result);\n\n // 2. Check HTML structure\n UIAssertions.assertProperHtmlStructure(html);\n\n // 3. Check XSS safety\n UIAssertions.assertXssSafe(html);\n\n // 4. Check data binding if keys provided\n if (boundKeys && boundKeys.length > 0) {\n try {\n const output = JSON.parse(result.text() || '{}');\n UIAssertions.assertDataBinding(html, output, boundKeys);\n } catch {\n // If we can't parse output, skip data binding check\n }\n }\n\n return html;\n },\n};\n"]}
1
+ {"version":3,"file":"ui-assertions.js","sourceRoot":"","sources":["../../../src/ui/ui-assertions.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @file ui-assertions.ts\n * @description UI-specific assertion helpers for testing tool UI responses\n *\n * The metadata keys used in these assertions align with the UIMetadata interface\n * from @frontmcp/ui/adapters. Key fields include:\n * - `ui/html`: Inline rendered HTML (universal)\n * - `ui/mimeType`: MIME type for the HTML content\n * - `openai/outputTemplate`: Resource URI for widget template (OpenAI)\n * - `openai/widgetAccessible`: Whether widget can invoke tools (OpenAI)\n *\n * @see {@link https://docs.agentfront.dev/docs/servers/tools#tool-ui | Tool UI Documentation}\n *\n * @example\n * ```typescript\n * import { UIAssertions } from '@frontmcp/testing';\n *\n * const result = await client.tools.call('my-tool', {});\n * const html = UIAssertions.assertRenderedUI(result);\n * UIAssertions.assertXssSafe(html);\n * UIAssertions.assertDataBinding(html, result.json(), ['location', 'temperature']);\n * ```\n */\n\nimport type { ToolResultWrapper } from '../client/mcp-test-client.types';\nimport type { TestPlatformType } from '../platform/platform-types';\nimport { getForbiddenMetaPrefixes, getToolCallMetaPrefixes, getPlatformMimeType } from '../platform/platform-types';\n\n// Type-only reference: Metadata keys used below align with UIMetadata from @frontmcp/ui/adapters\n// This is an optional peer dependency, so we don't import it directly\n\n// ═══════════════════════════════════════════════════════════════════\n// HELPER FUNCTIONS\n// ═══════════════════════════════════════════════════════════════════\n\n/**\n * Escape special regex metacharacters in a string.\n * This prevents user-provided tag/class names from being interpreted as regex patterns.\n */\nfunction escapeRegex(str: string): string {\n return str.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\n// ═══════════════════════════════════════════════════════════════════\n// UI ASSERTIONS\n// ═══════════════════════════════════════════════════════════════════\n\n/**\n * UI-specific assertion helpers.\n * Use these for imperative-style assertions with detailed error messages.\n */\nexport const UIAssertions = {\n /**\n * Assert tool result has valid rendered UI HTML.\n * @param result - The tool result wrapper\n * @returns The rendered HTML string\n * @throws Error if no UI HTML found or if mdx-fallback detected\n */\n assertRenderedUI(result: ToolResultWrapper): string {\n const meta = result.raw._meta as Record<string, unknown> | undefined;\n\n if (!meta) {\n throw new Error('Expected tool result to have _meta, but _meta is undefined');\n }\n\n const html = meta['ui/html'];\n\n if (!html) {\n throw new Error('Expected tool result to have ui/html in _meta, but it is missing');\n }\n\n if (typeof html !== 'string') {\n throw new Error(`Expected ui/html to be a string, but got ${typeof html}`);\n }\n\n if (html.includes('mdx-fallback')) {\n throw new Error(\n 'Got mdx-fallback instead of rendered HTML - MDX/React rendering failed. ' +\n 'Check that @mdx-js/mdx is installed and the template syntax is valid.',\n );\n }\n\n return html;\n },\n\n /**\n * Assert HTML contains all expected bound values from tool output.\n * @param html - The rendered HTML string\n * @param output - The tool output object\n * @param keys - Array of keys whose values should appear in the HTML\n * @throws Error if any expected value is missing from the HTML\n */\n assertDataBinding(html: string, output: Record<string, unknown>, keys: string[]): void {\n const missingKeys: string[] = [];\n\n for (const key of keys) {\n const value = output[key];\n if (value === undefined || value === null) {\n continue; // Skip undefined/null values\n }\n\n const stringValue = String(value);\n if (!html.includes(stringValue)) {\n missingKeys.push(`${key}=\"${stringValue}\"`);\n }\n }\n\n if (missingKeys.length > 0) {\n throw new Error(\n `Expected HTML to contain bound values for: ${missingKeys.join(', ')}. ` + 'Data binding may have failed.',\n );\n }\n },\n\n /**\n * Assert HTML is XSS-safe (no scripts, event handlers, or javascript: URIs).\n * @param html - The rendered HTML string\n * @throws Error if potential XSS vulnerabilities are detected\n */\n assertXssSafe(html: string): void {\n const vulnerabilities: string[] = [];\n\n if (/<script[\\s>]/i.test(html)) {\n vulnerabilities.push('<script> tag detected');\n }\n\n if (/\\son\\w+\\s*=/i.test(html)) {\n vulnerabilities.push('inline event handler detected (onclick, onerror, etc.)');\n }\n\n if (/javascript:/i.test(html)) {\n vulnerabilities.push('javascript: URI detected');\n }\n\n if (vulnerabilities.length > 0) {\n throw new Error(`Potential XSS vulnerabilities found: ${vulnerabilities.join('; ')}`);\n }\n },\n\n /**\n * Assert HTML has proper structure (not escaped raw content).\n * @param html - The rendered HTML string\n * @throws Error if HTML appears to be raw/unrendered content\n */\n assertProperHtmlStructure(html: string): void {\n // Check for escaped HTML entities that suggest content wasn't rendered\n if (html.includes('&lt;') && html.includes('&gt;')) {\n throw new Error(\n 'HTML contains escaped HTML entities (&lt;, &gt;) - content was likely not rendered. ' +\n 'Check that the template is being processed correctly.',\n );\n }\n\n // Check that there's at least one HTML tag\n if (!/<[a-z]/i.test(html)) {\n throw new Error('HTML contains no HTML tags - content may be plain text or rendering failed.');\n }\n },\n\n /**\n * Assert HTML contains a specific element.\n * @param html - The rendered HTML string\n * @param tag - The HTML tag name to look for\n * @throws Error if the element is not found\n */\n assertContainsElement(html: string, tag: string): void {\n // Escape regex metacharacters to prevent user input from breaking the regex\n const regex = new RegExp(`<${escapeRegex(tag)}[\\\\s>]`, 'i');\n if (!regex.test(html)) {\n throw new Error(`Expected HTML to contain <${tag}> element`);\n }\n },\n\n /**\n * Assert HTML contains a specific CSS class.\n * @param html - The rendered HTML string\n * @param className - The CSS class name to look for\n * @throws Error if the class is not found\n */\n assertHasCssClass(html: string, className: string): void {\n // Escape regex metacharacters to prevent user input from breaking the regex\n const classRegex = new RegExp(`class(?:Name)?\\\\s*=\\\\s*[\"'][^\"']*\\\\b${escapeRegex(className)}\\\\b[^\"']*[\"']`, 'i');\n if (!classRegex.test(html)) {\n throw new Error(`Expected HTML to have CSS class \"${className}\"`);\n }\n },\n\n /**\n * Assert HTML does NOT contain specific content.\n * Useful for verifying custom components were rendered, not left as raw tags.\n * @param html - The rendered HTML string\n * @param content - The content that should NOT appear\n * @throws Error if the content is found\n */\n assertNotContainsRaw(html: string, content: string): void {\n if (html.includes(content)) {\n throw new Error(\n `HTML contains raw content \"${content}\" - this component may not have been rendered. ` +\n 'Check that all custom components are properly passed to the renderer.',\n );\n }\n },\n\n /**\n * Assert that widget metadata is present in the result.\n * Checks for ui/html, openai/outputTemplate, or ui/mimeType.\n * @param result - The tool result wrapper\n * @throws Error if widget metadata is missing\n */\n assertWidgetMetadata(result: ToolResultWrapper): void {\n const meta = result.raw._meta as Record<string, unknown> | undefined;\n\n if (!meta) {\n throw new Error('Expected tool result to have _meta with widget metadata');\n }\n\n // Check for any widget-related metadata fields (aligned with toHaveWidgetMetadata matcher)\n const hasUiHtml = Boolean(meta['ui/html']);\n const hasOutputTemplate = Boolean(meta['openai/outputTemplate']);\n const hasMimeType = Boolean(meta['ui/mimeType']);\n\n if (!hasUiHtml && !hasOutputTemplate && !hasMimeType) {\n throw new Error('Expected _meta to have widget metadata (ui/html, openai/outputTemplate, or ui/mimeType)');\n }\n },\n\n /**\n * Comprehensive UI validation that runs all checks.\n * @param result - The tool result wrapper\n * @param boundKeys - Optional array of output keys to check for data binding\n * @returns The rendered HTML string\n * @throws Error if any validation fails\n */\n assertValidUI(result: ToolResultWrapper, boundKeys?: string[]): string {\n // 1. Get and validate HTML exists\n const html = UIAssertions.assertRenderedUI(result);\n\n // 2. Check HTML structure\n UIAssertions.assertProperHtmlStructure(html);\n\n // 3. Check XSS safety\n UIAssertions.assertXssSafe(html);\n\n // 4. Check data binding if keys provided\n if (boundKeys && boundKeys.length > 0) {\n try {\n const output = JSON.parse(result.text() || '{}');\n UIAssertions.assertDataBinding(html, output, boundKeys);\n } catch {\n // If we can't parse output, skip data binding check\n }\n }\n\n return html;\n },\n\n // ═══════════════════════════════════════════════════════════════════\n // PLATFORM META ASSERTIONS\n // ═══════════════════════════════════════════════════════════════════\n\n /**\n * Assert tool result has correct meta keys for OpenAI platform.\n * Verifies openai/* keys are present and ui/*, frontmcp/* keys are absent.\n * @param result - The tool result wrapper\n * @throws Error if meta keys don't match OpenAI expectations\n */\n assertOpenAIMeta(result: ToolResultWrapper): void {\n UIAssertions.assertPlatformMeta(result, 'openai');\n },\n\n /**\n * Assert tool result has correct meta keys for ext-apps platform (SEP-1865).\n * Verifies ui/* keys are present and openai/*, frontmcp/* keys are absent.\n * @param result - The tool result wrapper\n * @throws Error if meta keys don't match ext-apps expectations\n */\n assertExtAppsMeta(result: ToolResultWrapper): void {\n UIAssertions.assertPlatformMeta(result, 'ext-apps');\n },\n\n /**\n * Assert tool result has correct meta keys for FrontMCP platforms (Claude, Cursor, etc.).\n * Verifies frontmcp/* + ui/* keys are present and openai/* keys are absent.\n * @param result - The tool result wrapper\n * @throws Error if meta keys don't match FrontMCP expectations\n */\n assertFrontmcpMeta(result: ToolResultWrapper): void {\n UIAssertions.assertPlatformMeta(result, 'claude');\n },\n\n /**\n * Assert tool result has correct meta keys for a specific platform.\n * @param result - The tool result wrapper\n * @param platform - The platform type to check for\n * @throws Error if meta keys don't match platform expectations\n */\n assertPlatformMeta(result: ToolResultWrapper, platform: TestPlatformType): void {\n const meta = result.raw._meta as Record<string, unknown> | undefined;\n\n if (!meta) {\n throw new Error(`Expected tool result to have _meta with platform meta for \"${platform}\"`);\n }\n\n const expectedPrefixes = getToolCallMetaPrefixes(platform);\n const forbiddenPrefixes = getForbiddenMetaPrefixes(platform);\n const metaKeys = Object.keys(meta);\n\n // Check for expected prefixes\n const hasExpectedPrefix = metaKeys.some((key) => expectedPrefixes.some((prefix) => key.startsWith(prefix)));\n\n if (!hasExpectedPrefix) {\n throw new Error(\n `Expected _meta to have keys with prefixes [${expectedPrefixes.join(', ')}] for platform \"${platform}\", ` +\n `but found: [${metaKeys.join(', ')}]`,\n );\n }\n\n // Check for forbidden prefixes\n const forbiddenKeys = metaKeys.filter((key) => forbiddenPrefixes.some((prefix) => key.startsWith(prefix)));\n\n if (forbiddenKeys.length > 0) {\n throw new Error(\n `Expected _meta NOT to have keys [${forbiddenKeys.join(', ')}] for platform \"${platform}\" ` +\n `(forbidden prefixes: [${forbiddenPrefixes.join(', ')}])`,\n );\n }\n },\n\n /**\n * Assert that no cross-namespace pollution exists in meta.\n * @param result - The tool result wrapper\n * @param expectedNamespace - The namespace that SHOULD be present\n * @throws Error if other namespaces are found\n */\n assertNoMixedNamespaces(result: ToolResultWrapper, expectedNamespace: string): void {\n const meta = result.raw._meta as Record<string, unknown> | undefined;\n\n if (!meta) {\n throw new Error(`Expected tool result to have _meta with namespace \"${expectedNamespace}\"`);\n }\n\n const metaKeys = Object.keys(meta);\n const wrongKeys = metaKeys.filter((key) => !key.startsWith(expectedNamespace));\n\n if (wrongKeys.length > 0) {\n throw new Error(\n `Expected _meta to ONLY have keys with namespace \"${expectedNamespace}\", ` +\n `but found: [${wrongKeys.join(', ')}]`,\n );\n }\n },\n\n /**\n * Assert that _meta has the correct MIME type for a platform.\n * @param result - The tool result wrapper\n * @param platform - The platform type to check for\n * @throws Error if MIME type doesn't match platform expectations\n */\n assertPlatformMimeType(result: ToolResultWrapper, platform: TestPlatformType): void {\n const meta = result.raw._meta as Record<string, unknown> | undefined;\n const expectedMimeType = getPlatformMimeType(platform);\n\n if (!meta) {\n throw new Error(`Expected tool result to have _meta with MIME type for platform \"${platform}\"`);\n }\n\n // Determine which key to check based on platform\n let mimeTypeKey: string;\n switch (platform) {\n case 'openai':\n mimeTypeKey = 'openai/mimeType';\n break;\n case 'ext-apps':\n mimeTypeKey = 'ui/mimeType';\n break;\n default:\n mimeTypeKey = 'frontmcp/mimeType';\n }\n\n const actualMimeType = meta[mimeTypeKey];\n\n if (actualMimeType !== expectedMimeType) {\n throw new Error(\n `Expected _meta[\"${mimeTypeKey}\"] to be \"${expectedMimeType}\" for platform \"${platform}\", ` +\n `but got \"${actualMimeType}\"`,\n );\n }\n },\n\n /**\n * Assert that _meta has HTML in the correct platform-specific key.\n * @param result - The tool result wrapper\n * @param platform - The platform type to check for\n * @returns The HTML string\n * @throws Error if HTML is missing or in wrong key\n */\n assertPlatformHtml(result: ToolResultWrapper, platform: TestPlatformType): string {\n const meta = result.raw._meta as Record<string, unknown> | undefined;\n\n if (!meta) {\n throw new Error(`Expected tool result to have _meta with platform HTML for \"${platform}\"`);\n }\n\n // Determine which key to check based on platform\n let htmlKey: string;\n switch (platform) {\n case 'openai':\n htmlKey = 'openai/html';\n break;\n case 'ext-apps':\n htmlKey = 'ui/html';\n break;\n default:\n htmlKey = 'frontmcp/html';\n }\n\n const html = meta[htmlKey];\n\n if (typeof html !== 'string' || html.length === 0) {\n throw new Error(\n `Expected _meta[\"${htmlKey}\"] to contain HTML for platform \"${platform}\", ` +\n `but ${html === undefined ? 'key not found' : `got ${typeof html}`}`,\n );\n }\n\n return html;\n },\n\n /**\n * Comprehensive platform meta validation.\n * @param result - The tool result wrapper\n * @param platform - The platform type to validate for\n * @returns The platform-specific HTML string\n * @throws Error if any platform-specific validation fails\n */\n assertValidPlatformMeta(result: ToolResultWrapper, platform: TestPlatformType): string {\n // 1. Check correct namespace keys\n UIAssertions.assertPlatformMeta(result, platform);\n\n // 2. Check MIME type\n UIAssertions.assertPlatformMimeType(result, platform);\n\n // 3. Get and return HTML\n return UIAssertions.assertPlatformHtml(result, platform);\n },\n};\n"]}
@@ -24,6 +24,7 @@
24
24
  * ```
25
25
  */
26
26
  import type { MatcherFunction } from 'expect';
27
+ import type { TestPlatformType } from '../platform/platform-types';
27
28
  /**
28
29
  * All UI matchers as an object for expect.extend()
29
30
  */
@@ -36,4 +37,11 @@ export declare const uiMatchers: {
36
37
  toHaveCssClass: MatcherFunction<[className: string]>;
37
38
  toNotContainRawContent: MatcherFunction<[content: string]>;
38
39
  toHaveProperHtmlStructure: MatcherFunction<[]>;
40
+ toHavePlatformMeta: MatcherFunction<[platform: TestPlatformType]>;
41
+ toHaveMetaKey: MatcherFunction<[key: string]>;
42
+ toHaveMetaValue: MatcherFunction<[key: string, value: unknown]>;
43
+ toNotHaveMetaKey: MatcherFunction<[key: string]>;
44
+ toHaveOnlyNamespacedMeta: MatcherFunction<[namespace: string]>;
45
+ toHavePlatformMimeType: MatcherFunction<[platform: TestPlatformType]>;
46
+ toHavePlatformHtml: MatcherFunction<[platform: TestPlatformType]>;
39
47
  };