Skip to content

MCP Integration Guide

The @askable-ui/mcp package bridges the browser-captured UI context with any MCP-compatible AI client (Claude Desktop, Claude.ai, ChatGPT with extensions, custom agents).

Two deployment modes are supported:

ModeWhen to use
Browser-local page bridgeA browser extension or companion app reads context directly from the tab
Remote WebMCPYour server exposes a stateless Streamable HTTP MCP endpoint that agents call over the network

Install

bash
npm install @askable-ui/mcp @askable-ui/core

Browser-local page bridge

The page bridge handles a read_current_resource window-message request and returns the askable://current resource payload. A local MCP companion (for example, a browser extension with MCP proxy support) can map that response to MCP resources/read without a network round trip.

ts
// In your app entry point (e.g. main.ts / index.ts)
import { createAskableContext } from '@askable-ui/core';
import { createAskableMcpContextProvider, createAskableMcpPageBridge } from '@askable-ui/mcp';

const ctx = createAskableContext();
ctx.observe(document);

const bridge = createAskableMcpPageBridge({
  provider: createAskableMcpContextProvider(ctx),
  // requireRedacted: true, // refuse to serve packets with privacy.redacted === false
});

// The bridge installs a window message listener that local companions use.
// Call bridge.dispose() when tearing down.

Prompt-ready vs structured output

A local companion posts a message and receives a response shaped by the request type:

Request typeResponse
get_current_contextThe structured WebContextPacket
format_context_for_promptA prompt-ready string
read_current_resourceAn MCP resource at askable://current (packet JSON or prompt text)

The packet/string is produced by the provider you pass — createAskableMcpContextProvider(ctx) wires it to your context's toContextPacket() / toContext().

Remote WebMCP (server-side)

Expose UI context as a stateless Streamable HTTP MCP endpoint that any MCP client can connect to.

Next.js example

ts
// app/api/mcp/route.ts
import { createAskableMcpWebHandler } from '@askable-ui/mcp';
import { createAskableContext } from '@askable-ui/core';

const ctx = createAskableContext();

// In production, ctx is populated by packets sent from the browser client.
// The browser calls ctx.push() or ctx.select() before the user sends a message.

const handler = createAskableMcpWebHandler({
  provider: {
    getContext: () => ctx.toContextPacketAsync(),
  },
  cors: {
    origin: ['https://claude.ai', 'https://chat.openai.com'],
  },
});

export const GET = handler;
export const POST = handler;

Express example

ts
import express from 'express';
import { createAskableMcpWebHandler } from '@askable-ui/mcp';
import { createAskableContext } from '@askable-ui/core';

const app = express();
const ctx = createAskableContext();

const handler = createAskableMcpWebHandler({
  provider: {
    getContext: () => ctx.toContextPacketAsync(),
  },
  cors: { origin: true },
});

app.all('/api/mcp', async (req, res) => {
  const webRequest = new Request(`http://localhost${req.url}`, {
    method: req.method,
    headers: req.headers as HeadersInit,
    body: req.method !== 'GET' ? JSON.stringify(req.body) : undefined,
  });
  const webResponse = await handler(webRequest);
  res.status(webResponse.status);
  webResponse.headers.forEach((v, k) => res.setHeader(k, v));
  res.send(await webResponse.text());
});

Authorization

Use the authorize callback to validate bearer tokens or API keys:

ts
const handler = createAskableMcpWebHandler({
  provider: { getContext: () => ctx.toContextPacketAsync() },
  authorize: async (request) => {
    const token = request.headers.get('authorization')?.replace('Bearer ', '');
    if (token !== process.env.MCP_SECRET) {
      return new Response('Unauthorized', { status: 401 });
    }
    // Return void / true to allow the request through
  },
  cors: {
    origin: ['https://claude.ai'],
    credentials: true,
  },
});

Connecting clients

Claude Desktop

Add your endpoint to claude_desktop_config.json:

json
{
  "mcpServers": {
    "my-app": {
      "url": "https://your-app.com/api/mcp",
      "headers": {
        "Authorization": "Bearer YOUR_SECRET"
      }
    }
  }
}

Claude.ai (claude.ai/code)

In Claude.ai settings → Integrations, add the MCP server URL. Claude can read askable://current through MCP resources/read, or call get_current_context when it needs the current UI state.

ChatGPT plugins / connectors

Point the ChatGPT connector at your /api/mcp endpoint. The server exposes get_current_context and format_context_for_prompt tools, plus the askable://current resource.

Connect over stdio (CLI)

Some MCP clients launch a command over stdio instead of connecting to a URL. The @askable-ui/mcp package ships an askable-mcp binary for exactly this — no server framework or extra dependencies required. Point it at any endpoint that returns the current Context packet as JSON:

json
{
  "mcpServers": {
    "my-app": {
      "command": "npx",
      "args": [
        "-y", "@askable-ui/mcp",
        "--url", "http://localhost:3001/context",
        "--header", "Authorization: Bearer YOUR_SECRET"
      ]
    }
  }
}

Your app just needs to expose a GET endpoint that returns the latest packet (e.g. via ctx.toContextPacket()). Flags:

FlagDescription
--url <endpoint>HTTP(S) endpoint returning the current Context packet as JSON
--file <path>Serve a static packet file instead of fetching a URL
--header "K: V"Extra request header for --url (repeatable)
--name <name>Server name advertised to the client
--require-redactedRefuse to serve packets with privacy.redacted === false

The same remote provider is available programmatically via createAskableMcpRemoteProvider({ url, headers }), which you can pass to createAskableMcpServer.

MCP tools and resources exposed

createAskableMcpServer (and therefore the web handler and stdio CLI built on it) registers these tools:

ToolDescription
get_current_contextReturns the current UI context as a WebContextPacket
format_context_for_promptReturns a prompt-ready text rendering of the current context
list_context_sourcesLists the app-owned context sources currently available (label, role, metadata) so the agent can decide which to request
get_context_schemaReturns the JSON Schema for Context packets

And these resources:

ResourceURIDescription
Current contextaskable://currentThe current Context packet as JSON — read it via resources/read instead of calling a tool
Context schemacontext://schemaThe JSON Schema for Context packets

createAskableMcpServer (embedding MCP in-process)

If you want to embed an MCP server directly inside your Node.js process (e.g. for a CLI tool or Electron app):

ts
import { createAskableMcpServer } from '@askable-ui/mcp';
import { createAskableContext } from '@askable-ui/core';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';

const ctx = createAskableContext();

const server = createAskableMcpServer({
  name: 'my-app-context',
  version: '1.0.0',
  provider: {
    getContext: () => ctx.toContextPacketAsync(),
  },
});

const transport = new StdioServerTransport();
await server.connect(transport);

Validating agent requests

When your server receives requests from AI agents that include a serialized context packet, use isAskableAgentRequest to validate and unwrap them:

ts
import { isAskableAgentRequest } from '@askable-ui/core';

app.post('/api/chat', async (req, res) => {
  const body = req.body;

  if (isAskableAgentRequest(body)) {
    const { question, packet, sources } = body;
    // packet is a validated WebContextPacket
    // sources contains per-source context
    const context = packet ? JSON.stringify(packet) : body.context;
    // ... pass to your LLM
  }
});

React example with page bridge

tsx
import { useEffect } from 'react';
import { useAskable } from '@askable-ui/react';
import { createAskableMcpContextProvider, createAskableMcpPageBridge } from '@askable-ui/mcp';

function App() {
  const { ctx } = useAskable();

  useEffect(() => {
    const bridge = createAskableMcpPageBridge({
      provider: createAskableMcpContextProvider(ctx),
    });
    return () => bridge.dispose();
  }, [ctx]);

  return <Dashboard />;
}

Released under the MIT License.