@matrajs/mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,138 @@
1
+ # Frameworks
2
+
3
+ React, Vue, Svelte and Solid have bindings. Angular, Qwik and plain JavaScript need three calls, and here they are.
4
+
5
+ Matra is a document model and a view that attaches to an element. It has no opinion about what renders around it, so **every framework already works** — the only question is how much sugar there is.
6
+
7
+ ## Four have bindings
8
+
9
+ | | Package | What you get |
10
+
11
+ {
12
+ bindings.map(([name, pkg, what]) => (
13
+
14
+ | | `` | |
15
+ ))
16
+ }
17
+
18
+ All four are MIT, all four are under a hundred lines, and none re-implements anything. They exist for two reasons: turning the editor's changes into a re-render is the one piece of boilerplate worth removing, and mounting twice into one element is the mistake everybody makes once. Each binding guards it.
19
+
20
+ ## Everything else is three calls
21
+
22
+ Create it, mount it into an element, destroy it when the element goes away.
23
+
24
+ ```
25
+ {`import { createEditor, starterKit } from '@matrajs/core'
26
+
27
+ const editor = createEditor({ extensions: starterKit, content: '<p>Hello</p>' })
28
+ editor.mount(document.querySelector('#editor'))
29
+
30
+ // later
31
+ editor.destroy()`}
32
+ ```
33
+
34
+ That is the whole of it. `destroy()` removes the listeners and the `contenteditable` attribute, so the element is safe to reuse — which is what a hot reload, a route change and a recycled list row all rely on.
35
+
36
+ > Note: The guard worth copying is `editor.unsafe.view`. It is truthy once mounted, so a component that renders twice does not attach a second view to one element. Every binding uses it and so should yours.
37
+
38
+ ## Svelte
39
+
40
+ An action, which is exactly the shape Svelte has for this. Stores rather than runes, so the same package works in Svelte 4 and 5.
41
+
42
+ ```
43
+ {`<script>
44
+ import { starterKit } from '@matrajs/core'
45
+ import { matra } from '@matrajs/svelte'
46
+
47
+ const { action, editor, state } = matra({
48
+ extensions: starterKit,
49
+ content: '<p>Hello</p>',
50
+ })
51
+ </script>
52
+
53
+ <button onclick={() => editor.commands.toggleBold()}
54
+ aria-pressed={$state.isActive('bold')}>Bold</button>
55
+
56
+ <div use:action></div>`}
57
+ ```
58
+
59
+ ## Solid
60
+
61
+ A signal rather than an external store · Solid's reactivity is not a render loop, so everything reading `state()` re-runs and nothing else does.
62
+
63
+ ```
64
+ {`import { starterKit } from '@matrajs/core'
65
+ import { createMatra } from '@matrajs/solid'
66
+
67
+ export function Editor() {
68
+ const { editor, mount, state } = createMatra({
69
+ extensions: starterKit,
70
+ content: '<p>Hello</p>',
71
+ })
72
+
73
+ return (
74
+ <>
75
+ <button onClick={() => editor.commands.toggleBold()}
76
+ aria-pressed={state().isActive('bold')}>Bold</button>
77
+ <div ref={mount} />
78
+ </>
79
+ )
80
+ }`}
81
+ ```
82
+
83
+ ## Angular
84
+
85
+ A directive, so the element stays in the template where it belongs. There is no `@matrajs/angular` package, and the reason is worth saying rather than leaving you to wonder: an Angular library has to be published through `ng-packagr` in Angular's partial-compilation format, or it fails in every consumer's AOT build. Shipping one that breaks in production builds would be worse than these fifteen lines, which *your* compiler handles correctly because it compiles them.
86
+
87
+ ```
88
+ {`import { Directive, ElementRef, OnDestroy, OnInit, inject } from '@angular/core'
89
+ import { createEditor, starterKit } from '@matrajs/core'
90
+
91
+ @Directive({ selector: '[matra]', standalone: true, exportAs: 'matra' })
92
+ export class MatraDirective implements OnInit, OnDestroy {
93
+ private host = inject(ElementRef<HTMLElement>)
94
+ editor = createEditor({ extensions: starterKit, content: '<p>Hello</p>' })
95
+
96
+ ngOnInit() {
97
+ if (!this.editor.unsafe.view) this.editor.mount(this.host.nativeElement)
98
+ }
99
+
100
+ ngOnDestroy() {
101
+ this.editor.destroy()
102
+ }
103
+ }
104
+
105
+ // <div matra #ed="matra"></div>
106
+ // <button (click)="ed.editor.commands.toggleBold()">Bold</button>`}
107
+ ```
108
+
109
+ ## Qwik, Astro, Lit, or no framework
110
+
111
+ The same three calls, wherever you can get an element and a teardown hook. In Astro or a plain page, a `` tag is enough — every editor on this site is exactly that.
112
+
113
+ ## Keeping a toolbar in step
114
+
115
+ The one thing worth knowing without a binding: `editor.on('change')` and `editor.on('selectionChange')` both return an unsubscribe function, and `editor.isActive('bold')` reads the state from the document. That is everything a toolbar needs, in any framework:
116
+
117
+ ```
118
+ {`const off = editor.on('selectionChange', () => {
119
+ boldButton.setAttribute('aria-pressed', String(editor.isActive('bold')))
120
+ })
121
+
122
+ // on teardown
123
+ off()`}
124
+ ```
125
+
126
+ ## Server rendering
127
+
128
+ `createEditor` needs no DOM. Build a document, read `getHTML()` or `getJSON()`, and send it — the view only exists once something calls `mount`. That is also why [toMarkdown](https://matrajs.com/docs/api) runs on a server with no jsdom in sight.
129
+
130
+ > Note: One condition: content loaded on a server must be JSON or Markdown, not an HTML string. Reading HTML is a DOM job, so `content: '
131
+
132
+
133
+
134
+ '` needs a browser · `content: storedJson` and `content: fromMarkdown(text)` do not. Writing HTML out with `getHTML()` works either way.
135
+
136
+ ## The paid packages, anywhere
137
+
138
+ `@matrajs/ai`, `@matrajs/collab` and `@matrajs/versions` are extensions, not components. They go in the same array and know nothing about what renders around them, in every framework on this page.
@@ -0,0 +1,66 @@
1
+ # Introduction
2
+
3
+ Matra is a headless rich text editor framework. It gives you a document model, an extension API and a command system, and stays out of your interface.
4
+
5
+ Matra is a headless rich text editor framework. It gives you a document model, an extension API and a command system · and stays out of the way of your interface.
6
+
7
+ ## Why another editor
8
+
9
+ Every editor framework leaks its engine into your code. You import the engine's types, write against its internals, and upgrading it becomes your problem. Matra wraps the engine completely: your document is plain JSON and no engine type appears in a public signature.
10
+
11
+ It also has no runtime dependencies. The document model, transforms, position mapping, editor state and the editable view are written from scratch, which is why an app bundles kB gzipped instead of 117.
12
+
13
+ ## The three ideas
14
+
15
+ Everything else follows from these, and there is not a fourth.
16
+
17
+ - **The document is data.** Plain JSON — log it, store it, diff it, read it on a server that has never loaded an editor. [Document model →](https://matrajs.com/docs/document-model)
18
+
19
+ - **The extension array is the feature list.** What you pass in decides what the editor can do, what it accepts on paste, and what `editor.commands` is typed as. An editor with no heading extension has no heading command, and TypeScript knows. [Writing an extension →](https://matrajs.com/docs/extensions)
20
+
21
+ - **Positions are mapped, not remembered.** A position held across an `await` is wrong by the time you come back, so the async path never hands you a raw number. [Position mapping →](https://matrajs.com/docs/position-mapping)
22
+
23
+ ## Install
24
+
25
+ ```
26
+ npm i @matrajs/core
27
+ ```
28
+
29
+ ```
30
+ {`import { createEditor, starterKit } from '@matrajs/core'
31
+
32
+ const editor = createEditor({ extensions: starterKit, content: '<p>Hello.</p>' })
33
+ editor.mount(document.querySelector('#editor'))`}
34
+ ```
35
+
36
+ That is a working editor. It will look like unstyled text until you write some CSS, which is what headless means — [Styling](https://matrajs.com/docs/styling) covers it in a page.
37
+
38
+ ## What is in the box
39
+
40
+ | Package | Licence | What it is |
41
+
42
+ | `@matrajs/core` | MIT | The engine and 38 extensions |
43
+
44
+ | `@matrajs/react`, `/vue`, `/svelte`, /solid MIT | Framework bindings · thin, and optional |
45
+
46
+ | `@matrajs/ai` | Paid | Streaming edits that survive concurrent typing |
47
+
48
+ | `@matrajs/collab` | Paid | An authority, step rebasing, remote cursors |
49
+
50
+ | `@matrajs/versions` | Paid | Snapshots, a real diff, restore |
51
+
52
+ Table of contents, unique block ids, drag handle and comments are in the core and stay there. They are paid features elsewhere.
53
+
54
+ ## Where to go next
55
+
56
+ Depending on what you came here to do:
57
+
58
+ - **Get something working.** [Your first editor](https://matrajs.com/docs/first-editor), then [Styling](https://matrajs.com/docs/styling).
59
+
60
+ - **Fit it into your app.** [Frameworks](https://matrajs.com/docs/frameworks) — React, Vue, Svelte, Solid, Angular and plain JavaScript.
61
+
62
+ - **Understand it.** [Document model](https://matrajs.com/docs/document-model) and [Commands](https://matrajs.com/docs/commands).
63
+
64
+ - **Decide whether to trust it.** [Benchmarks](https://matrajs.com/docs/benchmarks), including the row where the harness was wrong and what it was measuring instead.
65
+
66
+ > Note: Matra is pre-1.0. The API may still change, and the view has not yet met real IME users on iOS and Android at scale · if your users type Bangla, Chinese or Japanese, test before you commit.
@@ -0,0 +1,50 @@
1
+ # Installation
2
+
3
+ Which packages exist, which are MIT, and which need a subscription.
4
+
5
+ One package. **Installing a binding installs the engine with it**, so a React application needs nothing else · no second package, and no third-party dependency arrives behind it.
6
+
7
+ ```
8
+ npm i @matrajs/react # or vue, svelte, solid
9
+ ```
10
+
11
+ Without a framework, or with one that needs no binding, install the engine directly.
12
+
13
+ ```
14
+ npm i @matrajs/core
15
+ ```
16
+
17
+ It ships no appearance. [Styling](https://matrajs.com/docs/styling) is a page of its own, and worth reading before the first afternoon rather than after it.
18
+
19
+ ## Framework bindings
20
+
21
+ Each one depends on `@matrajs/core` and pulls it in for you.
22
+
23
+ ```
24
+ npm i @matrajs/react # useEditor, useEditorState, EditorContent
25
+ npm i @matrajs/vue # useEditor, EditorContent
26
+ npm i @matrajs/svelte # matra — a use: action and a store
27
+ npm i @matrajs/solid # createMatra — a ref and a signal
28
+ ```
29
+
30
+ All four are MIT and all four are thin · the editor does not care which framework renders around it, and no binding re-implements anything. Angular, Qwik and plain JavaScript need no package at all — three calls and a teardown, which [Frameworks](https://matrajs.com/docs/frameworks) shows for each of them.
31
+
32
+ ## The paid packages
33
+
34
+ ```
35
+ npm i @matrajs/ai # streaming edits
36
+ npm i @matrajs/collab # authority, rebasing, remote cursors
37
+ npm i @matrajs/versions # snapshots, diff, restore
38
+ ```
39
+
40
+ Installed the same way as everything else. All three are source-available under a commercial licence rather than MIT · free for evaluation, development, testing, teaching, personal projects and organisations with fewer than three developers on the software, paid beyond that. Each has its own page — [AI](https://matrajs.com/docs/ai), [Collaboration and Version history](https://matrajs.com/docs/collab) — and the terms are on [the licence page](https://matrajs.com/licence).
41
+
42
+ > Note: There is no registry to configure, no token to obtain and no licence key. Nothing checks anything at runtime and nothing phones home. You install them, build with them, and pay when what you built goes to production · which means an unpaid invoice is a conversation rather than an outage.
43
+
44
+ ## Requirements
45
+
46
+ - Any bundler. The packages ship ESM and CJS with types.
47
+
48
+ - No peer dependencies for the core.
49
+
50
+ - Node 18+ for the parts that run on a server · Markdown needs no DOM.
@@ -0,0 +1,66 @@
1
+ # Docs for AI tools (MCP)
2
+
3
+ An MCP server that serves this documentation to Claude, Cursor, Codex and anything else that speaks the protocol. Zero dependencies, one command.
4
+
5
+ `@matrajs/mcp` is this documentation as a [Model Context Protocol](https://modelcontextprotocol.io) server. Connect it and an AI assistant answers questions about Matra from these pages rather than from memory — which matters for a framework whose API is inferred from an array and has no ProseMirror underneath, because memory will confidently describe something else.
6
+
7
+ ```
8
+ npx -y @matrajs/mcp # stdio · what a desktop client spawns
9
+ npx -y @matrajs/mcp --http # http://localhost:3333/mcp
10
+ ```
11
+
12
+ ## Step 1 · Connect it
13
+
14
+ **Claude Code**
15
+
16
+ ```
17
+ claude mcp add matra -- npx -y @matrajs/mcp
18
+ ```
19
+
20
+ **Claude Desktop** · in `claude_desktop_config.json`
21
+
22
+ ```
23
+ {
24
+ "mcpServers": {
25
+ "matra": { "command": "npx", "args": ["-y", "@matrajs/mcp"] }
26
+ }
27
+ }
28
+ ```
29
+
30
+ **Cursor** · the same object under `"mcpServers"` in .cursor/mcp.json.
31
+
32
+ **Codex** · in `~/.codex/config.toml`
33
+
34
+ ```
35
+ [mcp_servers.matra]
36
+ command = "npx"
37
+ args = ["-y", "@matrajs/mcp"]
38
+ ```
39
+
40
+ **Anything that speaks HTTP** · run `npx -y @matrajs/mcp --http 3333 and point the client at http://localhost:3333/mcp`.
41
+
42
+ ## Step 2 · Ask
43
+
44
+ Ask the assistant how to add search and replace, or what `ctx.mark()` is for. It calls `search_docs`, reads the page it needs with `read_doc`, and quotes it. Nothing is fetched at runtime · every page ships inside the package.
45
+
46
+ ## What it serves
47
+
48
+ | Tool | Does |
49
+
50
+ | `list_docs` | Every page, with its slug and a one-line description. `read_doc ` | One page, as Markdown. |
51
+ search_docs Ranked pages with a snippet each.
52
+
53
+ Every page is also a resource at `matra://docs/`. The pages are the repository's Markdown — the README, the engine notes, benchmarks, security, the changelog — and every page of this site, converted to Markdown when the package is built.
54
+
55
+ ## From code
56
+
57
+ ```
58
+ import { createServer } from '@matrajs/mcp'
59
+
60
+ const server = createServer(docs)
61
+ server.handle({ jsonrpc: '2.0', id: 1, method: 'tools/list' })
62
+ ```
63
+
64
+ `createServer` is the protocol without a transport: one message in, one reply out. Put it behind whatever transport you already run.
65
+
66
+ > Note: The server is read-only and speaks only about Matra. It declares three tools, all marked read-only and idempotent, and no prompts. It never sends a message the client did not ask for, which is why the HTTP mode needs no event stream.
@@ -0,0 +1,112 @@
1
+ # Position mapping
2
+
3
+ A position is an integer into the document. The moment anything changes, that integer points somewhere else. Mapping is how a position survives an edit it did not expect.
4
+
5
+ A position is an integer into the document. The moment anything changes, that integer points somewhere else. Mapping is how a position survives an edit it did not expect — and it is the single thing this editor is built around.
6
+
7
+ ## Why positions move
8
+
9
+ Insert five characters at the start and everything after them is five further along. The number you were holding is still a valid position; it just points at different text.
10
+
11
+ ```
12
+ {`hello world XXXXXhello world
13
+ ↑ ↑
14
+ 7 · "world" 7 · "ello world"`}
15
+ ```
16
+
17
+ That is fine when you act immediately — nothing happened in between. It stops being fine the moment there is a gap: you asked a model for a rewrite two seconds ago, or a colleague's edit arrived over the wire, and the person kept typing while you waited.
18
+
19
+ ## Markers
20
+
21
+ `ctx.mark()` takes a marker. It records where the document was at that instant, and `map` replays every change made since — so the position comes back pointing at the same *text* rather than at the same number.
22
+
23
+ ```
24
+ {`const at = ctx.mark()
25
+ const { from, to } = ctx.selection
26
+
27
+ for await (const chunk of stream) {
28
+ // Not from and to · they are two seconds old.
29
+ ctx.replace(at.mapRange({ from, to }), chunk)
30
+ }`}
31
+ ```
32
+
33
+ Between one chunk and the next the person may have typed, pasted, or pressed undo. The marker does not care which: it maps through whatever happened.
34
+
35
+ ## What it does in each case
36
+
37
+ A marker taken over `"world"` in `"hello world"`, then each of these happens, then the range is mapped:
38
+
39
+ | What happened | Document | 7–12 becomes | Result |
40
+
41
+ {
42
+ cases.map(([what, doc, moved, result]) => (
43
+
44
+ | | `` | `` | |
45
+ ))
46
+ }
47
+
48
+ > Note: The third row is the one to write code for. When the text a marker pointed at has been deleted, the range **collapses** — `from` and `to` come back equal. Check for it, because inserting into a collapsed range is not "the rewrite landed", it is a model's answer appearing where the user just deleted a sentence.
49
+
50
+ ```
51
+ {`const target = at.mapRange({ from, to })
52
+
53
+ if (target.from === target.to) {
54
+ // What we were rewriting is gone. Drop the answer rather than paste it
55
+ // somewhere nobody asked for.
56
+ return false
57
+ }`}
58
+ ```
59
+
60
+ ## A complete example
61
+
62
+ An AI rewrite that survives whatever the user does while it streams. This is `@matrajs/ai` in miniature:
63
+
64
+ ```
65
+ {`const rewrite: Command<[instruction: string]> = (ctx, instruction) => {
66
+ const { from, to } = ctx.selection
67
+ if (from === to) return false // nothing selected
68
+
69
+ const at = ctx.mark() // taken now, used later
70
+ const original = ctx.doc // for the request, not for positions
71
+
72
+ void (async () => {
73
+ let received = ''
74
+ for await (const chunk of ask(instruction, original)) {
75
+ received += chunk
76
+ const target = at.mapRange({ from, to })
77
+ if (target.from === target.to) return // the text went away
78
+ editor.commands.replace(target, received)
79
+ }
80
+ })()
81
+
82
+ return true // the request started
83
+ }`}
84
+ ```
85
+
86
+ Note what is *not* here: no locking the editor, no disabling input, no "please wait". The user keeps typing and the answer still lands on the words they chose.
87
+
88
+ ## Where else it matters
89
+
90
+ - **Collaboration** · a remote step arrives in the sender's coordinates, and your unsent work has to be rebased over it. That is mapping, run in both directions.
91
+
92
+ - **Comments** · anchored as marks rather than as numbers, so mapping keeps them on the right words for free — and a thread does not slide up the page when somebody deletes a paragraph above it.
93
+
94
+ - **Decorations** · a search highlight, a remote caret, a spell-check squiggle. All positions, all mapped on every change.
95
+
96
+ - **Version history** · a diff between two documents is only meaningful because an old document stays exactly what it was.
97
+
98
+ ## Is this new?
99
+
100
+ No, and it is worth being straight about that. ProseMirror has had step maps for a decade and every serious editor has some version of the idea. The difference is the default: there, mapping is something you must remember to do, and forgetting it produces a bug that appears only when somebody types during a slow request — which is to say, in production and not in your tests.
101
+
102
+ Here the async path does not offer you a raw number to hold. `ctx.mark()` is how you carry a position across an `await`, and there is no shorter way to do it wrong.
103
+
104
+ > Note: Never hold a raw position across an `await`. A number that was correct when you asked for it is not correct when you come back, and nothing will tell you — the insert will succeed, in the wrong place.
105
+
106
+ ## Next
107
+
108
+ - [Document model](https://matrajs.com/docs/document-model) · what the integers count.
109
+
110
+ - [Commands](https://matrajs.com/docs/commands) · where `ctx.mark()` lives.
111
+
112
+ - [Recipes](https://matrajs.com/docs/recipes) · autosave, and other things with a gap in them.
@@ -0,0 +1,91 @@
1
+ # React
2
+
3
+ useEditor, useEditorState and EditorContent: a toolbar whose active states stay honest.
4
+
5
+ ```
6
+ npm i @matrajs/react
7
+ ```
8
+
9
+ The engine comes with it · there is no second package, and nothing third-party behind it.
10
+
11
+ ## An editor
12
+
13
+ ```
14
+ import { starterKit } from '@matrajs/core'
15
+ import { EditorContent, useEditor } from '@matrajs/react'
16
+
17
+ export function Editor() {
18
+ const editor = useEditor({
19
+ extensions: starterKit,
20
+ content: '<p>Hello.</p>',
21
+ })
22
+
23
+ return <EditorContent editor={editor} />
24
+ }
25
+ ```
26
+
27
+ The editor is created once and destroyed on unmount. **Options are read once** · changing them later does not recreate it, because tearing down a live document on a prop change loses whatever the person was writing. Use commands instead.
28
+
29
+ ## A toolbar that stays honest
30
+
31
+ Commands mutate the document; React does not hear about that on its own, so a naive toolbar's active states go stale the moment the caret moves. `useEditorState` subscribes to both changes and selection changes.
32
+
33
+ ```
34
+ import { useEditorState } from '@matrajs/react'
35
+
36
+ function BoldButton({ editor }) {
37
+ const active = useEditorState(editor, (e) => e.isActive('bold'))
38
+ return (
39
+ <button
40
+ aria-pressed={active}
41
+ onMouseDown={(event) => {
42
+ event.preventDefault() // keep the caret in the editor
43
+ editor.commands.toggleBold()
44
+ }}
45
+ >
46
+ Bold
47
+ </button>
48
+ )
49
+ }
50
+ ```
51
+
52
+ > Note: `onMouseDown` with `preventDefault`, not `onClick`. A click moves focus to the button first, which collapses the selection · so the command runs against a caret rather than the words you highlighted.
53
+
54
+ ## Buttons that know when they cannot
55
+
56
+ `can` is the same command asking rather than doing. A button reads it to be *disabled* instead of looking enabled and doing nothing when pressed — the caret is in a code block, or the selection cannot hold that mark, and the answer is available before the user finds out by pressing.
57
+
58
+ ```
59
+ const canBold = useEditorState(editor, (e) => e.can.toggleBold())
60
+
61
+ <button disabled={!canBold} …>Bold</button>
62
+ ```
63
+
64
+ ## Several changes, one undo
65
+
66
+ ```
67
+ editor.batch((c) => {
68
+ c.toggleBold()
69
+ c.toggleItalic()
70
+ })
71
+ ```
72
+
73
+ One history step, and it rolls back entirely if any command in it returns false.
74
+
75
+ ## Saving
76
+
77
+ ```
78
+ useEffect(() => editor.on('change', () => save(editor.getJSON())), [editor])
79
+ ```
80
+
81
+ `on` returns its own unsubscribe function, so it works as an effect cleanup directly.
82
+
83
+ ## Focus
84
+
85
+ ```
86
+ const focused = useEditorFocus(editor)
87
+ ```
88
+
89
+ True while the editor has DOM focus · useful for showing a toolbar only while the caret is actually in the document.
90
+
91
+ > Note: StrictMode double-invokes effects in development. The mount is guarded, so that cannot leave two views attached to one element — you do not need to disable StrictMode to use this.