Validates MCP servers for UI extension (SEP-1865) support: connects via StreamableHTTP or SSE, discovers ui:// resources, validates HTML content, CSP, permissions, display modes, theming, tool linkage, graceful degradation, measures latency, and returns a structured snapshot with 12 boolean categories and 18 entry fields.
git clone https://github.com/agentprobe/mcp-apps-validator.git
cd mcp-apps-validator
npm iimport { McpAppsValidator } from 'mcp-apps-validator'
const { status, findings, categories, entries } = await McpAppsValidator.start( {
endpoint: 'https://your-mcp-server.example.com/mcp',
timeout: 15000
} )- Connects to MCP servers via StreamableHTTP with SSE fallback
- Discovers
ui://resources and tool-UI linkage via_meta.ui.resourceUri - Validates HTML content of UI resources
- Checks CSP (Content Security Policy) configuration
- Validates permissions declarations
- Detects display mode support (embedded, panel, fullscreen, popup)
- Checks theme/style variable support
- Verifies graceful degradation with text fallbacks for non-UI hosts
- Classifies 12 boolean categories (reachable, MCP, MCP Apps, UI resources, tool linkage, theming, display modes)
- Measures listResources and readResource latency
- Compares two snapshots and produces a structured diff
- Returns empty snapshot with all-false categories on connection failure
The validation pipeline processes an MCP server in six sequential steps:
flowchart LR
A[endpoint] --> B[McpAppsConnector.connect]
B --> C[McpAppsConnector.discover]
C --> D[UiResourceValidator.validate]
D --> E[CapabilityClassifier.classify]
E --> F[SnapshotBuilder.build]
All methods are static. Parameters are passed as objects, return values are objects.
Connects to an MCP server, discovers UI resources, validates HTML content, CSP, permissions, display modes, theming, tool linkage, measures latency, and returns a structured snapshot.
Method
.start( { endpoint, timeout } )
| Key | Type | Description | Required |
|---|---|---|---|
| endpoint | string | URL of the MCP server. Example 'https://server.example.com/mcp' |
Yes |
| timeout | number | Connection timeout in milliseconds. Default 10000 |
No |
Example
import { McpAppsValidator } from 'mcp-apps-validator'
const { status, findings, categories, entries } = await McpAppsValidator.start( {
endpoint: 'https://your-mcp-server.example.com/mcp',
timeout: 15000
} )
console.log( `Status: ${status ? 'PASS' : 'FAIL'}` )
console.log( `UI Resources: ${entries['uiResourceCount']}` )
console.log( `MCP Apps: ${categories['supportsMcpApps']}` )
console.log( `Extension: ${entries['extensionVersion']}` )Returns
{ status, findings, categories, entries }| Key | Type | Description |
|---|---|---|
| status | boolean | true when no findings were generated |
| findings | array of objects | Structured findings, each { code, severity, location, message } (see Validation Codes) |
| categories | object | 12 boolean flags (see Categories) |
| entries | object | 18 data fields (see Entries) |
Compares two snapshots produced by .start() and returns a structured diff with added, removed, and modified items per section.
Method
.compare( { before, after } )
| Key | Type | Description | Required |
|---|---|---|---|
| before | object | Snapshot from a previous .start() call. Must contain categories and entries |
Yes |
| after | object | Snapshot from a later .start() call. Must contain categories and entries |
Yes |
Example
import { McpAppsValidator } from 'mcp-apps-validator'
const before = await McpAppsValidator.start( { endpoint: 'https://server.example.com/mcp' } )
const after = await McpAppsValidator.start( { endpoint: 'https://server.example.com/mcp' } )
const { status, findings, hasChanges, diff } = McpAppsValidator.compare( { before, after } )
console.log( `Changes detected: ${hasChanges}` )
console.log( `UI resources added: ${diff['uiResources']['added'].length}` )
console.log( `UI resources removed: ${diff['uiResources']['removed'].length}` )Returns
{ status, findings, hasChanges, diff }| Key | Type | Description |
|---|---|---|
| status | boolean | true when comparison completed |
| findings | array of objects | CMP-* integrity findings { code, severity, location, message } (URL mismatch, timestamp issues) |
| hasChanges | boolean | true when any diff section has changes |
| diff | object | Structured diff with sections: server, uiResources, uiLinkedTools, csp, permissions, latency, categories |
12 boolean flags returned in categories:
| Key | Description |
|---|---|
| isReachable | Server responded to connection attempt |
| supportsMcp | MCP handshake completed |
| supportsMcpApps | Server has io.modelcontextprotocol/ui extension |
| hasUiResources | At least one ui:// resource discovered |
| hasUiToolLinkage | At least one tool has _meta.ui.resourceUri |
| hasValidUiHtml | UI resource contains valid HTML content |
| hasValidCsp | CSP configuration present on all validated resources |
| supportsTheming | Theme/style variables are supported |
| supportsDisplayModes | Display modes are declared |
| hasToolVisibility | Tool _meta.ui.visibility field is used |
| hasValidPermissions | No unknown permissions declared |
| hasGracefulDegradation | Text fallback available for non-UI hosts |
18 data fields returned in entries:
| Key | Type | Description |
|---|---|---|
| endpoint | string | MCP server endpoint URL that was validated |
| serverName | string | Server name from MCP handshake |
| serverVersion | string | Server version |
| serverDescription | string | Server description |
| protocolVersion | string | MCP protocol version |
| extensionVersion | string | io.modelcontextprotocol/ui extension version |
| capabilities | object | Raw server capabilities |
| uiResourceCount | number | Number of ui:// resources discovered |
| uiResources | array | UI resources with uri, name, mimeType, hasCsp, hasPermissions, displayModes |
| uiLinkedToolCount | number | Number of tools linked to UI resources |
| uiLinkedTools | array | Tools with name, resourceUri, visibility |
| appOnlyToolCount | number | Tools with visibility ["app"] only |
| cspSummary | object | connectDomains, resourceDomains, frameDomains |
| permissionsSummary | array | Aggregated permissions across all resources |
| displayModes | array | Aggregated display modes across all resources |
| tools | array | All discovered tools |
| resources | array | All discovered resources |
| latency | object | listResources and readResource in milliseconds |
| timestamp | string | ISO 8601 timestamp of validation |
Each finding is a structured object { code, severity, location, message } conforming to the
AgentProbe finding-object spec (spec/agentprobe/.../01-finding-object.md). severity is lowercase
(error / warning / info). This validator owns the UIR, UIV, CON-5xx and VAL-5xx
bands; the generic VAL / CON prefixes are de-collided across the fleet by disjoint number bands
(mcp-apps = 5xx).
| Code | Severity | Description |
|---|---|---|
| VAL-501 | error | endpoint: Missing value |
| VAL-502 | error | endpoint: Must be a string |
| VAL-503 | error | endpoint: Must not be empty |
| VAL-504 | error | endpoint: Must be a valid URL |
| VAL-505 | error | timeout: Must be a number |
| VAL-506 | error | timeout: Must be greater than 0 |
| VAL-510 | error | before: Missing value |
| VAL-511 | error | before: Must be an object |
| VAL-512 | error | before: Missing categories or entries |
| VAL-513 | error | after: Missing value |
| VAL-514 | error | after: Must be an object |
| VAL-515 | error | after: Missing categories or entries |
| Code | Severity | Description |
|---|---|---|
| CON-501 | error | endpoint: Server is not reachable |
| CON-504 | error | mcp: Initialize handshake failed |
| CON-508 | warning | tools/list: Request failed or invalid response format |
| CON-510 | warning | resources/list: Request failed |
| Code | Severity | Description |
|---|---|---|
| UIR-001 | warning | resources/read: Resource read failed |
| UIR-002 | warning | resources/read: Expected text/html content |
| Code | Severity | Description |
|---|---|---|
| UIV-010 | warning | HTML content is missing |
| UIV-011 | warning | HTML content is not a string |
| UIV-012 | warning | HTML content is empty |
| UIV-013 | warning | HTML content appears invalid |
| UIV-020 | warning | No CSP configuration declared |
| UIV-021 | warning | CSP domain should use https:// or wss:// |
| UIV-022 | warning | CSP contains wildcard domain |
| UIV-030 | warning | Unknown permissions declared |
| UIV-031 | warning | Sensitive permissions requested |
| UIV-040 | info | Unknown display modes |
| UIV-041 | info | No display modes declared |
| UIV-050 | info | No theming acknowledgment found |
| UIV-060 | warning | Tool references non-existent UI resource |
| UIV-061 | warning | Tool has invalid visibility values |
| UIV-062 | info | No tools linked to UI resources |
| UIV-063 | info | Tool has UI metadata but no resourceUri |
| UIV-070 | info | No graceful degradation found |
| UIV-080 | info | MCP Apps extension not declared |
| UIV-081 | info | Extension version not specified |
compare() emits CMP-* codes as structured finding objects
{ code, severity, location: 'compare', message } on its findings array — uniform with start().
The diff and hasChanges are keyed structurally, never on the codes.
| Code | Severity | Description |
|---|---|---|
| CMP-001 | warning | Snapshots are from different servers |
| CMP-002 | warning | Before snapshot has no timestamp |
| CMP-003 | warning | After snapshot is older than before |
MIT