Overview
ABP supports two discovery mechanisms depending on the type of ABP server:
Web apps (HTTP URLs): Pre-flight HTTP-based discovery — fetch the HTML <head>, parse the manifest link, fetch the manifest JSON. This lets agents validate ABP support without launching a browser.
Chrome extensions (local directory): Runtime-only discovery — launch the browser with --load-extension, discover the extension ID from browser targets, navigate to the ABP page, and discover capabilities via initialize() + listCapabilities(). No HTTP pre-flight is possible because chrome-extension:// URLs are not accessible from Node.js.
Web App Discovery
Before launching a browser, agents can check if a web app supports ABP by:
Fetching the HTML <head>
Parsing the manifest link
Fetching the manifest JSON
This allows agents to validate ABP support and filter apps by capability without the overhead of browser automation.
Discovery Flow
┌───────────────────────────────────────────────────┐
│ DISCOVERY FLOW │
├───────────────────────────────────────────────────┤
│ │
│ 1. Fetch HTML head (streaming) │
│ GET https://app.example.com │
│ → <link rel="abp-manifest" href="/abp.json"> │
│ │
│ 2. Parse manifest link │
│ Extract href="/abp.json" │
│ → https://app.example.com/abp.json │
│ │
│ 3. Fetch manifest │
│ GET https://app.example.com/abp.json │
│ → { abp, app, capabilities } │
│ │
│ 4. Validate manifest │
│ Check required fields │
│ Check capabilities │
│ │
│ 5. Decision │
│ Has required capability? → Launch browser │
│ Missing capability? → Skip or warn │
│ │
└───────────────────────────────────────────────────┘
Manifest Link
Apps MUST include this in their HTML <head>:
< link rel = "abp-manifest" href = "/abp.json" >
Variations:
<!-- Relative path -->
< link rel = "abp-manifest" href = "/abp.json" >
< link rel = "abp-manifest" href = "/api/v1/abp-manifest.json" >
<!-- Absolute URL -->
< link rel = "abp-manifest" href = "https://app.example.com/abp.json" >
<!-- CDN-hosted -->
< link rel = "abp-manifest" href = "https://cdn.example.com/manifests/app.json" >
{
"abp" : "0.1" ,
"app" : {
"id" : "com.example.myapp" ,
"name" : "My App" ,
"version" : "1.0.0" ,
"description" : "Brief description" ,
"homepage" : "https://example.com" ,
"icon" : "https://example.com/icon-192.png" ,
"support" : "https://example.com/support"
},
"capabilities" : [
{
"name" : "convert.markdownToHtml" ,
"description" : "Convert Markdown to HTML" ,
"inputSchema" : { /* JSON Schema */ },
"outputSchema" : { /* JSON Schema */ },
"experimental" : false ,
"deprecated" : false
}
]
}
The icon field should point to a 192x192 PNG image for best compatibility across agent UIs and app directories.
Example: Discovery Implementation
async function discoverABP ( url ) {
// Step 1: Fetch HTML head
const html = await fetchHead ( url );
// Step 2: Parse manifest link
const manifestUrl = parseManifestLink ( html , url );
if ( ! manifestUrl ) {
return { supported: false , reason: 'No manifest link found' };
}
// Step 3: Fetch manifest
const manifest = await fetch ( manifestUrl ). then ( r => r . json ());
// Step 4: Validate
if ( ! manifest . abp || ! manifest . app || ! manifest . capabilities ) {
return { supported: false , reason: 'Invalid manifest' };
}
// Step 5: Return discovery result
return {
supported: true ,
manifest ,
hasCapability : ( name ) => manifest . capabilities . some ( c => c . name === name )
};
}
async function fetchHead ( url ) {
const response = await fetch ( url );
const reader = response . body . getReader ();
const decoder = new TextDecoder ();
let html = '' ;
while ( true ) {
const { done , value } = await reader . read ();
if ( done ) break ;
html += decoder . decode ( value , { stream: true });
// Stop once we have </head>
if ( html . includes ( '</head>' )) {
reader . cancel ();
break ;
}
// Safety limit
if ( html . length > 50000 ) {
reader . cancel ();
break ;
}
}
return html ;
}
function parseManifestLink ( html , baseUrl ) {
const patterns = [
/<link [ ^ > ] + rel= [ "' ] abp-manifest [ "' ][ ^ > ] + href= [ "' ] ( [ ^ "' ] + ) [ "' ] / i ,
/<link [ ^ > ] + href= [ "' ] ( [ ^ "' ] + ) [ "' ][ ^ > ] + rel= [ "' ] abp-manifest [ "' ] / i
];
for ( const pattern of patterns ) {
const match = html . match ( pattern );
if ( match ) {
return new URL ( match [ 1 ], baseUrl ). href ;
}
}
return null ;
}
Usage
// Check if app supports ABP
const discovery = await discoverABP ( 'https://markdown-app.example.com' );
if ( discovery . supported ) {
console . log ( 'App:' , discovery . manifest . app . name );
console . log ( 'Capabilities:' , discovery . manifest . capabilities . map ( c => c . name ));
// Check for specific capability
if ( discovery . hasCapability ( 'convert.markdownToHtml' )) {
// Worth launching browser for this app
await launchBrowserAndConnect ( url );
}
} else {
console . log ( 'Not an ABP app:' , discovery . reason );
}
Chrome Extension Discovery
Chrome extensions cannot be discovered via HTTP. Instead, the client uses browser-side discovery :
┌───────────────────────────────────────────────────────┐
│ EXTENSION DISCOVERY FLOW │
├───────────────────────────────────────────────────────┤
│ │
│ 1. Launch browser with extension │
│ puppeteer.launch({ │
│ args: ['--load-extension=/path/to/ext', │
│ '--disable-extensions-except=/path'] │
│ }) │
│ │
│ 2. Discover extension ID │
│ Poll browser.targets() for URLs matching │
│ chrome-extension://[32-char-id]/ │
│ Extract extension ID from the hostname │
│ │
│ 3. Navigate to ABP page │
│ chrome-extension://ID/abp-app.html │
│ (configurable — "abp-app.html" is the default) │
│ │
│ 4. Runtime discovery │
│ Wait for window.abp │
│ Call initialize() → get app info + capabilities │
│ Call listCapabilities() → get full schemas │
│ │
│ 5. Build synthetic manifest │
│ Construct ABPManifest from runtime data │
│ (used internally for capability registry) │
│ │
└───────────────────────────────────────────────────────┘
Why No HTTP Discovery?
chrome-extension:// URLs are only accessible within the browser process — Node.js cannot HTTP-fetch them
Extensions don’t serve content over HTTP, so there is no HTML <head> to parse for a manifest link
The extension ID is dynamically assigned at load time (based on the extension’s directory), so the URL isn’t known in advance
What This Means for Extension Developers
No <link rel="abp-manifest"> needed — the ABP page doesn’t need a manifest link
No abp.json file required — though you can include one for documentation
listCapabilities() is essential — it’s the only way the client discovers full capability schemas
initialize() must return accurate capabilities — this is the primary discovery mechanism
See Chrome Extension Guide for complete implementation details.
Manifest vs Runtime
Aspect Manifest Runtime (initialize()) When Before browser launch After connection Accuracy May be stale Always current Purpose Pre-flight filtering Authoritative state Availability Static snapshot Real-time
Use manifest for:
Deciding whether to load an app
Filtering apps by capability
UI display (app name, description, icon)
Use initialize() for:
Getting actual capability availability
Real-time requirements status
Starting a session
Caching
Manifests SHOULD be cached:
Cache-Control : public, max-age=3600
Content-Type : application/json
Clients can cache manifests to reduce network requests.
CORS
If the manifest is on a different origin, CORS headers are required:
Access-Control-Allow-Origin : *
Edge Cases
Scenario Behavior Multiple <link rel="abp-manifest"> tags Use the first one encountered Manifest URL returns 404 Discovery fails - app is misconfigured Manifest is malformed JSON Discovery fails - app is misconfigured Manifest exists but window.abp missing at runtime Runtime error - app is broken Manifest on different origin without CORS Discovery fails - cannot fetch HTML has no </head> tag Continue reading until safety limit, then fail
Version Compatibility
The manifest’s abp field declares the protocol version. Agents SHOULD handle version mismatches gracefully:
Same major version : Proceed normally.
Higher major version : Warn but attempt initialize() - runtime negotiation may succeed.
Lower major version : Proceed if agent supports backward compatibility.
function checkVersionCompatibility ( manifestVersion , supportedVersion ) {
const [ manifestMajor , manifestMinor ] = manifestVersion . split ( '.' ). map ( Number );
const [ supportedMajor , supportedMinor ] = supportedVersion . split ( '.' ). map ( Number );
if ( manifestMajor === supportedMajor ) {
return {
compatible: true ,
action: 'proceed' ,
message: `Protocol versions compatible (manifest: ${ manifestVersion } , supported: ${ supportedVersion } )`
};
}
if ( manifestMajor > supportedMajor ) {
return {
compatible: false ,
action: 'warn-and-attempt' ,
message: `Manifest version ${ manifestVersion } is newer than supported ${ supportedVersion } - attempting initialize(), runtime negotiation may succeed`
};
}
// manifestMajor < supportedMajor
return {
compatible: false ,
action: 'proceed-with-fallback' ,
message: `Manifest version ${ manifestVersion } is older than supported ${ supportedVersion } - proceeding with backward compatibility`
};
}
// Usage
const result = checkVersionCompatibility ( '0.1' , '0.1' );
// → { compatible: true, action: 'proceed', message: '...' }
const result2 = checkVersionCompatibility ( '2.0' , '1.0' );
// → { compatible: false, action: 'warn-and-attempt', message: '...' }
const result3 = checkVersionCompatibility ( '0.1' , '1.0' );
// → { compatible: false, action: 'proceed-with-fallback', message: '...' }
Next Steps
Building ABP Apps Create a manifest for your app
MCP Bridge Quick Start Use discovery in practice
Protocol Overview Complete protocol details