Minimal ABP App
The simplest possible ABP implementation: a Markdown to HTML converter.File Structure
Copy
minimal-abp-app/
+-- public/
| +-- index.html
| +-- abp.json
| +-- abp-runtime.js
+-- server.js
public/index.html
Copy
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Minimal ABP App</title>
<link rel="abp-manifest" href="/abp.json">
<style>
body {
font-family: system-ui, -apple-system, sans-serif;
max-width: 600px;
margin: 50px auto;
padding: 20px;
}
code {
background: #f4f4f4;
padding: 2px 6px;
border-radius: 3px;
}
</style>
</head>
<body>
<h1>Minimal ABP App</h1>
<p>This app exposes a single capability: <code>convert.markdownToHtml</code></p>
<h2>Manual Test</h2>
<p>Open DevTools Console and run:</p>
<pre><code>const session = await window.abp.initialize({
agent: { name: 'test', version: '1.0' },
protocolVersion: '0.1',
features: { notifications: false, progress: false, elicitation: false }
});
const result = await window.abp.call('convert.markdownToHtml', {
markdown: '# Hello from ABP\n\nThis is **bold** text.'
});
console.log(result);</code></pre>
<script src="/abp-runtime.js"></script>
</body>
</html>
public/abp.json
Copy
{
"abp": "0.1",
"app": {
"id": "com.example.minimal",
"name": "Minimal ABP App",
"version": "1.0.0",
"description": "A minimal ABP implementation with Markdown to HTML conversion"
},
"capabilities": [
{
"name": "convert.markdownToHtml",
"description": "Convert Markdown text to HTML",
"inputSchema": {
"type": "object",
"properties": {
"markdown": {
"type": "string",
"description": "Markdown content to convert"
},
"options": {
"type": "object",
"properties": {
"sanitize": { "type": "boolean", "default": true }
},
"description": "Conversion options"
}
},
"required": ["markdown"]
},
"outputSchema": {
"type": "object",
"properties": {
"html": {
"type": "string",
"description": "Converted HTML output"
},
"inputLength": {
"type": "number",
"description": "Length of the input markdown"
}
}
}
}
]
}
public/abp-runtime.js
Copy
window.abp = {
protocolVersion: '0.1',
app: { id: 'com.example.minimal', name: 'Minimal ABP App', version: '1.0.0' },
initialized: false,
sessionId: null,
async initialize(params) {
console.log('[ABP] Initialize', params);
this.initialized = true;
this.sessionId = crypto.randomUUID();
return {
sessionId: this.sessionId,
protocolVersion: '0.1',
app: this.app,
capabilities: [{ name: 'convert.markdownToHtml', available: true }],
features: {
notifications: false,
progress: false,
elicitation: false,
dynamicCapabilities: false
}
};
},
async shutdown() {
console.log('[ABP] Shutdown');
this.initialized = false;
this.sessionId = null;
},
async call(capability, params = {}) {
if (!this.initialized) {
return {
success: false,
error: { code: 'NOT_INITIALIZED', message: 'Call initialize() first', retryable: true }
};
}
if (capability === 'convert.markdownToHtml') {
return await this.convertMarkdown(params);
}
return {
success: false,
error: { code: 'UNKNOWN_CAPABILITY', message: `Unknown: ${capability}`, retryable: false }
};
},
async convertMarkdown({ markdown, options = {} }) {
try {
// Simple markdown to HTML conversion using the browser DOM
const div = document.createElement('div');
div.textContent = markdown;
const html = div.innerHTML
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.*?)\*/g, '<em>$1</em>')
.replace(/^# (.*)/gm, '<h1>$1</h1>')
.replace(/^## (.*)/gm, '<h2>$1</h2>');
return { success: true, data: { html, inputLength: markdown.length } };
} catch (error) {
return {
success: false,
error: {
code: 'CONVERSION_ERROR',
message: error.message,
retryable: false
}
};
}
},
async listCapabilities() { return []; },
async supports(name) {
return name === 'convert.markdownToHtml'
? { supported: true, available: true }
: { supported: false, available: false };
},
notify() {},
notifyProgress() {},
async elicit() {
return { success: false, error: { code: 'NOT_SUPPORTED', message: 'Not supported', retryable: false } };
},
async cancel() { return { callId: '', cancelled: false }; }
};
console.log('[ABP] Runtime loaded');
server.js
Copy
const express = require('express');
const app = express();
app.use(express.static('public'));
const PORT = 3000;
app.listen(PORT, () => {
console.log(`Minimal ABP app running at http://localhost:${PORT}`);
console.log(`Manifest: http://localhost:${PORT}/abp.json`);
});
Run It
Copy
npm install express
node server.js
# Visit http://localhost:3000
Markdown to HTML Converter
A more realistic example using themarked library.
public/index.html
Copy
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Markdown Converter</title>
<link rel="abp-manifest" href="/abp.json">
</head>
<body>
<h1>Markdown to HTML Converter</h1>
<p>Exposes <code>convert.markdownToHtml</code> capability via ABP.</p>
<!-- Include marked.js library -->
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="/abp-runtime.js"></script>
</body>
</html>
public/abp.json
Copy
{
"abp": "0.1",
"app": {
"id": "com.example.markdown-converter",
"name": "Markdown Converter",
"version": "1.0.0"
},
"capabilities": [
{
"name": "convert.markdownToHtml",
"description": "Convert Markdown to HTML",
"inputSchema": {
"type": "object",
"properties": {
"markdown": { "type": "string" },
"options": {
"type": "object",
"properties": {
"gfm": { "type": "boolean", "default": true },
"breaks": { "type": "boolean", "default": false }
}
}
},
"required": ["markdown"]
}
}
]
}
public/abp-runtime.js
Copy
window.abp = {
protocolVersion: '0.1',
app: { id: 'com.example.markdown-converter', name: 'Markdown Converter', version: '1.0.0' },
initialized: false,
sessionId: null,
async initialize(params) {
this.initialized = true;
this.sessionId = crypto.randomUUID();
return {
sessionId: this.sessionId,
protocolVersion: '0.1',
app: this.app,
capabilities: [{ name: 'convert.markdownToHtml', available: true }],
features: { notifications: false, progress: false, elicitation: false, dynamicCapabilities: false }
};
},
async shutdown() {
this.initialized = false;
this.sessionId = null;
},
async call(capability, params = {}) {
if (!this.initialized) {
return { success: false, error: { code: 'NOT_INITIALIZED', message: 'Call initialize() first', retryable: true } };
}
if (capability === 'convert.markdownToHtml') {
return await this.convertMarkdown(params);
}
return { success: false, error: { code: 'UNKNOWN_CAPABILITY', message: `Unknown: ${capability}`, retryable: false } };
},
async convertMarkdown({ markdown, options = {} }) {
try {
const html = marked.parse(markdown, {
gfm: options.gfm !== false,
breaks: options.breaks || false
});
return {
success: true,
data: {
html,
inputLength: markdown.length,
outputLength: html.length
}
};
} catch (error) {
return {
success: false,
error: {
code: 'CONVERSION_ERROR',
message: error.message,
retryable: false
}
};
}
},
async listCapabilities() { return []; },
async supports(name) {
return name === 'convert.markdownToHtml'
? { supported: true, available: true }
: { supported: false, available: false };
},
notify() {},
notifyProgress() {},
async elicit() { return { success: false, error: { code: 'NOT_SUPPORTED', message: 'Not supported', retryable: false } }; },
async cancel() { return { callId: '', cancelled: false }; }
};
Usage with MCP Bridge
Copy
# In your AI agent (e.g., via the MCP Bridge)
abp_connect("http://localhost:3000")
# Use the capability
abp_convert_markdownToHtml("# Hello\n\nThis is **bold**.")
# Result:
# { success: true, data: { html: "<h1>Hello</h1>\n<p>This is <strong>bold</strong>.</p>" } }
PDF Generator
There are two approaches to PDF generation. The recommended approach is to return HTML content and let the agent or client generate a PDF. The advanced approach useswindow.print() as a transport signal when the app needs its own CSS context for rendering.
The example below shows the advanced window.print() pattern — useful when the app has specific @media print styles or complex page layout needs. For the simple pattern, see the Markdown Converter example above — the agent calls convert.markdownToHtml to get HTML, then uses a client-side tool to produce a PDF.
Advanced: Transport-Assisted PDF via window.print()
Capability Definition (public/abp.json excerpt)
Copy
{
"name": "export.pdf",
"description": "Generate PDF from HTML content. Renders the content with CSS styling and uses the browser print engine for high-quality vector PDF output.",
"inputSchema": {
"type": "object",
"properties": {
"html": {
"type": "string",
"description": "HTML content to convert to PDF"
},
"options": {
"type": "object",
"properties": {
"pageSize": {
"type": "string",
"enum": ["letter", "a4", "legal"],
"default": "a4"
},
"headerText": {
"type": "string",
"description": "Optional header text displayed on each page"
}
}
}
},
"required": ["html"]
},
"outputSchema": {
"type": "object",
"description": "PDF is captured by the transport layer via window.print() interception. The ABPResponse contains a render confirmation; the actual PDF is generated by the client using page.pdf().",
"properties": {
"rendered": { "type": "boolean" }
}
}
}
Handler (public/abp-runtime.js excerpt)
Copy
async exportPdf({ html, options: pdfOptions = {} }, callOptions = {}) {
const progressToken = callOptions.progressToken;
// Stage 1: Prepare the print container
if (progressToken) {
window.__abp_progress({
operationId: progressToken,
progress: 0, total: 100, percentage: 0,
status: 'Preparing content...', stage: 'prepare'
});
}
const container = document.getElementById('print-container');
container.innerHTML = html;
// Stage 2: Apply print-specific styles via CSS custom properties
if (progressToken) {
window.__abp_progress({
operationId: progressToken,
progress: 30, total: 100, percentage: 30,
status: 'Applying print styles...', stage: 'styling'
});
}
const root = document.documentElement;
root.style.setProperty('--page-size', pdfOptions.pageSize || 'A4');
if (pdfOptions.headerText) {
const header = container.querySelector('.print-header');
if (header) header.textContent = pdfOptions.headerText;
}
// Wait for all fonts and images to finish loading
await document.fonts.ready;
if (progressToken) {
window.__abp_progress({
operationId: progressToken,
progress: 80, total: 100, percentage: 80,
status: 'Generating PDF...', stage: 'print'
});
}
// Stage 3: Signal the transport to capture PDF
// Puppeteer-based clients intercept this call and generate PDF via page.pdf()
window.print();
if (progressToken) {
window.__abp_progress({
operationId: progressToken,
progress: 100, total: 100, percentage: 100,
status: 'Complete', stage: 'done'
});
}
// The bridge captures the PDF and returns it to the agent.
// This response is supplementary -- the bridge's captured PDF takes priority.
return {
success: true,
data: { rendered: true }
};
}
Companion CSS
Copy
/* Only show the print container when printing */
@media print {
body > *:not(#print-container) { display: none; }
#print-container { display: block; }
.no-print { display: none; }
.page-break { page-break-before: always; }
}
/* Page size is driven by a CSS variable set in the handler */
@page {
size: var(--page-size, A4) portrait;
margin: 15mm;
}
What the Agent Sees
The ABP client interceptswindow.print(), generates a PDF via page.pdf(), and routes the binary data to a file. The agent receives something like:
Copy
File saved: /tmp/abp-mcp-bridge/export_pdf_1707234567890.pdf
Type: application/pdf
Size: 45832 bytes
This is a transport-assisted capability. The app itself never generates the PDF binary — it only renders HTML into a print-ready container and calls
window.print(). Puppeteer-based clients listen for that signal and use page.pdf() (Chrome DevTools Protocol) to produce the actual PDF. This means the app works identically in any ABP-compliant client that supports print interception.Canvas Chart Renderer
An app that uses the browser’s Canvas API to render charts — a capability that only browsers have.public/abp.json (excerpt)
Copy
{
"abp": "0.1",
"app": {
"id": "com.example.chart-renderer",
"name": "Chart Renderer",
"version": "1.0.0"
},
"capabilities": [
{
"name": "render.chart",
"description": "Render a chart as a PNG image using the browser Canvas API",
"inputSchema": {
"type": "object",
"properties": {
"type": { "type": "string", "enum": ["bar", "line", "pie"], "description": "Chart type" },
"data": {
"type": "object",
"properties": {
"labels": { "type": "array", "items": { "type": "string" } },
"values": { "type": "array", "items": { "type": "number" } }
},
"required": ["labels", "values"]
},
"options": {
"type": "object",
"properties": {
"width": { "type": "number", "default": 800 },
"height": { "type": "number", "default": 400 },
"title": { "type": "string" }
}
}
},
"required": ["type", "data"]
}
}
]
}
public/abp-runtime.js (capability handler excerpt)
Copy
async _renderChart({ type, data, options = {} }) {
try {
const width = options.width || 800;
const height = options.height || 400;
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
// Use the app's charting library (e.g., Chart.js)
new Chart(ctx, {
type: type,
data: {
labels: data.labels,
datasets: [{ data: data.values }]
},
options: { animation: false, responsive: false }
});
// Wait for rendering to complete
await new Promise(resolve => setTimeout(resolve, 100));
// Return the chart as a PNG image
const dataUrl = canvas.toDataURL('image/png');
const base64 = dataUrl.split(',')[1];
return {
success: true,
data: {
image: {
content: base64,
mimeType: 'image/png',
encoding: 'base64',
size: atob(base64).length,
filename: `${type}-chart.png`
},
width,
height
}
};
} catch (error) {
return {
success: false,
error: { code: 'RENDER_ERROR', message: error.message, retryable: false }
};
}
}
Multi-Capability App
An app with multiple capabilities.public/abp-runtime.js
Copy
window.abp = {
protocolVersion: '0.1',
app: { id: 'com.example.multi', name: 'Multi-Capability App', version: '1.0.0' },
initialized: false,
sessionId: null,
async initialize(params) {
this.initialized = true;
this.sessionId = crypto.randomUUID();
return {
sessionId: this.sessionId,
protocolVersion: '0.1',
app: this.app,
capabilities: [
{ name: 'convert.markdownToHtml', available: true },
{ name: 'text.uppercase', available: true },
{ name: 'export.html', available: true }
],
features: { notifications: false, progress: false, elicitation: false, dynamicCapabilities: false }
};
},
async shutdown() {
this.initialized = false;
this.sessionId = null;
},
async call(capability, params = {}, options = {}) {
if (!this.initialized) {
return { success: false, error: { code: 'NOT_INITIALIZED', message: 'Call initialize() first', retryable: true } };
}
// Route to appropriate handler
switch (capability) {
case 'convert.markdownToHtml':
return await this.capabilities.convertMarkdown(params);
case 'text.uppercase':
return await this.capabilities.textUppercase(params);
case 'export.html':
return await this.capabilities.exportHtml(params);
default:
return { success: false, error: { code: 'UNKNOWN_CAPABILITY', message: `Unknown: ${capability}`, retryable: false } };
}
},
capabilities: {
async convertMarkdown({ markdown, options = {} }) {
try {
const html = marked.parse(markdown, {
gfm: options.gfm !== false,
breaks: options.breaks || false
});
return { success: true, data: { html } };
} catch (error) {
return { success: false, error: { code: 'CONVERSION_ERROR', message: error.message, retryable: false } };
}
},
async textUppercase({ text }) {
if (!text) {
return { success: false, error: { code: 'INVALID_PARAMS', message: 'Missing required parameter: text', retryable: false } };
}
return {
success: true,
data: {
result: text.toUpperCase(),
originalLength: text.length
}
};
},
async exportHtml({ content, title = 'Export' }) {
const html = `<!DOCTYPE html><html><head><title>${title}</title></head><body>${content}</body></html>`;
return {
success: true,
data: {
document: {
content: html,
mimeType: 'text/html',
encoding: 'utf-8',
size: new Blob([html]).size,
filename: 'export.html'
}
}
};
}
},
async listCapabilities() { return []; },
async supports(name) {
const supported = ['convert.markdownToHtml', 'text.uppercase', 'export.html'];
return supported.includes(name)
? { supported: true, available: true }
: { supported: false, available: false };
},
notify() {},
notifyProgress() {},
async elicit() { return { success: false, error: { code: 'NOT_SUPPORTED', message: 'Not supported', retryable: false } }; },
async cancel() { return { callId: '', cancelled: false }; }
};
Progress Reporting Example
Simulating a long-running operation with progress updates.Copy
window.abp = {
// ... (standard boilerplate)
async call(capability, params = {}, options = {}) {
if (!this.initialized) {
return { success: false, error: { code: 'NOT_INITIALIZED', message: 'Not initialized', retryable: true } };
}
if (capability === 'process.largefile') {
return await this.processLargeFile(params, options);
}
return { success: false, error: { code: 'UNKNOWN_CAPABILITY', message: 'Unknown', retryable: false } };
},
async processLargeFile({ content }, { progressToken } = {}) {
const chunks = content.match(/.{1,1000}/g) || [];
let processed = '';
for (let i = 0; i < chunks.length; i++) {
// Simulate processing delay
await new Promise(resolve => setTimeout(resolve, 100));
// Process chunk
processed += chunks[i].toUpperCase();
// Report progress
if (progressToken && window.__abp_progress) {
window.__abp_progress({
operationId: progressToken,
progress: i + 1,
total: chunks.length,
percentage: Math.round(((i + 1) / chunks.length) * 100),
status: `Processing chunk ${i + 1} of ${chunks.length}`,
stage: 'processing'
});
}
}
return {
success: true,
data: {
result: processed,
chunksProcessed: chunks.length
}
};
}
};
Copy
// Agent calls with progress tracking
const result = await abp.call('process.largefile',
{ content: longText },
{ progressToken: 'process-001' }
);
// Bridge receives progress via window.__abp_progress
// and reports to agent
Elicitation Example
Requesting input from the agent/user.Copy
window.abp = {
// ... (standard boilerplate)
async call(capability, params = {}) {
if (!this.initialized) {
return { success: false, error: { code: 'NOT_INITIALIZED', message: 'Not initialized', retryable: true } };
}
if (capability === 'export.customPdf') {
return await this.exportCustomPdf(params);
}
return { success: false, error: { code: 'UNKNOWN_CAPABILITY', message: 'Unknown', retryable: false } };
},
async exportCustomPdf({ html }) {
// Ask agent for page size preference
if (window.__abp_elicitation) {
const sizeResponse = await window.__abp_elicitation({
method: 'elicitation/input',
params: {
prompt: 'What page size for the PDF?',
schema: {
type: 'string',
enum: ['letter', 'a4', 'legal'],
default: 'letter'
}
}
});
if (!sizeResponse.success) {
return {
success: false,
error: {
code: 'ELICITATION_FAILED',
message: 'Failed to get page size preference',
retryable: true
}
};
}
const pageSize = sizeResponse.data.value;
// Generate PDF with selected page size
const pdf = await generatePdf(html, { pageSize });
return {
success: true,
data: {
pdf: pdf.base64,
pageSize
}
};
}
// Fallback if elicitation not supported
return {
success: false,
error: {
code: 'ELICITATION_REQUIRED',
message: 'This capability requires elicitation support',
retryable: false
}
};
}
};
Dynamic Capabilities Example
An app that detects browser features at runtime and builds its capability list dynamically. When asynchronous features become available (e.g., after browser AI models finish downloading), the app sends anotifications/capabilities/list_changed notification so the bridge can re-discover capabilities without a full reconnect.
File Structure
Copy
dynamic-caps-app/
+-- public/
| +-- index.html
| +-- abp.json (static baseline -- always-available capabilities)
| +-- abp-runtime.js (dynamic detection at runtime)
+-- server.js
public/abp.json (Static Baseline)
The manifest advertises only the capabilities that are always available. Dynamic capabilities are added at runtime during initialize().
Copy
{
"abp": "0.1",
"app": {
"id": "com.example.dynamic-caps",
"name": "Dynamic Capabilities App",
"version": "1.0.0",
"description": "Detects browser features at runtime and exposes matching ABP capabilities"
},
"capabilities": [
{
"name": "convert.markdownToHtml",
"description": "Convert Markdown to HTML (always available)",
"inputSchema": {
"type": "object",
"properties": {
"markdown": { "type": "string" }
},
"required": ["markdown"]
}
}
]
}
public/abp-runtime.js
Full dynamic capabilities runtime (long code block)
Full dynamic capabilities runtime (long code block)
Copy
window.abp = {
protocolVersion: '0.1',
app: { id: 'com.example.dynamic-caps', name: 'Dynamic Capabilities App', version: '1.0.0' },
initialized: false,
sessionId: null,
_capabilities: [],
// Dynamic capability detection
_detectCapabilities() {
const caps = [
// Always available
{ name: 'convert.markdownToHtml', available: true }
];
// Check for browser AI: Summarizer API
if (navigator.ai?.summarizer) {
caps.push({
name: 'ai.summarize',
description: 'Summarize text using the browser built-in AI',
inputSchema: {
type: 'object',
properties: {
text: { type: 'string', maxLength: 100000 },
maxLength: { type: 'number', default: 200 }
},
required: ['text']
},
available: true,
experimental: true,
browserRequirement: 'Chrome 130+ with AI features enabled'
});
}
// Check for browser AI: Translator API
if (navigator.ai?.translator) {
caps.push({
name: 'ai.translate',
description: 'Translate text using the browser built-in AI',
inputSchema: {
type: 'object',
properties: {
text: { type: 'string' },
sourceLanguage: { type: 'string', default: 'en' },
targetLanguage: { type: 'string' }
},
required: ['text', 'targetLanguage']
},
available: true,
experimental: true,
browserRequirement: 'Chrome 130+ with AI features enabled'
});
}
// Check for WebGPU
if (navigator.gpu) {
caps.push({
name: 'render.webgpu',
description: 'GPU-accelerated rendering via WebGPU',
inputSchema: {
type: 'object',
properties: {
scene: { type: 'string', description: 'Scene definition (JSON)' },
width: { type: 'number', default: 800 },
height: { type: 'number', default: 600 }
},
required: ['scene']
},
available: true,
features: { compute: true, render: true }
});
}
return caps;
},
// Watch for async capability changes (e.g., model downloads)
_watchForCapabilityChanges() {
if (navigator.ai?.summarizer) {
navigator.ai.summarizer.capabilities().then((caps) => {
if (caps.available === 'readily') {
console.log('[ABP] Summarizer model ready -- notifying capability change');
this.notify('notifications/capabilities/list_changed', {
changed: ['ai.summarize']
});
}
});
}
if (navigator.ai?.translator) {
navigator.ai.translator.capabilities().then((caps) => {
if (caps.available === 'readily') {
console.log('[ABP] Translator model ready -- notifying capability change');
this.notify('notifications/capabilities/list_changed', {
changed: ['ai.translate']
});
}
});
}
},
// ABP lifecycle
async initialize(params) {
this.initialized = true;
this.sessionId = crypto.randomUUID();
this._capabilities = this._detectCapabilities();
console.log('[ABP] Detected capabilities:', this._capabilities.map(c => c.name));
// Start watching for async changes after initialization
this._watchForCapabilityChanges();
return {
sessionId: this.sessionId,
protocolVersion: '0.1',
app: this.app,
capabilities: this._capabilities,
features: {
notifications: true,
progress: false,
elicitation: false,
dynamicCapabilities: true
}
};
},
async shutdown() {
this.initialized = false;
this.sessionId = null;
this._capabilities = [];
},
async call(capability, params = {}, options = {}) {
if (!this.initialized) {
return { success: false, error: { code: 'NOT_INITIALIZED', message: 'Call initialize() first', retryable: true } };
}
switch (capability) {
case 'convert.markdownToHtml':
return this._convertMarkdown(params);
case 'ai.summarize':
return this._aiSummarize(params);
case 'ai.translate':
return this._aiTranslate(params);
case 'render.webgpu':
return this._renderWebgpu(params);
default:
return { success: false, error: { code: 'UNKNOWN_CAPABILITY', message: `Unknown: ${capability}`, retryable: false } };
}
},
// Capability handlers
async _convertMarkdown({ markdown }) {
const div = document.createElement('div');
div.textContent = markdown;
const html = div.innerHTML
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.*?)\*/g, '<em>$1</em>')
.replace(/^# (.*)/gm, '<h1>$1</h1>')
.replace(/^## (.*)/gm, '<h2>$1</h2>');
return { success: true, data: { html } };
},
async _aiSummarize({ text, maxLength = 200 }) {
if (!navigator.ai?.summarizer) {
return { success: false, error: { code: 'NOT_AVAILABLE', message: 'Summarizer API not available', retryable: false } };
}
try {
const summarizer = await navigator.ai.summarizer.create();
const summary = await summarizer.summarize(text, { maxLength });
return { success: true, data: { summary } };
} catch (error) {
return { success: false, error: { code: 'AI_ERROR', message: error.message, retryable: false } };
}
},
async _aiTranslate({ text, sourceLanguage = 'en', targetLanguage }) {
if (!navigator.ai?.translator) {
return { success: false, error: { code: 'NOT_AVAILABLE', message: 'Translator API not available', retryable: false } };
}
try {
const translator = await navigator.ai.translator.create({ sourceLanguage, targetLanguage });
const translatedText = await translator.translate(text);
return { success: true, data: { translatedText } };
} catch (error) {
return { success: false, error: { code: 'AI_ERROR', message: error.message, retryable: false } };
}
},
async _renderWebgpu({ scene, width = 800, height = 600 }) {
if (!navigator.gpu) {
return { success: false, error: { code: 'NOT_AVAILABLE', message: 'WebGPU not available', retryable: false } };
}
// Placeholder -- real implementation would set up a GPU pipeline
return { success: true, data: { imageDataUrl: '(rendered output)', width, height } };
},
// Standard ABP interface methods
async listCapabilities() { return this._capabilities; },
async supports(name) {
const found = this._capabilities.find(c => c.name === name);
return found
? { supported: true, available: found.available }
: { supported: false, available: false };
},
notify(method, params) {
if (window.__abp_notify) {
window.__abp_notify({ method, params });
} else {
console.log('[ABP] Notification (no bridge):', method, params);
}
},
notifyProgress(params) {
if (window.__abp_progress) {
window.__abp_progress(params);
}
},
async elicit() {
return { success: false, error: { code: 'NOT_SUPPORTED', message: 'Not supported', retryable: false } };
},
async cancel() {
return { callId: '', cancelled: false };
}
};
console.log('[ABP] Dynamic capabilities runtime loaded');
How It Works
- At
initialize()time,_detectCapabilities()inspects the browser environment:navigator.ai?.summarizer— Chrome’s built-in Summarizer APInavigator.ai?.translator— Chrome’s built-in Translator APInavigator.gpu— WebGPU support
- Only capabilities whose APIs actually exist in the current browser are included in the response.
- After initialization,
_watchForCapabilityChanges()polls async readiness. Browser AI models may need to download before they are usable. Once a model reportsavailable === 'readily', the app sends anotifications/capabilities/list_changednotification. - The ABP client receives this notification, calls
listCapabilities()to get the updated list, and registers any new tools — all without requiring the agent to reconnect.
What the Agent Sees
On a browser with AI features and WebGPU:Copy
Connected to "Dynamic Capabilities App" (v1.0.0)
Available capabilities:
- convert.markdownToHtml
- ai.summarize (experimental)
- ai.translate (experimental)
- render.webgpu
Copy
Connected to "Dynamic Capabilities App" (v1.0.0)
Available capabilities:
- convert.markdownToHtml
Using ABP with the MCP Bridge
Complete Workflow
Copy
# 1. Start your ABP app
cd my-abp-app
node server.js
# Listening on http://localhost:3000
# 2. In Claude Code, connect
abp_connect("http://localhost:3000")
# Response:
# Connected to "Markdown Converter" (v1.0.0)
# Available capabilities:
# - convert.markdownToHtml
# 3. Use the capability
abp_convert_markdownToHtml("# Hello World\n\nThis is a **test**.")
# Response:
# { success: true, data: { html: "<h1>Hello World</h1>..." } }
# 4. When done, disconnect
abp_disconnect()
# Browser closes, dynamic tools removed