Skip to content

React table AI integration — OpenAI, MCP and JSON

@adapttable/ai does not host a model, a chat UI, or an AdaptTable service. A live table publishes a compact catalog. Your runtime maps that contract onto the tools it already speaks, then calls session.execute.

Try the interactive demo — a real Mantine table you talk to. The demo answers a fixed set of example requests with no model behind them; connect your own backend from the assistant’s settings for free-form conversation.

Nothing on this page is required to put a conversation in front of a reader. If that is what you want, start with the optional widget — one panel per kit — or the headless controller if you would rather draw it yourself. Six ways to wire it lays them side by side, from the ready widget to a transport of your own.

This page is the layer underneath: how any agent runtime reaches the same governed executor. See adaptive capabilities and @adapttable/ai for the session itself.

import { createAgentSession } from "@adapttable/ai";
import { executeEnvelope, toJsonTools } from "@adapttable/ai/json";
import { toOpenAITools } from "@adapttable/ai/openai";
import { mcpListChanged, toMcpResources, toMcpTools } from "@adapttable/ai/mcp";

The root entry stays React-free. @adapttable/ai-react is only for tableAgent. The three integration subpaths never import a model SDK.

Transform any agent’s action format, then call session.execute.

import {
runCustomBridge,
createBridgeSession,
} from "../examples/ai-custom-bridge";
const session = createBridgeSession({
setFilters: (filters) => applyHostFilters(filters),
});
await runCustomBridge(
session,
{ tool: "view.setFilters", input: { filters: { team: ["Core"] } } },
"bridge-filter-core"
);

Compiling source: ai-custom-bridge.ts.

Carry a versioned envelope over HTTP, a websocket, or postMessage. Parse it in the browser or worker, then execute. There is no second argument validator — session.execute owns that.

import { executeEnvelope, parseEnvelope } from "@adapttable/ai/json";
const envelope = parseEnvelope(body);
const result = await executeEnvelope(session, envelope);
interface AgentEnvelope {
schemaVersion: "adapttable.agent.v1";
tableId: string;
key: string;
args: unknown;
expectedRevision: number;
idempotencyKey: string;
}

Compiling source: ai-server-agent.ts.

Use these from your agent runtime. LangChain or any other framework may consume the JSON tools. None of them become an AdaptTable dependency.

const tools = toJsonTools(session);
const openai = toOpenAITools(session, { deferred: true });
const mcp = toMcpTools(session);

Compiling source: ai-mcp-host.ts.

One-call response — result stays in the app

Section titled “One-call response — result stays in the app”

A model may return text and structured actions in one response. Execute the actions, then show success or error in application or table UI. Sending ExecuteResult back to the model is optional.

import { applyTurnWithoutRoundTrip } from "../examples/ai-one-call";
const { text, results } = await applyTurnWithoutRoundTrip(session, turn);
showInApp(text, results);

Compiling source: ai-one-call.ts.

import { applyTurnAndReply } from "../examples/ai-result-return";
await applyTurnAndReply(session, turn, (text, results) => {
sendBackToYourRuntime(text, results);
});

Compiling source: ai-result-return.ts.

Do not add a chat-response wrapper to AdaptTable. The turn envelope is application-owned.

catalog() lists only capabilities enabled by mounted features, column permissions, source support and host callbacks. A table with filtering and pagination must not advertise editing, grouping or pivoting.

Two equally valid ways to learn schemas:

  1. Compact catalog → describe → execute — describe one key when needed.
  2. Eager helpers (toJsonTools, toOpenAITools) that call describe for every enabled key.
for (const entry of session.catalog()) {
const guide = session.describe(entry.key);
await session.execute(entry.key, args, revision, idempotencyKey);
}

Host write-safety chrome (not on the envelope):

  • approval: "writes" | "destructive" | "never" (default "writes")
  • commit: "stage" | "immediate" (default "stage")

Stage records a proposal in the host edit callback. Immediate persists through that same callback. Approve, then Save, then Undo — the table never owns the data.

toJsonTools returns JsonFunctionTool[]. executeJsonTool takes a JsonToolCall. executeEnvelope and parseEnvelope are re-exported from @adapttable/ai/json.

toOpenAITools(session, { deferred: true }) returns only catalog, describe and execute. strict: true is the default. Eager tools replace each . in a catalog key with _ (view.setPageview_setPage) because OpenAI function names cannot contain dots. executeOpenAITool maps those names back, and still accepts the dotted catalog key. Deferred execute requires both key and args (args is a JSON string under strict). Strict mode lists every property in required, forbids open maps, and treats originally-optional fields as nullable.

toMcpTools / toMcpResources list enabled keys. Each capability is also a resource at adapttable://table/{tableId}/capability/{key}. mcpListChanged decides when to emit notifications/tools/list_changed.

@adapttable/ai/http is the optional ready-made transport: post the permitted context to your endpoint and execute the calls that come back through the same session. Setup, protocol and the runnable example live on connect a backend.

Each of these speaks a protocol somebody else defined, and each ends in the same session.execute — the same exclusion predicate, the same revision check, the same approval policy and the same receipts. None adds an SDK dependency: the event and part types are this package’s own, and the one seam a host fills is “send this, stream that back.”

An integration is described as supported here only once its recorded-event conformance fixture runs against the real adapter code.

toMcpTools carries annotations derived from what each capability declares: readOnlyHint for view and read keys, destructiveHint true only for a destructive one and explicitly false elsewhere, idempotentHint where running it twice lands where running it once did, and openWorldHint always false — a capability acts on this table and nothing behind it. toMcpToolList and toMcpResourceList return the same lists as cacheable responses, stamped with the contract they describe. mcpToolResult maps one execute outcome into a tools/call result and keeps the provenance envelope on a row window.

@adapttable/ai/mcp-apps publishes the table as a view an MCP host embeds: mcpAppResource for the ui://adapttable/table/{tableId} descriptor, mcpAppCsp for a policy built only from the domains you declared, and createMcpAppBridge for the view’s side — ui/initialize, the tool-input and tool-result notifications, and tools/call for a reader’s actions. The bridge requires the host’s exact origin: a handshake posted to "*" would announce the table’s contract to whatever else is listening. approveThroughHost and askThroughHost use the host’s elicitation when it advertises one and return nothing when it does not, leaving the table’s own approval in charge.

import { registerWebMcpTools } from "@adapttable/ai/webmcp";
const registration = registerWebMcpTools(session, {
exposedTo: ["view.setPage", "view.setSort", "rows.read"],
onWarning: (warning) => console.warn(warning.message),
});
// A contract change is a different set of tools: dispose and register again.
onTeardown(() => registration.dispose());

Nothing happens at import: document.modelContext is read when you call it, so the module loads on a server and in a browser without the API and registers nothing in either. In React, tableAgent({ webmcp: true }) does the same and re-registers on a contract change for you.

An agent in the page reads the table through one call and changes it through another, and the reader can move the table in between. Every tool therefore takes an optional expectedRevision — the revision the call was planned against, from the revision an earlier result carried:

// The agent read the table at revision 5 and acts on what it read.
await tool.execute({ page: 3, expectedRevision: 5 });
// Refused if the reader has since moved it, and the refusal says where it is:
// { error: { code: "revision-mismatch", ... }, revision: 7 }

Omit it and the call acts on the table as it is, which is what an agent that never names a revision has always done. This is the one transport where the revision travels on the call: HTTP and the AI SDK send the view with each request, and AG-UI publishes it as state, so those bind it for you.

AG-UI — the table as a run’s frontend tools

Section titled “AG-UI — the table as a run’s frontend tools”
import { aguiTransport } from "@adapttable/ai/ag-ui";
const transport = aguiTransport({
connection: { run: (input, signal) => yourEndpoint(input, signal) },
onApprove, // the same seam `session.execute` uses
});

The enabled contract becomes the run’s tool definitions, the sanitized view its STATE_SNAPSHOT and then RFC 6902 STATE_DELTAs, and the conversation its MESSAGES_SNAPSHOT. A RUN_FINISHED interrupt with reason: "confirmation" becomes an ApprovalSubject and resumes with resume[{ interruptId, status, payload }]; input_required becomes a question for the reader. A tool call this table does not own is left to whoever registered it.

// Your route, your provider, unchanged apart from the spread.
import { aiSdkTools } from "@adapttable/ai/ai-sdk";
streamText({
model,
system,
tools: { ...yourTools, ...aiSdkTools(session) },
});

Every entry omits execute, which is how the AI SDK decides the client runs it. In the browser, aiSdkTransport answers those calls and returns each result as the tool output on the next request — the shape addToolOutput sends. tool-approval-request becomes an ApprovalSubject and answers as an approval response; output-denied carries the reject reason. A stream that declares a version this adapter does not speak is refused with unknown-stream-version rather than parsed as though it were one it does.

Intention fixtures (no live model) live in packages/ai/src/__fixtures__/intentions.json.