@octanejs/mcp-server 0.2.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dominic Gannaway
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,129 @@
1
+ # @octanejs/mcp-server
2
+
3
+ MCP server for agents working with [Octane](https://github.com/octanejs/octane).
4
+
5
+ It serves two audiences:
6
+
7
+ - **Octane users** (any project): skills and tools for bridging React packages
8
+ to Octane, migrating React components to `.tsrx`, understanding Octane's
9
+ intentional divergences from React, and setting up SSR. These work anywhere;
10
+ the skills ship inside this package.
11
+ - **Octane maintainers** (the octane monorepo): repo triage, validation
12
+ planning, benchmark and React-test-port automation. These tools register
13
+ only when the server detects an octane monorepo checkout at its root.
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ npm install -g @octanejs/mcp-server
19
+ ```
20
+
21
+ For local development inside the octane repository:
22
+
23
+ ```bash
24
+ pnpm --filter @octanejs/mcp-server start
25
+ ```
26
+
27
+ ## MCP transport
28
+
29
+ The server uses stdio transport.
30
+
31
+ ```json
32
+ {
33
+ "mcpServers": {
34
+ "octane": {
35
+ "command": "octane-mcp-server"
36
+ }
37
+ }
38
+ }
39
+ ```
40
+
41
+ Set `OCTANE_REPO_ROOT` to point the server at an octane checkout (enables the
42
+ maintainer tools):
43
+
44
+ ```json
45
+ {
46
+ "mcpServers": {
47
+ "octane": {
48
+ "command": "octane-mcp-server",
49
+ "env": {
50
+ "OCTANE_REPO_ROOT": "/path/to/octane"
51
+ }
52
+ }
53
+ }
54
+ }
55
+ ```
56
+
57
+ ## Tools (always available)
58
+
59
+ ### `octane_bridge_react_package`
60
+
61
+ Scans a React package (by name from `node_modules`, or any source directory by
62
+ path) for React API usage and returns an Octane compatibility report: which
63
+ APIs map one-to-one, which need rewrites (`forwardRef`, `useDebugValue`,
64
+ `lazy`, class components, synthetic `onChange`), whether a framework-agnostic
65
+ core can be reused verbatim, whether an official `@octanejs/*` binding already
66
+ exists, an overall verdict (`bridgeable`, `bridgeable-with-rewrites`,
67
+ `needs-rework`), and a step-by-step plan.
68
+
69
+ ```json
70
+ { "package": "jotai", "projectRoot": "/path/to/my-app" }
71
+ ```
72
+
73
+ ### `octane_bindings`
74
+
75
+ Returns the map of React packages with maintained `@octanejs/*` ports
76
+ (zustand, query, motion, stylex, router, lexical, floating-ui, radix).
77
+
78
+ ### `octane_skill`
79
+
80
+ Returns a skill by name. Bundled skills (shipped with this package):
81
+
82
+ - `bridge-react-package` — the full workflow for porting a React library.
83
+ - `migrate-react-component` — React JSX to `.tsrx` conversion reference.
84
+ - `react-divergences` — Octane's intentional differences from React.
85
+ - `setup-ssr` — server rendering and hydration setup.
86
+
87
+ When running inside the octane monorepo, the maintainer skills from
88
+ `.ai/skills` are also available: `react-library-port`, `bug-hunter`,
89
+ `create-a-pr`, `handle-issue`, `octane-core-extend`, `triage`,
90
+ `performance-audit`.
91
+
92
+ ## Tools (octane monorepo only)
93
+
94
+ ### `octane_project_map`
95
+
96
+ Returns `.ai/project-map.md` with package layout, authoritative sources,
97
+ invariants, and validation commands.
98
+
99
+ ### `octane_triage_paths`
100
+
101
+ Classifies repository-relative paths by Octane area (compiler, core runtime,
102
+ SSR, ecosystem binding, mcp-server, benchmark, docs, RuleSync source).
103
+
104
+ ### `octane_validate_plan`
105
+
106
+ Recommends validation commands for changed paths and task kind.
107
+
108
+ ### `octane_scaffold_react_port`
109
+
110
+ Runs `scripts/scaffold-react-port.mjs` for an upstream React test file and
111
+ optionally writes the generated Vitest skeleton to an output file.
112
+
113
+ ### `octane_benchmark`
114
+
115
+ Runs a known benchmark workspace (`news`, `js-framework`, `recursive-context`,
116
+ `signal-favoring`, `dbmon`) or all benchmarks.
117
+
118
+ ### `octane_issue_context`
119
+
120
+ Uses the GitHub CLI (`gh`) to fetch an issue and returns structured issue
121
+ context plus lightweight triage hints. Requires `gh` installed and
122
+ authenticated.
123
+
124
+ ## Development
125
+
126
+ ```bash
127
+ pnpm --filter @octanejs/mcp-server test
128
+ pnpm --filter @octanejs/mcp-server start
129
+ ```
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@octanejs/mcp-server",
3
+ "version": "0.2.0",
4
+ "type": "module",
5
+ "description": "MCP server exposing Octane repository automation for coding agents.",
6
+ "license": "MIT",
7
+ "author": {
8
+ "name": "Dominic Gannaway",
9
+ "email": "dg@domgan.com"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/octanejs/octane.git",
14
+ "directory": "packages/octane-mcp-server"
15
+ },
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "files": [
20
+ "src",
21
+ "skills",
22
+ "README.md"
23
+ ],
24
+ "bin": {
25
+ "octane-mcp-server": "src/index.js"
26
+ },
27
+ "exports": {
28
+ ".": "./src/index.js"
29
+ },
30
+ "dependencies": {
31
+ "@modelcontextprotocol/sdk": "^1.21.0",
32
+ "zod": "^4.4.3"
33
+ },
34
+ "devDependencies": {
35
+ "vitest": "^4.1.9"
36
+ },
37
+ "scripts": {
38
+ "start": "node src/index.js",
39
+ "test": "cd ../.. && vitest run --project octane-mcp-server"
40
+ }
41
+ }
@@ -0,0 +1,117 @@
1
+ # Skill: Bridge a React package to Octane
2
+
3
+ Use this when a user wants a React ecosystem library to work in their Octane app.
4
+
5
+ ## Check for an official binding first
6
+
7
+ These libraries already have maintained Octane ports. Install the binding instead
8
+ of bridging by hand:
9
+
10
+ | React package | Octane binding |
11
+ | --- | --- |
12
+ | `zustand` | `@octanejs/zustand` |
13
+ | `@tanstack/react-query` | `@octanejs/query` |
14
+ | `framer-motion` / `motion` | `@octanejs/motion` |
15
+ | `@stylexjs/stylex` | `@octanejs/stylex` |
16
+ | `react-router` / `react-router-dom` | `@octanejs/router` |
17
+ | `@lexical/react` | `@octanejs/lexical` |
18
+ | `@floating-ui/react` | `@octanejs/floating-ui` |
19
+ | `radix-ui` | `@octanejs/radix` |
20
+
21
+ For anything else, run the `octane_bridge_react_package` tool to get a scan of the
22
+ package's React API usage and a tailored plan, then follow the workflow below.
23
+
24
+ ## Mental model
25
+
26
+ Octane is a compiler framework, not a runtime VDOM. Two consequences drive
27
+ everything:
28
+
29
+ 1. Compiled React JSX (`jsx()` / `createElement` trees) cannot render on Octane.
30
+ Components must be authored in `.tsrx` (or `.tsx` compiled by the Octane
31
+ compiler).
32
+ 2. Every Octane hook call is bound to a compiler-injected slot. A slotless
33
+ `useState(0)` coming from a React build throws immediately.
34
+
35
+ So a bridge never means "run the React package unchanged". It means:
36
+
37
+ - Reuse the package's framework-agnostic core verbatim (store, query client,
38
+ state machine, form engine). Code with zero `react` imports runs on Octane
39
+ as-is.
40
+ - Re-implement the thin React binding layer (usually a handful of hooks) against
41
+ Octane's identically named hooks.
42
+ - Re-author any shipped JSX components in `.tsrx`.
43
+
44
+ ## Workflow
45
+
46
+ 1. **Classify the library.** Find its vanilla core (`zustand/vanilla`,
47
+ `@tanstack/query-core`, `jotai/vanilla`, `xstate`, `@floating-ui/dom`, a
48
+ `*-core` dependency, or a pure internal module). Identify the React surface:
49
+ hooks, components, providers, portals, refs.
50
+
51
+ 2. **Map the React APIs.** Same-name and same-semantics in Octane: `useState`,
52
+ `useReducer`, `useEffect`, `useLayoutEffect`, `useInsertionEffect`, `useMemo`,
53
+ `useCallback`, `useRef`, `useContext`, `useId`, `useImperativeHandle`,
54
+ `useSyncExternalStore` (full React 19 shape, including `getServerSnapshot`),
55
+ `useTransition`, `useDeferredValue`, `useActionState`, `useOptimistic`,
56
+ `useEffectEvent`, `use`, `startTransition`, `memo`, `createContext`,
57
+ `Suspense`, `createPortal`, `flushSync`, `createRoot`, `hydrateRoot`.
58
+ Everything imports from `octane` (no separate `react-dom`).
59
+
60
+ 3. **Handle the gaps:**
61
+ - `forwardRef`: does not exist. Accept `ref` as a normal prop (React 19
62
+ style) and drop the wrapper.
63
+ - `useDebugValue`: shim as a no-op.
64
+ - `lazy`: use dynamic `import()` plus `use()` inside a `Suspense` boundary.
65
+ - Class components: rewrite as function components. Error boundary classes
66
+ become `<ErrorBoundary>` or the `@try { } @catch (e) { }` directive.
67
+ - Synthetic `onChange` on text inputs: use native `onInput`. Octane events
68
+ are native and delegated.
69
+ - Controlled inputs: Octane inputs are uncontrolled and native; `value` and
70
+ `checked` are plain attributes. Port controlled-input logic to
71
+ read-from-DOM plus explicit writes, or keep state in the store and write
72
+ the attribute on change.
73
+ - StrictMode double-invoke: does not exist; delete test expectations that
74
+ count double renders.
75
+
76
+ 4. **Custom hooks in plain `.ts` files.** Octane's compiler auto-slots hook
77
+ calls in files it compiles. A binding published as plain `.ts` that calls
78
+ hooks internally must forward the caller's slot: accept a trailing `slot`
79
+ argument and derive stable child slots per call site. The convention used by
80
+ the official bindings:
81
+
82
+ ```ts
83
+ import { useMemo, useRef } from 'octane';
84
+
85
+ export function subSlot(slot: symbol | undefined, tag: string) {
86
+ return slot !== undefined ? Symbol.for((slot.description ?? '') + ':' + tag) : undefined;
87
+ }
88
+
89
+ export function useControllableState(opts, slot?: symbol) {
90
+ const valueRef = useRef(opts.defaultValue, subSlot(slot, 'value'));
91
+ return useMemo(() => build(valueRef), [opts.value], subSlot(slot, 'memo'));
92
+ }
93
+ ```
94
+
95
+ Callers compiled from `.tsrx`/`.tsx` pass their injected slot automatically as
96
+ the trailing argument when the hook file itself is excluded from the compiler's
97
+ auto-slotting pass. The simpler alternative: keep the binding in compiled
98
+ files so slots are injected for you.
99
+
100
+ 5. **Re-author shipped components in `.tsrx`.** `props.children` works, refs are
101
+ props, lists use `@for (const x of xs; key x.id) { }`, conditionals use
102
+ `@if`, dynamic text holes use `{expr as string}` unless the expression is
103
+ provably a string.
104
+
105
+ 6. **Validate.** Drive real DOM events against the bridged binding and, where
106
+ possible, run the same fixture against the React original and compare
107
+ rendered HTML after each step. Also test what HTML comparison cannot see:
108
+ render counts, subscription add/remove, effect ordering, ref lifecycle.
109
+
110
+ ## Verdict guide for the scan tool
111
+
112
+ - `bridgeable`: only same-name hooks used; a mechanical rename of imports to
113
+ `octane` plus a `.tsrx` re-author of components is enough.
114
+ - `bridgeable-with-rewrites`: needs the `forwardRef` / `useDebugValue` / `lazy` /
115
+ event rewrites above, but no architectural blockers.
116
+ - `needs-rework`: class components, `renderToPipeableStream`, `findDOMNode`, or
117
+ React internals. Bridge the core, redesign the binding.
@@ -0,0 +1,98 @@
1
+ # Skill: Migrate a React component to Octane
2
+
3
+ Use this when converting React component source (JSX/TSX) into an Octane `.tsrx`
4
+ component.
5
+
6
+ ## Imports
7
+
8
+ Everything comes from `octane`. Replace `react`, `react-dom`, and
9
+ `react-dom/client` imports:
10
+
11
+ ```ts
12
+ import { useState, useEffect, createPortal, createRoot, hydrateRoot } from 'octane';
13
+ ```
14
+
15
+ ## Component shape
16
+
17
+ Any function used at a `<Foo/>` site is a component. Two equivalent forms:
18
+
19
+ ```tsx
20
+ export function Counter() @{
21
+ const [count, setCount] = useState(0);
22
+ <button onClick={() => setCount(count + 1)}>{'Count: ' + count}</button>
23
+ }
24
+
25
+ export function Counter() {
26
+ const [count, setCount] = useState(0);
27
+ return <button onClick={() => setCount(count + 1)}>{'Count: ' + count}</button>;
28
+ }
29
+ ```
30
+
31
+ The `@{ ... }` body must end with exactly one output node. Setup code (hooks,
32
+ locals, early returns) stays above it.
33
+
34
+ ## Conversion table
35
+
36
+ | React pattern | Octane pattern |
37
+ | --- | --- |
38
+ | `items.map(x => <li key={x.id}>...` | `@for (const x of items; key x.id) { <li>... }` with optional `@empty { }` |
39
+ | `cond ? <A/> : <B/>` in JSX | `@if (cond) { <A/> } @else { <B/> }` |
40
+ | `{cond && <A/>}` | `@if (cond) { <A/> }` |
41
+ | switch on a value | `@switch (v) { @case (a) { } @default { } }` |
42
+ | `<Suspense fallback={...}>` | `<Suspense>` or `@try { } @pending { }` |
43
+ | Error boundary class | `<ErrorBoundary>` or `@try { } @catch (e) { }` |
44
+ | `forwardRef((props, ref) => ...)` | plain function; `ref` arrives as a prop |
45
+ | `<input onChange={...}>` | `<input onInput={...}>` (native event) |
46
+ | controlled `value={state}` | uncontrolled; `value` is a plain attribute, read the DOM in handlers |
47
+ | `className={clsx(...)}` | `class={[...]}` composes clsx-style natively |
48
+ | `useDebugValue(x)` | delete it |
49
+ | `React.lazy(() => import(...))` | dynamic `import()` + `use()` under Suspense |
50
+ | `defaultProps` | parameter defaults / destructuring defaults |
51
+
52
+ ## Text holes
53
+
54
+ A dynamic text hole needs `{expr as string}` unless the compiler can prove the
55
+ expression is a string (string literal, template literal, `+` concatenation with
56
+ a string, or a tracked local). A bare `{expr}` that is not provably a string is
57
+ treated as a renderable (component, element, coerced primitive).
58
+
59
+ ```tsx
60
+ <p>{'Elapsed: ' + seconds}</p>
61
+ <p>{seconds as string}</p>
62
+ ```
63
+
64
+ ## Hooks
65
+
66
+ The hook API matches React, and there are no rules of hooks: a hook may sit
67
+ behind a condition, after an early return, or in a loop, because identity comes
68
+ from the call site, not call order.
69
+
70
+ ```tsx
71
+ export function Panel(props) @{
72
+ const [n, setN] = useState(0);
73
+ if (props.hidden) return;
74
+ useEffect(() => log(n), [n]);
75
+ <button onClick={() => setN(n + 1)}>{'count: ' + n}</button>
76
+ }
77
+ ```
78
+
79
+ ## What does not port
80
+
81
+ - Class components (rewrite as functions).
82
+ - StrictMode double-invoke expectations (there is no double render; delete
83
+ render-count workarounds).
84
+ - Server Components / `'use client'` directives.
85
+ - Synthetic event pooling or `e.persist()` (events are native).
86
+ - `React.Children` traversal over arbitrary VDOM (children are descriptors, not
87
+ a VDOM tree; prefer explicit props over children introspection).
88
+
89
+ ## Events
90
+
91
+ Events are native, delegated DOM events. `onClick`, `onInput`, `onSubmit`,
92
+ `onKeyDown` behave exactly like the platform. `onChange` on a text input fires
93
+ on commit (native change), not per keystroke.
94
+
95
+ ## Refs
96
+
97
+ React 19 style. `ref={cb}` with optional cleanup return, `ref={refObject}`, or
98
+ an array `ref={[a, b]}` to compose. No `forwardRef` anywhere.
@@ -0,0 +1,50 @@
1
+ # Skill: Octane's intentional divergences from React
2
+
3
+ Use this when behavior differs from React and you need to decide whether it is a
4
+ bug or by design. Do not "fix" these toward React.
5
+
6
+ ## No rules of hooks
7
+
8
+ Hooks are tracked by compiler-assigned call-site slot, not call order. A hook may
9
+ sit behind a condition, after an early return, or in a loop. Code that relies on
10
+ hook-order errors firing does not apply.
11
+
12
+ ## No controlled components, no synthetic onChange
13
+
14
+ `value` and `checked` are plain attributes; inputs are uncontrolled and native.
15
+ There is no per-keystroke synthetic `onChange`; use native `onInput`. React's
16
+ controlled-input value-reassertion model does not exist and must not be added.
17
+
18
+ ## Native delegated events
19
+
20
+ `onClick`, `onInput`, `onSubmit` etc. are real DOM events via delegation, not a
21
+ synthetic layer. Timing, bubbling, and `event.target` semantics match the
22
+ platform, not React's wrapper.
23
+
24
+ ## Keyed reconciler moves differ
25
+
26
+ Reconciliation is LIS-based (minimal DOM moves), not React's `lastPlacedIndex`.
27
+ The final DOM and survivor node identity are guaranteed identical to React; the
28
+ set of physically moved nodes is not. Tests asserting which nodes moved will
29
+ diverge; tests asserting final order and identity will pass.
30
+
31
+ ## class / className composes clsx-style
32
+
33
+ Strings, numbers, arrays, objects, and nesting compose into a class string;
34
+ falsy parts drop out. React coerces an array to `"a,b"`; Octane yields `"a b"`.
35
+
36
+ ## Not present at all
37
+
38
+ - Class components.
39
+ - Server Components / `'use client'` / `'use server'`.
40
+ - StrictMode double-invoke (renders and effects run once).
41
+ - `forwardRef` (refs are props, React 19 style).
42
+ - `useDebugValue` (shim as no-op).
43
+ - SuspenseList, Profiler, findDOMNode.
44
+
45
+ ## Everything else matches
46
+
47
+ Observable hook, effect, Suspense, and transition semantics match React,
48
+ including effect ordering (child-first on mount, parent-first cleanup on
49
+ deletion), `Object.is` state bailouts, batching, and `useId` stability across
50
+ server render and hydration.
@@ -0,0 +1,73 @@
1
+ # Skill: Set up Octane SSR and hydration
2
+
3
+ Use this when adding server-side rendering to an Octane app.
4
+
5
+ ## The API
6
+
7
+ The entry points mirror React: `octane/server` (`react-dom/server`) has
8
+ `renderToString` (sync) and `renderToStaticMarkup`; `octane/static`
9
+ (`react-dom/static`) has `prerender` (async, awaits Suspense data). All return
10
+ `{ html, css }`.
11
+
12
+ ```ts
13
+ import { prerender } from 'octane/static';
14
+
15
+ const { html, css } = await prerender(App, props, {
16
+ signal: request.signal,
17
+ nonce: cspNonce,
18
+ timeoutMs: 5000,
19
+ });
20
+ ```
21
+
22
+ - `html`: rendered markup with hydration markers, plus an inline suspense seed
23
+ script when anything resolved. Hoisted `<title>/<meta>/<link>` fold in
24
+ (spliced into `<head>` if present, else prepended).
25
+ - `css`: deduped `<style data-octane>` tags from scoped styles; place inside
26
+ `<head>`.
27
+ - Use `renderToString` (from `octane/server`) for a single synchronous pass that
28
+ leaves `@pending` fallbacks in place; use `prerender` to await the data.
29
+ - Options are optional: `nonce` stamps CSP nonces on the emitted inline tags (all
30
+ renderers); `signal` aborts a suspended render with the request and `timeoutMs`
31
+ bounds how long a `use(thenable)` may take to settle (async `prerender`; global
32
+ default via `setSsrSuspenseTimeout`); `onError` observes render errors.
33
+
34
+ On the client:
35
+
36
+ ```ts
37
+ import { hydrateRoot } from 'octane';
38
+ hydrateRoot(document.getElementById('app')!, App, props);
39
+ ```
40
+
41
+ Pass the same component and props on both sides. `useId` and scoped styles are
42
+ hydration-stable; the client adopts server DOM instead of rebuilding it.
43
+
44
+ ## Two integration paths
45
+
46
+ 1. **Vite plugin (dev SSR + routing)**: `@octanejs/vite-plugin` matches routes
47
+ from `octane.config.ts`, renders pages into `index.html` at
48
+ `<!--ssr-head-->` / `<!--ssr-body-->`, and wires hydration automatically.
49
+ Production server output is not generated yet; for production SSR today use
50
+ path 2.
51
+ 2. **Custom server**: write `entry-server.ts` exporting a function that calls
52
+ `prerender()` (or `renderToString()`) and splices the result into your HTML template, and
53
+ `entry-client.ts` calling `hydrateRoot`. Serialize app data (for example a
54
+ dehydrated query-client cache) into your own inline JSON script and read it
55
+ before hydrating.
56
+
57
+ ## Data and Suspense on the server
58
+
59
+ `use(promise)` suspends a pass; `prerender()` awaits it and re-renders, so
60
+ `@try { } @pending { }` boundaries resolve to their success arm in the emitted
61
+ HTML. Resolved values serialize into the seed script and hydration consumes
62
+ them without re-fetching. For query-style data, prefetch into a cache before
63
+ rendering and dehydrate it yourself.
64
+
65
+ ## Constraints to remember
66
+
67
+ - Effects never run on the server; state hooks return initial values;
68
+ `useSyncExternalStore` uses `getServerSnapshot`.
69
+ - Server components must be compiled by the Octane compiler in server mode;
70
+ you cannot feed client-compiled output to the renderers.
71
+ - Output is buffered, not streamed: send it as one response.
72
+ - Render errors reject the promise unless an `ErrorBoundary`/`@catch` inside
73
+ the tree handles them; map rejections to HTTP status codes in your server.