@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.
- package/assets/examples/10-api/README.md +3 -3
- package/assets/prompts/core/README.md +1 -1
- package/dist/inference/core/rule-engine.d.ts +0 -12
- package/dist/inference/core/rule-engine.d.ts.map +1 -1
- package/dist/inference/core/rule-engine.js +99 -968
- package/dist/inference/core/rule-engine.js.map +1 -1
- package/dist/inference/core/template-helpers.d.ts +56 -0
- package/dist/inference/core/template-helpers.d.ts.map +1 -0
- package/dist/inference/core/template-helpers.js +87 -0
- package/dist/inference/core/template-helpers.js.map +1 -0
- package/dist/inference/logical/generators/service-generator.d.ts.map +1 -1
- package/dist/inference/logical/generators/service-generator.js +0 -4
- package/dist/inference/logical/generators/service-generator.js.map +1 -1
- package/dist/libs/instance-factories/cli/templates/commander/command-generator.js +27 -5
- package/dist/libs/instance-factories/tools/README.md +1 -1
- package/dist/libs/instance-factories/tools/mcp.yaml +1 -1
- package/dist/libs/instance-factories/tools/templates/mcp/mcp-server-generator.js +336 -116
- package/dist/libs/instance-factories/tools/templates/vscode/vscode-extension-generator.js +172 -8
- package/dist/libs/instance-factories/tools/vscode.yaml +1 -1
- package/libs/instance-factories/cli/templates/commander/command-generator.ts +27 -5
- package/libs/instance-factories/tools/README.md +1 -1
- package/libs/instance-factories/tools/mcp.yaml +1 -1
- package/libs/instance-factories/tools/templates/mcp/mcp-server-generator.ts +386 -141
- package/libs/instance-factories/tools/templates/vscode/static/extension.ts +9 -2
- package/libs/instance-factories/tools/templates/vscode/vscode-extension-generator.ts +246 -10
- package/libs/instance-factories/tools/vscode.yaml +1 -1
- package/package.json +5 -4
- package/libs/instance-factories/tools/templates/mcp/static/docs/DEPLOYMENT_GUIDE.md +0 -630
- package/libs/instance-factories/tools/templates/mcp/static/docs/HYBRID_RESOURCE_SYSTEM.md +0 -330
- package/libs/instance-factories/tools/templates/mcp/static/docs/deployments/EXTENSION_DEPLOYMENT.md +0 -552
- package/libs/instance-factories/tools/templates/mcp/static/docs/deployments/LOCAL_DEPLOYMENT.md +0 -164
- package/libs/instance-factories/tools/templates/mcp/static/docs/deployments/WEB_DEPLOYMENT.md +0 -247
- package/libs/instance-factories/tools/templates/mcp/static/package.json +0 -94
- package/libs/instance-factories/tools/templates/mcp/static/scripts/build-enterprise.js +0 -284
- package/libs/instance-factories/tools/templates/mcp/static/scripts/build-extension.js +0 -139
- package/libs/instance-factories/tools/templates/mcp/static/scripts/build-local.js +0 -74
- package/libs/instance-factories/tools/templates/mcp/static/scripts/build-web.js +0 -156
- package/libs/instance-factories/tools/templates/mcp/static/scripts/copy-canonical-files.js +0 -41
- package/libs/instance-factories/tools/templates/mcp/static/scripts/test-deployments.js +0 -259
- package/libs/instance-factories/tools/templates/mcp/static/scripts/test-hybrid-resources.js +0 -231
- package/libs/instance-factories/tools/templates/mcp/static/scripts/test-hybrid-simple.js +0 -196
- package/libs/instance-factories/tools/templates/mcp/static/src/controllers/MCPServerController.ts +0 -293
- package/libs/instance-factories/tools/templates/mcp/static/src/events/EventEmitter.ts +0 -90
- package/libs/instance-factories/tools/templates/mcp/static/src/index.ts +0 -24
- package/libs/instance-factories/tools/templates/mcp/static/src/interfaces/ResourceProvider.ts +0 -15
- package/libs/instance-factories/tools/templates/mcp/static/src/models/LibrarySuggestion.ts +0 -106
- package/libs/instance-factories/tools/templates/mcp/static/src/models/SpecVerseResource.ts +0 -75
- package/libs/instance-factories/tools/templates/mcp/static/src/server/mcp-server.ts +0 -239
- package/libs/instance-factories/tools/templates/mcp/static/src/services/CLIProxyService.ts +0 -1501
- package/libs/instance-factories/tools/templates/mcp/static/src/services/EmbeddedResourcesAdapter.ts +0 -211
- package/libs/instance-factories/tools/templates/mcp/static/src/services/EntityModuleService.ts +0 -308
- package/libs/instance-factories/tools/templates/mcp/static/src/services/HybridResourcesProvider.ts +0 -210
- package/libs/instance-factories/tools/templates/mcp/static/src/services/LibraryToolsService.ts +0 -356
- package/libs/instance-factories/tools/templates/mcp/static/src/services/OrchestratorBridge.ts +0 -522
- package/libs/instance-factories/tools/templates/mcp/static/src/services/OrchestratorToolsService.ts +0 -530
- package/libs/instance-factories/tools/templates/mcp/static/src/services/PromptToolsService.ts +0 -594
- package/libs/instance-factories/tools/templates/mcp/static/src/services/ResourcesProviderService.ts +0 -170
- package/libs/instance-factories/tools/templates/mcp/static/src/tests/unit/CLIProxyService.init.test.ts +0 -544
- package/libs/instance-factories/tools/templates/mcp/static/src/tests/unit/CLIProxyService.test.ts +0 -189
- package/libs/instance-factories/tools/templates/mcp/static/src/tests/unit/ResourcesProviderService.test.ts +0 -89
- package/libs/instance-factories/tools/templates/mcp/static/src/types/index.ts +0 -110
- package/libs/instance-factories/tools/templates/mcp/static/tsconfig.json +0 -28
- package/libs/instance-factories/tools/templates/vscode/static/schemas/specverse-v3-schema.json +0 -4279
- /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
|
-
*
|
|
6
|
-
*
|
|
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
|
|
11
|
-
import { join
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
37
|
-
const tools = extractMCPTools(spec);
|
|
38
|
-
const resources = extractMCPResources(spec);
|
|
39
|
-
const cliCommands = extractCLICommands(spec);
|
|
31
|
+
const cliCommands = extractCLITools(spec);
|
|
40
32
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
writeFileSync(join(
|
|
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 ${
|
|
40
|
+
return `MCP server generated in: ${mcpDir}\n ${cliCommands.length} tools (one per CLI command), 2 resources (schema + docs)`;
|
|
48
41
|
}
|
|
49
42
|
|
|
50
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
61
|
-
const
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
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
|
|
127
|
-
return
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
|
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
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
*
|
|
158
|
-
* Generated from SpecVerse self-specification.
|
|
275
|
+
* Thin specverse CLI runner.
|
|
159
276
|
*
|
|
160
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
170
|
-
|
|
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
|
|
174
|
-
|
|
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
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
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');
|