@octanejs/mcp-server 0.2.1 → 0.2.3
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/README.md +4 -2
- package/package.json +4 -1
- package/skills/bridge-react-package.md +9 -2
- package/src/bridge.js +37 -5
- package/src/bridge.test.js +45 -15
- package/src/index.js +17 -7
- package/src/index.test.js +15 -0
package/README.md
CHANGED
|
@@ -72,8 +72,10 @@ exists, an overall verdict (`bridgeable`, `bridgeable-with-rewrites`,
|
|
|
72
72
|
|
|
73
73
|
### `octane_bindings`
|
|
74
74
|
|
|
75
|
-
Returns the map of React packages with maintained `@octanejs/*` ports
|
|
76
|
-
(
|
|
75
|
+
Returns the map of React packages with maintained `@octanejs/*` ports. The map
|
|
76
|
+
lives in `src/bridge.js` (`KNOWN_BINDINGS`), and its tests derive the complete
|
|
77
|
+
binding package set from the workspace manifests, so adding a published binding
|
|
78
|
+
without registering its React-package mapping fails CI.
|
|
77
79
|
|
|
78
80
|
### `octane_skill`
|
|
79
81
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@octanejs/mcp-server",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"type": "module",
|
|
5
|
+
"engines": {
|
|
6
|
+
"node": ">=22"
|
|
7
|
+
},
|
|
5
8
|
"description": "MCP server exposing Octane repository automation for coding agents.",
|
|
6
9
|
"license": "MIT",
|
|
7
10
|
"author": {
|
|
@@ -10,13 +10,20 @@ of bridging by hand:
|
|
|
10
10
|
| React package | Octane binding |
|
|
11
11
|
| --- | --- |
|
|
12
12
|
| `zustand` | `@octanejs/zustand` |
|
|
13
|
-
|
|
|
13
|
+
| `jotai` | `@octanejs/jotai` |
|
|
14
|
+
| `@apollo/client` | `@octanejs/apollo-client` |
|
|
15
|
+
| `@tanstack/react-query` | `@octanejs/tanstack-query` |
|
|
16
|
+
| `@tanstack/react-router` | `@octanejs/tanstack-router` |
|
|
17
|
+
| `@tanstack/react-table` | `@octanejs/tanstack-table` |
|
|
18
|
+
| `@tanstack/react-virtual` | `@octanejs/tanstack-virtual` |
|
|
14
19
|
| `framer-motion` / `motion` | `@octanejs/motion` |
|
|
15
20
|
| `@stylexjs/stylex` | `@octanejs/stylex` |
|
|
16
|
-
| `react-router` / `react-router-dom` | `@octanejs/router` |
|
|
21
|
+
| `react-router` / `react-router-dom` | `@octanejs/remix-router` |
|
|
17
22
|
| `@lexical/react` | `@octanejs/lexical` |
|
|
23
|
+
| `lucide-react` | `@octanejs/lucide` |
|
|
18
24
|
| `@floating-ui/react` | `@octanejs/floating-ui` |
|
|
19
25
|
| `radix-ui` | `@octanejs/radix` |
|
|
26
|
+
| `react-i18next` | `@octanejs/i18next` |
|
|
20
27
|
|
|
21
28
|
For anything else, run the `octane_bridge_react_package` tool to get a scan of the
|
|
22
29
|
package's React API usage and a tailored plan, then follow the workflow below.
|
package/src/bridge.js
CHANGED
|
@@ -1,28 +1,57 @@
|
|
|
1
1
|
import { readdir, readFile } from 'node:fs/promises';
|
|
2
2
|
import { join, resolve } from 'node:path';
|
|
3
3
|
|
|
4
|
+
// React package → maintained @octanejs binding. bridge.test.js derives the
|
|
5
|
+
// expected value set from the workspace manifests, so publishing a new binding
|
|
6
|
+
// without registering it here fails the mcp-server tests.
|
|
4
7
|
export const KNOWN_BINDINGS = {
|
|
5
8
|
zustand: '@octanejs/zustand',
|
|
6
|
-
|
|
9
|
+
jotai: '@octanejs/jotai',
|
|
10
|
+
'@apollo/client': '@octanejs/apollo-client',
|
|
11
|
+
'@tanstack/react-query': '@octanejs/tanstack-query',
|
|
12
|
+
'@tanstack/react-router': '@octanejs/tanstack-router',
|
|
13
|
+
'@tanstack/react-table': '@octanejs/tanstack-table',
|
|
14
|
+
'@tanstack/react-virtual': '@octanejs/tanstack-virtual',
|
|
7
15
|
'framer-motion': '@octanejs/motion',
|
|
8
16
|
motion: '@octanejs/motion',
|
|
9
17
|
'@stylexjs/stylex': '@octanejs/stylex',
|
|
10
|
-
'react-router': '@octanejs/router',
|
|
11
|
-
'react-router-dom': '@octanejs/router',
|
|
18
|
+
'react-router': '@octanejs/remix-router',
|
|
19
|
+
'react-router-dom': '@octanejs/remix-router',
|
|
12
20
|
'@lexical/react': '@octanejs/lexical',
|
|
21
|
+
'lucide-react': '@octanejs/lucide',
|
|
13
22
|
'@floating-ui/react': '@octanejs/floating-ui',
|
|
14
23
|
'radix-ui': '@octanejs/radix',
|
|
24
|
+
'react-hook-form': '@octanejs/hook-form',
|
|
25
|
+
'@base-ui-components/react': '@octanejs/base-ui',
|
|
26
|
+
'@dnd-kit/react': '@octanejs/dnd-kit',
|
|
27
|
+
sonner: '@octanejs/sonner',
|
|
28
|
+
recharts: '@octanejs/recharts',
|
|
29
|
+
'react-redux': '@octanejs/redux',
|
|
30
|
+
'@reduxjs/toolkit': '@octanejs/redux-toolkit',
|
|
31
|
+
'@testing-library/react': '@octanejs/testing-library',
|
|
32
|
+
'react-i18next': '@octanejs/i18next',
|
|
33
|
+
'@mdx-js/react': '@octanejs/mdx',
|
|
15
34
|
};
|
|
16
35
|
|
|
36
|
+
// Workspace directory names for the maintained bindings. Keep this derived
|
|
37
|
+
// from KNOWN_BINDINGS so repository path routing cannot drift from the public
|
|
38
|
+
// binding list (aliases such as `motion` intentionally collapse to one dir).
|
|
39
|
+
export const KNOWN_BINDING_PACKAGE_DIRS = new Set(
|
|
40
|
+
Object.values(KNOWN_BINDINGS).map((name) => name.slice('@octanejs/'.length)),
|
|
41
|
+
);
|
|
42
|
+
|
|
17
43
|
export const KNOWN_VANILLA_CORES = {
|
|
44
|
+
'@apollo/client': '@apollo/client',
|
|
18
45
|
'@tanstack/react-query': '@tanstack/query-core',
|
|
19
46
|
'@tanstack/react-table': '@tanstack/table-core',
|
|
20
47
|
'@tanstack/react-virtual': '@tanstack/virtual-core',
|
|
21
48
|
'@tanstack/react-form': '@tanstack/form-core',
|
|
22
49
|
'@floating-ui/react': '@floating-ui/dom',
|
|
50
|
+
'@dnd-kit/react': '@dnd-kit/dom',
|
|
23
51
|
'@xstate/react': 'xstate',
|
|
24
52
|
'react-redux': 'redux',
|
|
25
53
|
'@reduxjs/toolkit': 'redux',
|
|
54
|
+
'react-i18next': 'i18next',
|
|
26
55
|
'react-hook-form': null,
|
|
27
56
|
zustand: 'zustand/vanilla',
|
|
28
57
|
valtio: 'valtio/vanilla',
|
|
@@ -33,9 +62,12 @@ export const KNOWN_VANILLA_CORES = {
|
|
|
33
62
|
export const REACT_API_MAP = {
|
|
34
63
|
useState: {
|
|
35
64
|
status: 'same',
|
|
36
|
-
note: '
|
|
65
|
+
note: 'Same lazy initializer and functional-update semantics; Octane additionally exposes a stable current-state getter at tuple index 2.',
|
|
66
|
+
},
|
|
67
|
+
useReducer: {
|
|
68
|
+
status: 'same',
|
|
69
|
+
note: 'Same reducer and lazy-init semantics; Octane additionally exposes a stable current-state getter at tuple index 2.',
|
|
37
70
|
},
|
|
38
|
-
useReducer: { status: 'same', note: 'Identical, including lazy init third argument.' },
|
|
39
71
|
useEffect: { status: 'same', note: 'Identical deps/cleanup semantics.' },
|
|
40
72
|
useLayoutEffect: { status: 'same', note: 'Identical: synchronous after DOM mutation.' },
|
|
41
73
|
useInsertionEffect: { status: 'same', note: 'Supported.' },
|
package/src/bridge.test.js
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { mkdir, mkdtemp, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { mkdir, mkdtemp, readdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
3
4
|
import { tmpdir } from 'node:os';
|
|
4
5
|
import { join } from 'node:path';
|
|
5
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
bridgeReport,
|
|
8
|
+
detectVanillaCore,
|
|
9
|
+
scanSource,
|
|
10
|
+
KNOWN_BINDINGS,
|
|
11
|
+
KNOWN_BINDING_PACKAGE_DIRS,
|
|
12
|
+
} from './bridge.js';
|
|
6
13
|
|
|
7
14
|
describe('scanSource', () => {
|
|
8
15
|
it('collects React API usage counts and import specifiers', () => {
|
|
@@ -33,6 +40,7 @@ describe('scanSource', () => {
|
|
|
33
40
|
|
|
34
41
|
describe('detectVanillaCore', () => {
|
|
35
42
|
it('prefers the known-core table', () => {
|
|
43
|
+
expect(detectVanillaCore('@apollo/client', {})).toBe('@apollo/client');
|
|
36
44
|
expect(detectVanillaCore('@tanstack/react-query', {})).toBe('@tanstack/query-core');
|
|
37
45
|
expect(detectVanillaCore('zustand', {})).toBe('zustand/vanilla');
|
|
38
46
|
});
|
|
@@ -137,18 +145,40 @@ describe('bridgeReport', () => {
|
|
|
137
145
|
});
|
|
138
146
|
|
|
139
147
|
describe('KNOWN_BINDINGS', () => {
|
|
140
|
-
it('covers every published @octanejs binding', () => {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
148
|
+
it('covers every published @octanejs binding (derived from workspace manifests)', async () => {
|
|
149
|
+
// The expected set is DERIVED from packages/*/package.json rather than
|
|
150
|
+
// hand-maintained, so publishing a new binding without registering it in
|
|
151
|
+
// KNOWN_BINDINGS fails here. Only genuinely-not-a-binding packages (the
|
|
152
|
+
// core runtime, build/deploy infrastructure, this MCP server) are
|
|
153
|
+
// excluded by name.
|
|
154
|
+
const NON_BINDINGS = new Set([
|
|
155
|
+
'octane',
|
|
156
|
+
'@octanejs/app-core',
|
|
157
|
+
'@octanejs/rspack-plugin',
|
|
158
|
+
'@octanejs/rsbuild-plugin',
|
|
159
|
+
'@octanejs/vite-plugin',
|
|
160
|
+
'@octanejs/adapter-vercel',
|
|
161
|
+
'@octanejs/mcp-server',
|
|
162
|
+
]);
|
|
163
|
+
const packagesRoot = fileURLToPath(new URL('../..', import.meta.url));
|
|
164
|
+
const bindings = [];
|
|
165
|
+
const bindingDirectories = [];
|
|
166
|
+
for (const entry of await readdir(packagesRoot, { withFileTypes: true })) {
|
|
167
|
+
if (!entry.isDirectory()) continue;
|
|
168
|
+
let manifest;
|
|
169
|
+
try {
|
|
170
|
+
manifest = JSON.parse(
|
|
171
|
+
await readFile(join(packagesRoot, entry.name, 'package.json'), 'utf8'),
|
|
172
|
+
);
|
|
173
|
+
} catch {
|
|
174
|
+
continue; // not a package dir
|
|
175
|
+
}
|
|
176
|
+
if (manifest.private || NON_BINDINGS.has(manifest.name)) continue;
|
|
177
|
+
bindings.push(manifest.name);
|
|
178
|
+
bindingDirectories.push(entry.name);
|
|
179
|
+
}
|
|
180
|
+
expect(bindings.length).toBeGreaterThan(0);
|
|
181
|
+
expect(new Set(Object.values(KNOWN_BINDINGS))).toEqual(new Set(bindings));
|
|
182
|
+
expect(KNOWN_BINDING_PACKAGE_DIRS).toEqual(new Set(bindingDirectories));
|
|
153
183
|
});
|
|
154
184
|
});
|
package/src/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import { readFile } from 'node:fs/promises';
|
|
|
7
7
|
import { dirname, join, resolve } from 'node:path';
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
9
|
import { z } from 'zod';
|
|
10
|
-
import { bridgeReport, KNOWN_BINDINGS } from './bridge.js';
|
|
10
|
+
import { bridgeReport, KNOWN_BINDINGS, KNOWN_BINDING_PACKAGE_DIRS } from './bridge.js';
|
|
11
11
|
|
|
12
12
|
const __filename = fileURLToPath(import.meta.url);
|
|
13
13
|
const PACKAGE_ROOT = resolve(dirname(__filename), '..');
|
|
@@ -59,9 +59,13 @@ export function areaForPath(path) {
|
|
|
59
59
|
return 'core-runtime';
|
|
60
60
|
}
|
|
61
61
|
if (path.startsWith('packages/octane/tests/')) return 'core-tests';
|
|
62
|
+
if (path.startsWith('packages/app-core/')) return 'metaframework-core';
|
|
63
|
+
if (path.startsWith('packages/rspack-plugin-octane/')) return 'rspack-plugin';
|
|
64
|
+
if (path.startsWith('packages/rsbuild-plugin-octane/')) return 'rsbuild-plugin';
|
|
62
65
|
if (path.startsWith('packages/vite-plugin-octane/')) return 'vite-plugin';
|
|
63
66
|
if (path.startsWith('packages/octane-mcp-server/')) return 'mcp-server';
|
|
64
|
-
|
|
67
|
+
const packageMatch = path.match(/^packages\/([^/]+)\//);
|
|
68
|
+
if (packageMatch && KNOWN_BINDING_PACKAGE_DIRS.has(packageMatch[1])) {
|
|
65
69
|
return 'ecosystem-binding';
|
|
66
70
|
}
|
|
67
71
|
if (path.startsWith('benchmarks/')) return 'benchmark';
|
|
@@ -88,13 +92,12 @@ export function validationFor(paths, taskKind) {
|
|
|
88
92
|
}
|
|
89
93
|
if (areas.has('ecosystem-binding')) {
|
|
90
94
|
for (const path of paths) {
|
|
91
|
-
const match = path.match(
|
|
92
|
-
|
|
93
|
-
);
|
|
94
|
-
if (match)
|
|
95
|
+
const match = path.match(/^packages\/([^/]+)\//);
|
|
96
|
+
if (match && KNOWN_BINDING_PACKAGE_DIRS.has(match[1])) {
|
|
95
97
|
commands.add(
|
|
96
98
|
`./node_modules/.bin/vitest run packages/${match[1]}/tests --project ${match[1]}`,
|
|
97
99
|
);
|
|
100
|
+
}
|
|
98
101
|
}
|
|
99
102
|
}
|
|
100
103
|
if (areas.has('mcp-server')) {
|
|
@@ -102,7 +105,14 @@ export function validationFor(paths, taskKind) {
|
|
|
102
105
|
'./node_modules/.bin/vitest run packages/octane-mcp-server --project octane-mcp-server',
|
|
103
106
|
);
|
|
104
107
|
}
|
|
105
|
-
if (
|
|
108
|
+
if (
|
|
109
|
+
areas.has('metaframework-core') ||
|
|
110
|
+
areas.has('rspack-plugin') ||
|
|
111
|
+
areas.has('rsbuild-plugin') ||
|
|
112
|
+
areas.has('vite-plugin')
|
|
113
|
+
) {
|
|
114
|
+
commands.add('pnpm typecheck');
|
|
115
|
+
}
|
|
106
116
|
if (areas.has('benchmark') || taskKind === 'performance') commands.add('pnpm bench');
|
|
107
117
|
if (taskKind === 'api' || taskKind === 'core' || taskKind === 'package')
|
|
108
118
|
commands.add('pnpm typecheck');
|
package/src/index.test.js
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
scaffoldReactPort,
|
|
12
12
|
validationFor,
|
|
13
13
|
} from './index.js';
|
|
14
|
+
import { KNOWN_BINDING_PACKAGE_DIRS } from './bridge.js';
|
|
14
15
|
|
|
15
16
|
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
16
17
|
|
|
@@ -26,6 +27,20 @@ describe('@octanejs/mcp-server helpers', () => {
|
|
|
26
27
|
expect(areaForPath('.rulesync/rules/project.md')).toBe('rulesync-source');
|
|
27
28
|
});
|
|
28
29
|
|
|
30
|
+
it('classifies every maintained binding and recommends its test project', () => {
|
|
31
|
+
const paths = [...KNOWN_BINDING_PACKAGE_DIRS].map(
|
|
32
|
+
(directory) => `packages/${directory}/src/index.ts`,
|
|
33
|
+
);
|
|
34
|
+
const commands = validationFor(paths, 'binding');
|
|
35
|
+
|
|
36
|
+
for (const directory of KNOWN_BINDING_PACKAGE_DIRS) {
|
|
37
|
+
expect(areaForPath(`packages/${directory}/src/index.ts`)).toBe('ecosystem-binding');
|
|
38
|
+
expect(commands).toContain(
|
|
39
|
+
`./node_modules/.bin/vitest run packages/${directory}/tests --project ${directory}`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
29
44
|
it('recommends validation commands from changed paths', () => {
|
|
30
45
|
const commands = validationFor(
|
|
31
46
|
[
|