@octanejs/mcp-server 0.2.9 → 0.2.11
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/package.json +2 -2
- package/skills/bridge-react-package.md +41 -6
- package/skills/build-octane-software.md +7 -0
- package/src/bridge.js +30 -2
- package/src/bridge.test.js +25 -0
- package/src/index.js +12 -0
- package/src/index.test.js +19 -0
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@octanejs/mcp-server",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.11",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"engines": {
|
|
6
|
-
"node": ">=22"
|
|
6
|
+
"node": ">=22.22.2"
|
|
7
7
|
},
|
|
8
8
|
"description": "MCP server exposing Octane repository automation for coding agents.",
|
|
9
9
|
"license": "MIT",
|
|
@@ -32,6 +32,11 @@ of bridging by hand:
|
|
|
32
32
|
| `@base-ui-components/react` | `@octanejs/base-ui` |
|
|
33
33
|
| `@dnd-kit/react` | `@octanejs/dnd-kit` |
|
|
34
34
|
| `sonner` | `@octanejs/sonner` |
|
|
35
|
+
| `streamdown` | `@octanejs/streamdown` |
|
|
36
|
+
| `@streamdown/code` | `@octanejs/streamdown/code` |
|
|
37
|
+
| `@streamdown/math` | `@octanejs/streamdown/math` |
|
|
38
|
+
| `@streamdown/mermaid` | `@octanejs/streamdown/mermaid` |
|
|
39
|
+
| `@streamdown/cjk` | `@octanejs/streamdown/cjk` |
|
|
35
40
|
| `recharts` | `@octanejs/recharts` |
|
|
36
41
|
| `@react-three/fiber` | `@octanejs/three` |
|
|
37
42
|
| `@visx/*` | `@octanejs/visx` |
|
|
@@ -71,7 +76,23 @@ So a bridge never means "run the React package unchanged". It means:
|
|
|
71
76
|
`*-core` dependency, or a pure internal module). Identify the React surface:
|
|
72
77
|
hooks, components, providers, portals, refs.
|
|
73
78
|
|
|
74
|
-
2. **
|
|
79
|
+
2. **Bridge from the pinned upstream source, not from memory.** Fix the exact
|
|
80
|
+
upstream version you are bridging and copy that release's React binding source
|
|
81
|
+
into your repository next to your port, for example
|
|
82
|
+
`src/vendor/<package>@<version>/`, keeping the upstream LICENSE and leaving
|
|
83
|
+
the copy unmodified. Put each Octane module beside the upstream module it
|
|
84
|
+
replaces, and work through them one by one. A bridge written from the README
|
|
85
|
+
or from type declarations covers the demo path and silently drops the rest of
|
|
86
|
+
the API. Anything you cannot reach (React internals, class components,
|
|
87
|
+
synthetic-event timing) goes in a short divergence note next to the port, with
|
|
88
|
+
what to do instead. On an upgrade, re-copy at the new version: the diff
|
|
89
|
+
against the old copy is your work list.
|
|
90
|
+
Inspect both the published package and the canonical repository at the pinned
|
|
91
|
+
tag. Do not assume the registry artifact contains source, tests, fixtures,
|
|
92
|
+
snapshots, or runner configuration; fetch missing evidence from the tagged
|
|
93
|
+
repository and record which artifact supplied it.
|
|
94
|
+
|
|
95
|
+
3. **Map the React APIs.** Same-name and same-semantics in Octane: `useState`,
|
|
75
96
|
`useReducer`, `useEffect`, `useLayoutEffect`, `useInsertionEffect`, `useMemo`,
|
|
76
97
|
`useCallback`, `useRef`, `useContext`, `useId`, `useImperativeHandle`,
|
|
77
98
|
`useSyncExternalStore` (full React 19 shape, including `getServerSnapshot`),
|
|
@@ -83,7 +104,7 @@ So a bridge never means "run the React package unchanged". It means:
|
|
|
83
104
|
server rendering imports from `octane/server`, including the streaming
|
|
84
105
|
`renderToPipeableStream`/`renderToReadableStream`.
|
|
85
106
|
|
|
86
|
-
|
|
107
|
+
4. **Handle the gaps:**
|
|
87
108
|
- `forwardRef`: does not exist. Accept `ref` as a normal prop (React 19
|
|
88
109
|
style) and drop the wrapper.
|
|
89
110
|
- Class components: rewrite as function components. Error boundary classes
|
|
@@ -98,7 +119,7 @@ So a bridge never means "run the React package unchanged". It means:
|
|
|
98
119
|
- StrictMode double-invoke: does not exist; delete test expectations that
|
|
99
120
|
count double renders.
|
|
100
121
|
|
|
101
|
-
|
|
122
|
+
5. **Custom hooks in plain `.ts` files.** Octane's compiler auto-slots hook
|
|
102
123
|
calls in files it compiles. A binding published as plain `.ts` that calls
|
|
103
124
|
hooks internally must forward the caller's slot: accept a trailing `slot`
|
|
104
125
|
argument and derive stable child slots per call site. The convention used by
|
|
@@ -122,13 +143,27 @@ So a bridge never means "run the React package unchanged". It means:
|
|
|
122
143
|
auto-slotting pass. The simpler alternative: keep the binding in compiled
|
|
123
144
|
files so slots are injected for you.
|
|
124
145
|
|
|
125
|
-
|
|
146
|
+
6. **Re-author shipped components in `.tsrx`.** `props.children` works, refs are
|
|
126
147
|
props, lists use `@for (const x of xs; key x.id) { }`, conditionals use
|
|
127
148
|
`@if`, dynamic text holes use `{expr as string}` unless the expression is
|
|
128
149
|
provably a string.
|
|
129
150
|
|
|
130
|
-
|
|
131
|
-
|
|
151
|
+
7. **Run the package's own tests.** If the pinned release ships a suite, that is
|
|
152
|
+
the parity oracle: it encodes what its maintainers care about, and it covers
|
|
153
|
+
cases a suite written against your own bridge will not think to check. Run the
|
|
154
|
+
framework-neutral suites unmodified against the core you reused. Port the
|
|
155
|
+
React-binding ones case by case: fixtures re-authored in `.tsrx`,
|
|
156
|
+
`@octanejs/testing-library` in place of `@testing-library/react`, upstream case
|
|
157
|
+
names kept. Write down which upstream test files you ran, ported, or left out
|
|
158
|
+
and why. Do not soften an upstream assertion to get it green; find out whether
|
|
159
|
+
it is a bridge bug or a documented Octane divergence first.
|
|
160
|
+
Prove the evidence machinery fails closed too: removing, renaming, skipping,
|
|
161
|
+
or failing to execute a recorded case, or changing pinned evidence, must make
|
|
162
|
+
validation fail. A green port suite is not trustworthy when its collector can
|
|
163
|
+
silently go stale.
|
|
164
|
+
|
|
165
|
+
8. **Validate the rest.** Drive real DOM events against the bridged binding and,
|
|
166
|
+
where possible, run the same fixture against the React original and compare
|
|
132
167
|
rendered HTML after each step. Also test what HTML comparison cannot see:
|
|
133
168
|
render counts, subscription add/remove, effect ordering, ref lifecycle.
|
|
134
169
|
|
|
@@ -34,6 +34,13 @@ details but do not replace these gates.
|
|
|
34
34
|
hydration with production-compiled output and preserve abort/error behavior.
|
|
35
35
|
- Treat bundle size and dependency cost as performance. Check for an official
|
|
36
36
|
binding before adding a compatibility layer or a second framework runtime.
|
|
37
|
+
- Publish every importable `.tsrx`, `.tsx`, `.ts`, and `.js` module as authored
|
|
38
|
+
and point package exports at that source. Do not ship Octane compiler output;
|
|
39
|
+
let the consuming application compile the source with its own toolchain.
|
|
40
|
+
- When you do port a React package yourself, work from a pinned copy of that
|
|
41
|
+
release's source kept beside your port, cover its exports rather than the demo
|
|
42
|
+
path, and write down what parity could not reach. The `bridge-react-package`
|
|
43
|
+
skill has the procedure.
|
|
37
44
|
|
|
38
45
|
## Validate behavior and performance
|
|
39
46
|
|
package/src/bridge.js
CHANGED
|
@@ -6,14 +6,20 @@ import { join, resolve } from 'node:path';
|
|
|
6
6
|
// expected union from the workspace manifests, so publishing a new binding
|
|
7
7
|
// without registering it in either catalog fails the mcp-server tests.
|
|
8
8
|
export const KNOWN_BINDINGS = {
|
|
9
|
+
'usehooks-ts': '@octanejs/usehooks-ts',
|
|
9
10
|
zustand: '@octanejs/zustand',
|
|
10
11
|
valtio: '@octanejs/valtio',
|
|
11
12
|
jotai: '@octanejs/jotai',
|
|
13
|
+
'@mantine/hooks': '@octanejs/mantine-hooks',
|
|
14
|
+
'mobx-react-lite': '@octanejs/mobx',
|
|
15
|
+
'mobx-react': '@octanejs/mobx',
|
|
12
16
|
'@apollo/client': '@octanejs/apollo-client',
|
|
13
17
|
'@tanstack/ai-react': '@octanejs/tanstack-ai',
|
|
14
18
|
'@tanstack/react-devtools': '@octanejs/tanstack-devtools',
|
|
15
19
|
'@tanstack/react-form': '@octanejs/tanstack-form',
|
|
16
20
|
'@tanstack/react-query': '@octanejs/tanstack-query',
|
|
21
|
+
wagmi: '@octanejs/wagmi',
|
|
22
|
+
'@rainbow-me/rainbowkit': '@octanejs/rainbowkit',
|
|
17
23
|
'@tanstack/react-router': '@octanejs/tanstack-router',
|
|
18
24
|
'@tanstack/react-store': '@octanejs/tanstack-store',
|
|
19
25
|
'@tanstack/react-router-ssr-query': '@octanejs/tanstack-router-ssr-query',
|
|
@@ -32,6 +38,7 @@ export const KNOWN_BINDINGS = {
|
|
|
32
38
|
'@lexical/react': '@octanejs/lexical',
|
|
33
39
|
'@tiptap/react': '@octanejs/tiptap',
|
|
34
40
|
'lucide-react': '@octanejs/lucide',
|
|
41
|
+
'@phosphor-icons/react': '@octanejs/phosphor-icons',
|
|
35
42
|
'@floating-ui/react': '@octanejs/floating-ui',
|
|
36
43
|
'react-aria': '@octanejs/aria',
|
|
37
44
|
'react-aria-components': '@octanejs/aria',
|
|
@@ -41,6 +48,14 @@ export const KNOWN_BINDINGS = {
|
|
|
41
48
|
'@base-ui-components/react': '@octanejs/base-ui',
|
|
42
49
|
'@dnd-kit/react': '@octanejs/dnd-kit',
|
|
43
50
|
sonner: '@octanejs/sonner',
|
|
51
|
+
'react-error-boundary': '@octanejs/react-error-boundary',
|
|
52
|
+
streamdown: '@octanejs/streamdown',
|
|
53
|
+
// The official plugins are consolidated as subpaths of the same package.
|
|
54
|
+
// The bundled bridge skill documents each exact import rewrite.
|
|
55
|
+
'@streamdown/code': '@octanejs/streamdown',
|
|
56
|
+
'@streamdown/math': '@octanejs/streamdown',
|
|
57
|
+
'@streamdown/mermaid': '@octanejs/streamdown',
|
|
58
|
+
'@streamdown/cjk': '@octanejs/streamdown',
|
|
44
59
|
shadcn: '@octanejs/shadcn',
|
|
45
60
|
recharts: '@octanejs/recharts',
|
|
46
61
|
'@react-three/fiber': '@octanejs/three',
|
|
@@ -95,6 +110,8 @@ export const KNOWN_BINDINGS = {
|
|
|
95
110
|
'@visx/zoom': '@octanejs/visx',
|
|
96
111
|
'react-redux': '@octanejs/redux',
|
|
97
112
|
'@reduxjs/toolkit': '@octanejs/redux-toolkit',
|
|
113
|
+
'@react-rxjs/core': '@octanejs/rxjs',
|
|
114
|
+
'@react-rxjs/utils': '@octanejs/rxjs',
|
|
98
115
|
'@testing-library/react': '@octanejs/testing-library',
|
|
99
116
|
'react-i18next': '@octanejs/i18next',
|
|
100
117
|
'@mdx-js/react': '@octanejs/mdx',
|
|
@@ -104,7 +121,11 @@ export const KNOWN_BINDINGS = {
|
|
|
104
121
|
// Octane-specific ecosystem packages that have no React import to rewrite.
|
|
105
122
|
// Keep these out of KNOWN_BINDINGS so the React bridge never invents a source
|
|
106
123
|
// package mapping for native tooling.
|
|
107
|
-
export const KNOWN_NATIVE_BINDINGS = new Set([
|
|
124
|
+
export const KNOWN_NATIVE_BINDINGS = new Set([
|
|
125
|
+
'@octanejs/devtools',
|
|
126
|
+
'@octanejs/electron',
|
|
127
|
+
'@octanejs/tauri',
|
|
128
|
+
]);
|
|
108
129
|
|
|
109
130
|
// Workspace directory names for the maintained bindings. Keep this derived
|
|
110
131
|
// from both catalogs so repository path routing cannot drift from the public
|
|
@@ -118,6 +139,7 @@ export const KNOWN_BINDING_PACKAGE_DIRS = new Set(
|
|
|
118
139
|
export const KNOWN_VANILLA_CORES = {
|
|
119
140
|
'@apollo/client': '@apollo/client',
|
|
120
141
|
'@tanstack/react-query': '@tanstack/query-core',
|
|
142
|
+
wagmi: '@wagmi/core',
|
|
121
143
|
'@tanstack/react-table': '@tanstack/table-core',
|
|
122
144
|
'@tanstack/react-virtual': '@tanstack/virtual-core',
|
|
123
145
|
'@tanstack/react-form': '@tanstack/form-core',
|
|
@@ -534,6 +556,9 @@ function planFor(report) {
|
|
|
534
556
|
`Reuse the framework-agnostic core '${report.vanillaCore}' unchanged; it has no React imports and runs on Octane as-is.`,
|
|
535
557
|
);
|
|
536
558
|
}
|
|
559
|
+
steps.push(
|
|
560
|
+
"Pin the upstream version you are bridging and copy that release's React binding source into your repository beside the port, keeping the upstream LICENSE and leaving the copy unmodified, then work through it module by module. A bridge written from the README or the type declarations covers the demo path and drops the rest of the API; the copy is also the diff you review on the next upgrade.",
|
|
561
|
+
);
|
|
537
562
|
steps.push(
|
|
538
563
|
'Re-implement the React binding layer (the hooks/components that import react) against Octane hooks of the same names. Most store bindings reduce to useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot).',
|
|
539
564
|
);
|
|
@@ -556,7 +581,10 @@ function planFor(report) {
|
|
|
556
581
|
'Re-author any JSX components shipped by the package in .tsrx: compiled React JSX output cannot run on Octane, and hooks called from non-compiled files need compiler slotting (see the bridge-react-package skill for the subSlot pattern).',
|
|
557
582
|
);
|
|
558
583
|
steps.push(
|
|
559
|
-
'
|
|
584
|
+
"Run the pinned release's own test suite against the bridge where it ships one: framework-neutral suites unmodified against the reused core, React-binding suites ported case by case (fixtures in .tsrx, @octanejs/testing-library for @testing-library/react, upstream case names kept). Note which upstream test files you ran, ported, or left out and why, and triage a failure before touching its assertion.",
|
|
585
|
+
);
|
|
586
|
+
steps.push(
|
|
587
|
+
'Validate the rest with tests that drive real DOM events and compare behavior against the React original where possible.',
|
|
560
588
|
);
|
|
561
589
|
return steps;
|
|
562
590
|
}
|
package/src/bridge.test.js
CHANGED
|
@@ -186,6 +186,18 @@ describe('bridgeReport', () => {
|
|
|
186
186
|
expect(report.plan[0]).toContain('@octanejs/zustand');
|
|
187
187
|
});
|
|
188
188
|
|
|
189
|
+
it('tells the caller to bridge from a pinned copy of the upstream source', async () => {
|
|
190
|
+
const root = await mkdtemp(join(tmpdir(), 'octane-bridge-'));
|
|
191
|
+
await writeFakePackage(root, 'widgets', {
|
|
192
|
+
'index.js': `
|
|
193
|
+
import { useState } from 'react';
|
|
194
|
+
export function useWidget() { return useState(0); }
|
|
195
|
+
`,
|
|
196
|
+
});
|
|
197
|
+
const report = await bridgeReport({ packageName: 'widgets', projectRoot: root });
|
|
198
|
+
expect(report.plan.join('\n')).toContain('Pin the upstream version');
|
|
199
|
+
});
|
|
200
|
+
|
|
189
201
|
it('errors clearly when the package is not installed', async () => {
|
|
190
202
|
const root = await mkdtemp(join(tmpdir(), 'octane-bridge-'));
|
|
191
203
|
const report = await bridgeReport({ packageName: 'missing-lib', projectRoot: root });
|
|
@@ -253,6 +265,19 @@ describe('bridgeReportFromSource', () => {
|
|
|
253
265
|
});
|
|
254
266
|
|
|
255
267
|
describe('KNOWN_BINDINGS', () => {
|
|
268
|
+
it('maps Streamdown and every official plugin package to the consolidated binding', () => {
|
|
269
|
+
const upstreamPackages = [
|
|
270
|
+
'streamdown',
|
|
271
|
+
'@streamdown/code',
|
|
272
|
+
'@streamdown/math',
|
|
273
|
+
'@streamdown/mermaid',
|
|
274
|
+
'@streamdown/cjk',
|
|
275
|
+
];
|
|
276
|
+
expect(upstreamPackages.every((name) => KNOWN_BINDINGS[name] === '@octanejs/streamdown')).toBe(
|
|
277
|
+
true,
|
|
278
|
+
);
|
|
279
|
+
});
|
|
280
|
+
|
|
256
281
|
it('maps every public Visx entry point to the aggregate Octane port', async () => {
|
|
257
282
|
const packagesRoot = fileURLToPath(new URL('../..', import.meta.url));
|
|
258
283
|
const manifest = JSON.parse(await readFile(join(packagesRoot, 'visx', 'package.json'), 'utf8'));
|
package/src/index.js
CHANGED
|
@@ -46,6 +46,7 @@ export const BENCHMARK_SUITES = [
|
|
|
46
46
|
'weather-app',
|
|
47
47
|
'weather-app-lighthouse',
|
|
48
48
|
'chat-stream',
|
|
49
|
+
'streamdown-hosted',
|
|
49
50
|
'dbmon',
|
|
50
51
|
'recursive-context',
|
|
51
52
|
'signal-favoring',
|
|
@@ -276,6 +277,17 @@ export function engineeringPlanFor(input, repoMode = false) {
|
|
|
276
277
|
if (scope === 'framework-core' && repoMode) {
|
|
277
278
|
plan.requiredSkills.push('octane-core-extend', 'performance-audit');
|
|
278
279
|
}
|
|
280
|
+
// A binding is a port of one pinned upstream release, so its gates are about
|
|
281
|
+
// coverage of that release rather than the runtime cost gates above.
|
|
282
|
+
if (plan.areas.some((entry) => entry.area === 'ecosystem-binding')) {
|
|
283
|
+
plan.gates.parity = [
|
|
284
|
+
'Port module by module from the pinned upstream release vendored under packages/<name>/upstream/, not from the README, the type declarations, or memory.',
|
|
285
|
+
'Account for every export of the pinned upstream React entry points in the packages/<name>/UPSTREAM.md crosswalk: ported, reused verbatim from a framework-neutral core, divergence, or not applicable, each with its evidence. An unfinished export is an explicit gap row.',
|
|
286
|
+
'Record what parity cannot reach as a divergence in UPSTREAM.md and status.json, with the reason, what the consumer should do instead, and a behavioral test pinning the Octane behavior.',
|
|
287
|
+
"Run the pinned release's own suite as the parity oracle: its framework-neutral tests unmodified against the reused core, its React-binding tests ported case by case with the upstream case names and citations. Record every upstream test file as run as-is, ported, or out of scope with the reason, and never weaken an upstream assertion to make it pass.",
|
|
288
|
+
];
|
|
289
|
+
if (repoMode) plan.requiredSkills.push('react-library-port');
|
|
290
|
+
}
|
|
279
291
|
if (scope === 'framework-core' && !repoMode) {
|
|
280
292
|
plan.blockingConditions = [
|
|
281
293
|
'Framework-core work requires the MCP server to run against an Octane monorepo checkout. Set OCTANE_REPO_ROOT, reconnect, and request this plan again so maintainer skills and repository validation are available.',
|
package/src/index.test.js
CHANGED
|
@@ -143,6 +143,25 @@ describe('@octanejs/mcp-server helpers', () => {
|
|
|
143
143
|
expect(plan.validationCommands).toContain('node benchmarks/bench.mjs --quick --ratios');
|
|
144
144
|
});
|
|
145
145
|
|
|
146
|
+
it('requires pinned-upstream coverage for binding work', () => {
|
|
147
|
+
const plan = engineeringPlanFor(
|
|
148
|
+
{ scope: 'library', changeKind: 'feature', paths: ['packages/zustand/src/index.ts'] },
|
|
149
|
+
true,
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
expect(plan.requiredSkills).toContain('react-library-port');
|
|
153
|
+
expect(plan.gates.parity.join('\n')).toContain('packages/<name>/UPSTREAM.md');
|
|
154
|
+
expect(plan.gates.parity.join('\n')).toContain('divergence');
|
|
155
|
+
expect(plan.gates.parity.join('\n')).toContain("pinned release's own suite");
|
|
156
|
+
|
|
157
|
+
const applicationPlan = engineeringPlanFor(
|
|
158
|
+
{ scope: 'application', changeKind: 'feature', paths: ['src/App.tsrx'] },
|
|
159
|
+
true,
|
|
160
|
+
);
|
|
161
|
+
expect(applicationPlan.gates.parity).toBeUndefined();
|
|
162
|
+
expect(applicationPlan.requiredSkills).not.toContain('react-library-port');
|
|
163
|
+
});
|
|
164
|
+
|
|
146
165
|
it('blocks framework-core plans when maintainer tools are unavailable', () => {
|
|
147
166
|
const plan = engineeringPlanFor({ scope: 'framework-core', changeKind: 'bug' });
|
|
148
167
|
|