@octanejs/mcp-server 0.2.5 → 0.2.8
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 +22 -15
- package/package.json +2 -2
- package/skills/setup-ssr.md +7 -3
- package/src/bridge.js +18 -5
- package/src/bridge.test.js +11 -35
- package/src/index.js +52 -24
- package/src/index.test.js +62 -6
package/README.md
CHANGED
|
@@ -57,11 +57,12 @@ maintainer tools):
|
|
|
57
57
|
|
|
58
58
|
## Tools (always available)
|
|
59
59
|
|
|
60
|
-
The server initialization instructions
|
|
61
|
-
`octane_engineering_plan
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
60
|
+
The server initialization instructions are one orienting sentence plus a pointer
|
|
61
|
+
to `octane_engineering_plan`, because they are injected into every session
|
|
62
|
+
whether or not it touches Octane. The correctness, performance-evidence,
|
|
63
|
+
self-review, and handoff requirements live in that tool response, so they stay
|
|
64
|
+
reachable even on hosts that do not discover skills. Each tool description says
|
|
65
|
+
when to call it.
|
|
65
66
|
|
|
66
67
|
### `octane_engineering_plan`
|
|
67
68
|
|
|
@@ -112,24 +113,30 @@ without registering its React-package mapping fails CI.
|
|
|
112
113
|
|
|
113
114
|
Returns a skill by name. Bundled skills (shipped with this package):
|
|
114
115
|
|
|
115
|
-
- `bridge-react-package
|
|
116
|
-
- `build-octane-software
|
|
116
|
+
- `bridge-react-package`: the full workflow for porting a React library.
|
|
117
|
+
- `build-octane-software`: production engineering, performance, validation,
|
|
117
118
|
and adversarial self-review gates for Octane code.
|
|
118
|
-
- `migrate-react-component
|
|
119
|
-
- `react-divergences
|
|
120
|
-
- `setup-ssr
|
|
119
|
+
- `migrate-react-component`: React JSX to `.tsrx` conversion reference.
|
|
120
|
+
- `react-divergences`: Octane's intentional differences from React.
|
|
121
|
+
- `setup-ssr`: server rendering and hydration setup.
|
|
121
122
|
|
|
122
|
-
When running inside the octane monorepo, the
|
|
123
|
-
|
|
123
|
+
When running inside the octane monorepo, the skills from `.rulesync/skills` are
|
|
124
|
+
also available: `authoring-tsrx`, `react-library-port`, `bug-hunter`,
|
|
124
125
|
`create-a-pr`, `handle-issue`, `octane-core-extend`, `triage`,
|
|
125
|
-
`performance-audit`.
|
|
126
|
+
`performance-audit`. A test compares this map against the directory in both
|
|
127
|
+
directions, so a new skill cannot stay unreachable here. This tool reads the RuleSync source, and
|
|
128
|
+
`pnpm rules:generate` writes the per-agent copies (`.claude/skills/`,
|
|
129
|
+
`.github/skills/`, `.cursor/skills/`, `.gemini/skills/`) from the same text, so
|
|
130
|
+
hosts that discover skills natively and hosts that call this tool see the same
|
|
131
|
+
thing.
|
|
126
132
|
|
|
127
133
|
## Tools (octane monorepo only)
|
|
128
134
|
|
|
129
135
|
### `octane_project_map`
|
|
130
136
|
|
|
131
|
-
Returns
|
|
132
|
-
|
|
137
|
+
Returns `AGENTS.md`: the RuleSync-generated root rule, covering what Octane is,
|
|
138
|
+
which source owns which behavior, the intentional divergences from React, and the
|
|
139
|
+
validation commands. CI fails if it drifts from its source in `.rulesync/rules/`.
|
|
133
140
|
|
|
134
141
|
### `octane_triage_paths`
|
|
135
142
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@octanejs/mcp-server",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.8",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=22"
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"zod": "^4.4.3"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"vitest": "^4.1.
|
|
39
|
+
"vitest": "^4.1.10"
|
|
40
40
|
},
|
|
41
41
|
"scripts": {
|
|
42
42
|
"start": "node src/index.js",
|
package/skills/setup-ssr.md
CHANGED
|
@@ -66,9 +66,13 @@ hydration-stable; the client adopts server DOM instead of rebuilding it.
|
|
|
66
66
|
self-contained SSR server at `dist/server/entry.js` (exports
|
|
67
67
|
`handler`/`nodeHandler`; preview with `octane-preview`);
|
|
68
68
|
`server.render: 'buffered'` switches it to the await-everything `prerender`.
|
|
69
|
-
A deploy adapter
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
A deploy adapter prepares the output for a host: `adapter: vercel()` from
|
|
70
|
+
`@octanejs/adapter-vercel` emits Vercel's Build Output API, while
|
|
71
|
+
`adapter: cloudflare()` from `@octanejs/adapter-cloudflare` emits a module
|
|
72
|
+
Worker at `dist/server/worker.js` for Workers Static Assets. Cloudflare apps
|
|
73
|
+
keep a user-owned `wrangler.jsonc` pointing `main` at that Worker and
|
|
74
|
+
`assets.directory` at `dist/client`, with `nodejs_compat` enabled. Leave
|
|
75
|
+
`assets.not_found_handling` unset or `"none"` so navigation misses reach SSR.
|
|
72
76
|
2. **Custom server**: write `entry-server.ts` exporting a function that calls
|
|
73
77
|
`prerender()` (or `renderToString()`, or a streaming renderer) and splices
|
|
74
78
|
the result into your HTML template, and `entry-client.ts` calling
|
package/src/bridge.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
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.
|
|
5
|
-
//
|
|
6
|
-
//
|
|
4
|
+
// React package → maintained @octanejs binding. Bindings with no React-package
|
|
5
|
+
// equivalent live in KNOWN_NATIVE_BINDINGS below. bridge.test.js derives the
|
|
6
|
+
// expected union from the workspace manifests, so publishing a new binding
|
|
7
|
+
// without registering it in either catalog fails the mcp-server tests.
|
|
7
8
|
export const KNOWN_BINDINGS = {
|
|
8
9
|
zustand: '@octanejs/zustand',
|
|
9
10
|
jotai: '@octanejs/jotai',
|
|
@@ -14,6 +15,9 @@ export const KNOWN_BINDINGS = {
|
|
|
14
15
|
'@tanstack/react-query': '@octanejs/tanstack-query',
|
|
15
16
|
'@tanstack/react-router': '@octanejs/tanstack-router',
|
|
16
17
|
'@tanstack/react-store': '@octanejs/tanstack-store',
|
|
18
|
+
'@tanstack/react-router-ssr-query': '@octanejs/tanstack-router-ssr-query',
|
|
19
|
+
'@tanstack/react-hotkeys': '@octanejs/tanstack-hotkeys',
|
|
20
|
+
'@tanstack/react-pacer': '@octanejs/tanstack-pacer',
|
|
17
21
|
'@tanstack/react-table': '@octanejs/tanstack-table',
|
|
18
22
|
'@tanstack/react-virtual': '@octanejs/tanstack-virtual',
|
|
19
23
|
'framer-motion': '@octanejs/motion',
|
|
@@ -22,6 +26,7 @@ export const KNOWN_BINDINGS = {
|
|
|
22
26
|
'styled-components': '@octanejs/styled-components',
|
|
23
27
|
'react-router': '@octanejs/remix-router',
|
|
24
28
|
'react-router-dom': '@octanejs/remix-router',
|
|
29
|
+
nuqs: '@octanejs/nuqs',
|
|
25
30
|
'@lexical/react': '@octanejs/lexical',
|
|
26
31
|
'@tiptap/react': '@octanejs/tiptap',
|
|
27
32
|
'lucide-react': '@octanejs/lucide',
|
|
@@ -34,6 +39,7 @@ export const KNOWN_BINDINGS = {
|
|
|
34
39
|
'@base-ui-components/react': '@octanejs/base-ui',
|
|
35
40
|
'@dnd-kit/react': '@octanejs/dnd-kit',
|
|
36
41
|
sonner: '@octanejs/sonner',
|
|
42
|
+
shadcn: '@octanejs/shadcn',
|
|
37
43
|
recharts: '@octanejs/recharts',
|
|
38
44
|
'@react-three/fiber': '@octanejs/three',
|
|
39
45
|
'@visx/visx': '@octanejs/visx',
|
|
@@ -93,11 +99,18 @@ export const KNOWN_BINDINGS = {
|
|
|
93
99
|
'dexie-react-hooks': '@octanejs/dexie',
|
|
94
100
|
};
|
|
95
101
|
|
|
102
|
+
// Octane-specific ecosystem packages that have no React import to rewrite.
|
|
103
|
+
// Keep these out of KNOWN_BINDINGS so the React bridge never invents a source
|
|
104
|
+
// package mapping for native tooling.
|
|
105
|
+
export const KNOWN_NATIVE_BINDINGS = new Set(['@octanejs/devtools']);
|
|
106
|
+
|
|
96
107
|
// Workspace directory names for the maintained bindings. Keep this derived
|
|
97
|
-
// from
|
|
108
|
+
// from both catalogs so repository path routing cannot drift from the public
|
|
98
109
|
// binding list (aliases such as `motion` intentionally collapse to one dir).
|
|
99
110
|
export const KNOWN_BINDING_PACKAGE_DIRS = new Set(
|
|
100
|
-
Object.values(KNOWN_BINDINGS).map((name) =>
|
|
111
|
+
[...Object.values(KNOWN_BINDINGS), ...KNOWN_NATIVE_BINDINGS].map((name) =>
|
|
112
|
+
name.slice('@octanejs/'.length),
|
|
113
|
+
),
|
|
101
114
|
);
|
|
102
115
|
|
|
103
116
|
export const KNOWN_VANILLA_CORES = {
|
package/src/bridge.test.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { mkdir, mkdtemp,
|
|
2
|
+
import { mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
|
+
import { getBindingPackages } from '../../../scripts/workspace-packages.mjs';
|
|
6
7
|
import {
|
|
7
8
|
bridgeReport,
|
|
8
9
|
bridgeReportFromSource,
|
|
9
10
|
detectVanillaCore,
|
|
10
11
|
scanSource,
|
|
11
12
|
KNOWN_BINDINGS,
|
|
13
|
+
KNOWN_NATIVE_BINDINGS,
|
|
12
14
|
KNOWN_BINDING_PACKAGE_DIRS,
|
|
13
15
|
} from './bridge.js';
|
|
14
16
|
|
|
@@ -261,40 +263,14 @@ describe('KNOWN_BINDINGS', () => {
|
|
|
261
263
|
expect(upstreamPackages.every((name) => KNOWN_BINDINGS[name] === '@octanejs/visx')).toBe(true);
|
|
262
264
|
});
|
|
263
265
|
|
|
264
|
-
it('covers every published @octanejs binding (derived from workspace manifests)',
|
|
265
|
-
//
|
|
266
|
-
//
|
|
267
|
-
|
|
268
|
-
// core runtime, build/deploy infrastructure, this MCP server) are
|
|
269
|
-
// excluded by name.
|
|
270
|
-
const NON_BINDINGS = new Set([
|
|
271
|
-
'octane',
|
|
272
|
-
'@octanejs/app-core',
|
|
273
|
-
'@octanejs/rspack-plugin',
|
|
274
|
-
'@octanejs/rsbuild-plugin',
|
|
275
|
-
'@octanejs/vite-plugin',
|
|
276
|
-
'@octanejs/adapter-vercel',
|
|
277
|
-
'@octanejs/mcp-server',
|
|
278
|
-
]);
|
|
279
|
-
const packagesRoot = fileURLToPath(new URL('../..', import.meta.url));
|
|
280
|
-
const bindings = [];
|
|
281
|
-
const bindingDirectories = [];
|
|
282
|
-
for (const entry of await readdir(packagesRoot, { withFileTypes: true })) {
|
|
283
|
-
if (!entry.isDirectory()) continue;
|
|
284
|
-
let manifest;
|
|
285
|
-
try {
|
|
286
|
-
manifest = JSON.parse(
|
|
287
|
-
await readFile(join(packagesRoot, entry.name, 'package.json'), 'utf8'),
|
|
288
|
-
);
|
|
289
|
-
} catch {
|
|
290
|
-
continue; // not a package dir
|
|
291
|
-
}
|
|
292
|
-
if (manifest.private || NON_BINDINGS.has(manifest.name)) continue;
|
|
293
|
-
bindings.push(manifest.name);
|
|
294
|
-
bindingDirectories.push(entry.name);
|
|
295
|
-
}
|
|
266
|
+
it('covers every published @octanejs binding (derived from workspace manifests)', () => {
|
|
267
|
+
// Use the same role classification as the generated package inventory so
|
|
268
|
+
// metaframeworks and infrastructure cannot drift into the binding catalog.
|
|
269
|
+
const bindings = getBindingPackages();
|
|
296
270
|
expect(bindings.length).toBeGreaterThan(0);
|
|
297
|
-
expect(new Set(Object.values(KNOWN_BINDINGS))).toEqual(
|
|
298
|
-
|
|
271
|
+
expect(new Set([...Object.values(KNOWN_BINDINGS), ...KNOWN_NATIVE_BINDINGS])).toEqual(
|
|
272
|
+
new Set(bindings.map(({ name }) => name)),
|
|
273
|
+
);
|
|
274
|
+
expect(KNOWN_BINDING_PACKAGE_DIRS).toEqual(new Set(bindings.map(({ dir }) => dir)));
|
|
299
275
|
});
|
|
300
276
|
});
|
package/src/index.js
CHANGED
|
@@ -13,9 +13,10 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
13
13
|
const PACKAGE_ROOT = resolve(dirname(__filename), '..');
|
|
14
14
|
|
|
15
15
|
// Bundled skills ship with the npm package and work in ANY project using
|
|
16
|
-
// octane. Repo skills
|
|
17
|
-
//
|
|
18
|
-
// workflows: triage, PRs, core
|
|
16
|
+
// octane. Repo skills are read from the RuleSync source, not a generated copy,
|
|
17
|
+
// so this server and every per-agent output (.claude/skills, .github/skills,
|
|
18
|
+
// …) serve the same text. They cover maintainer workflows: triage, PRs, core
|
|
19
|
+
// changes, and are only available when the server runs against a checkout.
|
|
19
20
|
export const BUNDLED_SKILLS = {
|
|
20
21
|
'bridge-react-package': 'skills/bridge-react-package.md',
|
|
21
22
|
'build-octane-software': 'skills/build-octane-software.md',
|
|
@@ -25,13 +26,14 @@ export const BUNDLED_SKILLS = {
|
|
|
25
26
|
};
|
|
26
27
|
|
|
27
28
|
export const REPO_SKILLS = {
|
|
28
|
-
'
|
|
29
|
-
'
|
|
30
|
-
'
|
|
31
|
-
'
|
|
32
|
-
'
|
|
33
|
-
'
|
|
34
|
-
|
|
29
|
+
'authoring-tsrx': '.rulesync/skills/authoring-tsrx/SKILL.md',
|
|
30
|
+
'bug-hunter': '.rulesync/skills/bug-hunter/SKILL.md',
|
|
31
|
+
'create-a-pr': '.rulesync/skills/create-a-pr/SKILL.md',
|
|
32
|
+
'handle-issue': '.rulesync/skills/handle-issue/SKILL.md',
|
|
33
|
+
'octane-core-extend': '.rulesync/skills/octane-core-extend/SKILL.md',
|
|
34
|
+
'performance-audit': '.rulesync/skills/performance-audit/SKILL.md',
|
|
35
|
+
'react-library-port': '.rulesync/skills/react-library-port/SKILL.md',
|
|
36
|
+
triage: '.rulesync/skills/triage/SKILL.md',
|
|
35
37
|
};
|
|
36
38
|
|
|
37
39
|
// Suite names from the unified runner manifest (`SUITES` in
|
|
@@ -48,16 +50,22 @@ export const BENCHMARK_SUITES = [
|
|
|
48
50
|
'recursive-context',
|
|
49
51
|
'signal-favoring',
|
|
50
52
|
'news',
|
|
53
|
+
'hydration-interactivity',
|
|
51
54
|
'effectful-list',
|
|
52
55
|
'memo-wall',
|
|
53
56
|
'portal-swarm',
|
|
54
57
|
'react-hosted-islands',
|
|
55
58
|
'ssr-throughput',
|
|
56
59
|
'streaming-ssr',
|
|
60
|
+
'ssr-http',
|
|
61
|
+
'ssr-workerd',
|
|
62
|
+
'tanstack-start',
|
|
57
63
|
'dbmon-deopt',
|
|
58
64
|
'js-framework-deopt',
|
|
59
65
|
'async-waterfall',
|
|
60
66
|
'async-composition',
|
|
67
|
+
'lynx-list',
|
|
68
|
+
'lynx-bundle-size',
|
|
61
69
|
'codegen-size',
|
|
62
70
|
'bundle-size',
|
|
63
71
|
'three-renderer',
|
|
@@ -67,10 +75,15 @@ export const BENCHMARK_SUITES = [
|
|
|
67
75
|
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
68
76
|
|
|
69
77
|
export function instructionsFor(repoMode) {
|
|
78
|
+
// This text is injected into every session of every connected agent, whether
|
|
79
|
+
// or not it touches Octane, so it stays one orienting sentence plus one
|
|
80
|
+
// pointer. The gates themselves live in octane_engineering_plan, and each
|
|
81
|
+
// tool's own description says when to call it: that is where a model looks
|
|
82
|
+
// when it is actually deciding.
|
|
70
83
|
const common =
|
|
71
|
-
|
|
84
|
+
"Octane is a compiler-first UI framework with React's programming model, so React habits are the main source of wrong changes here. Before creating or materially changing Octane code, call octane_engineering_plan: it returns the gates that apply and names the skills to load.";
|
|
72
85
|
return repoMode
|
|
73
|
-
? `${common}
|
|
86
|
+
? `${common} Inside this monorepo checkout it also returns the repository validation commands for the paths you changed.`
|
|
74
87
|
: common;
|
|
75
88
|
}
|
|
76
89
|
|
|
@@ -94,7 +107,7 @@ export function areaForPath(path) {
|
|
|
94
107
|
if (path.startsWith('packages/rspack-plugin-octane/')) return 'rspack-plugin';
|
|
95
108
|
if (path.startsWith('packages/rsbuild-plugin-octane/')) return 'rsbuild-plugin';
|
|
96
109
|
if (path.startsWith('packages/vite-plugin-octane/')) return 'vite-plugin';
|
|
97
|
-
if (
|
|
110
|
+
if (/^packages\/adapter-[^/]+\//.test(path)) return 'deploy-adapter';
|
|
98
111
|
if (path.startsWith('packages/octane-evals/')) return 'evals';
|
|
99
112
|
if (path.startsWith('packages/octane-mcp-server/')) return 'mcp-server';
|
|
100
113
|
const packageMatch = path.match(/^packages\/([^/]+)\//);
|
|
@@ -104,7 +117,7 @@ export function areaForPath(path) {
|
|
|
104
117
|
if (path.startsWith('benchmarks/')) return 'benchmark';
|
|
105
118
|
if (path.startsWith('website/')) return 'website';
|
|
106
119
|
if (path.startsWith('.rulesync/')) return 'rulesync-source';
|
|
107
|
-
if (path.startsWith('.
|
|
120
|
+
if (path.startsWith('.codex/') || path.startsWith('.claude/')) {
|
|
108
121
|
return 'agent-instructions';
|
|
109
122
|
}
|
|
110
123
|
if (path.startsWith('docs/') || path.endsWith('.md')) return 'docs';
|
|
@@ -166,9 +179,14 @@ export function validationFor(paths, taskKind) {
|
|
|
166
179
|
);
|
|
167
180
|
}
|
|
168
181
|
if (areas.has('deploy-adapter')) {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
182
|
+
for (const path of paths) {
|
|
183
|
+
const match = path.match(/^packages\/(adapter-[^/]+)\//);
|
|
184
|
+
if (match) {
|
|
185
|
+
commands.add(
|
|
186
|
+
`./node_modules/.bin/vitest run packages/${match[1]}/tests --project ${match[1]}`,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
172
190
|
}
|
|
173
191
|
if (
|
|
174
192
|
areas.has('metaframework-core') ||
|
|
@@ -381,7 +399,7 @@ function registerUserTools(server, repoRoot, repoMode) {
|
|
|
381
399
|
{
|
|
382
400
|
title: 'Octane skill',
|
|
383
401
|
description:
|
|
384
|
-
'
|
|
402
|
+
'Fetch an Octane agent skill by name. Call when starting work the skill covers: build-octane-software for engineering gates, bridge-react-package for porting a React library, migrate-react-component for JSX to .tsrx, react-divergences before assuming React behavior, setup-ssr for server rendering.' +
|
|
385
403
|
(repoMode ? ' Repo skills cover octane maintainer workflows.' : ''),
|
|
386
404
|
inputSchema: {
|
|
387
405
|
name: z.enum(Object.keys(skills)),
|
|
@@ -467,12 +485,19 @@ function registerRepoTools(server, repoRoot) {
|
|
|
467
485
|
{
|
|
468
486
|
title: 'Octane project map',
|
|
469
487
|
description:
|
|
470
|
-
'
|
|
488
|
+
'Call when you need Octane repo orientation: what the framework is, which source owns which behavior, the intentional divergences from React, the validation commands, and the full package inventory.',
|
|
471
489
|
inputSchema: {},
|
|
472
490
|
},
|
|
473
491
|
async () => {
|
|
474
|
-
|
|
475
|
-
|
|
492
|
+
// Composed from the two generated sources rather than a hand-written
|
|
493
|
+
// summary, because both are CI-gated: AGENTS.md against .rulesync/rules,
|
|
494
|
+
// and packages.md against the workspace manifests. A third restatement
|
|
495
|
+
// would be the one free to drift.
|
|
496
|
+
const [rootRule, packages] = await Promise.all([
|
|
497
|
+
readFile(resolve(repoRoot, 'AGENTS.md'), 'utf8'),
|
|
498
|
+
readFile(resolve(repoRoot, 'docs/packages.md'), 'utf8'),
|
|
499
|
+
]);
|
|
500
|
+
return text(`${rootRule.trimEnd()}\n\n---\n\n${packages.trimStart()}`);
|
|
476
501
|
},
|
|
477
502
|
);
|
|
478
503
|
|
|
@@ -480,7 +505,8 @@ function registerRepoTools(server, repoRoot) {
|
|
|
480
505
|
'octane_triage_paths',
|
|
481
506
|
{
|
|
482
507
|
title: 'Triage Octane paths',
|
|
483
|
-
description:
|
|
508
|
+
description:
|
|
509
|
+
'Call before editing unfamiliar paths in this monorepo to learn which area owns them.',
|
|
484
510
|
inputSchema: {
|
|
485
511
|
paths: z.array(z.string()).describe('Repository-relative paths'),
|
|
486
512
|
},
|
|
@@ -495,7 +521,8 @@ function registerRepoTools(server, repoRoot) {
|
|
|
495
521
|
'octane_validate_plan',
|
|
496
522
|
{
|
|
497
523
|
title: 'Octane validation plan',
|
|
498
|
-
description:
|
|
524
|
+
description:
|
|
525
|
+
'Call when a change is finished to get the validation commands that cover the paths you touched.',
|
|
499
526
|
inputSchema: {
|
|
500
527
|
paths: z.array(z.string()).default([]).describe('Repository-relative changed paths'),
|
|
501
528
|
taskKind: z
|
|
@@ -525,7 +552,8 @@ function registerRepoTools(server, repoRoot) {
|
|
|
525
552
|
'octane_scaffold_react_port',
|
|
526
553
|
{
|
|
527
554
|
title: 'Scaffold React test port',
|
|
528
|
-
description:
|
|
555
|
+
description:
|
|
556
|
+
'Call when porting a React test file into the conformance suite, returns a triage skeleton of in-scope cases and out-of-scope reasons.',
|
|
529
557
|
inputSchema: {
|
|
530
558
|
reactTestFile: z
|
|
531
559
|
.string()
|
package/src/index.test.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
3
3
|
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
|
|
4
|
-
import { existsSync } from 'node:fs';
|
|
4
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
5
5
|
import { mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises';
|
|
6
6
|
import { tmpdir } from 'node:os';
|
|
7
7
|
import { dirname, join, resolve } from 'node:path';
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
areaForPath,
|
|
11
11
|
BENCHMARK_SUITES,
|
|
12
12
|
BUNDLED_SKILLS,
|
|
13
|
+
REPO_SKILLS,
|
|
13
14
|
createServer,
|
|
14
15
|
engineeringPlanFor,
|
|
15
16
|
isOctaneRepo,
|
|
@@ -30,6 +31,7 @@ describe('@octanejs/mcp-server helpers', () => {
|
|
|
30
31
|
expect(areaForPath('packages/radix/src/index.ts')).toBe('ecosystem-binding');
|
|
31
32
|
expect(areaForPath('packages/octane-mcp-server/src/index.js')).toBe('mcp-server');
|
|
32
33
|
expect(areaForPath('packages/adapter-vercel/src/index.ts')).toBe('deploy-adapter');
|
|
34
|
+
expect(areaForPath('packages/adapter-cloudflare/src/index.js')).toBe('deploy-adapter');
|
|
33
35
|
expect(areaForPath('packages/octane-evals/tools/run.mjs')).toBe('evals');
|
|
34
36
|
expect(areaForPath('website/src/pages/index.tsrx')).toBe('website');
|
|
35
37
|
expect(areaForPath('benchmarks/news/run.mjs')).toBe('benchmark');
|
|
@@ -40,6 +42,7 @@ describe('@octanejs/mcp-server helpers', () => {
|
|
|
40
42
|
const commands = validationFor(
|
|
41
43
|
[
|
|
42
44
|
'packages/adapter-vercel/src/index.ts',
|
|
45
|
+
'packages/adapter-cloudflare/src/index.js',
|
|
43
46
|
'packages/octane-evals/tools/run.mjs',
|
|
44
47
|
'website/src/pages/index.tsrx',
|
|
45
48
|
],
|
|
@@ -49,6 +52,9 @@ describe('@octanejs/mcp-server helpers', () => {
|
|
|
49
52
|
expect(commands).toContain(
|
|
50
53
|
'./node_modules/.bin/vitest run packages/adapter-vercel/tests --project adapter-vercel',
|
|
51
54
|
);
|
|
55
|
+
expect(commands).toContain(
|
|
56
|
+
'./node_modules/.bin/vitest run packages/adapter-cloudflare/tests --project adapter-cloudflare',
|
|
57
|
+
);
|
|
52
58
|
expect(commands).toContain(
|
|
53
59
|
'./node_modules/.bin/vitest run packages/octane-evals/tests --project octane-evals',
|
|
54
60
|
);
|
|
@@ -168,20 +174,50 @@ describe('@octanejs/mcp-server helpers', () => {
|
|
|
168
174
|
}
|
|
169
175
|
});
|
|
170
176
|
|
|
171
|
-
it('
|
|
177
|
+
it('routes to the engineering gates without restating them at initialization', async () => {
|
|
178
|
+
// Initialization instructions are injected into every session, so they stay
|
|
179
|
+
// short and point at the tool. The gates themselves must still be reachable
|
|
180
|
+
//: that is the contract, not any particular wording.
|
|
172
181
|
const server = createServer({ repoRoot: resolve(PACKAGE_ROOT, '../..') });
|
|
173
182
|
const client = new Client({ name: 'octane-mcp-test', version: '1.0.0' });
|
|
174
183
|
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
175
184
|
|
|
176
185
|
try {
|
|
177
186
|
await Promise.all([server.connect(serverTransport), client.connect(clientTransport)]);
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
);
|
|
181
|
-
expect(client.getInstructions()).toContain('establish a relevant baseline before editing');
|
|
187
|
+
const instructions = client.getInstructions();
|
|
188
|
+
expect(instructions).toContain('octane_engineering_plan');
|
|
189
|
+
expect(instructions.length).toBeLessThan(600);
|
|
182
190
|
|
|
183
191
|
const tools = await client.listTools();
|
|
184
192
|
expect(tools.tools.map((tool) => tool.name)).toContain('octane_engineering_plan');
|
|
193
|
+
|
|
194
|
+
const plan = await client.callTool({
|
|
195
|
+
name: 'octane_engineering_plan',
|
|
196
|
+
arguments: { scope: 'framework-core', changeKind: 'performance' },
|
|
197
|
+
});
|
|
198
|
+
const body = plan.content[0].text;
|
|
199
|
+
expect(body).toContain('Identify hot paths and record a relevant baseline before editing.');
|
|
200
|
+
expect(body).toContain('build-octane-software');
|
|
201
|
+
} finally {
|
|
202
|
+
await client.close();
|
|
203
|
+
await server.close();
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('serves the project map from both generated, CI-gated sources', async () => {
|
|
208
|
+
// A hand-written map is the copy free to drift, which is why the old one
|
|
209
|
+
// did. Both halves here are regenerated and checked by CI.
|
|
210
|
+
const server = createServer({ repoRoot: resolve(PACKAGE_ROOT, '../..') });
|
|
211
|
+
const client = new Client({ name: 'octane-mcp-test', version: '1.0.0' });
|
|
212
|
+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
await Promise.all([server.connect(serverTransport), client.connect(clientTransport)]);
|
|
216
|
+
const map = (await client.callTool({ name: 'octane_project_map', arguments: {} })).content[0]
|
|
217
|
+
.text;
|
|
218
|
+
expect(map).toContain('Your React instincts are the main failure mode here');
|
|
219
|
+
expect(map).toContain('Package inventory (generated)');
|
|
220
|
+
expect(map).toContain('framework binding');
|
|
185
221
|
} finally {
|
|
186
222
|
await client.close();
|
|
187
223
|
await server.close();
|
|
@@ -202,6 +238,26 @@ describe('@octanejs/mcp-server helpers', () => {
|
|
|
202
238
|
}
|
|
203
239
|
});
|
|
204
240
|
|
|
241
|
+
it('serves every RuleSync skill, and only skills that exist', async () => {
|
|
242
|
+
// Checking that each mapped path resolves catches a dangling entry but not
|
|
243
|
+
// a missing one, which is how authoring-tsrx was added to .rulesync and
|
|
244
|
+
// stayed unreachable through octane_skill. Compare both directions.
|
|
245
|
+
const repoRoot = resolve(PACKAGE_ROOT, '../..');
|
|
246
|
+
const onDisk = readdirSync(resolve(repoRoot, '.rulesync/skills'), { withFileTypes: true })
|
|
247
|
+
.filter((entry) => entry.isDirectory())
|
|
248
|
+
.map((entry) => entry.name)
|
|
249
|
+
.sort();
|
|
250
|
+
|
|
251
|
+
expect(Object.keys(REPO_SKILLS).sort()).toEqual(onDisk);
|
|
252
|
+
|
|
253
|
+
for (const file of Object.values(REPO_SKILLS)) {
|
|
254
|
+
expect(file.startsWith('.rulesync/skills/')).toBe(true);
|
|
255
|
+
expect(existsSync(resolve(repoRoot, file))).toBe(true);
|
|
256
|
+
const body = await readFile(resolve(repoRoot, file), 'utf8');
|
|
257
|
+
expect(body).toMatch(/^---\n[\s\S]*?\ndescription: /);
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
|
|
205
261
|
it('runs the React port scaffolder wrapper', async () => {
|
|
206
262
|
const repoRoot = await mkdtemp(join(tmpdir(), 'octane-mcp-test-'));
|
|
207
263
|
await mkdir(join(repoRoot, 'scripts'), { recursive: true });
|