@octanejs/mcp-server 0.2.3 → 0.2.6

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/src/index.js CHANGED
@@ -18,6 +18,7 @@ const PACKAGE_ROOT = resolve(dirname(__filename), '..');
18
18
  // workflows: triage, PRs, core changes).
19
19
  export const BUNDLED_SKILLS = {
20
20
  'bridge-react-package': 'skills/bridge-react-package.md',
21
+ 'build-octane-software': 'skills/build-octane-software.md',
21
22
  'migrate-react-component': 'skills/migrate-react-component.md',
22
23
  'react-divergences': 'skills/react-divergences.md',
23
24
  'setup-ssr': 'skills/setup-ssr.md',
@@ -33,16 +34,50 @@ export const REPO_SKILLS = {
33
34
  triage: '.ai/skills/triage.md',
34
35
  };
35
36
 
36
- const BENCHMARKS = {
37
- dbmon: '@benchmarks/dbmon',
38
- 'js-framework': 'octane-js-framework-benchmarks',
39
- news: '@benchmarks/news',
40
- 'recursive-context': '@benchmarks/recursive-context',
41
- 'signal-favoring': '@benchmarks/signal-favoring',
42
- };
37
+ // Suite names from the unified runner manifest (`SUITES` in
38
+ // benchmarks/bench.mjs; `node benchmarks/bench.mjs --list` prints the same
39
+ // set). index.test.js keeps this list in sync with the runner.
40
+ export const BENCHMARK_SUITES = [
41
+ 'js-framework',
42
+ 'js-framework-reorder',
43
+ 'todomvc',
44
+ 'weather-app',
45
+ 'weather-app-lighthouse',
46
+ 'chat-stream',
47
+ 'dbmon',
48
+ 'recursive-context',
49
+ 'signal-favoring',
50
+ 'news',
51
+ 'effectful-list',
52
+ 'memo-wall',
53
+ 'portal-swarm',
54
+ 'react-hosted-islands',
55
+ 'ssr-throughput',
56
+ 'streaming-ssr',
57
+ 'ssr-http',
58
+ 'ssr-workerd',
59
+ 'tanstack-start',
60
+ 'dbmon-deopt',
61
+ 'js-framework-deopt',
62
+ 'async-waterfall',
63
+ 'async-composition',
64
+ 'lynx-list',
65
+ 'codegen-size',
66
+ 'bundle-size',
67
+ 'three-renderer',
68
+ 'three-bundle-size',
69
+ ];
43
70
 
44
71
  const DEFAULT_TIMEOUT_MS = 120_000;
45
72
 
73
+ export function instructionsFor(repoMode) {
74
+ const common =
75
+ 'Before creating or materially changing Octane software, call octane_engineering_plan and load the build-octane-software skill. Treat its correctness, performance evidence, and adversarial self-review gates as required. Load the task-specific migration, binding, divergence, or SSR skill in addition when relevant. Do not claim a performance improvement without comparable measurements.';
76
+ return repoMode
77
+ ? `${common} For Octane framework-fundamental work, also load octane-core-extend and performance-audit, establish a relevant baseline before editing, and use octane_validate_plan for the final changed paths.`
78
+ : common;
79
+ }
80
+
46
81
  export function text(content) {
47
82
  return { content: [{ type: 'text', text: content }] };
48
83
  }
@@ -63,12 +98,15 @@ export function areaForPath(path) {
63
98
  if (path.startsWith('packages/rspack-plugin-octane/')) return 'rspack-plugin';
64
99
  if (path.startsWith('packages/rsbuild-plugin-octane/')) return 'rsbuild-plugin';
65
100
  if (path.startsWith('packages/vite-plugin-octane/')) return 'vite-plugin';
101
+ if (/^packages\/adapter-[^/]+\//.test(path)) return 'deploy-adapter';
102
+ if (path.startsWith('packages/octane-evals/')) return 'evals';
66
103
  if (path.startsWith('packages/octane-mcp-server/')) return 'mcp-server';
67
104
  const packageMatch = path.match(/^packages\/([^/]+)\//);
68
105
  if (packageMatch && KNOWN_BINDING_PACKAGE_DIRS.has(packageMatch[1])) {
69
106
  return 'ecosystem-binding';
70
107
  }
71
108
  if (path.startsWith('benchmarks/')) return 'benchmark';
109
+ if (path.startsWith('website/')) return 'website';
72
110
  if (path.startsWith('.rulesync/')) return 'rulesync-source';
73
111
  if (path.startsWith('.ai/') || path.startsWith('.codex/') || path.startsWith('.claude/')) {
74
112
  return 'agent-instructions';
@@ -105,15 +143,54 @@ export function validationFor(paths, taskKind) {
105
143
  './node_modules/.bin/vitest run packages/octane-mcp-server --project octane-mcp-server',
106
144
  );
107
145
  }
146
+ if (areas.has('evals')) {
147
+ commands.add(
148
+ './node_modules/.bin/vitest run packages/octane-evals/tests --project octane-evals',
149
+ );
150
+ }
151
+ if (areas.has('website')) {
152
+ commands.add('./node_modules/.bin/vitest run website/tests --project website');
153
+ }
154
+ if (areas.has('metaframework-core')) {
155
+ commands.add('./node_modules/.bin/vitest run packages/app-core/tests --project app-core');
156
+ }
157
+ if (areas.has('rspack-plugin')) {
158
+ commands.add(
159
+ './node_modules/.bin/vitest run packages/rspack-plugin-octane/tests --project rspack-plugin',
160
+ );
161
+ }
162
+ if (areas.has('rsbuild-plugin')) {
163
+ commands.add(
164
+ './node_modules/.bin/vitest run packages/rsbuild-plugin-octane/tests --project rsbuild-plugin',
165
+ );
166
+ }
167
+ if (areas.has('vite-plugin')) {
168
+ commands.add(
169
+ './node_modules/.bin/vitest run packages/vite-plugin-octane/tests --project vite-plugin',
170
+ );
171
+ }
172
+ if (areas.has('deploy-adapter')) {
173
+ for (const path of paths) {
174
+ const match = path.match(/^packages\/(adapter-[^/]+)\//);
175
+ if (match) {
176
+ commands.add(
177
+ `./node_modules/.bin/vitest run packages/${match[1]}/tests --project ${match[1]}`,
178
+ );
179
+ }
180
+ }
181
+ }
108
182
  if (
109
183
  areas.has('metaframework-core') ||
110
184
  areas.has('rspack-plugin') ||
111
185
  areas.has('rsbuild-plugin') ||
112
- areas.has('vite-plugin')
186
+ areas.has('vite-plugin') ||
187
+ areas.has('deploy-adapter')
113
188
  ) {
114
189
  commands.add('pnpm typecheck');
115
190
  }
116
- if (areas.has('benchmark') || taskKind === 'performance') commands.add('pnpm bench');
191
+ if (areas.has('benchmark') || taskKind === 'performance' || taskKind === 'core') {
192
+ commands.add('node benchmarks/bench.mjs --quick --ratios');
193
+ }
117
194
  if (taskKind === 'api' || taskKind === 'core' || taskKind === 'package')
118
195
  commands.add('pnpm typecheck');
119
196
  commands.add('pnpm format:check');
@@ -121,6 +198,74 @@ export function validationFor(paths, taskKind) {
121
198
  return [...commands];
122
199
  }
123
200
 
201
+ export function engineeringPlanFor(input, repoMode = false) {
202
+ const paths = input.paths ?? [];
203
+ const scope = input.scope;
204
+ const changeKind = input.changeKind ?? 'feature';
205
+ const performanceSensitive =
206
+ scope === 'framework-core' ||
207
+ changeKind === 'performance' ||
208
+ input.performanceSensitive === true;
209
+ const correctnessGate =
210
+ changeKind === 'bug'
211
+ ? 'Reproduce the bug through a realistic public boundary and verify that the test has a credible pre-fix failure.'
212
+ : 'Protect the new or changed behavior through a realistic public boundary with an assertion that would fail if the contract were absent.';
213
+ const plan = {
214
+ scope,
215
+ changeKind,
216
+ performanceSensitive,
217
+ areas: paths.map((path) => ({ path, area: areaForPath(path) })),
218
+ requiredSkills: ['build-octane-software'],
219
+ gates: {
220
+ contract: [
221
+ 'State the consumer-observable behavior, invariants, failure states, and supported execution modes.',
222
+ 'Inspect current source, callers, tests, configuration, and documented Octane divergences before editing.',
223
+ ],
224
+ correctness: [
225
+ correctnessGate,
226
+ 'Exercise applicable empty, large, repeated, nested, error, abort, cleanup, production, SSR, and hydration cases.',
227
+ ],
228
+ performance: performanceSensitive
229
+ ? [
230
+ 'Identify hot paths and record a relevant baseline before editing.',
231
+ 'Compare baseline and candidate with the same environment, warmup, iterations, and semantic controls.',
232
+ 'Inspect allocations, retained memory, DOM work, compiler and generated-code cost, SSR/hydration work, and bundle size as applicable.',
233
+ 'Do not claim improvement when the delta is within noise or no trustworthy measurement exists.',
234
+ ]
235
+ : [
236
+ 'Check that the change does not add unnecessary reactive work, retained state, dependencies, or common-path cost.',
237
+ 'Measure the important user journey when the change can materially affect it.',
238
+ ],
239
+ selfReview: [
240
+ 'Read the complete diff adversarially and try to falsify the solution with boundary and lifecycle cases.',
241
+ 'Trace new state and allocations through invalidation, cleanup, errors, and aborts.',
242
+ 'Compare with a simpler design and remove complexity that does not justify its permanent cost.',
243
+ 'Resolve findings, rerun affected checks, and repeat the review on the final diff.',
244
+ ],
245
+ handoff: [
246
+ 'Report the protected contract, validation commands and results, and applicable baseline/candidate measurements.',
247
+ 'Report improvements made during self-review, untested modes, inconclusive evidence, and residual risk.',
248
+ ],
249
+ },
250
+ };
251
+
252
+ if (scope === 'framework-core' && repoMode) {
253
+ plan.requiredSkills.push('octane-core-extend', 'performance-audit');
254
+ }
255
+ if (scope === 'framework-core' && !repoMode) {
256
+ plan.blockingConditions = [
257
+ '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.',
258
+ ];
259
+ }
260
+ if (repoMode) {
261
+ const taskKind =
262
+ scope === 'framework-core' ? 'core' : performanceSensitive ? 'performance' : changeKind;
263
+ plan.validationCommands = validationFor(paths, taskKind);
264
+ }
265
+
266
+ return plan;
267
+ }
268
+
124
269
  export function runCommand(command, args, options = {}) {
125
270
  const cwd = options.cwd ?? process.cwd();
126
271
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
@@ -182,15 +327,14 @@ export async function scaffoldReactPort(repoRoot, input) {
182
327
  }
183
328
 
184
329
  export async function runBenchmark(repoRoot, input) {
185
- const args =
186
- input.benchmark === 'all'
187
- ? ['bench']
188
- : ['--filter', BENCHMARKS[input.benchmark], 'run', 'bench'];
189
- const result = await runCommand('pnpm', args, {
330
+ const args = ['benchmarks/bench.mjs'];
331
+ if (input.benchmark && input.benchmark !== 'all') args.push(input.benchmark);
332
+ if (input.quick) args.push('--quick');
333
+ const result = await runCommand(process.execPath, args, {
190
334
  cwd: repoRoot,
191
- timeoutMs: input.timeoutMs ?? 300_000,
335
+ timeoutMs: input.timeoutMs ?? 600_000,
192
336
  });
193
- return { command: ['pnpm', ...args], benchmark: input.benchmark, ...result };
337
+ return { command: [process.execPath, ...args], benchmark: input.benchmark, ...result };
194
338
  }
195
339
 
196
340
  export async function issueContext(repoRoot, input) {
@@ -246,7 +390,7 @@ function registerUserTools(server, repoRoot, repoMode) {
246
390
  {
247
391
  title: 'Octane skill',
248
392
  description:
249
- 'Return an Octane agent skill by name. Bundled skills cover working WITH octane in any project: bridging React packages, migrating React components to .tsrx, intentional React divergences, and SSR setup.' +
393
+ 'Return an Octane agent skill by name. Load build-octane-software before creating or materially changing Octane code; other bundled skills cover React package bridges, component migration, intentional divergences, and SSR setup.' +
250
394
  (repoMode ? ' Repo skills cover octane maintainer workflows.' : ''),
251
395
  inputSchema: {
252
396
  name: z.enum(Object.keys(skills)),
@@ -259,12 +403,33 @@ function registerUserTools(server, repoRoot, repoMode) {
259
403
  },
260
404
  );
261
405
 
406
+ server.registerTool(
407
+ 'octane_engineering_plan',
408
+ {
409
+ title: 'Plan high-quality Octane engineering work',
410
+ description:
411
+ 'Return required correctness, performance-evidence, adversarial self-review, and handoff gates before creating or materially changing Octane software. Framework-core scope always requires baseline/candidate performance evidence and the maintainer core/performance skills.',
412
+ inputSchema: {
413
+ scope: z.enum(['application', 'library', 'framework-core']),
414
+ changeKind: z
415
+ .enum(['bug', 'feature', 'performance', 'refactor', 'api', 'docs', 'test', 'unknown'])
416
+ .default('feature'),
417
+ paths: z.array(z.string()).default([]).describe('Repository-relative changed paths.'),
418
+ performanceSensitive: z
419
+ .boolean()
420
+ .optional()
421
+ .describe('Force performance evidence for application or library work.'),
422
+ },
423
+ },
424
+ async (input) => text(JSON.stringify(engineeringPlanFor(input, repoMode), null, 2)),
425
+ );
426
+
262
427
  server.registerTool(
263
428
  'octane_bridge_react_package',
264
429
  {
265
430
  title: 'Bridge a React package to Octane',
266
431
  description:
267
- 'Scan a React package (from node_modules by name, or any source directory by path) for React API usage and return an Octane compatibility report: which APIs map 1:1, which need rewrites (forwardRef, useDebugValue, lazy, class components), whether a framework-agnostic core can be reused verbatim, whether an official @octanejs binding already exists, and a step-by-step bridge plan. Follow up with the bridge-react-package skill for the full workflow.',
432
+ 'Scan a React package (from node_modules by name, or any source directory by path) for React API usage and return an Octane compatibility report: which APIs map 1:1, which need rewrites (forwardRef, class components, React-style text-host onChange, react-dom/server imports), whether a framework-agnostic core can be reused verbatim, whether an official @octanejs binding already exists, and a step-by-step bridge plan. Follow up with the bridge-react-package skill for the full workflow.',
268
433
  inputSchema: {
269
434
  package: z
270
435
  .string()
@@ -389,9 +554,10 @@ function registerRepoTools(server, repoRoot) {
389
554
  {
390
555
  title: 'Run Octane benchmark',
391
556
  description:
392
- 'Run one of the known Octane benchmark workspaces, or all benchmarks via pnpm bench.',
557
+ 'Run benchmark suites through the unified runner (node benchmarks/bench.mjs): one manifest suite by name, or every suite with "all". Set quick for the reduced-iteration smoke pass.',
393
558
  inputSchema: {
394
- benchmark: z.enum(['all', ...Object.keys(BENCHMARKS)]).default('all'),
559
+ benchmark: z.enum(['all', ...BENCHMARK_SUITES]).default('all'),
560
+ quick: z.boolean().default(false),
395
561
  timeoutMs: z.number().int().positive().optional(),
396
562
  },
397
563
  },
@@ -418,7 +584,10 @@ function registerRepoTools(server, repoRoot) {
418
584
  export function createServer(options = {}) {
419
585
  const repoRoot = resolve(options.repoRoot || process.env.OCTANE_REPO_ROOT || process.cwd());
420
586
  const repoMode = isOctaneRepo(repoRoot);
421
- const server = new McpServer({ name: 'octane', version: '0.2.0' });
587
+ const server = new McpServer(
588
+ { name: 'octane', version: '0.2.0' },
589
+ { instructions: instructionsFor(repoMode) },
590
+ );
422
591
  registerUserTools(server, repoRoot, repoMode);
423
592
  if (repoMode) registerRepoTools(server, repoRoot);
424
593
  return server;
package/src/index.test.js CHANGED
@@ -1,4 +1,6 @@
1
1
  import { describe, expect, it } from 'vitest';
2
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
3
+ import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
2
4
  import { existsSync } from 'node:fs';
3
5
  import { mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises';
4
6
  import { tmpdir } from 'node:os';
@@ -6,8 +8,12 @@ import { dirname, join, resolve } from 'node:path';
6
8
  import { fileURLToPath } from 'node:url';
7
9
  import {
8
10
  areaForPath,
11
+ BENCHMARK_SUITES,
9
12
  BUNDLED_SKILLS,
13
+ createServer,
14
+ engineeringPlanFor,
10
15
  isOctaneRepo,
16
+ runCommand,
11
17
  scaffoldReactPort,
12
18
  validationFor,
13
19
  } from './index.js';
@@ -23,10 +29,54 @@ describe('@octanejs/mcp-server helpers', () => {
23
29
  expect(areaForPath('packages/zustand/src/index.ts')).toBe('ecosystem-binding');
24
30
  expect(areaForPath('packages/radix/src/index.ts')).toBe('ecosystem-binding');
25
31
  expect(areaForPath('packages/octane-mcp-server/src/index.js')).toBe('mcp-server');
32
+ expect(areaForPath('packages/adapter-vercel/src/index.ts')).toBe('deploy-adapter');
33
+ expect(areaForPath('packages/adapter-cloudflare/src/index.js')).toBe('deploy-adapter');
34
+ expect(areaForPath('packages/octane-evals/tools/run.mjs')).toBe('evals');
35
+ expect(areaForPath('website/src/pages/index.tsrx')).toBe('website');
26
36
  expect(areaForPath('benchmarks/news/run.mjs')).toBe('benchmark');
27
37
  expect(areaForPath('.rulesync/rules/project.md')).toBe('rulesync-source');
28
38
  });
29
39
 
40
+ it('recommends the adapter, evals, and website test projects', () => {
41
+ const commands = validationFor(
42
+ [
43
+ 'packages/adapter-vercel/src/index.ts',
44
+ 'packages/adapter-cloudflare/src/index.js',
45
+ 'packages/octane-evals/tools/run.mjs',
46
+ 'website/src/pages/index.tsrx',
47
+ ],
48
+ 'feature',
49
+ );
50
+
51
+ expect(commands).toContain(
52
+ './node_modules/.bin/vitest run packages/adapter-vercel/tests --project adapter-vercel',
53
+ );
54
+ expect(commands).toContain(
55
+ './node_modules/.bin/vitest run packages/adapter-cloudflare/tests --project adapter-cloudflare',
56
+ );
57
+ expect(commands).toContain(
58
+ './node_modules/.bin/vitest run packages/octane-evals/tests --project octane-evals',
59
+ );
60
+ expect(commands).toContain('./node_modules/.bin/vitest run website/tests --project website');
61
+ expect(commands).toContain('pnpm typecheck');
62
+ });
63
+
64
+ it('keeps the benchmark suite list in sync with the unified runner manifest', async () => {
65
+ // BENCHMARK_SUITES is hand-maintained in index.js; the runner manifest in
66
+ // benchmarks/bench.mjs is the source of truth. --list prints one suite
67
+ // name per line, in manifest order.
68
+ const repoRoot = resolve(PACKAGE_ROOT, '../..');
69
+ const result = await runCommand(process.execPath, ['benchmarks/bench.mjs', '--list'], {
70
+ cwd: repoRoot,
71
+ });
72
+ expect(result.code).toBe(0);
73
+ const suites = result.stdout
74
+ .split('\n')
75
+ .map((line) => line.trim())
76
+ .filter((line) => line && line !== 'Available suites:');
77
+ expect(suites).toEqual(BENCHMARK_SUITES);
78
+ });
79
+
30
80
  it('classifies every maintained binding and recommends its test project', () => {
31
81
  const paths = [...KNOWN_BINDING_PACKAGE_DIRS].map(
32
82
  (directory) => `packages/${directory}/src/index.ts`,
@@ -63,9 +113,86 @@ describe('@octanejs/mcp-server helpers', () => {
63
113
  './node_modules/.bin/vitest run packages/radix/tests --project radix',
64
114
  );
65
115
  expect(commands).toContain('pnpm typecheck');
116
+ expect(commands).toContain('node benchmarks/bench.mjs --quick --ratios');
66
117
  expect(commands).toContain('pnpm format:check');
67
118
  });
68
119
 
120
+ it('requires performance evidence and adversarial review for framework fundamentals', () => {
121
+ const plan = engineeringPlanFor(
122
+ {
123
+ scope: 'framework-core',
124
+ changeKind: 'refactor',
125
+ paths: ['packages/octane/src/runtime.ts'],
126
+ },
127
+ true,
128
+ );
129
+
130
+ expect(plan.performanceSensitive).toBe(true);
131
+ expect(plan.requiredSkills).toEqual([
132
+ 'build-octane-software',
133
+ 'octane-core-extend',
134
+ 'performance-audit',
135
+ ]);
136
+ expect(plan.gates.performance).toContain(
137
+ 'Identify hot paths and record a relevant baseline before editing.',
138
+ );
139
+ expect(plan.gates.selfReview).toContain(
140
+ 'Resolve findings, rerun affected checks, and repeat the review on the final diff.',
141
+ );
142
+ expect(plan.validationCommands).toContain('node benchmarks/bench.mjs --quick --ratios');
143
+ });
144
+
145
+ it('blocks framework-core plans when maintainer tools are unavailable', () => {
146
+ const plan = engineeringPlanFor({ scope: 'framework-core', changeKind: 'bug' });
147
+
148
+ expect(plan.requiredSkills).toEqual(['build-octane-software']);
149
+ expect(plan.blockingConditions).toContain(
150
+ '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.',
151
+ );
152
+ expect(plan.gates.correctness).toContain(
153
+ 'Reproduce the bug through a realistic public boundary and verify that the test has a credible pre-fix failure.',
154
+ );
155
+ });
156
+
157
+ it('keeps performance gates and validation commands aligned', () => {
158
+ const performancePlan = engineeringPlanFor(
159
+ { scope: 'application', changeKind: 'performance' },
160
+ true,
161
+ );
162
+ const flaggedPlan = engineeringPlanFor(
163
+ { scope: 'library', changeKind: 'feature', performanceSensitive: true },
164
+ true,
165
+ );
166
+
167
+ for (const plan of [performancePlan, flaggedPlan]) {
168
+ expect(plan.performanceSensitive).toBe(true);
169
+ expect(plan.gates.performance).toContain(
170
+ 'Identify hot paths and record a relevant baseline before editing.',
171
+ );
172
+ expect(plan.validationCommands).toContain('node benchmarks/bench.mjs --quick --ratios');
173
+ }
174
+ });
175
+
176
+ it('advertises the engineering gates during MCP initialization', async () => {
177
+ const server = createServer({ repoRoot: resolve(PACKAGE_ROOT, '../..') });
178
+ const client = new Client({ name: 'octane-mcp-test', version: '1.0.0' });
179
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
180
+
181
+ try {
182
+ await Promise.all([server.connect(serverTransport), client.connect(clientTransport)]);
183
+ expect(client.getInstructions()).toContain(
184
+ 'Before creating or materially changing Octane software',
185
+ );
186
+ expect(client.getInstructions()).toContain('establish a relevant baseline before editing');
187
+
188
+ const tools = await client.listTools();
189
+ expect(tools.tools.map((tool) => tool.name)).toContain('octane_engineering_plan');
190
+ } finally {
191
+ await client.close();
192
+ await server.close();
193
+ }
194
+ });
195
+
69
196
  it('detects the octane monorepo for repo-mode tools', async () => {
70
197
  expect(isOctaneRepo(resolve(PACKAGE_ROOT, '../..'))).toBe(true);
71
198
  const elsewhere = await mkdtemp(join(tmpdir(), 'octane-mcp-test-'));