Let AI safely control your TipTap editor via the Model Context Protocol (MCP) and OpenAI Function Calling.
tiptap-apcore wraps every TipTap editor command as a schema-driven APCore module — complete with JSON Schema validation, safety annotations, and fine-grained access control. Any MCP-compatible AI agent can then discover and invoke these modules to read, format, insert, or restructure rich-text content.
- 79 built-in commands across 7 categories (query, format, content, destructive, selection, history, unknown)
- Automatic extension discovery — scans TipTap extensions at runtime, no manual wiring
- MCP Server in one line —
serve(executor)exposes all commands via stdio / HTTP / SSE - OpenAI Function Calling —
toOpenaiTools(executor)exports tool definitions for GPT - Role-based ACL —
readonly,editor,adminroles with tag-level and module-level overrides - Safety annotations — every command tagged
readonly,destructive,idempotent,requiresApproval,openWorld,streaming - Strict JSON Schemas —
inputSchema+outputSchemawithadditionalProperties: falsefor all known commands - Dynamic re-discovery — call
registry.discover()to pick up extensions added at runtime - 925 tests, 99.7% statement coverage
npm install tiptap-apcore apcore-js apcore-mcp @tiptap/coreapcore-js, apcore-mcp, and @tiptap/core are peer dependencies.
import { Editor } from "@tiptap/core";
import StarterKit from "@tiptap/starter-kit";
import { withApcore, serve, toOpenaiTools } from "tiptap-apcore";
// 1. Create a TipTap editor
const editor = new Editor({
extensions: [StarterKit],
content: "<p>Hello world</p>",
});
// 2. Create APCore registry + executor
const { registry, executor } = withApcore(editor, {
acl: { role: "editor" }, // no destructive ops
});
// 3a. Launch an MCP Server (stdio)
await serve(executor);
// 3b. Or export OpenAI tool definitions
const tools = toOpenaiTools(executor);
// 3c. Or call commands directly
await executor.call("tiptap.format.toggleBold", {});
const { html } = await executor.call("tiptap.query.getHTML", {});All commands follow the module ID pattern {prefix}.{category}.{commandName}.
| Command | Input | Output |
|---|---|---|
getHTML |
— | { html: string } |
getJSON |
— | { json: object } |
getText |
{ blockSeparator?: string } |
{ text: string } |
isActive |
{ name: string, attrs?: object } |
{ active: boolean } |
getAttributes |
{ typeOrName: string } |
{ attributes: object } |
isEmpty |
— | { value: boolean } |
isEditable |
— | { value: boolean } |
isFocused |
— | { value: boolean } |
getCharacterCount |
— | { count: number } |
getWordCount |
— | { count: number } |
toggleBold, toggleItalic, toggleStrike, toggleCode, toggleUnderline, toggleSubscript, toggleSuperscript, toggleHighlight, toggleHeading, toggleBulletList, toggleOrderedList, toggleTaskList, toggleCodeBlock, toggleBlockquote, setTextAlign, setMark, unsetMark, unsetAllMarks, clearNodes, updateAttributes, setLink, unsetLink, setHardBreak, setHorizontalRule, setBold, setItalic, setStrike, setCode, unsetBold, unsetItalic, unsetStrike, unsetCode, setBlockquote, unsetBlockquote, setHeading, setParagraph
insertContent, insertContentAt, setNode, splitBlock, liftListItem, sinkListItem, wrapIn, joinBackward, joinForward, lift, splitListItem, wrapInList, toggleList, exitCode, deleteNode
clearContent, setContent, deleteSelection, deleteRange, deleteCurrentNode, cut
setTextSelection, setNodeSelection, selectAll, selectParentNode, selectTextblockStart, selectTextblockEnd, selectText, focus, blur, scrollIntoView
undo, redo
Commands discovered from extensions but not in the built-in catalog. Excluded by default (includeUnsafe: false). Set includeUnsafe: true to include them with permissive schemas.
// Read-only: only query commands
withApcore(editor, { acl: { role: "readonly" } });
// Editor: query + format + content + history + selection
withApcore(editor, { acl: { role: "editor" } });
// Admin: everything including destructive
withApcore(editor, { acl: { role: "admin" } });
// Custom: readonly base + allow format tag
withApcore(editor, { acl: { role: "readonly", allowTags: ["format"] } });
// Custom: admin but deny destructive tag
withApcore(editor, { acl: { role: "admin", denyTags: ["destructive"] } });
// Module-level: deny specific commands
withApcore(editor, {
acl: { role: "admin", denyModules: ["tiptap.destructive.clearContent"] },
});Precedence: denyModules > allowModules > denyTags > allowTags > role
Note:
allowModulesis additive — it grants access to listed modules but does not deny unlisted ones. Combine with a role to restrict the baseline.
import { withApcore, serve } from "tiptap-apcore";
const { executor } = withApcore(editor);
// stdio (default)
await serve(executor);
// HTTP streaming
await serve(executor, {
transport: "streamable-http",
host: "127.0.0.1",
port: 8000,
});
// Server-Sent Events
await serve(executor, { transport: "sse", port: 3000 });import { withApcore, toOpenaiTools } from "tiptap-apcore";
const { executor } = withApcore(editor);
const tools = toOpenaiTools(executor);
// Use with OpenAI API
const response = await openai.chat.completions.create({
model: "gpt-4o",
messages: [...],
tools,
});APCore's JSON schemas work directly with AI SDK's jsonSchema() — no Zod conversion needed. Combined with generateText({ maxSteps }), the tool-use loop is fully automatic.
import { generateText, tool, jsonSchema } from "ai";
import { openai } from "@ai-sdk/openai";
import { withApcore } from "tiptap-apcore";
const { registry, executor } = withApcore(editor, { acl: { role: "editor" } });
// Convert APCore modules to AI SDK tools
const tools: Record<string, CoreTool> = {};
for (const id of registry.list()) {
const def = registry.getDefinition(id)!;
tools[id.replaceAll(".", "-")] = tool({
description: def.description,
parameters: jsonSchema(def.inputSchema),
execute: (args) => executor.call(id, args),
});
}
const { text, steps } = await generateText({
model: openai("gpt-4o"),
system: "You are an editor assistant...",
messages,
tools,
maxSteps: 10,
});Creates an APCore { registry, executor } pair from a TipTap editor.
| Option | Type | Default | Description |
|---|---|---|---|
prefix |
string |
"tiptap" |
Module ID prefix (lowercase alphanumeric) |
acl |
AclConfig |
undefined |
Access control configuration (permissive if omitted) |
includeUnsafe |
boolean |
false |
Include commands not in the built-in catalog |
| Method | Description |
|---|---|
list(options?) |
List module IDs, optionally filtered by tags (OR) and/or prefix |
getDefinition(moduleId) |
Get full ModuleDescriptor or null |
has(moduleId) |
Check if a module exists |
iter() |
Iterate [moduleId, descriptor] pairs |
count |
Number of registered modules |
moduleIds |
Array of all module IDs |
on(event, callback) |
Listen for "register" / "unregister" events |
discover() |
Re-scan extensions and update registry |
| Method | Description |
|---|---|
call(moduleId, inputs) |
Execute a module (async) |
callAsync(moduleId, inputs) |
Alias for call() |
| Code | Description |
|---|---|
MODULE_NOT_FOUND |
Module ID not registered |
COMMAND_NOT_FOUND |
Command not available on editor |
ACL_DENIED |
Access denied by ACL policy |
EDITOR_NOT_READY |
Editor is destroyed |
COMMAND_FAILED |
TipTap command returned false |
SCHEMA_VALIDATION_ERROR |
Invalid options (bad prefix, bad role) |
INTERNAL_ERROR |
Unexpected error |
TipTap's official AI solution is the AI Toolkit (@tiptap-pro/ai-toolkit), a paid extension for client-side AI-powered editing. The two projects serve different use cases and are complementary.
| TipTap AI Toolkit | tiptap-apcore | |
|---|---|---|
| Type | Client-side TipTap extension | Server-side / headless adapter |
| License | Proprietary (TipTap Pro subscription) | Apache-2.0 (open source) |
| Runtime | Browser only | Browser + Node.js + headless |
| Protocol | Provider-specific adapters | MCP standard + OpenAI Function Calling |
| Approach | AI generates content, streams into editor | AI invokes structured commands on editor |
| TipTap AI Toolkit | tiptap-apcore | |
|---|---|---|
| Tools exposed | 5 coarse tools | 79+ fine-grained commands |
| Read | tiptapRead, tiptapReadSelection |
getHTML, getJSON, getText, isActive, getAttributes, isEmpty, isEditable, isFocused, getCharacterCount, getWordCount |
| Write | tiptapEdit (accepts operations array) |
Individual commands: toggleBold, insertContent, setNode, wrapIn, ... |
| Comments | getThreads, editThreads |
Not supported |
| Schemas | Tool parameters with descriptions | Strict JSON Schema per command (additionalProperties: false) |
The AI Toolkit bundles all editing into a single tiptapEdit tool that accepts an array of operations. tiptap-apcore exposes each operation as a standalone tool with its own schema — this gives the LLM more precise tool selection and lower token usage per call.
| TipTap AI Toolkit | tiptap-apcore | |
|---|---|---|
| Access control | None built-in | 3 roles + tag/module allow/deny lists |
| Safety annotations | None | readonly, destructive, idempotent, requiresApproval, openWorld, streaming per command |
| Approval workflow | Review mode (accept/reject UI) | requiresApproval annotation for MCP clients |
| Input validation | Basic parameter types | Strict JSON Schema with additionalProperties: false |
| TipTap AI Toolkit | tiptap-apcore | |
|---|---|---|
| MCP | Not supported | stdio, streamable-http, SSE |
| OpenAI | Via adapter (@tiptap-pro/ai-adapter-openai) |
toOpenaiTools() one-liner |
| Anthropic | Via adapter (@tiptap-pro/ai-adapter-anthropic) |
Via MCP (any MCP client) |
| Vercel AI SDK | Via adapter | Direct (generateText + tool + jsonSchema) or via MCP |
| Custom agents | Adapter required per provider | Any MCP-compatible agent works |
| TipTap AI Toolkit | tiptap-apcore | |
|---|---|---|
| Streaming output | streamText(), streamHtml() |
Not yet supported |
| Review mode | Accept / Reject UI | Not supported (planned) |
| Content generation | Built-in (prompts → editor) | Delegated to LLM (tool use → commands) |
The AI Toolkit streams LLM-generated content directly into the editor with a review UI. tiptap-apcore takes a different approach: the LLM decides which commands to call, and the executor applies them. Content generation is the LLM's responsibility, not the editor's.
| TipTap AI Toolkit | tiptap-apcore | |
|---|---|---|
| Headless mode | Not supported | Full support |
| Batch processing | Not possible | Process multiple documents programmatically |
| CI/CD pipelines | Not applicable | Can validate, transform, or test content |
| Multi-tenant | One editor per user | One executor per editor, server-side isolation |
Use TipTap AI Toolkit when:
- You need real-time streaming of AI-generated content into the editor
- You want a built-in accept/reject review UI
- You're building a client-side-only application
- You need comment thread management with AI
Use tiptap-apcore when:
- You want any MCP-compatible agent to control the editor
- You need fine-grained access control (roles, tag/module blocking)
- You're running headless / server-side (batch processing, CI/CD)
- You want strict schema validation and safety annotations
- You need to support multiple AI providers without per-provider adapters
- You want open-source with no licensing fees
Use both when:
- You want streaming AI content generation (AI Toolkit) AND structured command control (tiptap-apcore) in the same application
- You want client-side AI chat + server-side AI automation on the same editor
tiptap-apcore exposes 79 built-in commands that an AI agent can invoke:
| Category | Count | Commands |
|---|---|---|
| Query | 10 | getHTML, getJSON, getText, isActive, getAttributes, isEmpty, isEditable, isFocused, getCharacterCount, getWordCount |
| Format | 36 | Toggle: toggleBold, toggleItalic, toggleStrike, toggleCode, toggleUnderline, toggleSubscript, toggleSuperscript, toggleHighlight, toggleHeading, toggleBulletList, toggleOrderedList, toggleTaskList, toggleCodeBlock, toggleBlockquote. Set/Unset: setBold, setItalic, setStrike, setCode, unsetBold, unsetItalic, unsetStrike, unsetCode, setBlockquote, unsetBlockquote, setHeading, setParagraph. Other: setTextAlign, setMark, unsetMark, unsetAllMarks, clearNodes, updateAttributes, setLink, unsetLink, setHardBreak, setHorizontalRule |
| Content | 15 | insertContent, insertContentAt, setNode, splitBlock, liftListItem, sinkListItem, wrapIn, joinBackward, joinForward, lift, splitListItem, wrapInList, toggleList, exitCode, deleteNode |
| Destructive | 6 | clearContent, setContent, deleteSelection, deleteRange, deleteCurrentNode, cut |
| Selection | 10 | setTextSelection, setNodeSelection, selectAll, selectParentNode, selectTextblockStart, selectTextblockEnd, selectText, focus, blur, scrollIntoView |
| History | 2 | undo, redo |
The selectText command enables semantic text selection — the AI can select text by content rather than by position, which is more natural for LLM-driven editing.
| Feature | Reason |
|---|---|
Clipboard operations (copy, paste) |
Requires browser Clipboard API — not available in headless / server-side |
| Drag and drop | Requires browser DOM events |
| IME / composition events | Requires browser input events |
| Real-time collaboration (Yjs/Hocuspocus) | Collaboration is handled at the transport layer, not the command layer |
| Streaming content generation | Content generation is delegated to the LLM; the executor applies discrete commands |
| Comment threads | Not part of core TipTap — requires @tiptap-pro extensions |
tiptap-apcore is the TipTap adapter that wraps editor commands as APCore modules. apcore-mcp is the protocol layer that exposes those modules to AI agents via MCP or OpenAI Function Calling.
┌──────────────────┐ ┌──────────────────┐ ┌──────────────┐
│ TipTap Editor │────▶│ tiptap-apcore │────▶│ apcore-mcp │
│ (@tiptap/core) │ │ (this package) │ │ (protocol) │
└──────────────────┘ └──────────────────┘ └──────────────┘
Registry + Executor MCP / OpenAI
tiptap-apcore provides:
- Extension discovery (
ExtensionScanner) - Module building (
ModuleBuilder+AnnotationCatalog+SchemaCatalog) - Command execution (
TiptapExecutor) - Access control (
AclGuard)
apcore-mcp provides:
serve(executor)— Launch an MCP server (stdio / HTTP / SSE)toOpenaiTools(executor)— Export OpenAI Function Calling tool definitionsresolveRegistry(executor)— Access the registry from an executorresolveExecutor(registry)— Create an executor from a registry- Types and constants for the APCore protocol
The demo/ directory contains a full-stack example: a React + Vite frontend with a TipTap editor, and an Express backend that uses the Vercel AI SDK to let any LLM edit the document via APCore tools.
cd demo/server && npm install && npm run dev # Terminal 1
cd demo && npm install && npm run dev # Terminal 2Set LLM_MODEL (e.g. openai:gpt-4o, anthropic:claude-sonnet-4-5) in demo/.env. See demo/README.md for details.
# Install dependencies
npm install
# Run tests
npm test
# Type check
npm run typecheck
# Build
npm run buildApache-2.0