@grampro/kit 2.4.2 → 2.4.4
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/CHANGELOG.md +25 -0
- package/README.md +12 -0
- package/index.cjs +8 -1
- package/package.json +1 -1
- package/source/components/accordion/passport.json +2 -2
- package/source/components/alert/passport.json +2 -2
- package/source/components/avatar/passport.json +2 -2
- package/source/components/badge/passport.json +2 -2
- package/source/components/breadcrumb/passport.json +2 -2
- package/source/components/button/passport.json +2 -2
- package/source/components/card/passport.json +2 -2
- package/source/components/checkbox/passport.json +2 -2
- package/source/components/combobox/passport.json +2 -2
- package/source/components/data-grid/passport.json +2 -2
- package/source/components/date-picker/passport.json +2 -2
- package/source/components/dialog/passport.json +2 -2
- package/source/components/file-uploader/passport.json +2 -2
- package/source/components/input/passport.json +2 -2
- package/source/components/menu/passport.json +2 -2
- package/source/components/modal/passport.json +2 -2
- package/source/components/number-input/passport.json +2 -2
- package/source/components/popover/passport.json +2 -2
- package/source/components/progress/passport.json +2 -2
- package/source/components/radio-group/passport.json +2 -2
- package/source/components/skeleton/passport.json +2 -2
- package/source/components/spinner/passport.json +2 -2
- package/source/components/switch/passport.json +2 -2
- package/source/components/tabs/passport.json +2 -2
- package/source/components/textarea/passport.json +2 -2
- package/source/components/toaster/passport.json +2 -2
- package/source/components/tooltip/passport.json +2 -2
- package/tools/mcp/README.md +132 -0
- package/tools/mcp/figma.cjs +388 -0
- package/tools/mcp/server.cjs +458 -0
- package/tools/passport/extract.cjs +7 -1
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/*
|
|
3
|
+
* An MCP server over the component catalogue.
|
|
4
|
+
*
|
|
5
|
+
* A coding agent asked to build a page has two ways to learn an API: read the
|
|
6
|
+
* source, which costs a lot of context and invites invention, or be told. This
|
|
7
|
+
* tells it — one component at a time, from the passport that was generated
|
|
8
|
+
* from the source and cannot drift from it.
|
|
9
|
+
*
|
|
10
|
+
* It also marks its own homework. `validate_usage` checks written JSX against
|
|
11
|
+
* the passports, which catches the failure that matters: a prop the model
|
|
12
|
+
* remembered from somewhere rather than read here. That was the only mistake
|
|
13
|
+
* in the experiment that justified building this — every type, union value and
|
|
14
|
+
* handler signature the passport carried came out right, and the one error was
|
|
15
|
+
* an invented prop.
|
|
16
|
+
*
|
|
17
|
+
* No dependencies on purpose. MCP is newline-delimited JSON-RPC over stdio,
|
|
18
|
+
* which is a few hundred lines, and a server that ships inside a library every
|
|
19
|
+
* consumer installs should not drag an SDK along for the ride.
|
|
20
|
+
*/
|
|
21
|
+
"use strict";
|
|
22
|
+
|
|
23
|
+
const fs = require("fs");
|
|
24
|
+
const path = require("path");
|
|
25
|
+
const { execFileSync } = require("child_process");
|
|
26
|
+
const { figmaFrame } = require("./figma.cjs");
|
|
27
|
+
|
|
28
|
+
const CATALOGUE = path.join(__dirname, "..", "..", "source", "components");
|
|
29
|
+
const REFERENCES = path.join(__dirname, "..", "..", ".gbs", "skills", "gbs-components", "references");
|
|
30
|
+
|
|
31
|
+
/** The newest protocol revision this server knows. Older clients get theirs back. */
|
|
32
|
+
const PROTOCOL_VERSION = "2025-06-18";
|
|
33
|
+
const SUPPORTED_PROTOCOLS = new Set(["2024-11-05", "2025-03-26", "2025-06-18", "2025-11-25"]);
|
|
34
|
+
|
|
35
|
+
// --------------------------------------------------------------- catalogue
|
|
36
|
+
|
|
37
|
+
const readJson = (file) => JSON.parse(fs.readFileSync(file, "utf8"));
|
|
38
|
+
|
|
39
|
+
const componentFolders = () =>
|
|
40
|
+
fs
|
|
41
|
+
.readdirSync(CATALOGUE, { withFileTypes: true })
|
|
42
|
+
.filter((e) => e.isDirectory() && fs.existsSync(path.join(CATALOGUE, e.name, "passport.json")))
|
|
43
|
+
.map((e) => e.name)
|
|
44
|
+
.sort();
|
|
45
|
+
|
|
46
|
+
/** Resolve a name the way an agent would write it: "DataGrid", "data-grid", "datagrid". */
|
|
47
|
+
function findFolder(name) {
|
|
48
|
+
const wanted = String(name || "").toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
49
|
+
for (const folder of componentFolders()) {
|
|
50
|
+
if (folder.replace(/[^a-z0-9]/g, "") === wanted) return folder;
|
|
51
|
+
const passport = readJson(path.join(CATALOGUE, folder, "passport.json"));
|
|
52
|
+
const names = [passport.identity.component, ...(passport.identity.exports || [])];
|
|
53
|
+
if (names.some((n) => String(n).toLowerCase() === wanted)) return folder;
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/*
|
|
59
|
+
* Most passports carry no `purpose`, so the README's opening line stands in.
|
|
60
|
+
* It is the sentence a human wrote about what the component is for, which is
|
|
61
|
+
* exactly what an agent needs to pick one.
|
|
62
|
+
*/
|
|
63
|
+
function purposeOf(folder, passport) {
|
|
64
|
+
if (passport.purpose && passport.purpose.trim()) return passport.purpose.trim();
|
|
65
|
+
const readme = path.join(CATALOGUE, folder, "README.md");
|
|
66
|
+
if (!fs.existsSync(readme)) return "";
|
|
67
|
+
const line = fs
|
|
68
|
+
.readFileSync(readme, "utf8")
|
|
69
|
+
.split("\n")
|
|
70
|
+
.map((l) => l.trim())
|
|
71
|
+
.find((l) => l && !l.startsWith("#") && !l.startsWith("<!--"));
|
|
72
|
+
return line ? line.replace(/\s+/g, " ") : "";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// -------------------------------------------------------------------- tools
|
|
76
|
+
|
|
77
|
+
function listComponents() {
|
|
78
|
+
return componentFolders().map((folder) => {
|
|
79
|
+
const passport = readJson(path.join(CATALOGUE, folder, "passport.json"));
|
|
80
|
+
return {
|
|
81
|
+
component: passport.identity.component,
|
|
82
|
+
folder,
|
|
83
|
+
exports: passport.identity.exports || [],
|
|
84
|
+
purpose: purposeOf(folder, passport),
|
|
85
|
+
};
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The passport, trimmed to what writing code needs.
|
|
91
|
+
*
|
|
92
|
+
* The whole file averages 8.6 KB and runs to 26 KB; most of that is provenance
|
|
93
|
+
* an agent cannot act on. What it can act on is the shape: names, types, the
|
|
94
|
+
* union values for enum props, which are required, and the handler signatures.
|
|
95
|
+
*/
|
|
96
|
+
function getComponent(name) {
|
|
97
|
+
const folder = findFolder(name);
|
|
98
|
+
if (!folder) {
|
|
99
|
+
return { error: `No component named "${name}". Call list_components for the catalogue.` };
|
|
100
|
+
}
|
|
101
|
+
const p = readJson(path.join(CATALOGUE, folder, "passport.json"));
|
|
102
|
+
return {
|
|
103
|
+
component: p.identity.component,
|
|
104
|
+
folder,
|
|
105
|
+
exports: p.identity.exports || [],
|
|
106
|
+
importFrom: `@/component-lib/${folder}`,
|
|
107
|
+
purpose: purposeOf(folder, p),
|
|
108
|
+
props: (p.props || []).map((prop) => ({
|
|
109
|
+
name: prop.name,
|
|
110
|
+
type: prop.typeText || prop.type,
|
|
111
|
+
values: prop.values,
|
|
112
|
+
required: prop.required || undefined,
|
|
113
|
+
default: prop.default,
|
|
114
|
+
description: prop.description,
|
|
115
|
+
})),
|
|
116
|
+
events: (p.events || []).map((e) => ({
|
|
117
|
+
name: e.name,
|
|
118
|
+
signature: e.signature,
|
|
119
|
+
description: e.description,
|
|
120
|
+
})),
|
|
121
|
+
slots: p.slots || [],
|
|
122
|
+
states: p.states || [],
|
|
123
|
+
composition: p.composition || {},
|
|
124
|
+
accessibility: p.accessibility || {},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** The README's code fences: how the component is meant to be used. */
|
|
129
|
+
function getExample(name) {
|
|
130
|
+
const folder = findFolder(name);
|
|
131
|
+
if (!folder) return { error: `No component named "${name}".` };
|
|
132
|
+
const readme = path.join(CATALOGUE, folder, "README.md");
|
|
133
|
+
if (!fs.existsSync(readme)) return { error: `No README for ${folder}.` };
|
|
134
|
+
|
|
135
|
+
const blocks = [];
|
|
136
|
+
const text = fs.readFileSync(readme, "utf8");
|
|
137
|
+
const fence = /```(\w*)\n([\s\S]*?)```/g;
|
|
138
|
+
let match;
|
|
139
|
+
while ((match = fence.exec(text))) blocks.push({ language: match[1] || "txt", code: match[2].trim() });
|
|
140
|
+
return { component: folder, examples: blocks };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** The authored guidance that ships with the agent skill. */
|
|
144
|
+
function getGuidance(topic) {
|
|
145
|
+
const available = fs.existsSync(REFERENCES)
|
|
146
|
+
? fs.readdirSync(REFERENCES).filter((f) => f.endsWith(".md")).map((f) => f.replace(/\.md$/, ""))
|
|
147
|
+
: [];
|
|
148
|
+
if (!topic) return { topics: available };
|
|
149
|
+
const file = path.join(REFERENCES, `${topic}.md`);
|
|
150
|
+
if (!fs.existsSync(file)) return { error: `No guidance for "${topic}".`, topics: available };
|
|
151
|
+
return { topic, text: fs.readFileSync(file, "utf8") };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/*
|
|
155
|
+
* Check written JSX against the passports.
|
|
156
|
+
*
|
|
157
|
+
* Deliberately tolerant parsing rather than a real one: the agent hands over a
|
|
158
|
+
* fragment, not a file, and the failure worth catching is a prop that does not
|
|
159
|
+
* exist. Spread props and expressions are skipped rather than guessed at — a
|
|
160
|
+
* validator that invents errors is worse than none.
|
|
161
|
+
*/
|
|
162
|
+
function validateUsage(code) {
|
|
163
|
+
const source = String(code || "");
|
|
164
|
+
const known = new Map();
|
|
165
|
+
for (const folder of componentFolders()) {
|
|
166
|
+
const p = readJson(path.join(CATALOGUE, folder, "passport.json"));
|
|
167
|
+
const props = new Set((p.props || []).map((x) => x.name));
|
|
168
|
+
for (const e of p.events || []) props.add(e.name);
|
|
169
|
+
const required = (p.props || []).filter((x) => x.required).map((x) => x.name);
|
|
170
|
+
for (const exported of [p.identity.component, ...(p.identity.exports || [])]) {
|
|
171
|
+
known.set(exported, { folder, props, required, values: p.props || [] });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const issues = [];
|
|
176
|
+
const used = [];
|
|
177
|
+
const opening = /<([A-Z][A-Za-z0-9]*)(?=[\s/>])/g;
|
|
178
|
+
let match;
|
|
179
|
+
|
|
180
|
+
while ((match = opening.exec(source))) {
|
|
181
|
+
const [, name] = match;
|
|
182
|
+
const entry = known.get(name);
|
|
183
|
+
if (!entry) continue; // Not ours; the agent may compose with its own components.
|
|
184
|
+
|
|
185
|
+
/*
|
|
186
|
+
* Scan to the tag's own `>` rather than matching one. An attribute can
|
|
187
|
+
* hold an arrow function — `onDismiss={() => {}}` — so the first `>` is
|
|
188
|
+
* usually inside a brace, and a regex that stops there finds nothing.
|
|
189
|
+
*/
|
|
190
|
+
let depth = 0;
|
|
191
|
+
let quote = null;
|
|
192
|
+
let end = -1;
|
|
193
|
+
for (let i = match.index + match[0].length; i < source.length; i += 1) {
|
|
194
|
+
const ch = source[i];
|
|
195
|
+
if (quote) {
|
|
196
|
+
if (ch === quote) quote = null;
|
|
197
|
+
} else if (ch === '"' || ch === "'" || ch === "`") {
|
|
198
|
+
quote = ch;
|
|
199
|
+
} else if (ch === "{") {
|
|
200
|
+
depth += 1;
|
|
201
|
+
} else if (ch === "}") {
|
|
202
|
+
depth -= 1;
|
|
203
|
+
} else if (ch === ">" && depth === 0) {
|
|
204
|
+
end = i;
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (end === -1) continue; // Unterminated fragment; nothing reliable to say.
|
|
209
|
+
|
|
210
|
+
const rawAttrs = source.slice(match.index + match[0].length, end);
|
|
211
|
+
const attrs = [...rawAttrs.matchAll(/(?:^|\s)([a-zA-Z][a-zA-Z0-9:_-]*)(?=\s*=|\s|$)/g)].map(
|
|
212
|
+
(m) => m[1],
|
|
213
|
+
);
|
|
214
|
+
used.push({ component: name, props: attrs });
|
|
215
|
+
|
|
216
|
+
for (const attr of attrs) {
|
|
217
|
+
if (entry.props.has(attr)) continue;
|
|
218
|
+
if (attr.startsWith("data-") || attr.startsWith("aria-") || attr === "key") continue;
|
|
219
|
+
/*
|
|
220
|
+
* A wrong prop is usually a near miss for a real one — `dismissible`
|
|
221
|
+
* for `onDismiss`, `onChange` for `onValueChange`. Matching on a shared
|
|
222
|
+
* stem in either direction finds those; matching on a prefix does not,
|
|
223
|
+
* because the handler's name starts with `on`.
|
|
224
|
+
*/
|
|
225
|
+
const low = attr.toLowerCase();
|
|
226
|
+
const stem = low.slice(0, Math.max(4, Math.round(low.length * 0.6)));
|
|
227
|
+
const near = [...entry.props].filter((p) => {
|
|
228
|
+
const other = p.toLowerCase();
|
|
229
|
+
return other.includes(stem) || low.includes(other.replace(/^on/, "").slice(0, 5));
|
|
230
|
+
});
|
|
231
|
+
issues.push({
|
|
232
|
+
component: name,
|
|
233
|
+
prop: attr,
|
|
234
|
+
problem: "not a prop of this component",
|
|
235
|
+
didYouMean: near.slice(0, 4),
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
for (const req of entry.required) {
|
|
240
|
+
if (!attrs.includes(req) && !/\{\s*\.\.\./.test(rawAttrs)) {
|
|
241
|
+
issues.push({ component: name, prop: req, problem: "required prop is missing" });
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
checked: used,
|
|
248
|
+
issues,
|
|
249
|
+
ok: issues.length === 0,
|
|
250
|
+
summary: issues.length
|
|
251
|
+
? `${issues.length} problem(s) across ${used.length} element(s).`
|
|
252
|
+
: `${used.length} element(s) check out against their passports.`,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/*
|
|
257
|
+
* Put components into the project the agent is working in.
|
|
258
|
+
*
|
|
259
|
+
* The CLI already knows where they belong — it finds an existing install or
|
|
260
|
+
* follows the project's own layout — so this runs it rather than
|
|
261
|
+
* reimplementing that judgement.
|
|
262
|
+
*/
|
|
263
|
+
function installComponents(names) {
|
|
264
|
+
const list = Array.isArray(names) ? names : String(names || "").split(/[\s,]+/);
|
|
265
|
+
const wanted = list.map((n) => String(n).trim()).filter(Boolean);
|
|
266
|
+
if (wanted.length === 0) return { error: "Name at least one component." };
|
|
267
|
+
|
|
268
|
+
const unknown = wanted.filter((n) => !findFolder(n));
|
|
269
|
+
if (unknown.length) {
|
|
270
|
+
return {
|
|
271
|
+
error: `Not in the catalogue: ${unknown.join(", ")}. Call list_components.`,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const cli = path.join(__dirname, "..", "..", "index.cjs");
|
|
276
|
+
try {
|
|
277
|
+
const output = execFileSync(process.execPath, [cli, "add", ...wanted], {
|
|
278
|
+
cwd: process.cwd(),
|
|
279
|
+
encoding: "utf8",
|
|
280
|
+
timeout: 120000,
|
|
281
|
+
});
|
|
282
|
+
// The installer prints the one manual step; the agent needs to see it.
|
|
283
|
+
const importLine = (output.match(/^\s*@import .+$/gm) || []).map((l) => l.trim());
|
|
284
|
+
return { installed: wanted, stylesheet: importLine, output: output.trim().slice(-1200) };
|
|
285
|
+
} catch (error) {
|
|
286
|
+
return { error: `Install failed: ${(error.stdout || error.message || "").toString().slice(-600)}` };
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const TOOLS = [
|
|
291
|
+
{
|
|
292
|
+
name: "list_components",
|
|
293
|
+
description:
|
|
294
|
+
"Every component in the catalogue with its exports and what it is for. Call this first when deciding which component a design calls for.",
|
|
295
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
296
|
+
run: () => listComponents(),
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
name: "get_component",
|
|
300
|
+
description:
|
|
301
|
+
"A component's full contract: props with types and allowed values, which are required, event handler signatures, slots, states and composition rules. Read this before writing any JSX that uses it, rather than recalling the API.",
|
|
302
|
+
inputSchema: {
|
|
303
|
+
type: "object",
|
|
304
|
+
properties: { name: { type: "string", description: 'Component or folder name, e.g. "DataGrid".' } },
|
|
305
|
+
required: ["name"],
|
|
306
|
+
additionalProperties: false,
|
|
307
|
+
},
|
|
308
|
+
run: (args) => getComponent(args.name),
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
name: "get_example",
|
|
312
|
+
description: "Working usage taken from the component's own README.",
|
|
313
|
+
inputSchema: {
|
|
314
|
+
type: "object",
|
|
315
|
+
properties: { name: { type: "string" } },
|
|
316
|
+
required: ["name"],
|
|
317
|
+
additionalProperties: false,
|
|
318
|
+
},
|
|
319
|
+
run: (args) => getExample(args.name),
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
name: "get_guidance",
|
|
323
|
+
description:
|
|
324
|
+
"Authored guidance on a topic: forms, overlays, data-grid, pickers, styling, accessibility, data-display, toaster, install. Omit the topic to list them.",
|
|
325
|
+
inputSchema: {
|
|
326
|
+
type: "object",
|
|
327
|
+
properties: { topic: { type: "string" } },
|
|
328
|
+
additionalProperties: false,
|
|
329
|
+
},
|
|
330
|
+
run: (args) => getGuidance(args.topic),
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
name: "figma_frame",
|
|
334
|
+
description:
|
|
335
|
+
"BETA. Read a Figma design, from a URL (needs FIGMA_TOKEN) or from the plugin export (no token). Called with just a URL it lists the frames in the file; called with `frame` it returns that screen's structure, text, the repeated regions that indicate lists and tables, and the theme variables its colours and radii map onto. Needs FIGMA_TOKEN.",
|
|
336
|
+
inputSchema: {
|
|
337
|
+
type: "object",
|
|
338
|
+
properties: {
|
|
339
|
+
url: { type: "string", description: "A figma.com/design/... URL. Needs FIGMA_TOKEN." },
|
|
340
|
+
design: {
|
|
341
|
+
type: "string",
|
|
342
|
+
description:
|
|
343
|
+
"Alternative to url, needing no token: JSON from the kit's Figma plugin, 'Export selection for an agent'.",
|
|
344
|
+
},
|
|
345
|
+
frame: { type: "string", description: "Which frame to read, from the listing." },
|
|
346
|
+
depth: { type: "number", description: "How deep to descend. Lower it if the payload is large." },
|
|
347
|
+
},
|
|
348
|
+
required: [],
|
|
349
|
+
additionalProperties: false,
|
|
350
|
+
},
|
|
351
|
+
run: (args) => figmaFrame(args),
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
name: "install_component",
|
|
355
|
+
description:
|
|
356
|
+
"Copy components into the project being worked in, with their shared folder and stylesheet. Run this before importing them.",
|
|
357
|
+
inputSchema: {
|
|
358
|
+
type: "object",
|
|
359
|
+
properties: {
|
|
360
|
+
names: {
|
|
361
|
+
type: "array",
|
|
362
|
+
items: { type: "string" },
|
|
363
|
+
description: 'Component names, e.g. ["DataGrid", "Button"].',
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
required: ["names"],
|
|
367
|
+
additionalProperties: false,
|
|
368
|
+
},
|
|
369
|
+
run: (args) => installComponents(args.names),
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
name: "validate_usage",
|
|
373
|
+
description:
|
|
374
|
+
"Check JSX against the passports and report props that do not exist or required props left out. Run this on code you have written before telling the user it is done.",
|
|
375
|
+
inputSchema: {
|
|
376
|
+
type: "object",
|
|
377
|
+
properties: { code: { type: "string", description: "JSX source to check." } },
|
|
378
|
+
required: ["code"],
|
|
379
|
+
additionalProperties: false,
|
|
380
|
+
},
|
|
381
|
+
run: (args) => validateUsage(args.code),
|
|
382
|
+
},
|
|
383
|
+
];
|
|
384
|
+
|
|
385
|
+
// ------------------------------------------------------------- the protocol
|
|
386
|
+
|
|
387
|
+
const send = (message) => process.stdout.write(JSON.stringify(message) + "\n");
|
|
388
|
+
const reply = (id, result) => send({ jsonrpc: "2.0", id, result });
|
|
389
|
+
const fail = (id, code, message) => send({ jsonrpc: "2.0", id, error: { code, message } });
|
|
390
|
+
|
|
391
|
+
function handle(request) {
|
|
392
|
+
const { id, method, params } = request;
|
|
393
|
+
|
|
394
|
+
if (method === "initialize") {
|
|
395
|
+
const asked = params && params.protocolVersion;
|
|
396
|
+
return reply(id, {
|
|
397
|
+
protocolVersion: SUPPORTED_PROTOCOLS.has(asked) ? asked : PROTOCOL_VERSION,
|
|
398
|
+
capabilities: { tools: {} },
|
|
399
|
+
serverInfo: { name: "grampro-kit", version: require("../../package.json").version },
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (method === "notifications/initialized") return; // A notification; no reply.
|
|
404
|
+
if (method === "ping") return reply(id, {});
|
|
405
|
+
|
|
406
|
+
if (method === "tools/list") {
|
|
407
|
+
return reply(id, {
|
|
408
|
+
tools: TOOLS.map(({ name, description, inputSchema }) => ({ name, description, inputSchema })),
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (method === "tools/call") {
|
|
413
|
+
const tool = TOOLS.find((t) => t.name === (params && params.name));
|
|
414
|
+
if (!tool) return fail(id, -32602, `Unknown tool: ${params && params.name}`);
|
|
415
|
+
try {
|
|
416
|
+
// Some tools reach the network, so a result may be a promise.
|
|
417
|
+
const result = tool.run((params && params.arguments) || {});
|
|
418
|
+
return Promise.resolve(result).then(
|
|
419
|
+
(value) => reply(id, { content: [{ type: "text", text: JSON.stringify(value, null, 2) }] }),
|
|
420
|
+
(error) =>
|
|
421
|
+
reply(id, { content: [{ type: "text", text: `Tool failed: ${error.message}` }], isError: true }),
|
|
422
|
+
);
|
|
423
|
+
} catch (error) {
|
|
424
|
+
// Reported as a result, not a protocol error: the model should see it and retry.
|
|
425
|
+
return reply(id, {
|
|
426
|
+
content: [{ type: "text", text: `Tool failed: ${error.message}` }],
|
|
427
|
+
isError: true,
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (id !== undefined) fail(id, -32601, `Method not found: ${method}`);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function main() {
|
|
436
|
+
let buffer = "";
|
|
437
|
+
process.stdin.setEncoding("utf8");
|
|
438
|
+
process.stdin.on("data", (chunk) => {
|
|
439
|
+
buffer += chunk;
|
|
440
|
+
// Newline-delimited JSON: a message may arrive split across chunks.
|
|
441
|
+
let newline;
|
|
442
|
+
while ((newline = buffer.indexOf("\n")) !== -1) {
|
|
443
|
+
const line = buffer.slice(0, newline).trim();
|
|
444
|
+
buffer = buffer.slice(newline + 1);
|
|
445
|
+
if (!line) continue;
|
|
446
|
+
try {
|
|
447
|
+
handle(JSON.parse(line));
|
|
448
|
+
} catch {
|
|
449
|
+
fail(null, -32700, "Parse error");
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
process.stdin.on("end", () => process.exit(0));
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (require.main === module) main();
|
|
457
|
+
|
|
458
|
+
module.exports = { main, listComponents, getComponent, getExample, getGuidance, validateUsage, installComponents, TOOLS };
|
|
@@ -65,13 +65,19 @@ function sourceFiles(dir) {
|
|
|
65
65
|
/**
|
|
66
66
|
* Hash of the inputs the passport is derived from. Deterministic: sorted paths,
|
|
67
67
|
* file contents only, no timestamps.
|
|
68
|
+
*
|
|
69
|
+
* Line endings are normalised first. The same file arrives as CRLF on a
|
|
70
|
+
* Windows checkout and LF on CI, so hashing the bytes verbatim made a
|
|
71
|
+
* passport generated on one platform read as stale on the other — which it
|
|
72
|
+
* did, for all 27 components at once. A content hash should not depend on
|
|
73
|
+
* how the file was checked out.
|
|
68
74
|
*/
|
|
69
75
|
function hashSources(files, root) {
|
|
70
76
|
const h = crypto.createHash("sha256");
|
|
71
77
|
for (const f of [...files].sort()) {
|
|
72
78
|
h.update(posix(path.relative(root, f)));
|
|
73
79
|
h.update("\0");
|
|
74
|
-
h.update(fs.readFileSync(f));
|
|
80
|
+
h.update(fs.readFileSync(f, "utf8").replace(/\r\n/g, "\n"));
|
|
75
81
|
h.update("\0");
|
|
76
82
|
}
|
|
77
83
|
return `sha256:${h.digest("hex")}`;
|