@svgrid/mcp 2.6.4 → 2.6.6
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/README.md +61 -1
- package/dist/compile-svelte.d.ts +2 -0
- package/dist/compile-svelte.js +74 -0
- package/dist/data.d.ts +31 -0
- package/dist/data.js +3530 -285
- package/dist/index.d.ts +14 -0
- package/dist/index.js +152 -25
- package/dist/project-tools.d.ts +16 -0
- package/dist/search.d.ts +37 -0
- package/dist/search.js +99 -0
- package/dist/validate.d.ts +101 -0
- package/dist/validate.js +1018 -0
- package/package.json +21 -1
- package/server.json +25 -25
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* SvGrid MCP server (stdio).
|
|
4
|
+
*
|
|
5
|
+
* Exposes the SvGrid example sources, docs, and curated API reference as
|
|
6
|
+
* Model Context Protocol tools. Point an MCP-capable client (Claude
|
|
7
|
+
* Desktop, Claude Code, etc.) at this server to give the model accurate,
|
|
8
|
+
* version-pinned answers about SvGrid - no hallucinated APIs, no stale
|
|
9
|
+
* blog-post output.
|
|
10
|
+
*
|
|
11
|
+
* Run with:
|
|
12
|
+
* npx @svgrid/mcp
|
|
13
|
+
*/
|
|
14
|
+
export {};
|
package/dist/index.js
CHANGED
|
@@ -15,8 +15,11 @@ import { createRequire } from 'node:module';
|
|
|
15
15
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
16
16
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
17
17
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
18
|
-
import { apiReference, docs, examples } from './data.js';
|
|
18
|
+
import { apiReference, apiSurface, docs, examples } from './data.js';
|
|
19
19
|
import { projectTools, handleProjectTool } from './project-tools.js';
|
|
20
|
+
import { checkSvGridCode } from './validate.js';
|
|
21
|
+
import { compileWithSvelte } from './compile-svelte.js';
|
|
22
|
+
import { rankDocs } from './search.js';
|
|
20
23
|
import { checkLicenseKey, introspectDrizzle, introspectJson, scaffold, summarizeVerify, verifyScaffold, } from '@svgrid/enterprise/studio';
|
|
21
24
|
/**
|
|
22
25
|
* Soft commercial gate. Uses the SAME classifier as the browser
|
|
@@ -43,6 +46,28 @@ const DOCS_FOOTER = '\n\nSvGrid reference: full docs & 370+ live demos at https:
|
|
|
43
46
|
function withDocs(text) {
|
|
44
47
|
return { content: [{ type: 'text', text: text + DOCS_FOOTER }] };
|
|
45
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Shorten a blurb for a listing. Demo blurbs run to ~240 chars and 373 of them
|
|
51
|
+
* is most of a context window, so the listings carry a one-line version and
|
|
52
|
+
* get_example_source still returns the full text.
|
|
53
|
+
*/
|
|
54
|
+
function trimBlurb(text, max = 120) {
|
|
55
|
+
const s = String(text ?? '').replace(/\s+/g, ' ').trim();
|
|
56
|
+
if (s.length <= max)
|
|
57
|
+
return s;
|
|
58
|
+
const cut = s.slice(0, max);
|
|
59
|
+
const space = cut.lastIndexOf(' ');
|
|
60
|
+
return (space > 40 ? cut.slice(0, space) : cut) + '...';
|
|
61
|
+
}
|
|
62
|
+
/** { value: count } over a key, ordered by descending count. */
|
|
63
|
+
function countBy(rows, key) {
|
|
64
|
+
const counts = new Map();
|
|
65
|
+
for (const row of rows) {
|
|
66
|
+
const k = key(row) || 'Other';
|
|
67
|
+
counts.set(k, (counts.get(k) ?? 0) + 1);
|
|
68
|
+
}
|
|
69
|
+
return Object.fromEntries([...counts].sort((a, b) => b[1] - a[1]));
|
|
70
|
+
}
|
|
46
71
|
// Report the real package version to MCP clients (read from package.json, which
|
|
47
72
|
// ships in the tarball at ../package.json relative to the built dist/index.js),
|
|
48
73
|
// so serverInfo.version never drifts from the published version.
|
|
@@ -67,8 +92,21 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
67
92
|
tools: [
|
|
68
93
|
{
|
|
69
94
|
name: 'list_examples',
|
|
70
|
-
description: '
|
|
71
|
-
inputSchema: {
|
|
95
|
+
description: 'Find SvGrid example demos. Returns id, title, category and a one-line blurb (not source). Call with no arguments for a category index plus the first page; filter with `query` and/or `category` to find a specific demo, then call get_example_source with its id.',
|
|
96
|
+
inputSchema: {
|
|
97
|
+
type: 'object',
|
|
98
|
+
properties: {
|
|
99
|
+
query: {
|
|
100
|
+
type: 'string',
|
|
101
|
+
description: 'Free-text filter over id, title, blurb and category, e.g. "kanban" or "server side".',
|
|
102
|
+
},
|
|
103
|
+
category: {
|
|
104
|
+
type: 'string',
|
|
105
|
+
description: 'Exact category, e.g. "Kanban" or "Inputs". Call with no arguments to see the available categories.',
|
|
106
|
+
},
|
|
107
|
+
limit: { type: 'number', description: 'Max results, default 25, max 100.', default: 25 },
|
|
108
|
+
},
|
|
109
|
+
},
|
|
72
110
|
},
|
|
73
111
|
{
|
|
74
112
|
name: 'get_example_source',
|
|
@@ -81,8 +119,21 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
81
119
|
},
|
|
82
120
|
{
|
|
83
121
|
name: 'list_docs',
|
|
84
|
-
description: '
|
|
85
|
-
inputSchema: {
|
|
122
|
+
description: 'Find SvGrid documentation pages. Returns slug, title and section. Call with no arguments for a section index plus the first page; filter with `query` and/or `section`, then call get_doc with a slug. Slugs use forward slashes, e.g. "help/columns/column-definitions". To search page CONTENT rather than titles, use search_docs.',
|
|
123
|
+
inputSchema: {
|
|
124
|
+
type: 'object',
|
|
125
|
+
properties: {
|
|
126
|
+
query: {
|
|
127
|
+
type: 'string',
|
|
128
|
+
description: 'Free-text filter over slug, title and section, e.g. "column" or "export".',
|
|
129
|
+
},
|
|
130
|
+
section: {
|
|
131
|
+
type: 'string',
|
|
132
|
+
description: 'Exact section, e.g. "Columns" or "Server data". Call with no arguments to see the available sections.',
|
|
133
|
+
},
|
|
134
|
+
limit: { type: 'number', description: 'Max results, default 30, max 100.', default: 30 },
|
|
135
|
+
},
|
|
136
|
+
},
|
|
86
137
|
},
|
|
87
138
|
{
|
|
88
139
|
name: 'get_doc',
|
|
@@ -95,7 +146,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
95
146
|
},
|
|
96
147
|
{
|
|
97
148
|
name: 'search_docs',
|
|
98
|
-
description: '
|
|
149
|
+
description: 'Ranked full-text search across all SvGrid docs. Matches the query term by term (so "row virtualization" finds a page phrasing it either way) and returns the best pages first, each with a relevance score and an excerpt around the hit. Use this to find grounding before writing SvGrid code.',
|
|
99
150
|
inputSchema: {
|
|
100
151
|
type: 'object',
|
|
101
152
|
properties: {
|
|
@@ -110,6 +161,21 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
110
161
|
description: 'Return the curated SvGrid public-API surface, grouped by category (components, headless, scheduler, data ops, export, row models, features, virtualization, accessibility, utilities).',
|
|
111
162
|
inputSchema: { type: 'object', properties: {} },
|
|
112
163
|
},
|
|
164
|
+
{
|
|
165
|
+
name: 'check_svgrid_code',
|
|
166
|
+
description: 'Verify SvGrid code BEFORE handing it to the user. Checks the source against the real exported surface of the installed version - <SvGrid> prop names, ColumnDef keys, grid API methods, importable symbols and theme files - plus Svelte 5 runes rules, and compiles it with the Svelte compiler when one is reachable. Returns line-numbered diagnostics with the exact replacement for each. Run this on every .svelte or .ts file you write that uses SvGrid, then fix what it reports and run it again.',
|
|
167
|
+
inputSchema: {
|
|
168
|
+
type: 'object',
|
|
169
|
+
properties: {
|
|
170
|
+
source: { type: 'string', description: 'The full file contents to check.' },
|
|
171
|
+
filename: {
|
|
172
|
+
type: 'string',
|
|
173
|
+
description: 'File name, used to pick the rules that apply. Defaults to "Component.svelte". Use the real name when you have one (e.g. "src/routes/+page.svelte", "state.svelte.ts").',
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
required: ['source'],
|
|
177
|
+
},
|
|
178
|
+
},
|
|
113
179
|
{
|
|
114
180
|
name: 'introspect_source',
|
|
115
181
|
description: 'SvGrid Studio (commercial): infer an EntitySchema from a data source. Pass a Drizzle schema file (kind:"drizzle", source: the file text) or sample rows (kind:"json", rows, name). Returns a DRAFT EntitySchema to review/refine before scaffolding code.',
|
|
@@ -160,9 +226,41 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
160
226
|
if (projectResult)
|
|
161
227
|
return projectResult;
|
|
162
228
|
switch (name) {
|
|
229
|
+
// Returning all 373 demos cost ~31k tokens on the call this tool's own
|
|
230
|
+
// description invites a model to start with. Filtered and capped instead,
|
|
231
|
+
// and a bare call answers with the category index to drill into.
|
|
163
232
|
case 'list_examples': {
|
|
164
|
-
const
|
|
165
|
-
|
|
233
|
+
const a = (args ?? {});
|
|
234
|
+
const limit = Math.max(1, Math.min(100, Number(a.limit ?? 25)));
|
|
235
|
+
const q = String(a.query ?? '').trim().toLowerCase();
|
|
236
|
+
const category = String(a.category ?? '').trim().toLowerCase();
|
|
237
|
+
const pool = examples.filter((e) => {
|
|
238
|
+
if (category && e.category.toLowerCase() !== category)
|
|
239
|
+
return false;
|
|
240
|
+
if (q && !`${e.id} ${e.title} ${e.blurb} ${e.category}`.toLowerCase().includes(q))
|
|
241
|
+
return false;
|
|
242
|
+
return true;
|
|
243
|
+
});
|
|
244
|
+
const shown = pool.slice(0, limit);
|
|
245
|
+
const body = {
|
|
246
|
+
total: pool.length,
|
|
247
|
+
shown: shown.length,
|
|
248
|
+
examples: shown.map((e) => ({
|
|
249
|
+
id: e.id,
|
|
250
|
+
title: e.title,
|
|
251
|
+
category: e.category,
|
|
252
|
+
blurb: trimBlurb(e.blurb),
|
|
253
|
+
})),
|
|
254
|
+
};
|
|
255
|
+
if (!q && !category)
|
|
256
|
+
body.categories = countBy(examples, (e) => e.category);
|
|
257
|
+
if (shown.length < pool.length) {
|
|
258
|
+
body.hint = `Showing ${shown.length} of ${pool.length}. Narrow with \`query\` or \`category\`, or raise \`limit\` (max 100).`;
|
|
259
|
+
}
|
|
260
|
+
if (!pool.length) {
|
|
261
|
+
body.hint = 'No match. Drop `category`, or try a broader `query`.';
|
|
262
|
+
}
|
|
263
|
+
return withDocs(JSON.stringify(body, null, 2));
|
|
166
264
|
}
|
|
167
265
|
case 'get_example_source': {
|
|
168
266
|
const id = String(args?.id ?? '');
|
|
@@ -179,9 +277,36 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
179
277
|
],
|
|
180
278
|
};
|
|
181
279
|
}
|
|
280
|
+
// Same shape as list_examples, for the same reason: all 370 pages was
|
|
281
|
+
// ~12.7k tokens. `path` is dropped from the listing because it is always
|
|
282
|
+
// "docs/<slug>.md" and get_doc takes the slug.
|
|
182
283
|
case 'list_docs': {
|
|
183
|
-
const
|
|
184
|
-
|
|
284
|
+
const a = (args ?? {});
|
|
285
|
+
const limit = Math.max(1, Math.min(100, Number(a.limit ?? 30)));
|
|
286
|
+
const q = String(a.query ?? '').trim().toLowerCase();
|
|
287
|
+
const section = String(a.section ?? '').trim().toLowerCase();
|
|
288
|
+
const pool = docs.filter((d) => {
|
|
289
|
+
if (section && d.section.toLowerCase() !== section)
|
|
290
|
+
return false;
|
|
291
|
+
if (q && !`${d.slug} ${d.title} ${d.section}`.toLowerCase().includes(q))
|
|
292
|
+
return false;
|
|
293
|
+
return true;
|
|
294
|
+
});
|
|
295
|
+
const shown = pool.slice(0, limit);
|
|
296
|
+
const body = {
|
|
297
|
+
total: pool.length,
|
|
298
|
+
shown: shown.length,
|
|
299
|
+
docs: shown.map((d) => ({ slug: d.slug, title: d.title, section: d.section })),
|
|
300
|
+
};
|
|
301
|
+
if (!q && !section)
|
|
302
|
+
body.sections = countBy(docs, (d) => d.section);
|
|
303
|
+
if (shown.length < pool.length) {
|
|
304
|
+
body.hint = `Showing ${shown.length} of ${pool.length}. Narrow with \`query\` or \`section\`, raise \`limit\` (max 100), or use search_docs to search page content.`;
|
|
305
|
+
}
|
|
306
|
+
if (!pool.length) {
|
|
307
|
+
body.hint = 'No match. Drop `section`, or try search_docs to search page content instead of titles.';
|
|
308
|
+
}
|
|
309
|
+
return withDocs(JSON.stringify(body, null, 2));
|
|
185
310
|
}
|
|
186
311
|
case 'get_doc': {
|
|
187
312
|
const slug = String(args?.slug ?? '');
|
|
@@ -201,25 +326,27 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
201
326
|
if (!query) {
|
|
202
327
|
return { isError: true, content: [{ type: 'text', text: 'query is required' }] };
|
|
203
328
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
const idx = lower.indexOf(q);
|
|
209
|
-
if (idx >= 0) {
|
|
210
|
-
const start = Math.max(0, idx - 60);
|
|
211
|
-
const end = Math.min(d.markdown.length, idx + q.length + 120);
|
|
212
|
-
const excerpt = d.markdown.slice(start, end).replace(/\s+/g, ' ').trim();
|
|
213
|
-
hits.push({ slug: d.slug, title: d.title, excerpt });
|
|
214
|
-
if (hits.length >= limit)
|
|
215
|
-
break;
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
return withDocs(JSON.stringify({ query, total: hits.length, hits }, null, 2));
|
|
329
|
+
// Ranking lives in ./search.ts so the remote server answers the same
|
|
330
|
+
// query the same way.
|
|
331
|
+
const { hits, total, partial } = rankDocs(docs, query, limit);
|
|
332
|
+
return withDocs(JSON.stringify({ query, total, shown: hits.length, partial: partial || undefined, hits }, null, 2));
|
|
219
333
|
}
|
|
220
334
|
case 'get_api_reference': {
|
|
221
335
|
return withDocs(JSON.stringify(apiReference, null, 2));
|
|
222
336
|
}
|
|
337
|
+
case 'check_svgrid_code': {
|
|
338
|
+
const a = (args ?? {});
|
|
339
|
+
if (typeof a.source !== 'string' || !a.source.trim()) {
|
|
340
|
+
return errText('source (the file contents to check) is required');
|
|
341
|
+
}
|
|
342
|
+
const result = await checkSvGridCode(a.source, apiSurface, {
|
|
343
|
+
filename: a.filename,
|
|
344
|
+
compile: compileWithSvelte,
|
|
345
|
+
});
|
|
346
|
+
// No docs footer: this output is a work list, and a marketing line at the
|
|
347
|
+
// end of it is noise the model has to read past on every iteration.
|
|
348
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
349
|
+
}
|
|
223
350
|
case 'introspect_source': {
|
|
224
351
|
const a = (args ?? {});
|
|
225
352
|
try {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
type ToolResult = {
|
|
2
|
+
content: Array<{
|
|
3
|
+
type: 'text';
|
|
4
|
+
text: string;
|
|
5
|
+
}>;
|
|
6
|
+
isError?: boolean;
|
|
7
|
+
};
|
|
8
|
+
export type ProjectTool = {
|
|
9
|
+
name: string;
|
|
10
|
+
description: string;
|
|
11
|
+
inputSchema: Record<string, unknown>;
|
|
12
|
+
};
|
|
13
|
+
export declare const projectTools: ProjectTool[];
|
|
14
|
+
/** Handle a studio_* project tool. Returns undefined if `name` isn't one of ours. */
|
|
15
|
+
export declare function handleProjectTool(name: string, args: Record<string, unknown>): ToolResult | undefined;
|
|
16
|
+
export {};
|
package/dist/search.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Doc ranking, shared by the stdio server and the remote Worker so both answer
|
|
3
|
+
* the same query the same way.
|
|
4
|
+
*
|
|
5
|
+
* Pure and dependency-free: it takes the docs it should rank rather than
|
|
6
|
+
* importing them, because the Worker loads its corpus from static assets at
|
|
7
|
+
* request time while the stdio server has it bundled.
|
|
8
|
+
*/
|
|
9
|
+
export type RankableDoc = {
|
|
10
|
+
slug: string;
|
|
11
|
+
title: string;
|
|
12
|
+
section: string;
|
|
13
|
+
markdown: string;
|
|
14
|
+
};
|
|
15
|
+
export type DocHit = {
|
|
16
|
+
slug: string;
|
|
17
|
+
title: string;
|
|
18
|
+
section: string;
|
|
19
|
+
score: number;
|
|
20
|
+
excerpt: string;
|
|
21
|
+
};
|
|
22
|
+
export declare function occurrences(haystack: string, needle: string): number;
|
|
23
|
+
/** Split a query into distinct lowercase terms, dropping one-character noise. */
|
|
24
|
+
export declare function queryTokens(query: string): string[];
|
|
25
|
+
/** A window of text around the first needle that appears, for search results. */
|
|
26
|
+
export declare function excerptAround(markdown: string, needles: string[]): string;
|
|
27
|
+
/**
|
|
28
|
+
* Rank docs for a query. Whole-phrase hits outrank term hits, and the slug is
|
|
29
|
+
* weighted heavily: a page named after the topic is the reference page for it,
|
|
30
|
+
* where a recipe that merely mentions it is not. Pages matching every term win
|
|
31
|
+
* outright; partial matches are only returned when nothing covers the query.
|
|
32
|
+
*/
|
|
33
|
+
export declare function rankDocs<T extends RankableDoc>(docs: readonly T[], query: string, limit: number): {
|
|
34
|
+
hits: DocHit[];
|
|
35
|
+
total: number;
|
|
36
|
+
partial: boolean;
|
|
37
|
+
};
|
package/dist/search.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Doc ranking, shared by the stdio server and the remote Worker so both answer
|
|
3
|
+
* the same query the same way.
|
|
4
|
+
*
|
|
5
|
+
* Pure and dependency-free: it takes the docs it should rank rather than
|
|
6
|
+
* importing them, because the Worker loads its corpus from static assets at
|
|
7
|
+
* request time while the stdio server has it bundled.
|
|
8
|
+
*/
|
|
9
|
+
export function occurrences(haystack, needle) {
|
|
10
|
+
if (!needle)
|
|
11
|
+
return 0;
|
|
12
|
+
let n = 0;
|
|
13
|
+
let i = haystack.indexOf(needle);
|
|
14
|
+
while (i !== -1) {
|
|
15
|
+
n += 1;
|
|
16
|
+
i = haystack.indexOf(needle, i + needle.length);
|
|
17
|
+
}
|
|
18
|
+
return n;
|
|
19
|
+
}
|
|
20
|
+
/** Split a query into distinct lowercase terms, dropping one-character noise. */
|
|
21
|
+
export function queryTokens(query) {
|
|
22
|
+
const tokens = [...new Set(query.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 1))];
|
|
23
|
+
return tokens.length ? tokens : [query.toLowerCase().trim()];
|
|
24
|
+
}
|
|
25
|
+
/** A window of text around the first needle that appears, for search results. */
|
|
26
|
+
export function excerptAround(markdown, needles) {
|
|
27
|
+
const lower = markdown.toLowerCase();
|
|
28
|
+
let idx = -1;
|
|
29
|
+
for (const n of needles) {
|
|
30
|
+
idx = lower.indexOf(n);
|
|
31
|
+
if (idx >= 0)
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
if (idx < 0)
|
|
35
|
+
idx = 0;
|
|
36
|
+
const start = Math.max(0, idx - 60);
|
|
37
|
+
const end = Math.min(markdown.length, idx + 180);
|
|
38
|
+
return markdown.slice(start, end).replace(/\s+/g, ' ').trim();
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Rank docs for a query. Whole-phrase hits outrank term hits, and the slug is
|
|
42
|
+
* weighted heavily: a page named after the topic is the reference page for it,
|
|
43
|
+
* where a recipe that merely mentions it is not. Pages matching every term win
|
|
44
|
+
* outright; partial matches are only returned when nothing covers the query.
|
|
45
|
+
*/
|
|
46
|
+
export function rankDocs(docs, query, limit) {
|
|
47
|
+
const phrase = query.toLowerCase();
|
|
48
|
+
const tokens = queryTokens(query);
|
|
49
|
+
const scored = [];
|
|
50
|
+
for (const d of docs) {
|
|
51
|
+
const title = d.title.toLowerCase();
|
|
52
|
+
const markdown = d.markdown.toLowerCase();
|
|
53
|
+
const headings = (d.markdown.match(/^#{1,6}\s+.*$/gm) ?? []).join('\n').toLowerCase();
|
|
54
|
+
const slugWords = d.slug.toLowerCase().replace(/[/-]/g, ' ');
|
|
55
|
+
let score = 0;
|
|
56
|
+
if (title.includes(phrase))
|
|
57
|
+
score += 100;
|
|
58
|
+
if (slugWords.includes(phrase))
|
|
59
|
+
score += 60;
|
|
60
|
+
if (headings.includes(phrase))
|
|
61
|
+
score += 30;
|
|
62
|
+
if (markdown.includes(phrase))
|
|
63
|
+
score += 20;
|
|
64
|
+
let matched = 0;
|
|
65
|
+
for (const t of tokens) {
|
|
66
|
+
const inTitle = title.includes(t);
|
|
67
|
+
const inHeading = headings.includes(t);
|
|
68
|
+
const count = occurrences(markdown, t);
|
|
69
|
+
if (inTitle || inHeading || count > 0)
|
|
70
|
+
matched += 1;
|
|
71
|
+
if (inTitle)
|
|
72
|
+
score += 25;
|
|
73
|
+
if (slugWords.includes(t))
|
|
74
|
+
score += 10;
|
|
75
|
+
if (inHeading)
|
|
76
|
+
score += 8;
|
|
77
|
+
// Capped so a long page cannot outrank a precise one on bulk alone.
|
|
78
|
+
score += Math.min(count, 5);
|
|
79
|
+
}
|
|
80
|
+
if (score > 0)
|
|
81
|
+
scored.push({ d, score, complete: matched === tokens.length });
|
|
82
|
+
}
|
|
83
|
+
const complete = scored.filter((s) => s.complete);
|
|
84
|
+
const pool = complete.length ? complete : scored;
|
|
85
|
+
const ranked = pool
|
|
86
|
+
.sort((a, b) => b.score - a.score || a.d.slug.localeCompare(b.d.slug))
|
|
87
|
+
.slice(0, limit);
|
|
88
|
+
return {
|
|
89
|
+
hits: ranked.map((s) => ({
|
|
90
|
+
slug: s.d.slug,
|
|
91
|
+
title: s.d.title,
|
|
92
|
+
section: s.d.section,
|
|
93
|
+
score: s.score,
|
|
94
|
+
excerpt: excerptAround(s.d.markdown, [phrase, ...tokens]),
|
|
95
|
+
})),
|
|
96
|
+
total: pool.length,
|
|
97
|
+
partial: complete.length === 0 && scored.length > 0,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* check_svgrid_code - the verification half of the MCP server.
|
|
3
|
+
*
|
|
4
|
+
* Retrieval tools (docs, demos, API listing) only ever give a model something
|
|
5
|
+
* to read. This one closes the loop: the model writes SvGrid code, this checks
|
|
6
|
+
* it against the REAL exported surface of the installed version and hands back
|
|
7
|
+
* diagnostics it can act on. A wrong prop name, a symbol imported from the
|
|
8
|
+
* wrong package, or Svelte 4 syntax in a Svelte 5 component all come back with
|
|
9
|
+
* the exact replacement rather than "hmm, that didn't work" three edits later.
|
|
10
|
+
*
|
|
11
|
+
* Two layers:
|
|
12
|
+
* 1. Static analysis (this file) - pure, no dependencies, no filesystem, so
|
|
13
|
+
* it runs identically in the Node stdio server and in a Worker.
|
|
14
|
+
* 2. The Svelte compiler - injected by the caller when it is available
|
|
15
|
+
* (`compile` option). Node has it; the Worker does not, and skips.
|
|
16
|
+
*
|
|
17
|
+
* The API surface is generated from the workspace sources at build time by
|
|
18
|
+
* scripts/api-surface.mjs, so it cannot drift from what the package exports.
|
|
19
|
+
*/
|
|
20
|
+
export type Severity = 'error' | 'warning' | 'info';
|
|
21
|
+
export type Diagnostic = {
|
|
22
|
+
/** Stable rule id, e.g. "svgrid/unknown-prop". */
|
|
23
|
+
rule: string;
|
|
24
|
+
severity: Severity;
|
|
25
|
+
/** 1-based line in the checked source. */
|
|
26
|
+
line: number;
|
|
27
|
+
message: string;
|
|
28
|
+
/** The concrete edit that fixes it, when there is one. */
|
|
29
|
+
fix?: string;
|
|
30
|
+
/** Doc slug or demo id to read for the full story. */
|
|
31
|
+
see?: string;
|
|
32
|
+
};
|
|
33
|
+
export type TypeMember = {
|
|
34
|
+
readonly name: string;
|
|
35
|
+
readonly optional: boolean;
|
|
36
|
+
readonly type: string;
|
|
37
|
+
};
|
|
38
|
+
export type ApiSurface = {
|
|
39
|
+
readonly gridVersion: string;
|
|
40
|
+
readonly enterpriseVersion: string;
|
|
41
|
+
readonly grid: {
|
|
42
|
+
readonly values: readonly string[];
|
|
43
|
+
readonly types: readonly string[];
|
|
44
|
+
readonly subpaths: readonly string[];
|
|
45
|
+
};
|
|
46
|
+
readonly enterprise: {
|
|
47
|
+
readonly values: readonly string[];
|
|
48
|
+
readonly types: readonly string[];
|
|
49
|
+
readonly subpaths: readonly string[];
|
|
50
|
+
};
|
|
51
|
+
readonly props: readonly TypeMember[];
|
|
52
|
+
readonly columnDef: readonly TypeMember[];
|
|
53
|
+
/** Methods on the free `SvGridApi`. */
|
|
54
|
+
readonly apiMethods: readonly string[];
|
|
55
|
+
/** Methods `installEnterprise(api)` adds on top. */
|
|
56
|
+
readonly enterpriseApiMethods: readonly string[];
|
|
57
|
+
readonly themes: readonly string[];
|
|
58
|
+
readonly features: readonly string[];
|
|
59
|
+
readonly rowModels: readonly string[];
|
|
60
|
+
};
|
|
61
|
+
export type CheckResult = {
|
|
62
|
+
ok: boolean;
|
|
63
|
+
/** Which version the code was checked against. */
|
|
64
|
+
checkedAgainst: string;
|
|
65
|
+
compiler: 'svelte' | 'unavailable' | 'not-svelte';
|
|
66
|
+
counts: {
|
|
67
|
+
errors: number;
|
|
68
|
+
warnings: number;
|
|
69
|
+
info: number;
|
|
70
|
+
};
|
|
71
|
+
diagnostics: Diagnostic[];
|
|
72
|
+
summary: string;
|
|
73
|
+
};
|
|
74
|
+
/** A compile pass supplied by the host when a Svelte compiler is reachable. */
|
|
75
|
+
export type CompileFn = (source: string, filename: string) => Promise<{
|
|
76
|
+
available: boolean;
|
|
77
|
+
diagnostics: Diagnostic[];
|
|
78
|
+
}>;
|
|
79
|
+
/**
|
|
80
|
+
* Blank out comments and the inside of strings, keeping every offset and line
|
|
81
|
+
* break intact. All structural scanning runs on this copy so a prop name in a
|
|
82
|
+
* doc comment or a `<SvGrid>` inside a template string never trips a rule.
|
|
83
|
+
*/
|
|
84
|
+
export declare function blankOut(src: string): string;
|
|
85
|
+
/**
|
|
86
|
+
* Closest known name to `word`, or null when nothing is near enough. A
|
|
87
|
+
* case-only difference always wins; otherwise the edit distance has to be
|
|
88
|
+
* small relative to the word so "foo" does not "resolve" to "bar".
|
|
89
|
+
*/
|
|
90
|
+
export declare function nearest(word: string, candidates: readonly string[]): string | null;
|
|
91
|
+
/** Run every static rule. Exported for tests and for hosts that skip compiling. */
|
|
92
|
+
export declare function checkStatic(source: string, surface: ApiSurface, filename?: string): Diagnostic[];
|
|
93
|
+
/**
|
|
94
|
+
* Check a snippet and report what a model should do next. `compile` is the
|
|
95
|
+
* optional second gate: when the host can reach a Svelte compiler, real parse
|
|
96
|
+
* errors are merged in with the static findings.
|
|
97
|
+
*/
|
|
98
|
+
export declare function checkSvGridCode(source: string, surface: ApiSurface, opts?: {
|
|
99
|
+
filename?: string;
|
|
100
|
+
compile?: CompileFn;
|
|
101
|
+
}): Promise<CheckResult>;
|