@specverse/engines 4.3.5 → 5.0.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.
Files changed (64) hide show
  1. package/assets/examples/10-api/README.md +3 -3
  2. package/assets/prompts/core/README.md +1 -1
  3. package/dist/inference/core/rule-engine.d.ts +0 -12
  4. package/dist/inference/core/rule-engine.d.ts.map +1 -1
  5. package/dist/inference/core/rule-engine.js +99 -968
  6. package/dist/inference/core/rule-engine.js.map +1 -1
  7. package/dist/inference/core/template-helpers.d.ts +56 -0
  8. package/dist/inference/core/template-helpers.d.ts.map +1 -0
  9. package/dist/inference/core/template-helpers.js +87 -0
  10. package/dist/inference/core/template-helpers.js.map +1 -0
  11. package/dist/inference/logical/generators/service-generator.d.ts.map +1 -1
  12. package/dist/inference/logical/generators/service-generator.js +0 -4
  13. package/dist/inference/logical/generators/service-generator.js.map +1 -1
  14. package/dist/libs/instance-factories/cli/templates/commander/command-generator.js +27 -5
  15. package/dist/libs/instance-factories/tools/README.md +1 -1
  16. package/dist/libs/instance-factories/tools/mcp.yaml +1 -1
  17. package/dist/libs/instance-factories/tools/templates/mcp/mcp-server-generator.js +336 -116
  18. package/dist/libs/instance-factories/tools/templates/vscode/vscode-extension-generator.js +172 -8
  19. package/dist/libs/instance-factories/tools/vscode.yaml +1 -1
  20. package/libs/instance-factories/cli/templates/commander/command-generator.ts +27 -5
  21. package/libs/instance-factories/tools/README.md +1 -1
  22. package/libs/instance-factories/tools/mcp.yaml +1 -1
  23. package/libs/instance-factories/tools/templates/mcp/mcp-server-generator.ts +386 -141
  24. package/libs/instance-factories/tools/templates/vscode/static/extension.ts +9 -2
  25. package/libs/instance-factories/tools/templates/vscode/vscode-extension-generator.ts +246 -10
  26. package/libs/instance-factories/tools/vscode.yaml +1 -1
  27. package/package.json +5 -4
  28. package/libs/instance-factories/tools/templates/mcp/static/docs/DEPLOYMENT_GUIDE.md +0 -630
  29. package/libs/instance-factories/tools/templates/mcp/static/docs/HYBRID_RESOURCE_SYSTEM.md +0 -330
  30. package/libs/instance-factories/tools/templates/mcp/static/docs/deployments/EXTENSION_DEPLOYMENT.md +0 -552
  31. package/libs/instance-factories/tools/templates/mcp/static/docs/deployments/LOCAL_DEPLOYMENT.md +0 -164
  32. package/libs/instance-factories/tools/templates/mcp/static/docs/deployments/WEB_DEPLOYMENT.md +0 -247
  33. package/libs/instance-factories/tools/templates/mcp/static/package.json +0 -94
  34. package/libs/instance-factories/tools/templates/mcp/static/scripts/build-enterprise.js +0 -284
  35. package/libs/instance-factories/tools/templates/mcp/static/scripts/build-extension.js +0 -139
  36. package/libs/instance-factories/tools/templates/mcp/static/scripts/build-local.js +0 -74
  37. package/libs/instance-factories/tools/templates/mcp/static/scripts/build-web.js +0 -156
  38. package/libs/instance-factories/tools/templates/mcp/static/scripts/copy-canonical-files.js +0 -41
  39. package/libs/instance-factories/tools/templates/mcp/static/scripts/test-deployments.js +0 -259
  40. package/libs/instance-factories/tools/templates/mcp/static/scripts/test-hybrid-resources.js +0 -231
  41. package/libs/instance-factories/tools/templates/mcp/static/scripts/test-hybrid-simple.js +0 -196
  42. package/libs/instance-factories/tools/templates/mcp/static/src/controllers/MCPServerController.ts +0 -293
  43. package/libs/instance-factories/tools/templates/mcp/static/src/events/EventEmitter.ts +0 -90
  44. package/libs/instance-factories/tools/templates/mcp/static/src/index.ts +0 -24
  45. package/libs/instance-factories/tools/templates/mcp/static/src/interfaces/ResourceProvider.ts +0 -15
  46. package/libs/instance-factories/tools/templates/mcp/static/src/models/LibrarySuggestion.ts +0 -106
  47. package/libs/instance-factories/tools/templates/mcp/static/src/models/SpecVerseResource.ts +0 -75
  48. package/libs/instance-factories/tools/templates/mcp/static/src/server/mcp-server.ts +0 -239
  49. package/libs/instance-factories/tools/templates/mcp/static/src/services/CLIProxyService.ts +0 -1501
  50. package/libs/instance-factories/tools/templates/mcp/static/src/services/EmbeddedResourcesAdapter.ts +0 -211
  51. package/libs/instance-factories/tools/templates/mcp/static/src/services/EntityModuleService.ts +0 -308
  52. package/libs/instance-factories/tools/templates/mcp/static/src/services/HybridResourcesProvider.ts +0 -210
  53. package/libs/instance-factories/tools/templates/mcp/static/src/services/LibraryToolsService.ts +0 -356
  54. package/libs/instance-factories/tools/templates/mcp/static/src/services/OrchestratorBridge.ts +0 -522
  55. package/libs/instance-factories/tools/templates/mcp/static/src/services/OrchestratorToolsService.ts +0 -530
  56. package/libs/instance-factories/tools/templates/mcp/static/src/services/PromptToolsService.ts +0 -594
  57. package/libs/instance-factories/tools/templates/mcp/static/src/services/ResourcesProviderService.ts +0 -170
  58. package/libs/instance-factories/tools/templates/mcp/static/src/tests/unit/CLIProxyService.init.test.ts +0 -544
  59. package/libs/instance-factories/tools/templates/mcp/static/src/tests/unit/CLIProxyService.test.ts +0 -189
  60. package/libs/instance-factories/tools/templates/mcp/static/src/tests/unit/ResourcesProviderService.test.ts +0 -89
  61. package/libs/instance-factories/tools/templates/mcp/static/src/types/index.ts +0 -110
  62. package/libs/instance-factories/tools/templates/mcp/static/tsconfig.json +0 -28
  63. package/libs/instance-factories/tools/templates/vscode/static/schemas/specverse-v3-schema.json +0 -4279
  64. /package/libs/instance-factories/tools/templates/vscode/static/themes/{specverse-complete-theme.json → specverse-dark-theme.json} +0 -0
@@ -1,190 +1,435 @@
1
1
  /**
2
- * MCP Server Generator
2
+ * MCP Server Generator (lean)
3
3
  *
4
- * Generates a Model Context Protocol server from the SpecVerse spec.
5
- * Ships the core server framework as static assets, generates tool/resource
6
- * registration from the spec's ToolsSupport and CLI components.
4
+ * Generates a Model Context Protocol server fully from the SpecVerse spec.
5
+ *
6
+ * Design: the MCP server is a thin wrapper around the specverse CLI. Every
7
+ * CLI command declared in the spec becomes an invocable MCP tool; the live
8
+ * JSON schema + user guide from @specverse/entities are served as MCP
9
+ * resources. No static framework to maintain — everything is emitted from
10
+ * the spec so a new CLI command auto-appears as a new MCP tool on the next
11
+ * realize.
7
12
  */
8
13
 
9
14
  import type { TemplateContext } from '@specverse/types';
10
- import { existsSync, mkdirSync, writeFileSync, readdirSync, statSync, copyFileSync } from 'fs';
11
- import { join, dirname } from 'path';
12
- import { fileURLToPath } from 'url';
13
-
14
- const __generatorDir = dirname(fileURLToPath(import.meta.url));
15
+ import { existsSync, mkdirSync, writeFileSync } from 'fs';
16
+ import { join } from 'path';
15
17
 
16
18
  export default function generateMCPServer(context: TemplateContext): string {
17
19
  const { spec, outputDir } = context;
18
20
 
19
21
  const mcpDir = join(outputDir || '.', 'tools', 'specverse-mcp');
20
- if (!existsSync(mcpDir)) mkdirSync(mcpDir, { recursive: true });
21
-
22
- // 1. Copy static MCP server framework
23
- // Check both source location (libs/) and compiled location (dist/libs/)
24
- let staticDir = join(__generatorDir, 'static');
25
- if (!existsSync(staticDir)) {
26
- // Compiled JS is in dist/libs/ static assets are in libs/
27
- staticDir = __generatorDir.replace('/dist/libs/', '/libs/');
28
- staticDir = join(staticDir, 'static');
29
- }
30
- if (existsSync(staticDir)) {
31
- copyRecursive(staticDir, mcpDir);
32
- } else {
33
- console.warn(`[MCP Generator] Static assets not found at ${staticDir}`);
34
- }
22
+ const srcDir = join(mcpDir, 'src');
23
+ if (!existsSync(srcDir)) mkdirSync(srcDir, { recursive: true });
24
+
25
+ const distribution = extractMCPDistribution(spec);
26
+ const version = distribution?.version || spec?.metadata?.version || spec?.version || '5.0.0';
27
+ const description = distribution?.description
28
+ || 'SpecVerse MCP server exposes the specverse CLI as MCP tools and the live spec schema + docs as MCP resources.';
29
+ const displayName = distribution?.displayName || 'SpecVerse MCP';
35
30
 
36
- // 2. Generate tool registry from spec
37
- const tools = extractMCPTools(spec);
38
- const resources = extractMCPResources(spec);
39
- const cliCommands = extractCLICommands(spec);
31
+ const cliCommands = extractCLITools(spec);
40
32
 
41
- // 3. Generate a spec-driven registration file
42
- const registryCode = generateToolRegistry(tools, resources, cliCommands);
43
- const registryDir = join(mcpDir, 'src', 'generated');
44
- if (!existsSync(registryDir)) mkdirSync(registryDir, { recursive: true });
45
- writeFileSync(join(registryDir, 'spec-registry.ts'), registryCode);
33
+ writeFileSync(join(mcpDir, 'package.json'), generatePackageJson(version, description));
34
+ writeFileSync(join(mcpDir, 'tsconfig.json'), generateTsconfig());
35
+ writeFileSync(join(srcDir, 'server.ts'), generateServer(displayName, version));
36
+ writeFileSync(join(srcDir, 'cli-runner.ts'), generateCliRunner());
37
+ writeFileSync(join(srcDir, 'resources.ts'), generateResources());
38
+ writeFileSync(join(srcDir, 'tools.ts'), generateTools(cliCommands));
46
39
 
47
- return `MCP server generated in: ${mcpDir}\n ${tools.length} tools, ${resources.length} resources, ${cliCommands.length} CLI commands`;
40
+ return `MCP server generated in: ${mcpDir}\n ${cliCommands.length} tools (one per CLI command), 2 resources (schema + docs)`;
48
41
  }
49
42
 
50
- function extractMCPTools(spec: any): Array<{ name: string; description: string; isEntityTool: boolean }> {
51
- const tools: Array<{ name: string; description: string; isEntityTool: boolean }> = [];
52
- const components = spec?.components || {};
53
- const componentList = Array.isArray(components) ? components : Object.values(components);
43
+ // ─── spec extraction ───────────────────────────────────────────────────────
54
44
 
55
- // Also check top-level services (AI-optimized spec format)
56
- if (spec?.services && !Array.isArray(components)) {
57
- componentList.push({ services: spec.services, models: spec.models } as any);
58
- }
45
+ /**
46
+ * Flatten the spec's CLI commands (components[*].commands[*].subcommands[*])
47
+ * into one entry per invocable leaf. Each becomes an MCP tool.
48
+ *
49
+ * - `validate <file>` → name `validate`, cliArgs `['validate']`
50
+ * - `gen diagrams <file>` → name `gen_diagrams`, cliArgs `['gen', 'diagrams']`
51
+ *
52
+ * Arguments/flags declared in the spec become the tool's JSON-Schema input.
53
+ */
54
+ function extractCLITools(spec: any): CLITool[] {
55
+ const out: CLITool[] = [];
56
+ const components = spec?.components || {};
57
+ const componentList = Array.isArray(components)
58
+ ? components
59
+ : Object.entries(components).map(([name, data]) => ({ name, ...(data as any) }));
59
60
 
60
- for (const component of componentList) {
61
- const comp = component as any;
62
-
63
- // Extract from services — each service operation becomes an MCP tool
64
- const services = comp?.services;
65
- if (services) {
66
- const serviceList = Array.isArray(services) ? services : Object.entries(services).map(([n, d]) => ({ name: n, ...(d as any) }));
67
- for (const service of serviceList) {
68
- const operations = service.operations;
69
- if (operations) {
70
- const opEntries = Array.isArray(operations)
71
- ? operations
72
- : Object.entries(operations).map(([n, d]) => ({ name: n, ...(d as any) }));
73
- for (const op of opEntries) {
74
- tools.push({
75
- name: `${service.name.replace(/Service$/, '').toLowerCase()}-${op.name}`,
76
- description: op.description || `${op.name} via ${service.name}`,
77
- isEntityTool: false,
78
- });
79
- }
80
- }
81
- }
82
- }
61
+ for (const comp of componentList) {
62
+ const cliCommands = (comp as any)?.commands;
63
+ if (!cliCommands) continue;
83
64
 
84
- // Extract from model behaviors each behavior becomes an MCP tool
85
- const models = comp?.models;
86
- if (models) {
87
- const modelList = Array.isArray(models) ? models : Object.entries(models).map(([n, d]) => ({ name: n, ...(d as any) }));
88
- for (const model of modelList) {
89
- const behaviors = model.behaviors;
90
- if (behaviors) {
91
- const behaviorEntries = typeof behaviors === 'object' && !Array.isArray(behaviors)
92
- ? Object.entries(behaviors).map(([n, d]) => ({ name: n, ...(d as any) }))
93
- : [];
94
- for (const behavior of behaviorEntries) {
95
- tools.push({
96
- name: `${model.name.toLowerCase()}-${behavior.name}`,
97
- description: behavior.description || `${behavior.name} on ${model.name}`,
98
- isEntityTool: true,
99
- });
65
+ for (const [, rootDef] of Object.entries(cliCommands as Record<string, any>)) {
66
+ const subcommands = (rootDef as any)?.subcommands || {};
67
+ for (const [subName, subDef] of Object.entries(subcommands as Record<string, any>)) {
68
+ const sub = subDef as any;
69
+ const nestedSubs = sub?.subcommands;
70
+ if (nestedSubs && Object.keys(nestedSubs).length > 0) {
71
+ for (const [nestedName, nestedDef] of Object.entries(nestedSubs as Record<string, any>)) {
72
+ out.push(buildCLITool([subName, nestedName], nestedDef as any));
100
73
  }
74
+ } else {
75
+ out.push(buildCLITool([subName], sub));
101
76
  }
102
77
  }
103
78
  }
104
79
  }
80
+ return out;
81
+ }
105
82
 
106
- // Also extract from CLI commands
107
- const cliCmds = extractCLICommands(spec);
108
- for (const cmd of cliCmds) {
109
- tools.push({
110
- name: `specverse-${cmd}`,
111
- description: `Execute specverse ${cmd} command`,
112
- isEntityTool: false,
113
- });
83
+ interface CLITool {
84
+ name: string; // MCP tool name (underscored)
85
+ description: string;
86
+ cliArgs: string[]; // argv pieces before user args (["gen","diagrams"])
87
+ positional: string[]; // input-schema keys to emit as positional argv, in order
88
+ inputSchema: Record<string, any>;
89
+ }
90
+
91
+ function buildCLITool(cliArgs: string[], def: any): CLITool {
92
+ const name = cliArgs.join('_');
93
+ const description = def?.description || `Run specverse ${cliArgs.join(' ')}`;
94
+ const properties: Record<string, any> = {};
95
+ const required: string[] = [];
96
+
97
+ const args = def?.arguments || {};
98
+ // Collect positionals in declaration order, respecting an optional
99
+ // `position` field (rank ascending; default 0). Named flags that
100
+ // happen to share a key with a positional are impossible — the spec
101
+ // forbids it.
102
+ const positionalEntries: Array<[string, any]> = Object.entries(args as Record<string, any>)
103
+ .filter(([, a]) => (a as any).positional)
104
+ .sort(([, a], [, b]) => ((a as any).position || 0) - ((b as any).position || 0));
105
+ const positional: string[] = positionalEntries.map(([k]) => k);
106
+
107
+ for (const [argName, arg] of Object.entries(args as Record<string, any>)) {
108
+ properties[argName] = {
109
+ type: mapArgTypeToJsonSchema((arg as any).type),
110
+ description: (arg as any).description || `${argName} argument`,
111
+ };
112
+ if ((arg as any).required) required.push(argName);
114
113
  }
115
114
 
116
- // Default fallback
117
- if (tools.length === 0) {
118
- tools.push(
119
- { name: 'specverse-validate', description: 'Validate a specification', isEntityTool: false },
120
- { name: 'specverse-create', description: 'Create a specification', isEntityTool: false },
121
- );
115
+ const flags = def?.flags || {};
116
+ for (const [flagName, flag] of Object.entries(flags as Record<string, any>)) {
117
+ const propName = flagName.replace(/^--/, '').replace(/-([a-z])/g, (_, c: string) => c.toUpperCase());
118
+ properties[propName] = {
119
+ type: mapArgTypeToJsonSchema((flag as any).type),
120
+ description: (flag as any).description || `${flagName} flag`,
121
+ };
122
+ if ((flag as any).default !== undefined) {
123
+ properties[propName].default = (flag as any).default;
124
+ }
122
125
  }
123
- return tools;
126
+
127
+ return {
128
+ name,
129
+ description,
130
+ cliArgs,
131
+ positional,
132
+ inputSchema: {
133
+ type: 'object',
134
+ properties,
135
+ ...(required.length > 0 ? { required } : {}),
136
+ },
137
+ };
124
138
  }
125
139
 
126
- function extractMCPResources(spec: any): Array<{ uri: string; name: string; description: string }> {
127
- return [
128
- { uri: 'specverse://schema', name: 'SpecVerse Schema', description: 'JSON Schema for .specly validation' },
129
- { uri: 'specverse://conventions', name: 'Convention Reference', description: 'Convention syntax patterns' },
130
- { uri: 'specverse://library-catalog', name: 'Library Catalog', description: 'Available SpecVerse libraries' },
131
- { uri: 'specverse://prompts', name: 'Prompt Templates', description: 'AI prompt template versions' },
132
- ];
140
+ function mapArgTypeToJsonSchema(type?: string): string {
141
+ if (!type) return 'string';
142
+ const t = type.toLowerCase();
143
+ if (t === 'boolean') return 'boolean';
144
+ if (t === 'integer' || t === 'number') return 'number';
145
+ return 'string';
133
146
  }
134
147
 
135
- function extractCLICommands(spec: any): string[] {
136
- const commands: string[] = [];
137
- const components = spec?.components || [];
138
- for (const component of Array.isArray(components) ? components : Object.values(components)) {
139
- const cliCommands = (component as any)?.commands;
140
- if (!cliCommands) continue;
141
- for (const [, rootDef] of Object.entries(cliCommands as Record<string, any>)) {
142
- const subs = (rootDef as any)?.subcommands || {};
143
- for (const subName of Object.keys(subs)) {
144
- commands.push(subName);
145
- }
146
- }
148
+ function extractMCPDistribution(spec: any): any | null {
149
+ const all: any[] = [];
150
+ if (spec?.distributions) {
151
+ all.push(...(Array.isArray(spec.distributions)
152
+ ? spec.distributions
153
+ : Object.entries(spec.distributions).map(([name, data]) => ({ name, ...(data as any) }))));
154
+ }
155
+ const components = spec?.components || {};
156
+ const componentList = Array.isArray(components) ? components : Object.values(components);
157
+ for (const comp of componentList) {
158
+ const distributions = (comp as any)?.distributions;
159
+ if (!distributions) continue;
160
+ all.push(...(Array.isArray(distributions)
161
+ ? distributions
162
+ : Object.entries(distributions).map(([name, data]) => ({ name, ...(data as any) }))));
147
163
  }
148
- return commands;
164
+ for (const dist of all) if ((dist as any).type === 'mcp') return dist;
165
+ return null;
166
+ }
167
+
168
+ // ─── emitted files ─────────────────────────────────────────────────────────
169
+
170
+ function generatePackageJson(version: string, description: string): string {
171
+ return JSON.stringify({
172
+ name: '@specverse/mcp',
173
+ version,
174
+ description,
175
+ type: 'module',
176
+ main: 'dist/server.js',
177
+ bin: { 'specverse-mcp': 'dist/server.js' },
178
+ scripts: {
179
+ build: 'tsc',
180
+ start: 'node dist/server.js',
181
+ clean: 'rm -rf dist',
182
+ },
183
+ dependencies: {
184
+ '@modelcontextprotocol/sdk': '^1.17.4',
185
+ '@specverse/entities': '^5.0.0',
186
+ },
187
+ devDependencies: {
188
+ '@types/node': '^20.19.11',
189
+ typescript: '^5.9.2',
190
+ },
191
+ files: ['dist', 'README.md'],
192
+ engines: { node: '>=18.0.0' },
193
+ publishConfig: { access: 'public' },
194
+ license: 'MIT',
195
+ }, null, 2) + '\n';
149
196
  }
150
197
 
151
- function generateToolRegistry(
152
- tools: Array<{ name: string; description: string; isEntityTool: boolean }>,
153
- resources: Array<{ uri: string; name: string; description: string }>,
154
- cliCommands: string[]
155
- ): string {
198
+ function generateTsconfig(): string {
199
+ return JSON.stringify({
200
+ compilerOptions: {
201
+ target: 'ES2022',
202
+ module: 'ESNext',
203
+ moduleResolution: 'bundler',
204
+ esModuleInterop: true,
205
+ strict: true,
206
+ skipLibCheck: true,
207
+ declaration: true,
208
+ outDir: './dist',
209
+ rootDir: './src',
210
+ resolveJsonModule: true,
211
+ },
212
+ include: ['src/**/*'],
213
+ exclude: ['node_modules', 'dist'],
214
+ }, null, 2) + '\n';
215
+ }
216
+
217
+ function generateServer(displayName: string, version: string): string {
218
+ return `#!/usr/bin/env node
219
+ /**
220
+ * SpecVerse MCP Server — stdio transport.
221
+ *
222
+ * Wires the MCP protocol to the generated tool registry + live resources.
223
+ * No handwritten business logic — everything is derived from the spec
224
+ * (tools from CLI commands) or from @specverse/entities (schema + docs).
225
+ */
226
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
227
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
228
+ import {
229
+ ListToolsRequestSchema,
230
+ CallToolRequestSchema,
231
+ ListResourcesRequestSchema,
232
+ ReadResourceRequestSchema,
233
+ } from '@modelcontextprotocol/sdk/types.js';
234
+ import { TOOLS, callTool } from './tools.js';
235
+ import { RESOURCES, readResource } from './resources.js';
236
+
237
+ const server = new Server(
238
+ { name: ${JSON.stringify(displayName)}, version: ${JSON.stringify(version)} },
239
+ { capabilities: { tools: {}, resources: {} } },
240
+ );
241
+
242
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
243
+ tools: TOOLS.map(t => ({
244
+ name: t.name,
245
+ description: t.description,
246
+ inputSchema: t.inputSchema,
247
+ })),
248
+ }));
249
+
250
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
251
+ const { name, arguments: args } = req.params;
252
+ return callTool(name, args ?? {});
253
+ });
254
+
255
+ server.setRequestHandler(ListResourcesRequestSchema, async () => ({
256
+ resources: RESOURCES.map(r => ({
257
+ uri: r.uri,
258
+ name: r.name,
259
+ description: r.description,
260
+ mimeType: r.mimeType,
261
+ })),
262
+ }));
263
+
264
+ server.setRequestHandler(ReadResourceRequestSchema, async (req) => {
265
+ return readResource(req.params.uri);
266
+ });
267
+
268
+ const transport = new StdioServerTransport();
269
+ await server.connect(transport);
270
+ `;
271
+ }
272
+
273
+ function generateCliRunner(): string {
156
274
  return `/**
157
- * Spec-Driven MCP Registry
158
- * Generated from SpecVerse self-specification.
275
+ * Thin specverse CLI runner.
159
276
  *
160
- * Tools, resources, and CLI command mappings derived from the spec.
277
+ * Each MCP tool maps to a specverse CLI command + a set of user-supplied
278
+ * arguments. The tool registry tells us which argument keys are positional
279
+ * (declared in the spec as \`positional: true\`) — those get emitted in
280
+ * order before any flags, matching the CLI's expected argv shape. The
281
+ * specverse binary must be on PATH (the AI client that launches the
282
+ * server inherits env from its parent).
161
283
  */
284
+ import { spawn } from 'child_process';
285
+
286
+ export interface CliResult {
287
+ stdout: string;
288
+ stderr: string;
289
+ code: number | null;
290
+ }
291
+
292
+ export function runCli(
293
+ cliArgs: string[],
294
+ positional: string[],
295
+ userArgs: Record<string, any>,
296
+ cwd?: string,
297
+ ): Promise<CliResult> {
298
+ const argv = [...cliArgs];
162
299
 
163
- export const SPEC_TOOLS = ${JSON.stringify(tools, null, 2)};
300
+ // 1. Positional args first, in spec-declared order. Undefined values
301
+ // are skipped — commander will surface missing-required errors.
302
+ for (const key of positional) {
303
+ const v = userArgs[key];
304
+ if (v === undefined || v === null) continue;
305
+ argv.push(String(v));
306
+ }
307
+
308
+ // 2. Remaining keys become --flag or --flag value.
309
+ const positionalSet = new Set(positional);
310
+ for (const [k, v] of Object.entries(userArgs)) {
311
+ if (positionalSet.has(k)) continue;
312
+ if (v === undefined || v === null) continue;
313
+ if (typeof v === 'boolean') {
314
+ if (v) argv.push(\`--\${k}\`);
315
+ } else {
316
+ argv.push(\`--\${k}\`, String(v));
317
+ }
318
+ }
164
319
 
165
- export const SPEC_RESOURCES = ${JSON.stringify(resources, null, 2)};
320
+ return new Promise(resolve => {
321
+ const child = spawn('specverse', argv, { cwd: cwd || process.cwd(), env: process.env });
322
+ let stdout = '';
323
+ let stderr = '';
324
+ child.stdout.on('data', d => { stdout += d.toString(); });
325
+ child.stderr.on('data', d => { stderr += d.toString(); });
326
+ child.on('close', code => resolve({ stdout, stderr, code }));
327
+ child.on('error', err => resolve({ stdout, stderr: stderr + '\\n' + err.message, code: -1 }));
328
+ });
329
+ }
330
+ `;
331
+ }
166
332
 
167
- export const CLI_COMMANDS = ${JSON.stringify(cliCommands, null, 2)};
333
+ function generateTools(tools: CLITool[]): string {
334
+ return `/**
335
+ * Tool registry — one MCP tool per specverse CLI subcommand.
336
+ * Generated from the spec at realize time.
337
+ */
338
+ import { runCli } from './cli-runner.js';
168
339
 
169
- export function getToolByName(name: string) {
170
- return SPEC_TOOLS.find(t => t.name === name);
340
+ export interface Tool {
341
+ name: string;
342
+ description: string;
343
+ cliArgs: string[];
344
+ positional: string[];
345
+ inputSchema: Record<string, any>;
171
346
  }
172
347
 
173
- export function getResourceByUri(uri: string) {
174
- return SPEC_RESOURCES.find(r => r.uri === uri);
348
+ export const TOOLS: Tool[] = ${JSON.stringify(tools, null, 2)};
349
+
350
+ const BY_NAME = new Map<string, Tool>(TOOLS.map(t => [t.name, t]));
351
+
352
+ export async function callTool(name: string, args: Record<string, any>) {
353
+ const tool = BY_NAME.get(name);
354
+ if (!tool) {
355
+ return {
356
+ isError: true,
357
+ content: [{ type: 'text', text: \`Unknown tool: \${name}\` }],
358
+ };
359
+ }
360
+ const { stdout, stderr, code } = await runCli(tool.cliArgs, tool.positional, args);
361
+ if (code !== 0) {
362
+ return {
363
+ isError: true,
364
+ content: [{ type: 'text', text: \`specverse \${tool.cliArgs.join(' ')} exited \${code}\\n\\n\${stderr || stdout}\` }],
365
+ };
366
+ }
367
+ return {
368
+ content: [{ type: 'text', text: stdout || '(no output)' }],
369
+ };
175
370
  }
176
371
  `;
177
372
  }
178
373
 
179
- function copyRecursive(src: string, dest: string) {
180
- for (const entry of readdirSync(src)) {
181
- const srcPath = join(src, entry);
182
- const destPath = join(dest, entry);
183
- if (statSync(srcPath).isDirectory()) {
184
- if (!existsSync(destPath)) mkdirSync(destPath, { recursive: true });
185
- copyRecursive(srcPath, destPath);
186
- } else {
187
- copyFileSync(srcPath, destPath);
188
- }
374
+ function generateResources(): string {
375
+ return `/**
376
+ * Resource registry — exposes the live SpecVerse schema + user guide as
377
+ * MCP resources. Read lazily from @specverse/entities at request time so
378
+ * the contents always match the installed entities version.
379
+ */
380
+ import { readFileSync } from 'fs';
381
+ import { createRequire } from 'module';
382
+ import { dirname, join } from 'path';
383
+
384
+ const require = createRequire(import.meta.url);
385
+
386
+ export interface Resource {
387
+ uri: string;
388
+ name: string;
389
+ description: string;
390
+ mimeType: string;
391
+ resolve: () => { text: string; mimeType: string };
392
+ }
393
+
394
+ function resolveEntitiesFile(relative: string): string {
395
+ const pkg = require.resolve('@specverse/entities/package.json');
396
+ return join(dirname(pkg), relative);
397
+ }
398
+
399
+ export const RESOURCES: Resource[] = [
400
+ {
401
+ uri: 'specverse://schema',
402
+ name: 'SpecVerse JSON Schema',
403
+ description: 'JSON Schema (draft 2020-12) for validating .specly files — composed from entity-module fragments.',
404
+ mimeType: 'application/json',
405
+ resolve: () => ({
406
+ text: readFileSync(resolveEntitiesFile('schema/SPECVERSE-SCHEMA.json'), 'utf8'),
407
+ mimeType: 'application/json',
408
+ }),
409
+ },
410
+ {
411
+ uri: 'specverse://guide',
412
+ name: 'SpecVerse Complete Guide',
413
+ description: 'The canonical user guide — spec language, convention patterns, CLI reference.',
414
+ mimeType: 'text/markdown',
415
+ resolve: () => ({
416
+ text: readFileSync(resolveEntitiesFile('schema/SPECVERSE-COMPLETE-GUIDE.md'), 'utf8'),
417
+ mimeType: 'text/markdown',
418
+ }),
419
+ },
420
+ ];
421
+
422
+ const BY_URI = new Map<string, Resource>(RESOURCES.map(r => [r.uri, r]));
423
+
424
+ export async function readResource(uri: string) {
425
+ const resource = BY_URI.get(uri);
426
+ if (!resource) {
427
+ throw new Error(\`Unknown resource URI: \${uri}\`);
189
428
  }
429
+ const { text, mimeType } = resource.resolve();
430
+ return {
431
+ contents: [{ uri, mimeType, text }],
432
+ };
433
+ }
434
+ `;
190
435
  }
@@ -1002,8 +1002,12 @@ async function executeCliCommand(document: vscode.TextDocument, commandName: str
1002
1002
  'cache': 'cache',
1003
1003
  'help': 'help',
1004
1004
  'gen': 'gen',
1005
- 'dev': 'dev',
1005
+ 'dev': 'dev',
1006
1006
  'test': 'test',
1007
+ // realize requires a positional type arg; default the menu action to
1008
+ // "realize all" (full regeneration). Per-entity dispatch is a future
1009
+ // design — see TODO: "Derive realize subtypes from entity registry".
1010
+ 'realize': 'realize all',
1007
1011
 
1008
1012
  // Grouped commands - map compressed names to CLI format
1009
1013
  'genyaml': 'gen yaml',
@@ -1018,7 +1022,10 @@ async function executeCliCommand(document: vscode.TextDocument, commandName: str
1018
1022
  'testbatch': 'test batch'
1019
1023
  };
1020
1024
 
1021
- const cliCommand = commandMap[commandName] || commandName;
1025
+ // Fall through: convert dotted form (e.g. "realize.all") to space-separated
1026
+ // CLI args (e.g. "realize all") so auto-generated subcommand entries work
1027
+ // without needing a commandMap entry for every one.
1028
+ const cliCommand = commandMap[commandName] || commandName.replace(/\./g, ' ');
1022
1029
 
1023
1030
  try {
1024
1031
  const config = vscode.workspace.getConfiguration('specverse');