@specverse/engines 4.3.5 → 5.0.1

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 (68) 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/inference/ui-contracts/rules/lifecycle-state-visible-in-detail.d.ts +9 -7
  15. package/dist/inference/ui-contracts/rules/lifecycle-state-visible-in-detail.d.ts.map +1 -1
  16. package/dist/inference/ui-contracts/rules/lifecycle-state-visible-in-detail.js +27 -9
  17. package/dist/inference/ui-contracts/rules/lifecycle-state-visible-in-detail.js.map +1 -1
  18. package/dist/libs/instance-factories/cli/templates/commander/command-generator.js +27 -5
  19. package/dist/libs/instance-factories/tools/README.md +1 -1
  20. package/dist/libs/instance-factories/tools/mcp.yaml +1 -1
  21. package/dist/libs/instance-factories/tools/templates/mcp/mcp-server-generator.js +342 -116
  22. package/dist/libs/instance-factories/tools/templates/vscode/vscode-extension-generator.js +172 -8
  23. package/dist/libs/instance-factories/tools/vscode.yaml +1 -1
  24. package/libs/instance-factories/cli/templates/commander/command-generator.ts +27 -5
  25. package/libs/instance-factories/tools/README.md +1 -1
  26. package/libs/instance-factories/tools/mcp.yaml +1 -1
  27. package/libs/instance-factories/tools/templates/mcp/mcp-server-generator.ts +392 -141
  28. package/libs/instance-factories/tools/templates/vscode/static/extension.ts +9 -2
  29. package/libs/instance-factories/tools/templates/vscode/vscode-extension-generator.ts +246 -10
  30. package/libs/instance-factories/tools/vscode.yaml +1 -1
  31. package/package.json +5 -4
  32. package/libs/instance-factories/tools/templates/mcp/static/docs/DEPLOYMENT_GUIDE.md +0 -630
  33. package/libs/instance-factories/tools/templates/mcp/static/docs/HYBRID_RESOURCE_SYSTEM.md +0 -330
  34. package/libs/instance-factories/tools/templates/mcp/static/docs/deployments/EXTENSION_DEPLOYMENT.md +0 -552
  35. package/libs/instance-factories/tools/templates/mcp/static/docs/deployments/LOCAL_DEPLOYMENT.md +0 -164
  36. package/libs/instance-factories/tools/templates/mcp/static/docs/deployments/WEB_DEPLOYMENT.md +0 -247
  37. package/libs/instance-factories/tools/templates/mcp/static/package.json +0 -94
  38. package/libs/instance-factories/tools/templates/mcp/static/scripts/build-enterprise.js +0 -284
  39. package/libs/instance-factories/tools/templates/mcp/static/scripts/build-extension.js +0 -139
  40. package/libs/instance-factories/tools/templates/mcp/static/scripts/build-local.js +0 -74
  41. package/libs/instance-factories/tools/templates/mcp/static/scripts/build-web.js +0 -156
  42. package/libs/instance-factories/tools/templates/mcp/static/scripts/copy-canonical-files.js +0 -41
  43. package/libs/instance-factories/tools/templates/mcp/static/scripts/test-deployments.js +0 -259
  44. package/libs/instance-factories/tools/templates/mcp/static/scripts/test-hybrid-resources.js +0 -231
  45. package/libs/instance-factories/tools/templates/mcp/static/scripts/test-hybrid-simple.js +0 -196
  46. package/libs/instance-factories/tools/templates/mcp/static/src/controllers/MCPServerController.ts +0 -293
  47. package/libs/instance-factories/tools/templates/mcp/static/src/events/EventEmitter.ts +0 -90
  48. package/libs/instance-factories/tools/templates/mcp/static/src/index.ts +0 -24
  49. package/libs/instance-factories/tools/templates/mcp/static/src/interfaces/ResourceProvider.ts +0 -15
  50. package/libs/instance-factories/tools/templates/mcp/static/src/models/LibrarySuggestion.ts +0 -106
  51. package/libs/instance-factories/tools/templates/mcp/static/src/models/SpecVerseResource.ts +0 -75
  52. package/libs/instance-factories/tools/templates/mcp/static/src/server/mcp-server.ts +0 -239
  53. package/libs/instance-factories/tools/templates/mcp/static/src/services/CLIProxyService.ts +0 -1501
  54. package/libs/instance-factories/tools/templates/mcp/static/src/services/EmbeddedResourcesAdapter.ts +0 -211
  55. package/libs/instance-factories/tools/templates/mcp/static/src/services/EntityModuleService.ts +0 -308
  56. package/libs/instance-factories/tools/templates/mcp/static/src/services/HybridResourcesProvider.ts +0 -210
  57. package/libs/instance-factories/tools/templates/mcp/static/src/services/LibraryToolsService.ts +0 -356
  58. package/libs/instance-factories/tools/templates/mcp/static/src/services/OrchestratorBridge.ts +0 -522
  59. package/libs/instance-factories/tools/templates/mcp/static/src/services/OrchestratorToolsService.ts +0 -530
  60. package/libs/instance-factories/tools/templates/mcp/static/src/services/PromptToolsService.ts +0 -594
  61. package/libs/instance-factories/tools/templates/mcp/static/src/services/ResourcesProviderService.ts +0 -170
  62. package/libs/instance-factories/tools/templates/mcp/static/src/tests/unit/CLIProxyService.init.test.ts +0 -544
  63. package/libs/instance-factories/tools/templates/mcp/static/src/tests/unit/CLIProxyService.test.ts +0 -189
  64. package/libs/instance-factories/tools/templates/mcp/static/src/tests/unit/ResourcesProviderService.test.ts +0 -89
  65. package/libs/instance-factories/tools/templates/mcp/static/src/types/index.ts +0 -110
  66. package/libs/instance-factories/tools/templates/mcp/static/tsconfig.json +0 -28
  67. package/libs/instance-factories/tools/templates/vscode/static/schemas/specverse-v3-schema.json +0 -4279
  68. /package/libs/instance-factories/tools/templates/vscode/static/themes/{specverse-complete-theme.json → specverse-dark-theme.json} +0 -0
@@ -1,190 +1,441 @@
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
+ // Under moduleResolution: 'bundler' tsc's auto-inclusion of
212
+ // @types/* packages from node_modules/@types is environment-
213
+ // dependent — Node 20 on CI doesn't pick up @types/node while
214
+ // Node 24 locally does. Listing it explicitly forces consistent
215
+ // resolution everywhere.
216
+ types: ['node'],
217
+ },
218
+ include: ['src/**/*'],
219
+ exclude: ['node_modules', 'dist'],
220
+ }, null, 2) + '\n';
221
+ }
222
+
223
+ function generateServer(displayName: string, version: string): string {
224
+ return `#!/usr/bin/env node
225
+ /**
226
+ * SpecVerse MCP Server — stdio transport.
227
+ *
228
+ * Wires the MCP protocol to the generated tool registry + live resources.
229
+ * No handwritten business logic — everything is derived from the spec
230
+ * (tools from CLI commands) or from @specverse/entities (schema + docs).
231
+ */
232
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
233
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
234
+ import {
235
+ ListToolsRequestSchema,
236
+ CallToolRequestSchema,
237
+ ListResourcesRequestSchema,
238
+ ReadResourceRequestSchema,
239
+ } from '@modelcontextprotocol/sdk/types.js';
240
+ import { TOOLS, callTool } from './tools.js';
241
+ import { RESOURCES, readResource } from './resources.js';
242
+
243
+ const server = new Server(
244
+ { name: ${JSON.stringify(displayName)}, version: ${JSON.stringify(version)} },
245
+ { capabilities: { tools: {}, resources: {} } },
246
+ );
247
+
248
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
249
+ tools: TOOLS.map(t => ({
250
+ name: t.name,
251
+ description: t.description,
252
+ inputSchema: t.inputSchema,
253
+ })),
254
+ }));
255
+
256
+ server.setRequestHandler(CallToolRequestSchema, async (req: { params: { name: string; arguments?: Record<string, unknown> } }) => {
257
+ const { name, arguments: args } = req.params;
258
+ return callTool(name, args ?? {});
259
+ });
260
+
261
+ server.setRequestHandler(ListResourcesRequestSchema, async () => ({
262
+ resources: RESOURCES.map(r => ({
263
+ uri: r.uri,
264
+ name: r.name,
265
+ description: r.description,
266
+ mimeType: r.mimeType,
267
+ })),
268
+ }));
269
+
270
+ server.setRequestHandler(ReadResourceRequestSchema, async (req: { params: { uri: string } }) => {
271
+ return readResource(req.params.uri);
272
+ });
273
+
274
+ const transport = new StdioServerTransport();
275
+ await server.connect(transport);
276
+ `;
277
+ }
278
+
279
+ function generateCliRunner(): string {
156
280
  return `/**
157
- * Spec-Driven MCP Registry
158
- * Generated from SpecVerse self-specification.
281
+ * Thin specverse CLI runner.
159
282
  *
160
- * Tools, resources, and CLI command mappings derived from the spec.
283
+ * Each MCP tool maps to a specverse CLI command + a set of user-supplied
284
+ * arguments. The tool registry tells us which argument keys are positional
285
+ * (declared in the spec as \`positional: true\`) — those get emitted in
286
+ * order before any flags, matching the CLI's expected argv shape. The
287
+ * specverse binary must be on PATH (the AI client that launches the
288
+ * server inherits env from its parent).
161
289
  */
290
+ import { spawn } from 'child_process';
291
+
292
+ export interface CliResult {
293
+ stdout: string;
294
+ stderr: string;
295
+ code: number | null;
296
+ }
297
+
298
+ export function runCli(
299
+ cliArgs: string[],
300
+ positional: string[],
301
+ userArgs: Record<string, any>,
302
+ cwd?: string,
303
+ ): Promise<CliResult> {
304
+ const argv = [...cliArgs];
162
305
 
163
- export const SPEC_TOOLS = ${JSON.stringify(tools, null, 2)};
306
+ // 1. Positional args first, in spec-declared order. Undefined values
307
+ // are skipped — commander will surface missing-required errors.
308
+ for (const key of positional) {
309
+ const v = userArgs[key];
310
+ if (v === undefined || v === null) continue;
311
+ argv.push(String(v));
312
+ }
313
+
314
+ // 2. Remaining keys become --flag or --flag value.
315
+ const positionalSet = new Set(positional);
316
+ for (const [k, v] of Object.entries(userArgs)) {
317
+ if (positionalSet.has(k)) continue;
318
+ if (v === undefined || v === null) continue;
319
+ if (typeof v === 'boolean') {
320
+ if (v) argv.push(\`--\${k}\`);
321
+ } else {
322
+ argv.push(\`--\${k}\`, String(v));
323
+ }
324
+ }
164
325
 
165
- export const SPEC_RESOURCES = ${JSON.stringify(resources, null, 2)};
326
+ return new Promise<CliResult>(resolve => {
327
+ const child = spawn('specverse', argv, { cwd: cwd || process.cwd(), env: process.env });
328
+ let stdout = '';
329
+ let stderr = '';
330
+ child.stdout.on('data', (d: Buffer) => { stdout += d.toString(); });
331
+ child.stderr.on('data', (d: Buffer) => { stderr += d.toString(); });
332
+ child.on('close', (code: number | null) => resolve({ stdout, stderr, code }));
333
+ child.on('error', (err: Error) => resolve({ stdout, stderr: stderr + '\\n' + err.message, code: -1 }));
334
+ });
335
+ }
336
+ `;
337
+ }
166
338
 
167
- export const CLI_COMMANDS = ${JSON.stringify(cliCommands, null, 2)};
339
+ function generateTools(tools: CLITool[]): string {
340
+ return `/**
341
+ * Tool registry — one MCP tool per specverse CLI subcommand.
342
+ * Generated from the spec at realize time.
343
+ */
344
+ import { runCli } from './cli-runner.js';
168
345
 
169
- export function getToolByName(name: string) {
170
- return SPEC_TOOLS.find(t => t.name === name);
346
+ export interface Tool {
347
+ name: string;
348
+ description: string;
349
+ cliArgs: string[];
350
+ positional: string[];
351
+ inputSchema: Record<string, any>;
171
352
  }
172
353
 
173
- export function getResourceByUri(uri: string) {
174
- return SPEC_RESOURCES.find(r => r.uri === uri);
354
+ export const TOOLS: Tool[] = ${JSON.stringify(tools, null, 2)};
355
+
356
+ const BY_NAME = new Map<string, Tool>(TOOLS.map(t => [t.name, t]));
357
+
358
+ export async function callTool(name: string, args: Record<string, any>) {
359
+ const tool = BY_NAME.get(name);
360
+ if (!tool) {
361
+ return {
362
+ isError: true,
363
+ content: [{ type: 'text', text: \`Unknown tool: \${name}\` }],
364
+ };
365
+ }
366
+ const { stdout, stderr, code } = await runCli(tool.cliArgs, tool.positional, args);
367
+ if (code !== 0) {
368
+ return {
369
+ isError: true,
370
+ content: [{ type: 'text', text: \`specverse \${tool.cliArgs.join(' ')} exited \${code}\\n\\n\${stderr || stdout}\` }],
371
+ };
372
+ }
373
+ return {
374
+ content: [{ type: 'text', text: stdout || '(no output)' }],
375
+ };
175
376
  }
176
377
  `;
177
378
  }
178
379
 
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
- }
380
+ function generateResources(): string {
381
+ return `/**
382
+ * Resource registry — exposes the live SpecVerse schema + user guide as
383
+ * MCP resources. Read lazily from @specverse/entities at request time so
384
+ * the contents always match the installed entities version.
385
+ */
386
+ import { readFileSync } from 'fs';
387
+ import { createRequire } from 'module';
388
+ import { dirname, join } from 'path';
389
+
390
+ const require = createRequire(import.meta.url);
391
+
392
+ export interface Resource {
393
+ uri: string;
394
+ name: string;
395
+ description: string;
396
+ mimeType: string;
397
+ resolve: () => { text: string; mimeType: string };
398
+ }
399
+
400
+ function resolveEntitiesFile(relative: string): string {
401
+ const pkg = require.resolve('@specverse/entities/package.json');
402
+ return join(dirname(pkg), relative);
403
+ }
404
+
405
+ export const RESOURCES: Resource[] = [
406
+ {
407
+ uri: 'specverse://schema',
408
+ name: 'SpecVerse JSON Schema',
409
+ description: 'JSON Schema (draft 2020-12) for validating .specly files — composed from entity-module fragments.',
410
+ mimeType: 'application/json',
411
+ resolve: () => ({
412
+ text: readFileSync(resolveEntitiesFile('schema/SPECVERSE-SCHEMA.json'), 'utf8'),
413
+ mimeType: 'application/json',
414
+ }),
415
+ },
416
+ {
417
+ uri: 'specverse://guide',
418
+ name: 'SpecVerse Complete Guide',
419
+ description: 'The canonical user guide — spec language, convention patterns, CLI reference.',
420
+ mimeType: 'text/markdown',
421
+ resolve: () => ({
422
+ text: readFileSync(resolveEntitiesFile('schema/SPECVERSE-COMPLETE-GUIDE.md'), 'utf8'),
423
+ mimeType: 'text/markdown',
424
+ }),
425
+ },
426
+ ];
427
+
428
+ const BY_URI = new Map<string, Resource>(RESOURCES.map(r => [r.uri, r]));
429
+
430
+ export async function readResource(uri: string) {
431
+ const resource = BY_URI.get(uri);
432
+ if (!resource) {
433
+ throw new Error(\`Unknown resource URI: \${uri}\`);
189
434
  }
435
+ const { text, mimeType } = resource.resolve();
436
+ return {
437
+ contents: [{ uri, mimeType, text }],
438
+ };
439
+ }
440
+ `;
190
441
  }
@@ -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');