@totemsdk/mcp-server 0.4.0 → 0.5.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.
- package/LICENSE +202 -21
- package/NOTICE +60 -0
- package/README.md +6 -1
- package/data/sdk-index.json +1 -1
- package/dist/composition.d.ts +77 -0
- package/dist/composition.js +375 -0
- package/dist/contracts.d.ts +18 -0
- package/dist/contracts.js +108 -0
- package/dist/indexer.js +82 -5
- package/dist/recipes.d.ts +15 -0
- package/dist/recipes.js +48 -0
- package/dist/resources.js +66 -7
- package/dist/template-catalog.js +4 -4
- package/dist/tools.js +194 -57
- package/dist/types.d.ts +34 -0
- package/dist/workflow.d.ts +36 -0
- package/dist/workflow.js +200 -0
- package/package.json +6 -7
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency-aware composition planning and application scaffolding.
|
|
3
|
+
*
|
|
4
|
+
* The planner considers the entire indexed catalogue but selects only a
|
|
5
|
+
* justified closure. It preserves declaration provenance and never upgrades a
|
|
6
|
+
* package's maturity from a successful plan or scaffold.
|
|
7
|
+
*/
|
|
8
|
+
import type { DependencyEdge, SdkIndex } from './types.js';
|
|
9
|
+
import { type WorkflowPlan } from './workflow.js';
|
|
10
|
+
export type AppRuntime = 'node-cjs' | 'node-esm' | 'bare' | 'browser' | 'unknown';
|
|
11
|
+
export interface PlanCompositionInput {
|
|
12
|
+
brief?: string;
|
|
13
|
+
runtime?: AppRuntime;
|
|
14
|
+
requiredPackages?: string[];
|
|
15
|
+
suggestedPackages?: string[];
|
|
16
|
+
optionalPackages?: string[];
|
|
17
|
+
excludedPackages?: string[];
|
|
18
|
+
requiredCapabilities?: string[];
|
|
19
|
+
}
|
|
20
|
+
export type Disposition = 'direct' | 'transitive' | 'enabled-optional' | 'alternative' | 'excluded' | 'unresolved';
|
|
21
|
+
export interface PackageDisposition {
|
|
22
|
+
name: string;
|
|
23
|
+
version?: string;
|
|
24
|
+
maturity?: string;
|
|
25
|
+
disposition: Disposition;
|
|
26
|
+
reasons: string[];
|
|
27
|
+
dependencyPath?: string[];
|
|
28
|
+
}
|
|
29
|
+
export interface CompositionPlan {
|
|
30
|
+
brief?: string;
|
|
31
|
+
runtime: AppRuntime;
|
|
32
|
+
directDependencies: string[];
|
|
33
|
+
transitiveDependencies: string[];
|
|
34
|
+
enabledOptional: string[];
|
|
35
|
+
excluded: string[];
|
|
36
|
+
unresolved: string[];
|
|
37
|
+
dispositions: PackageDisposition[];
|
|
38
|
+
edges: DependencyEdge[];
|
|
39
|
+
capabilityMap: Record<string, {
|
|
40
|
+
selected: string[];
|
|
41
|
+
candidates: string[];
|
|
42
|
+
unresolved: boolean;
|
|
43
|
+
}>;
|
|
44
|
+
briefMatches: string[];
|
|
45
|
+
maturity: Record<string, string>;
|
|
46
|
+
installOrder: string[];
|
|
47
|
+
conflicts: Array<{
|
|
48
|
+
package: string;
|
|
49
|
+
reason: string;
|
|
50
|
+
}>;
|
|
51
|
+
hostPorts: string[];
|
|
52
|
+
workflow: WorkflowPlan;
|
|
53
|
+
fixtures: string[];
|
|
54
|
+
acceptance: string[];
|
|
55
|
+
warnings: string[];
|
|
56
|
+
}
|
|
57
|
+
export declare function planComposition(input: PlanCompositionInput, index: SdkIndex): CompositionPlan;
|
|
58
|
+
export interface ScaffoldAppInput {
|
|
59
|
+
appName?: string;
|
|
60
|
+
runtime?: 'node-cjs' | 'node-esm';
|
|
61
|
+
plan: CompositionPlan;
|
|
62
|
+
}
|
|
63
|
+
export interface ScaffoldAppResult {
|
|
64
|
+
files: Array<{
|
|
65
|
+
path: string;
|
|
66
|
+
content: string;
|
|
67
|
+
}>;
|
|
68
|
+
dependencies: Record<string, string>;
|
|
69
|
+
install: string[];
|
|
70
|
+
generationManifest: {
|
|
71
|
+
runtime: string;
|
|
72
|
+
directDependencies: string[];
|
|
73
|
+
maturity: Record<string, string>;
|
|
74
|
+
warnings: string[];
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
export declare function scaffoldApp(input: ScaffoldAppInput, index: SdkIndex): ScaffoldAppResult;
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Dependency-aware composition planning and application scaffolding.
|
|
4
|
+
*
|
|
5
|
+
* The planner considers the entire indexed catalogue but selects only a
|
|
6
|
+
* justified closure. It preserves declaration provenance and never upgrades a
|
|
7
|
+
* package's maturity from a successful plan or scaffold.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.planComposition = planComposition;
|
|
11
|
+
exports.scaffoldApp = scaffoldApp;
|
|
12
|
+
const workflow_js_1 = require("./workflow.js");
|
|
13
|
+
const RUNTIME_KINDS = ['runtime', 'peer'];
|
|
14
|
+
function names(input) {
|
|
15
|
+
return Array.isArray(input) ? input.filter((v) => typeof v === 'string' && v.length > 0) : [];
|
|
16
|
+
}
|
|
17
|
+
function capabilityMatches(index, token) {
|
|
18
|
+
const terms = token.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
|
|
19
|
+
return Object.values(index.packages)
|
|
20
|
+
.map((pkg) => {
|
|
21
|
+
const haystack = [
|
|
22
|
+
pkg.name, pkg.description, ...(pkg.keywords ?? []),
|
|
23
|
+
...pkg.exports.functions, ...pkg.exports.types, ...pkg.exports.classes,
|
|
24
|
+
...pkg.exports.interfaces, ...pkg.exports.consts,
|
|
25
|
+
].join(' ').toLowerCase();
|
|
26
|
+
return { name: pkg.name, score: terms.filter((term) => haystack.includes(term)).length };
|
|
27
|
+
})
|
|
28
|
+
.filter((entry) => entry.score > 0)
|
|
29
|
+
.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name))
|
|
30
|
+
.map((entry) => entry.name);
|
|
31
|
+
}
|
|
32
|
+
function installOrder(selected, index) {
|
|
33
|
+
const visited = new Set();
|
|
34
|
+
const visiting = new Set();
|
|
35
|
+
const order = [];
|
|
36
|
+
const visit = (name) => {
|
|
37
|
+
if (visited.has(name) || visiting.has(name))
|
|
38
|
+
return;
|
|
39
|
+
visiting.add(name);
|
|
40
|
+
const pkg = index.packages[name];
|
|
41
|
+
for (const kind of RUNTIME_KINDS) {
|
|
42
|
+
for (const dep of Object.keys(pkg?.dependencyGroups?.[kind] ?? {})) {
|
|
43
|
+
if (selected.has(dep))
|
|
44
|
+
visit(dep);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
visiting.delete(name);
|
|
48
|
+
visited.add(name);
|
|
49
|
+
order.push(name);
|
|
50
|
+
};
|
|
51
|
+
for (const name of [...selected].sort())
|
|
52
|
+
visit(name);
|
|
53
|
+
return order;
|
|
54
|
+
}
|
|
55
|
+
function planComposition(input, index) {
|
|
56
|
+
const runtime = input.runtime ?? 'unknown';
|
|
57
|
+
const required = names(input.requiredPackages);
|
|
58
|
+
const suggested = names(input.suggestedPackages);
|
|
59
|
+
const optional = names(input.optionalPackages);
|
|
60
|
+
const excluded = new Set(names(input.excludedPackages));
|
|
61
|
+
const unresolved = [...new Set([...required, ...suggested, ...optional])]
|
|
62
|
+
.filter((name) => !index.packages[name] && !excluded.has(name))
|
|
63
|
+
.sort();
|
|
64
|
+
const requested = [...new Set([...required, ...suggested])]
|
|
65
|
+
.filter((name) => index.packages[name] && !excluded.has(name));
|
|
66
|
+
const direct = new Set(requested);
|
|
67
|
+
const transitive = new Set();
|
|
68
|
+
const enabledOptional = new Set();
|
|
69
|
+
const edges = new Map();
|
|
70
|
+
const dependencyPath = new Map();
|
|
71
|
+
const addEdge = (from, to, kind, range) => {
|
|
72
|
+
edges.set(`${from}\u0000${to}\u0000${kind}`, { from, to, kind, range });
|
|
73
|
+
};
|
|
74
|
+
const queue = [...direct].sort();
|
|
75
|
+
while (queue.length > 0) {
|
|
76
|
+
const name = queue.shift();
|
|
77
|
+
const pkg = index.packages[name];
|
|
78
|
+
for (const kind of RUNTIME_KINDS) {
|
|
79
|
+
for (const [dep, range] of Object.entries(pkg?.dependencyGroups?.[kind] ?? {})) {
|
|
80
|
+
if (!index.packages[dep])
|
|
81
|
+
continue;
|
|
82
|
+
addEdge(name, dep, kind, String(range));
|
|
83
|
+
if (direct.has(dep) || transitive.has(dep))
|
|
84
|
+
continue;
|
|
85
|
+
transitive.add(dep);
|
|
86
|
+
dependencyPath.set(dep, [...(dependencyPath.get(name) ?? [name]), dep]);
|
|
87
|
+
queue.push(dep);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
for (const name of optional) {
|
|
92
|
+
if (!index.packages[name] || excluded.has(name))
|
|
93
|
+
continue;
|
|
94
|
+
enabledOptional.add(name);
|
|
95
|
+
for (const kind of RUNTIME_KINDS) {
|
|
96
|
+
for (const [dep, range] of Object.entries(index.packages[name]?.dependencyGroups?.[kind] ?? {})) {
|
|
97
|
+
addEdge(name, dep, kind, String(range));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const selected = new Set([...direct, ...transitive, ...enabledOptional]);
|
|
102
|
+
const capabilityMap = {};
|
|
103
|
+
const alternatives = new Set();
|
|
104
|
+
const briefMatches = input.brief ? capabilityMatches(index, input.brief).slice(0, 20) : [];
|
|
105
|
+
for (const name of briefMatches)
|
|
106
|
+
if (!selected.has(name))
|
|
107
|
+
alternatives.add(name);
|
|
108
|
+
for (const capability of names(input.requiredCapabilities)) {
|
|
109
|
+
const candidates = capabilityMatches(index, capability);
|
|
110
|
+
capabilityMap[capability] = {
|
|
111
|
+
selected: candidates.filter((name) => selected.has(name)),
|
|
112
|
+
candidates: candidates.slice(0, 10),
|
|
113
|
+
unresolved: candidates.length === 0,
|
|
114
|
+
};
|
|
115
|
+
for (const name of candidates)
|
|
116
|
+
if (!selected.has(name))
|
|
117
|
+
alternatives.add(name);
|
|
118
|
+
}
|
|
119
|
+
const dispositions = Object.values(index.packages)
|
|
120
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
121
|
+
.map((pkg) => {
|
|
122
|
+
const base = {
|
|
123
|
+
name: pkg.name, version: pkg.version,
|
|
124
|
+
...(pkg.maturity ? { maturity: pkg.maturity } : {}),
|
|
125
|
+
};
|
|
126
|
+
if (unresolved.includes(pkg.name)) {
|
|
127
|
+
return { ...base, disposition: 'unresolved', reasons: ['requested but not present in the selected catalogue'] };
|
|
128
|
+
}
|
|
129
|
+
if (excluded.has(pkg.name)) {
|
|
130
|
+
return { ...base, disposition: 'excluded', reasons: ['explicitly excluded by the brief'] };
|
|
131
|
+
}
|
|
132
|
+
if (direct.has(pkg.name)) {
|
|
133
|
+
return { ...base, disposition: 'direct', reasons: ['required or suggested by the brief'] };
|
|
134
|
+
}
|
|
135
|
+
if (transitive.has(pkg.name)) {
|
|
136
|
+
return {
|
|
137
|
+
...base, disposition: 'transitive',
|
|
138
|
+
reasons: ['required by a selected package'],
|
|
139
|
+
...(dependencyPath.has(pkg.name) ? { dependencyPath: dependencyPath.get(pkg.name) } : {}),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
if (enabledOptional.has(pkg.name)) {
|
|
143
|
+
return { ...base, disposition: 'enabled-optional', reasons: ['optional integration enabled by the brief'] };
|
|
144
|
+
}
|
|
145
|
+
if (alternatives.has(pkg.name)) {
|
|
146
|
+
return { ...base, disposition: 'alternative', reasons: ['matched a required capability but not selected'] };
|
|
147
|
+
}
|
|
148
|
+
return { ...base, disposition: 'excluded', reasons: ['not selected by this composition'] };
|
|
149
|
+
});
|
|
150
|
+
const warnings = [];
|
|
151
|
+
if (direct.size === 0 && input.brief) {
|
|
152
|
+
warnings.push('No direct packages were selected; inspect briefMatches and choose a composition.');
|
|
153
|
+
}
|
|
154
|
+
if (briefMatches.length === 0 && input.brief) {
|
|
155
|
+
warnings.push('No package metadata matched the brief; no recipe is assumed.');
|
|
156
|
+
}
|
|
157
|
+
const unresolvedCapabilities = Object.entries(capabilityMap)
|
|
158
|
+
.filter(([, value]) => value.unresolved)
|
|
159
|
+
.map(([capability]) => capability);
|
|
160
|
+
if (unresolvedCapabilities.length > 0) {
|
|
161
|
+
warnings.push(`No package provides capability: ${unresolvedCapabilities.join(', ')}`);
|
|
162
|
+
}
|
|
163
|
+
if (runtime === 'browser' || runtime === 'unknown') {
|
|
164
|
+
warnings.push(`Runtime '${runtime}' support is not verified for this composition.`);
|
|
165
|
+
}
|
|
166
|
+
const selectedMissingMaturity = [...selected].filter((name) => !index.packages[name]?.maturity);
|
|
167
|
+
if (selectedMissingMaturity.length > 0) {
|
|
168
|
+
warnings.push(`Maturity unknown for: ${selectedMissingMaturity.sort().join(', ')}`);
|
|
169
|
+
}
|
|
170
|
+
const workflow = (0, workflow_js_1.buildWorkflow)({
|
|
171
|
+
directDependencies: [...direct].sort(),
|
|
172
|
+
maturity: Object.fromEntries([...selected].sort().map((name) => [
|
|
173
|
+
name, index.packages[name]?.maturity ?? 'unknown',
|
|
174
|
+
])),
|
|
175
|
+
requiredCapabilities: names(input.requiredCapabilities),
|
|
176
|
+
index,
|
|
177
|
+
});
|
|
178
|
+
if (workflow.unresolved.length > 0) {
|
|
179
|
+
warnings.push(`No verified workflow adapter for: ${workflow.unresolved.sort().join(', ')}`);
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
...(input.brief ? { brief: input.brief } : {}),
|
|
183
|
+
runtime,
|
|
184
|
+
directDependencies: [...direct].sort(),
|
|
185
|
+
transitiveDependencies: [...transitive].sort(),
|
|
186
|
+
enabledOptional: [...enabledOptional].sort(),
|
|
187
|
+
excluded: [...excluded].sort(),
|
|
188
|
+
unresolved,
|
|
189
|
+
dispositions,
|
|
190
|
+
edges: [...edges.values()].sort((a, b) => `${a.from}:${a.to}:${a.kind}`.localeCompare(`${b.from}:${b.to}:${b.kind}`)),
|
|
191
|
+
capabilityMap,
|
|
192
|
+
briefMatches,
|
|
193
|
+
maturity: Object.fromEntries([...selected].sort().map((name) => [
|
|
194
|
+
name, index.packages[name]?.maturity ?? 'unknown',
|
|
195
|
+
])),
|
|
196
|
+
installOrder: installOrder(selected, index),
|
|
197
|
+
conflicts: [],
|
|
198
|
+
hostPorts: workflow.hostPorts,
|
|
199
|
+
workflow,
|
|
200
|
+
fixtures: workflow.fixtures,
|
|
201
|
+
acceptance: workflow.acceptance,
|
|
202
|
+
warnings,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
function scaffoldApp(input, index) {
|
|
206
|
+
const runtime = input.runtime ?? 'node-esm';
|
|
207
|
+
const appName = (input.appName ?? 'totem-app').toLowerCase().replace(/[^a-z0-9-]+/g, '-');
|
|
208
|
+
const dependencies = {};
|
|
209
|
+
for (const name of input.plan.directDependencies) {
|
|
210
|
+
const pkg = index.packages[name];
|
|
211
|
+
if (pkg)
|
|
212
|
+
dependencies[name] = pkg.version;
|
|
213
|
+
}
|
|
214
|
+
const codeSteps = input.plan.workflow.steps.filter((step) => step.kind === 'code');
|
|
215
|
+
const functions = codeSteps.map((step, index) => {
|
|
216
|
+
const adapter = (0, workflow_js_1.getWorkflowAdapter)(step.package);
|
|
217
|
+
if (!adapter)
|
|
218
|
+
throw new Error(`Missing workflow adapter for ${step.package}`);
|
|
219
|
+
return {
|
|
220
|
+
step,
|
|
221
|
+
name: `step${index + 1}`,
|
|
222
|
+
imports: adapter.imports,
|
|
223
|
+
body: adapter.body,
|
|
224
|
+
};
|
|
225
|
+
});
|
|
226
|
+
if (codeSteps.length > 0) {
|
|
227
|
+
for (const runtimePkg of ['@totemsdk/core', '@totemsdk/core-wasm']) {
|
|
228
|
+
if (index.packages[runtimePkg])
|
|
229
|
+
dependencies[runtimePkg] = index.packages[runtimePkg].version;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
const moduleType = runtime === 'node-esm' ? 'module' : 'commonjs';
|
|
233
|
+
const files = [];
|
|
234
|
+
files.push({
|
|
235
|
+
path: 'package.json',
|
|
236
|
+
content: JSON.stringify({
|
|
237
|
+
name: appName,
|
|
238
|
+
private: true,
|
|
239
|
+
type: moduleType,
|
|
240
|
+
scripts: {
|
|
241
|
+
build: 'tsc -p tsconfig.json',
|
|
242
|
+
start: runtime === 'node-esm' ? 'node dist/index.js' : 'node dist/index.js',
|
|
243
|
+
test: 'npm run build && node --test test/*.test.mjs',
|
|
244
|
+
},
|
|
245
|
+
dependencies,
|
|
246
|
+
devDependencies: { typescript: '^5.9.3', '@types/node': '^20.0.0' },
|
|
247
|
+
}, null, 2) + '\n',
|
|
248
|
+
});
|
|
249
|
+
files.push({
|
|
250
|
+
path: 'tsconfig.json',
|
|
251
|
+
content: JSON.stringify({
|
|
252
|
+
compilerOptions: {
|
|
253
|
+
target: 'ES2022',
|
|
254
|
+
module: runtime === 'node-esm' ? 'NodeNext' : 'CommonJS',
|
|
255
|
+
moduleResolution: runtime === 'node-esm' ? 'NodeNext' : 'Node',
|
|
256
|
+
outDir: 'dist',
|
|
257
|
+
rootDir: 'src',
|
|
258
|
+
strict: true,
|
|
259
|
+
esModuleInterop: true,
|
|
260
|
+
skipLibCheck: true,
|
|
261
|
+
},
|
|
262
|
+
include: ['src/**/*.ts'],
|
|
263
|
+
}, null, 2) + '\n',
|
|
264
|
+
});
|
|
265
|
+
const stepFunctions = functions.flatMap(({ step, name, imports, body }) => [
|
|
266
|
+
`async function ${name}(): Promise<string> {`,
|
|
267
|
+
...imports.map((line) => ` ${line}`),
|
|
268
|
+
...body.map((line) => ` ${line}`),
|
|
269
|
+
'}',
|
|
270
|
+
'',
|
|
271
|
+
]);
|
|
272
|
+
const runners = functions.map(({ step, name }) => ` '${step.id}': ${name},`).join('\n');
|
|
273
|
+
files.push({
|
|
274
|
+
path: 'src/workflow.ts',
|
|
275
|
+
content: `export const workflow = ${JSON.stringify(input.plan.workflow, null, 2)} as const;\n`,
|
|
276
|
+
});
|
|
277
|
+
const bootstrap = runtime === 'node-esm' && codeSteps.length > 0
|
|
278
|
+
? [
|
|
279
|
+
"import { initSync } from '@totemsdk/core';",
|
|
280
|
+
"import { readFileSync } from 'node:fs';",
|
|
281
|
+
"import { fileURLToPath } from 'node:url';",
|
|
282
|
+
'',
|
|
283
|
+
"const wasmModuleUrl = (import.meta as ImportMeta & { resolve: (id: string) => string }).resolve('@totemsdk/core-wasm/web');",
|
|
284
|
+
"const wasmPath = fileURLToPath(new URL('totemsdk_core_wasm_bg.wasm', wasmModuleUrl));",
|
|
285
|
+
'(initSync as (options: { module: WebAssembly.Module }) => void)({ module: new WebAssembly.Module(readFileSync(wasmPath)) });',
|
|
286
|
+
'',
|
|
287
|
+
]
|
|
288
|
+
: [];
|
|
289
|
+
files.push({
|
|
290
|
+
path: 'src/index.ts',
|
|
291
|
+
content: [
|
|
292
|
+
...bootstrap,
|
|
293
|
+
"import { workflow } from './workflow.js';",
|
|
294
|
+
'',
|
|
295
|
+
`export const appName = '${appName}';`,
|
|
296
|
+
'export { workflow };',
|
|
297
|
+
'',
|
|
298
|
+
...stepFunctions,
|
|
299
|
+
'const runners: Record<string, () => Promise<string>> = {',
|
|
300
|
+
runners,
|
|
301
|
+
'};',
|
|
302
|
+
'',
|
|
303
|
+
'export interface WorkflowResult {',
|
|
304
|
+
' step: string;',
|
|
305
|
+
" status: 'ok' | 'host-port' | 'unresolved';",
|
|
306
|
+
' result?: string;',
|
|
307
|
+
' detail?: string;',
|
|
308
|
+
'}',
|
|
309
|
+
'',
|
|
310
|
+
'export async function run(): Promise<WorkflowResult[]> {',
|
|
311
|
+
' const results: WorkflowResult[] = [];',
|
|
312
|
+
' for (const step of workflow.steps) {',
|
|
313
|
+
' const runner = runners[step.id];',
|
|
314
|
+
' if (runner) {',
|
|
315
|
+
" results.push({ step: step.id, status: 'ok', result: await runner() });",
|
|
316
|
+
' } else {',
|
|
317
|
+
" results.push({ step: step.id, status: step.kind === 'host-port' ? 'host-port' : 'unresolved', detail: step.contract });",
|
|
318
|
+
' }',
|
|
319
|
+
' }',
|
|
320
|
+
' return results;',
|
|
321
|
+
'}',
|
|
322
|
+
'',
|
|
323
|
+
'if (process.argv[1]?.endsWith("index.js") || process.argv[1]?.endsWith("index.cjs")) {',
|
|
324
|
+
' run().then((results) => console.log(JSON.stringify(results, null, 2))).catch((error) => {',
|
|
325
|
+
' console.error(error);',
|
|
326
|
+
' process.exitCode = 1;',
|
|
327
|
+
' });',
|
|
328
|
+
'}',
|
|
329
|
+
'',
|
|
330
|
+
].join('\n'),
|
|
331
|
+
});
|
|
332
|
+
files.push({
|
|
333
|
+
path: 'src/integrations.ts',
|
|
334
|
+
content: [
|
|
335
|
+
'// Generated integration matrix. Unknown capabilities remain explicit.',
|
|
336
|
+
`export const integrations = ${JSON.stringify(Object.fromEntries(input.plan.directDependencies.map((name) => [name, {
|
|
337
|
+
maturity: input.plan.maturity[name] ?? 'unknown',
|
|
338
|
+
status: 'selected',
|
|
339
|
+
}])), null, 2)} as const;`,
|
|
340
|
+
'',
|
|
341
|
+
`export const limitations = ${JSON.stringify(input.plan.warnings, null, 2)} as const;`,
|
|
342
|
+
'',
|
|
343
|
+
].join('\n'),
|
|
344
|
+
});
|
|
345
|
+
files.push({
|
|
346
|
+
path: 'test/workflow.test.mjs',
|
|
347
|
+
content: [
|
|
348
|
+
"import test from 'node:test';",
|
|
349
|
+
"import assert from 'node:assert/strict';",
|
|
350
|
+
"import { run, workflow } from '../dist/index.js';",
|
|
351
|
+
'',
|
|
352
|
+
"test('generated workflow executes every declared step', async () => {",
|
|
353
|
+
' const results = await run();',
|
|
354
|
+
' assert.equal(results.length, workflow.steps.length);',
|
|
355
|
+
" assert.ok(results.every((entry) => entry.status === 'ok' || entry.status === 'host-port' || entry.status === 'unresolved'));",
|
|
356
|
+
'});',
|
|
357
|
+
'',
|
|
358
|
+
].join('\n'),
|
|
359
|
+
});
|
|
360
|
+
files.push({
|
|
361
|
+
path: 'README.md',
|
|
362
|
+
content: `# ${appName}\n\nGenerated by @totemsdk/mcp-server.\n\nRuntime: ${runtime}\n\n## Install\n\n\`\`\`bash\nnpm install\nnpm run build\nnpm test\n\`\`\`\n\n## Maturity\n\n${Object.entries(input.plan.maturity).map(([name, maturity]) => `- ${name}: ${maturity}`).join('\n')}\n`,
|
|
363
|
+
});
|
|
364
|
+
return {
|
|
365
|
+
files,
|
|
366
|
+
dependencies,
|
|
367
|
+
install: ['npm install', 'npm run build', 'npm test'],
|
|
368
|
+
generationManifest: {
|
|
369
|
+
runtime,
|
|
370
|
+
directDependencies: input.plan.directDependencies,
|
|
371
|
+
maturity: input.plan.maturity,
|
|
372
|
+
warnings: input.plan.warnings,
|
|
373
|
+
},
|
|
374
|
+
};
|
|
375
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded contract inspection by package, export subpath and symbol.
|
|
3
|
+
*
|
|
4
|
+
* Contract metadata comes from the selected index; implementation source is
|
|
5
|
+
* never implied. A subpath is verified against package.json exports, while
|
|
6
|
+
* symbols remain package-scoped because the index does not yet map each symbol
|
|
7
|
+
* to its originating subpath.
|
|
8
|
+
*/
|
|
9
|
+
import type { SdkIndex, ToolResponse } from './types.js';
|
|
10
|
+
export interface InspectContractInput {
|
|
11
|
+
package?: string;
|
|
12
|
+
subpath?: string;
|
|
13
|
+
symbol?: string;
|
|
14
|
+
offset?: number;
|
|
15
|
+
limit?: number;
|
|
16
|
+
includeDocs?: boolean;
|
|
17
|
+
}
|
|
18
|
+
export declare function inspectContract(args: InspectContractInput, index: SdkIndex): ToolResponse;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Bounded contract inspection by package, export subpath and symbol.
|
|
4
|
+
*
|
|
5
|
+
* Contract metadata comes from the selected index; implementation source is
|
|
6
|
+
* never implied. A subpath is verified against package.json exports, while
|
|
7
|
+
* symbols remain package-scoped because the index does not yet map each symbol
|
|
8
|
+
* to its originating subpath.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.inspectContract = inspectContract;
|
|
12
|
+
function hasExportPath(exportsMap, subpath) {
|
|
13
|
+
if (!exportsMap)
|
|
14
|
+
return subpath === '.';
|
|
15
|
+
return exportsMap[subpath] !== undefined;
|
|
16
|
+
}
|
|
17
|
+
function bounded(value, fallback, min, max) {
|
|
18
|
+
const number = typeof value === 'number' && Number.isFinite(value) ? Math.floor(value) : fallback;
|
|
19
|
+
return Math.min(max, Math.max(min, number));
|
|
20
|
+
}
|
|
21
|
+
function inspectContract(args, index) {
|
|
22
|
+
const pkgName = args.package;
|
|
23
|
+
if (!pkgName) {
|
|
24
|
+
return { content: [{ type: 'text', text: JSON.stringify({
|
|
25
|
+
ok: false, code: 'package-required', message: 'package is required',
|
|
26
|
+
}, null, 2) }], isError: true };
|
|
27
|
+
}
|
|
28
|
+
const pkg = index.packages[pkgName];
|
|
29
|
+
if (!pkg) {
|
|
30
|
+
return { content: [{ type: 'text', text: JSON.stringify({
|
|
31
|
+
ok: false, code: 'package-not-found', message: `Package '${pkgName}' is not in the selected SDK index`,
|
|
32
|
+
}, null, 2) }], isError: true };
|
|
33
|
+
}
|
|
34
|
+
const subpath = args.subpath ?? '.';
|
|
35
|
+
const subpathExists = hasExportPath(pkg.exportsMap, subpath);
|
|
36
|
+
if (!subpathExists) {
|
|
37
|
+
return { content: [{ type: 'text', text: JSON.stringify({
|
|
38
|
+
ok: false, code: 'subpath-not-found', package: pkg.name, subpath,
|
|
39
|
+
availableSubpaths: Object.keys(pkg.exportsMap ?? { '.': null }),
|
|
40
|
+
message: `'${pkg.name}' does not export subpath '${subpath}'`,
|
|
41
|
+
}, null, 2) }], isError: true };
|
|
42
|
+
}
|
|
43
|
+
const allSymbols = Object.entries(pkg.symbols ?? {}).sort(([a], [b]) => a.localeCompare(b));
|
|
44
|
+
if (args.symbol) {
|
|
45
|
+
const symbol = allSymbols.find(([name]) => name === args.symbol);
|
|
46
|
+
if (!symbol) {
|
|
47
|
+
const suggestions = allSymbols
|
|
48
|
+
.filter(([name]) => name.toLowerCase().includes(args.symbol.toLowerCase()))
|
|
49
|
+
.slice(0, 10)
|
|
50
|
+
.map(([name]) => name);
|
|
51
|
+
return { content: [{ type: 'text', text: JSON.stringify({
|
|
52
|
+
ok: false, code: 'symbol-not-found', package: pkg.name, symbol: args.symbol,
|
|
53
|
+
suggestions,
|
|
54
|
+
message: `'${pkg.name}' does not export symbol '${args.symbol}'`,
|
|
55
|
+
}, null, 2) }], isError: true };
|
|
56
|
+
}
|
|
57
|
+
return { content: [{ type: 'text', text: JSON.stringify({
|
|
58
|
+
ok: true,
|
|
59
|
+
contentKind: 'contract',
|
|
60
|
+
package: pkg.name,
|
|
61
|
+
version: pkg.version,
|
|
62
|
+
domain: pkg.domain,
|
|
63
|
+
...(pkg.maturity ? { maturity: pkg.maturity } : {}),
|
|
64
|
+
origin: index.source ?? 'unknown',
|
|
65
|
+
sourceRevision: index.sourceRevision ?? null,
|
|
66
|
+
subpath,
|
|
67
|
+
subpathScope: 'package',
|
|
68
|
+
symbol: symbol[0],
|
|
69
|
+
declaration: symbol[1],
|
|
70
|
+
limitations: ['Symbol-to-subpath mapping is package-scoped; verify the import path against package exports.'],
|
|
71
|
+
}, null, 2) }] };
|
|
72
|
+
}
|
|
73
|
+
const offset = bounded(args.offset, 0, 0, allSymbols.length);
|
|
74
|
+
const limit = bounded(args.limit, 25, 1, 100);
|
|
75
|
+
const page = allSymbols.slice(offset, offset + limit);
|
|
76
|
+
const includeDocs = args.includeDocs !== false;
|
|
77
|
+
const symbols = page.map(([name, meta]) => {
|
|
78
|
+
const entry = { name, kind: meta.kind };
|
|
79
|
+
if (meta.signature)
|
|
80
|
+
entry.signature = meta.signature;
|
|
81
|
+
if (meta.deprecated)
|
|
82
|
+
entry.deprecated = true;
|
|
83
|
+
if (includeDocs && meta.doc)
|
|
84
|
+
entry.doc = meta.doc;
|
|
85
|
+
return entry;
|
|
86
|
+
});
|
|
87
|
+
return { content: [{ type: 'text', text: JSON.stringify({
|
|
88
|
+
ok: true,
|
|
89
|
+
contentKind: 'contract',
|
|
90
|
+
package: pkg.name,
|
|
91
|
+
version: pkg.version,
|
|
92
|
+
domain: pkg.domain,
|
|
93
|
+
...(pkg.maturity ? { maturity: pkg.maturity } : {}),
|
|
94
|
+
origin: index.source ?? 'unknown',
|
|
95
|
+
sourceRevision: index.sourceRevision ?? null,
|
|
96
|
+
subpath,
|
|
97
|
+
subpathScope: 'package',
|
|
98
|
+
totalSymbols: allSymbols.length,
|
|
99
|
+
offset,
|
|
100
|
+
limit,
|
|
101
|
+
nextOffset: offset + symbols.length < allSymbols.length ? offset + symbols.length : null,
|
|
102
|
+
symbols,
|
|
103
|
+
limitations: [
|
|
104
|
+
'Symbols are package-scoped; the index does not yet map each symbol to its export subpath.',
|
|
105
|
+
'Contract metadata is not implementation source.',
|
|
106
|
+
],
|
|
107
|
+
}, null, 2) }] };
|
|
108
|
+
}
|