Skip to content

aipartnerup/tiptap-apcore

Repository files navigation

tiptap-apcore

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.

License TipTap

Features

  • 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 CallingtoOpenaiTools(executor) exports tool definitions for GPT
  • Role-based ACLreadonly, editor, admin roles with tag-level and module-level overrides
  • Safety annotations — every command tagged readonly, destructive, idempotent, requiresApproval, openWorld, streaming
  • Strict JSON SchemasinputSchema + outputSchema with additionalProperties: false for all known commands
  • Dynamic re-discovery — call apcore.refresh() or registry.discover() to pick up extensions added at runtime
  • Dynamic ACL — call apcore.setAcl() to switch roles without recreating the instance
  • Framework agnostic — works with React, Vue, Angular, or Vanilla JS

Installation

npm install tiptap-apcore apcore-js apcore-mcp @tiptap/core

apcore-js, apcore-mcp, and @tiptap/core are peer dependencies.

Quick Start

Using TiptapAPCore class (recommended)

import { Editor } from "@tiptap/core";
import StarterKit from "@tiptap/starter-kit";
import { TiptapAPCore } from "tiptap-apcore";

// 1. Create a TipTap editor
const editor = new Editor({
  extensions: [StarterKit],
  content: "<p>Hello world</p>",
});

// 2. Create the APCore instance
const apcore = new TiptapAPCore(editor, {
  acl: { role: "editor" },   // no destructive ops
});

// 3. Call commands directly
await apcore.call("tiptap.format.toggleBold", {});
const { html } = await apcore.call("tiptap.query.getHTML", {});

// 4. Switch roles at runtime (e.g. when user toggles admin mode)
apcore.setAcl({ role: "admin" });

// 5. Launch an MCP Server (Node.js only — import from tiptap-apcore/server)
import { serve } from "tiptap-apcore/server";
await serve(apcore.executor);

// 6. Or export OpenAI tool definitions
import { toOpenaiTools } from "tiptap-apcore/server";
const tools = toOpenaiTools(apcore.executor);

Using withApcore factory (shortcut)

import { withApcore } from "tiptap-apcore";

const { registry, executor } = withApcore(editor, {
  acl: { role: "editor" },
});

await executor.call("tiptap.format.toggleBold", {});

withApcore returns a { registry, executor } pair. Use it when you don't need dynamic ACL updates or the convenience methods on TiptapAPCore.

Commands

All commands follow the module ID pattern {prefix}.{category}.{commandName}.

Query (10 commands) — readonly, idempotent

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 }

Format (36 commands) — non-destructive

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

Content (15 commands)

insertContent, insertContentAt, setNode, splitBlock, liftListItem, sinkListItem, wrapIn, joinBackward, joinForward, lift, splitListItem, wrapInList, toggleList, exitCode, deleteNode

Destructive (6 commands) — requiresApproval

clearContent, setContent, deleteSelection, deleteRange, deleteCurrentNode, cut

Selection (10 commands) — idempotent

setTextSelection, setNodeSelection, selectAll, selectParentNode, selectTextblockStart, selectTextblockEnd, selectText, focus, blur, scrollIntoView

History (2 commands)

undo, redo

Unknown

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.

Access Control (ACL)

// Read-only: only query commands
new TiptapAPCore(editor, { acl: { role: "readonly" } });

// Editor: query + format + content + history + selection
new TiptapAPCore(editor, { acl: { role: "editor" } });

// Admin: everything including destructive
new TiptapAPCore(editor, { acl: { role: "admin" } });

// Custom: readonly base + allow format tag
new TiptapAPCore(editor, { acl: { role: "readonly", allowTags: ["format"] } });

// Custom: admin but deny destructive tag
new TiptapAPCore(editor, { acl: { role: "admin", denyTags: ["destructive"] } });

// Module-level: deny specific commands
new TiptapAPCore(editor, {
  acl: { role: "admin", denyModules: ["tiptap.destructive.clearContent"] },
});

// Dynamic: switch roles at runtime
apcore.setAcl({ role: "admin" });

Precedence: denyModules > allowModules > denyTags > allowTags > role

Note: allowModules is additive — it grants access to listed modules but does not deny unlisted ones. Combine with a role to restrict the baseline.

MCP Server

Server functions must be imported from the tiptap-apcore/server subpath (Node.js only).

import { TiptapAPCore } from "tiptap-apcore";
import { serve } from "tiptap-apcore/server";

const apcore = new TiptapAPCore(editor);

// stdio (default)
await serve(apcore.executor);

// HTTP streaming
await serve(apcore.executor, {
  transport: "streamable-http",
  host: "127.0.0.1",
  port: 8000,
});

// Server-Sent Events
await serve(apcore.executor, { transport: "sse", port: 3000 });

Embedding in Express / Koa / Fastify (asyncServe)

asyncServe returns a Node.js HTTP request handler that you can mount in any existing server — no separate process needed.

import express from "express";
import { TiptapAPCore } from "tiptap-apcore";
import { asyncServe } from "tiptap-apcore/server";

const app = express();
const apcore = new TiptapAPCore(editor, { acl: { role: "admin" } });

// Create the MCP handler with the built-in Tool Explorer UI
const { handler, close } = await asyncServe(apcore.executor, {
  endpoint: "/mcp",           // MCP protocol endpoint
  explorer: true,             // Enable /explorer UI for interactive testing
  explorerPrefix: "/explorer",
  allowExecute: true,         // Allow tool execution from the explorer
  name: "my-editor-mcp",
});

// Mount alongside your existing routes
app.all("/mcp", (req, res) => handler(req, res));
app.all("/explorer", (req, res) => handler(req, res));
app.all("/explorer/*", (req, res) => handler(req, res));

app.listen(8000);

// On shutdown:
await close();

MCP clients (Claude Desktop, Cursor, etc.) can then connect to your server:

{
  "mcpServers": {
    "my-editor": {
      "url": "http://localhost:8000/mcp"
    }
  }
}

OpenAI Function Calling

import { TiptapAPCore } from "tiptap-apcore";
import { toOpenaiTools } from "tiptap-apcore/server";

const apcore = new TiptapAPCore(editor);
const tools = toOpenaiTools(apcore.executor);

// Use with OpenAI API
const response = await openai.chat.completions.create({
  model: "gpt-4o",
  messages: [...],
  tools,
});

Vercel AI SDK

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 { TiptapAPCore } from "tiptap-apcore";

const apcore = new TiptapAPCore(editor, { acl: { role: "editor" } });

// Convert APCore modules to AI SDK tools
const tools: Record<string, CoreTool> = {};
for (const id of apcore.list()) {
  const def = apcore.getDefinition(id)!;
  tools[id.replaceAll(".", "--")] = tool({
    description: def.description,
    parameters: jsonSchema(def.inputSchema),
    execute: (args) => apcore.call(id, args),
  });
}

const { text, steps } = await generateText({
  model: openai("gpt-4o"),
  system: "You are an editor assistant...",
  messages,
  tools,
  maxSteps: 10,
});

API Reference

TiptapAPCore class

The primary entry point. Encapsulates registry, executor, ACL, and extension discovery.

const apcore = new TiptapAPCore(editor, options?);
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
logger Logger undefined Logger for diagnostic output
sanitizeHtml (html: string) => string undefined HTML sanitizer for insertContent/setContent
Method / Property Description
registry The APCore Registry (read-only)
executor The APCore Executor (read-only)
call(moduleId, inputs) Execute a command (async)
list(options?) List module IDs, optionally filtered by tags and/or prefix
getDefinition(moduleId) Get full ModuleDescriptor or null
setAcl(acl) Update ACL configuration at runtime (validates role)
refresh() Re-scan extensions and update registry; returns module count

withApcore(editor, options?)

Factory function that creates a TiptapAPCore instance and returns { registry, executor }. Accepts the same options as TiptapAPCore.

Registry Methods

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

Executor Methods

Method Description
call(moduleId, inputs) Execute a module (async)
callAsync(moduleId, inputs) Alias for call()
registry Access the underlying registry

Error Codes

All errors are instances of TiptapModuleError (extends Error).

import { TiptapModuleError, ErrorCodes } from "tiptap-apcore";

try {
  await apcore.call("tiptap.format.toggleBold", {});
} catch (err) {
  if (err instanceof TiptapModuleError) {
    console.log(err.code, err.message, err.details);
  }
}
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

Server Exports (tiptap-apcore/server)

Function Description
serve(executor, options?) Launch an MCP server (stdio / streamable-http / sse)
asyncServe(executor, options?) Build an embeddable HTTP handler — returns { handler, close }
toOpenaiTools(executor, options?) Export OpenAI Function Calling tool definitions
resolveRegistry(executor) Access the registry from an executor
resolveExecutor(registry) Create an executor from a registry

asyncServe Options

Option Type Default Description
name string "apcore-mcp" MCP server name
endpoint string "/mcp" MCP protocol endpoint path
explorer boolean false Enable the browser-based Tool Explorer UI
explorerPrefix string "/explorer" URL prefix for the explorer
allowExecute boolean false Allow tool execution from the explorer UI
validateInputs boolean false Validate inputs against JSON schemas
tags string[] null Filter modules by tags
prefix string null Filter modules by prefix

asyncServe returns { handler, close } where handler is (IncomingMessage, ServerResponse) => Promise<void>.

Architecture

┌──────────────────┐     ┌──────────────────┐     ┌──────────────┐
│  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 definitions
  • Types and constants for the APCore protocol

AI Capabilities

Supported (79 commands)

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.

Not Supported

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

Comparison with TipTap AI Toolkit

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
Tools exposed 5 coarse tools 79+ fine-grained commands
Access control None built-in 3 roles + tag/module allow/deny lists
Safety annotations None readonly, destructive, idempotent, requiresApproval per command
Streaming output streamText(), streamHtml() Not yet supported
Headless mode Not supported Full support

Use TipTap AI Toolkit when you need real-time streaming of AI-generated content with a built-in accept/reject review UI.

Use tiptap-apcore when you want any MCP-compatible agent to control the editor with fine-grained access control, strict schema validation, and headless/server-side support.

Use both when you want streaming AI content generation AND structured command control in the same application.

Demo

The demo/ directory contains a full-stack example with two modes:

cd demo/server && npm install && npm run dev   # Terminal 1
cd demo/frontend && npm install && npm run dev # Terminal 2
# Open http://localhost:5173

Set LLM_MODEL (e.g. openai:gpt-4o, anthropic:claude-sonnet-4-5) in demo/.env. See demo/README.md for details.

AI Editor Demo tab

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. Includes role-based ACL switching, demo scenarios, and a tool call log.

MCP Server tab

A persistent headless TipTap editor exposed as an MCP streamable-http endpoint via asyncServe(). The tab shows:

  • Status — live server status, tool count, and endpoint URL
  • Tool Explorer — embedded /explorer UI for browsing and executing all 79+ tools interactively
  • Connect — copy-paste config snippets for Claude Desktop, Cursor, and generic MCP clients

The MCP endpoint is available at http://localhost:8000/mcp — connect any MCP client to control the editor remotely.

Documentation

Development

npm install       # Install dependencies
npm test          # Run tests
npm run typecheck # Type check
npm run build     # Build

License

Apache-2.0

About

Let AI safely control your TipTap editor via the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) and OpenAI Function Calling.

Topics

Resources

Stars

Watchers

Forks

Packages