Skip to main content

Overview

Making your web app ABP-compatible involves:
  1. Implementing window.abp — A JavaScript object with required methods
  2. Creating a manifest — A JSON file describing your app and capabilities
  3. Adding a manifest link — A <link> tag in your HTML head
  4. Testing — Verify everything works with the MCP Bridge
Effort estimate: 100-150 lines for initial ABP wrapper, 10-20 lines per capability.
For a comprehensive, step-by-step implementation process, see the ABP Implementation Guide. Read Common Pitfalls before shipping. Developers who skip these consistently produce implementations that fail silently when called by agents.

Prerequisites

  • Basic JavaScript/TypeScript knowledge
  • A web application you want to expose to AI agents
  • Node.js (for testing with the MCP Bridge)

Step-by-Step Implementation

1

Add Manifest Link to HTML

In your index.html (or template), add this to the <head>:
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>My App</title>

  <!-- ABP manifest link -->
  <link rel="abp-manifest" href="/abp.json">

  <!-- Your other tags -->
</head>
<body>
  <!-- Your app -->
</body>
</html>
Key points:
  • Use rel="abp-manifest" exactly (case-sensitive)
  • href can be relative (/abp.json) or absolute (https://cdn.example.com/abp.json)
  • Place it in <head>, before or after other <link> tags
  • Framework users: The link must be in the server-rendered HTML. ABP clients discover the manifest via raw HTTP fetch (no JS execution), so a link injected via useEffect, onMounted, or similar hooks will never be found. Use your framework’s server-side metadata API instead. See the ABP Implementation Guide — Framework Environments for a framework-by-framework table.
2

Create the Manifest File

Create public/abp.json (or wherever your static files are served):
{
  "abp": "0.1",
  "app": {
    "id": "com.example.myapp",
    "name": "My Awesome App",
    "version": "1.0.0",
    "description": "Does cool things with browser APIs",
    "homepage": "https://myapp.example.com",
    "icon": "https://myapp.example.com/icon-192.png"
  },
  "capabilities": [
    {
      "name": "convert.markdownToHtml",
      "description": "Convert Markdown to HTML",
      "inputSchema": {
        "type": "object",
        "properties": {
          "markdown": {
            "type": "string",
            "description": "Markdown content to convert"
          },
          "options": {
            "type": "object",
            "properties": {
              "gfm": { "type": "boolean", "description": "Enable GitHub Flavored Markdown" }
            }
          }
        },
        "required": ["markdown"]
      }
    }
  ]
}
Key points:
  • abp: Protocol version (currently "1.0")
  • app.id: Reverse-domain notation (e.g., com.yourcompany.appname)
  • app.version: Semantic versioning
  • capabilities: Array of capabilities you expose
  • inputSchema: JSON Schema defining parameters
3

Implement window.abp

Create abp-runtime.js:
// ABP Runtime Implementation
window.abp = {
  // Metadata
  protocolVersion: '0.1',
  app: {
    id: 'com.example.myapp',
    name: 'My Awesome App',
    version: '1.0.0'
  },

  // Session state
  initialized: false,
  sessionId: null,

  // Initialize session
  async initialize(params) {
    console.log('[ABP] Initializing session', 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
      }
    };
  },

  // Shutdown session
  async shutdown(params) {
    console.log('[ABP] Shutting down session', params);
    this.initialized = false;
    this.sessionId = null;
  },

  // Call a capability
  async call(capability, params = {}) {
    console.log('[ABP] Calling capability', capability, params);

    // Check session is initialized
    if (!this.initialized) {
      return {
        success: false,
        error: {
          code: 'NOT_INITIALIZED',
          message: 'Call initialize() first',
          retryable: true
        }
      };
    }

    // Route to capability implementation
    switch (capability) {
      case 'convert.markdownToHtml':
        return await this.capabilities.convertMarkdownToHtml(params);

      default:
        return {
          success: false,
          error: {
            code: 'UNKNOWN_CAPABILITY',
            message: `Unknown capability: ${capability}`,
            retryable: false
          }
        };
    }
  },

  // Capability implementations
  capabilities: {
    async convertMarkdownToHtml({ 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
          }
        };
      }
    }
  },

  // Optional: Runtime discovery methods
  async listCapabilities() {
    return [
      {
        name: 'convert.markdownToHtml',
        description: 'Convert Markdown to HTML',
        available: true
      }
    ];
  },

  async supports(name) {
    if (name === 'convert.markdownToHtml') {
      return {
        supported: true,
        available: true
      };
    }
    return {
      supported: false,
      available: false
    };
  },

  // Stubs for optional features
  notify() {},
  notifyProgress() {},
  async elicit() {
    return {
      success: false,
      error: {
        code: 'NOT_SUPPORTED',
        message: 'Elicitation not supported',
        retryable: false
      }
    };
  },
  async cancel() {
    return { callId: '', cancelled: false };
  }
};

console.log('[ABP] Runtime loaded');
4

Load the Runtime

In your index.html, load the ABP runtime:
<!DOCTYPE html>
<html>
<head>
  <link rel="abp-manifest" href="/abp.json">
  <!-- Your other head content -->
</head>
<body>
  <!-- Your app content -->

  <!-- Load ABP runtime BEFORE your app scripts -->
  <script src="/abp-runtime.js"></script>

  <!-- Your app scripts -->
  <script src="/app.js"></script>
</body>
</html>
Important: Load abp-runtime.js early so window.abp is available when agents connect.
Framework users: window.abp must be assigned at module scope (top-level code that runs when the JS bundle executes), not inside lifecycle hooks like useEffect or onMounted. Lifecycle hooks run after framework hydration, which may be too late for ABP clients. Guard with typeof window !== 'undefined' for SSR safety. See the ABP Implementation Guide — Framework Environments for details.

Complete Example

Here’s a complete minimal ABP app:

File: public/index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Minimal ABP App</title>
  <link rel="abp-manifest" href="/abp.json">
</head>
<body>
  <h1>Minimal ABP App</h1>
  <p>This app exposes convert.markdownToHtml via ABP.</p>

  <script src="/abp-runtime.js"></script>
</body>
</html>

File: public/abp.json

{
  "abp": "0.1",
  "app": {
    "id": "com.example.minimal",
    "name": "Minimal ABP App",
    "version": "1.0.0"
  },
  "capabilities": [
    {
      "name": "convert.markdownToHtml",
      "description": "Convert Markdown to HTML",
      "inputSchema": {
        "type": "object",
        "properties": {
          "markdown": { "type": "string", "description": "Markdown content to convert" },
          "options": {
            "type": "object",
            "properties": {
              "gfm": { "type": "boolean", "description": "Enable GitHub Flavored Markdown" }
            }
          }
        },
        "required": ["markdown"]
      }
    }
  ]
}

File: public/abp-runtime.js

(Use the code from Step 3 above)

Serve it

python3 -m http.server 8000 --directory public
File: server.js (if using Express):
const express = require('express');
const app = express();

app.use(express.static('public'));

app.listen(8000, () => {
  console.log('Server running at http://localhost:8000');
});

Testing Your Implementation

1. Manual Test

Open http://localhost:8000 in Chrome and open DevTools Console:
// Check window.abp exists
console.log(window.abp);

// Initialize
const session = await window.abp.initialize({
  agent: { name: 'test', version: '1.0' },
  protocolVersion: '0.1',
  features: { notifications: false, progress: false, elicitation: false }
});
console.log('Session:', session);

// Call capability
const result = await window.abp.call('convert.markdownToHtml', {
  markdown: '# Hello from ABP\n\nThis is **bold** text.'
});
console.log('Result:', result);

// Shutdown
await window.abp.shutdown();

2. Test with MCP Bridge

# In your AI agent (e.g., via the MCP Bridge)
abp_connect("http://localhost:8000")

# Should see:
# - Connection successful
# - New tool: abp_convert_markdownToHtml

# Test the capability
abp_convert_markdownToHtml("# Hello World")

# Should return:
# { success: true, data: { html: "<h1>Hello World</h1>" } }

3. Test Discovery

# Fetch manifest
curl http://localhost:8000/abp.json | jq

# Should return your manifest JSON

# Check HTML
curl http://localhost:8000 | grep abp-manifest

# Should find: <link rel="abp-manifest" href="/abp.json">

Common Patterns

Pattern: Multiple Capabilities

window.abp = {
  // ... (initialize, shutdown, etc.)

  async call(capability, params = {}) {
    if (!this.initialized) {
      return { success: false, error: { code: 'NOT_INITIALIZED', ... } };
    }

    switch (capability) {
      case 'convert.markdownToHtml':
        return await this.capabilities.convertMarkdownToHtml(params);

      case 'export.pdf':
        return await this.capabilities.exportPdf(params);

      case 'export.html':
        return await this.capabilities.exportHtml(params);

      default:
        return { success: false, error: { code: 'UNKNOWN_CAPABILITY', ... } };
    }
  },

  capabilities: {
    async convertMarkdownToHtml({ markdown, options = {} }) {
      // ...
    },

    async exportPdf({ html, options = {} }) {
      // Preferred: return HTML, let agent/client handle PDF generation
      // Advanced: use window.print() for app-specific rendering
      // ...
    },

    async exportHtml({ html, options = {} }) {
      // Wrap and return HTML document
      // ...
    }
  }
};

Pattern: Parameter Validation

async convertMarkdownToHtml({ markdown, options = {} }) {
  // Validate required params
  if (!markdown) {
    return {
      success: false,
      error: {
        code: 'INVALID_PARAMS',
        message: 'Missing required parameter: markdown',
        retryable: false
      }
    };
  }

  // Validate param types
  if (typeof markdown !== 'string') {
    return {
      success: false,
      error: {
        code: 'INVALID_PARAMS',
        message: `Expected string for markdown, got ${typeof markdown}`,
        retryable: false
      }
    };
  }

  // Execute capability
  // ...
}

Pattern: Progress Reporting

capabilities: {
  async exportPdf({ html, options = {} }, { progressToken } = {}) {
    const pages = splitIntoPages(html);

    for (let i = 0; i < pages.length; i++) {
      await renderPage(pages[i]);

      // Report progress if token provided
      if (progressToken) {
        window.__abp_progress({
          operationId: progressToken,
          progress: i + 1,
          total: pages.length,
          percentage: Math.round(((i + 1) / pages.length) * 100),
          status: `Rendering page ${i + 1} of ${pages.length}`
        });
      }
    }

    const pdfBlob = await finalizePdf();
    const pdfBase64 = await blobToBase64(pdfBlob);

    return {
      success: true,
      data: {
        pdf: pdfBase64,
        mimeType: 'application/pdf'
      }
    };
  }
}

Pattern: Using Existing Libraries

// Add marked.js for markdown conversion
// <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>

capabilities: {
  async convertMarkdownToHtml({ 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
        }
      };
    }
  }
}

Best Practices

For comprehensive guidance, see Common Pitfalls. The examples below are a quick overview.

1. Return Actual Data, Not Status Messages

// Wrong
async convertMarkdownToHtml({ markdown }) {
  const html = marked.parse(markdown);
  return {
    success: true,
    data: { message: 'Conversion successful' }
  };
}
// Correct
async convertMarkdownToHtml({ markdown }) {
  const html = marked.parse(markdown);
  return {
    success: true,
    data: { html }
  };
}
See Common Pitfalls: Status Messages for why this matters.

2. Never Trigger Native UI

// Wrong
async exportPdf({ html }) {
  window.print();  // Opens print dialog
  return { success: true, data: { message: 'Print dialog opened' } };
}
// Correct (for Puppeteer-based client consumers)
async exportPdf({ html }) {
  // Render content into print-ready container
  const container = document.getElementById('print-container');
  container.innerHTML = html;

  // Signal transport to capture PDF (Puppeteer-based clients intercept this)
  window.print();

  // Bridge captures PDF via page.pdf() and returns to agent
  return { success: true, data: { rendered: true } };
}
See Common Pitfalls: Native Browser UI for complete guidance.

3. Validate Inputs

Always validate against your inputSchema:
async call(capability, params = {}) {
  // Validate params match the schema
  const schema = this.getInputSchema(capability);
  const validation = validateAgainstSchema(params, schema);

  if (!validation.valid) {
    return {
      success: false,
      error: {
        code: 'INVALID_PARAMS',
        message: validation.error,
        retryable: false
      }
    };
  }

  // Proceed with capability
  // ...
}

4. Handle Errors Gracefully

async convertMarkdownToHtml({ markdown, options = {} }) {
  try {
    const html = marked.parse(markdown, {
      gfm: options.gfm !== false,
      breaks: options.breaks || false
    });
    return { success: true, data: { html } };
  } catch (error) {
    // Syntax error in input
    if (error instanceof SyntaxError) {
      return {
        success: false,
        error: {
          code: 'INVALID_PARAMS',
          message: 'Invalid markdown syntax',
          retryable: false
        }
      };
    }

    // Other error
    return {
      success: false,
      error: {
        code: 'CONVERSION_ERROR',
        message: error.message,
        retryable: false
      }
    };
  }
}

5. Keep Manifest in Sync

When you add/remove capabilities, update both:
  1. abp.json manifest
  2. window.abp.call() switch statement

Common Pitfalls

Pitfall 1: Manifest Not Served

Symptom: Discovery fails Cause: Manifest file not accessible Fix:
# Verify manifest is served
curl http://localhost:8000/abp.json

# Should return JSON, not 404

Pitfall 2: CORS Issues

Symptom: Manifest fetch fails from different origin Fix: Add CORS headers if manifest is on a different domain:
// Express example
app.get('/abp.json', (req, res) => {
  res.header('Access-Control-Allow-Origin', '*');
  res.json(manifest);
});

Pitfall 3: window.abp Not Available

Symptom: window.abp is undefined Fix:
  1. Vanilla HTML: Load abp-runtime.js before other scripts
  2. Framework apps (React, Next.js, Vue, etc.): Assign window.abp at module scope, not inside lifecycle hooks (useEffect, onMounted, etc.) — they run after hydration, which may be too late. See ABP Implementation Guide — Framework Environments for details.

Pitfall 4: Returning Promises Instead of Results

// Wrong
async call(capability, params) {
  return this.capabilities.convertMarkdownToHtml(params);  // Returns a Promise
}
// Correct
async call(capability, params) {
  return await this.capabilities.convertMarkdownToHtml(params);  // Awaits and returns result
}

Pitfall 5: Not Handling Initialization State

// Wrong
async call(capability, params) {
  // No check for initialization
  return await this.capabilities.convertMarkdownToHtml(params);
}
// Correct
async call(capability, params) {
  if (!this.initialized) {
    return {
      success: false,
      error: { code: 'NOT_INITIALIZED', message: 'Call initialize() first', retryable: true }
    };
  }
  return await this.capabilities.convertMarkdownToHtml(params);
}

Chrome Extensions

This guide covers web applications served at HTTP URLs. If you want to expose Chrome extension APIs (chrome.tabs, chrome.scripting, chrome.bookmarks, etc.) as ABP capabilities, see the dedicated Chrome Extension Guide. Key differences for extensions:
  • No <link rel="abp-manifest"> or abp.json needed — discovery is runtime-only
  • The ABP entry page is abp-app.html (loaded via chrome-extension://ID/abp-app.html)
  • Capabilities can call chrome.* APIs directly from window.abp.call()
  • Connect via abp_connect({ extensionPath: "/path/to/extension" }) instead of a URL

Next Steps

Additional Resources