@svgrid/mcp 2.6.3 → 2.6.5
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/dist/data.js +743 -0
- package/dist/index.js +209 -21
- package/package.json +1 -1
- package/server.json +2 -2
package/dist/index.js
CHANGED
|
@@ -43,6 +43,59 @@ const DOCS_FOOTER = '\n\nSvGrid reference: full docs & 370+ live demos at https:
|
|
|
43
43
|
function withDocs(text) {
|
|
44
44
|
return { content: [{ type: 'text', text: text + DOCS_FOOTER }] };
|
|
45
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Shorten a blurb for a listing. Demo blurbs run to ~240 chars and 373 of them
|
|
48
|
+
* is most of a context window, so the listings carry a one-line version and
|
|
49
|
+
* get_example_source still returns the full text.
|
|
50
|
+
*/
|
|
51
|
+
function trimBlurb(text, max = 120) {
|
|
52
|
+
const s = String(text ?? '').replace(/\s+/g, ' ').trim();
|
|
53
|
+
if (s.length <= max)
|
|
54
|
+
return s;
|
|
55
|
+
const cut = s.slice(0, max);
|
|
56
|
+
const space = cut.lastIndexOf(' ');
|
|
57
|
+
return (space > 40 ? cut.slice(0, space) : cut) + '...';
|
|
58
|
+
}
|
|
59
|
+
/** { value: count } over a key, ordered by descending count. */
|
|
60
|
+
function countBy(rows, key) {
|
|
61
|
+
const counts = new Map();
|
|
62
|
+
for (const row of rows) {
|
|
63
|
+
const k = key(row) || 'Other';
|
|
64
|
+
counts.set(k, (counts.get(k) ?? 0) + 1);
|
|
65
|
+
}
|
|
66
|
+
return Object.fromEntries([...counts].sort((a, b) => b[1] - a[1]));
|
|
67
|
+
}
|
|
68
|
+
function occurrences(haystack, needle) {
|
|
69
|
+
if (!needle)
|
|
70
|
+
return 0;
|
|
71
|
+
let n = 0;
|
|
72
|
+
let i = haystack.indexOf(needle);
|
|
73
|
+
while (i !== -1) {
|
|
74
|
+
n += 1;
|
|
75
|
+
i = haystack.indexOf(needle, i + needle.length);
|
|
76
|
+
}
|
|
77
|
+
return n;
|
|
78
|
+
}
|
|
79
|
+
/** Split a query into distinct lowercase terms, dropping one-character noise. */
|
|
80
|
+
function queryTokens(query) {
|
|
81
|
+
const tokens = [...new Set(query.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 1))];
|
|
82
|
+
return tokens.length ? tokens : [query.toLowerCase().trim()];
|
|
83
|
+
}
|
|
84
|
+
/** A window of text around the first needle that appears, for search results. */
|
|
85
|
+
function excerptAround(markdown, needles) {
|
|
86
|
+
const lower = markdown.toLowerCase();
|
|
87
|
+
let idx = -1;
|
|
88
|
+
for (const n of needles) {
|
|
89
|
+
idx = lower.indexOf(n);
|
|
90
|
+
if (idx >= 0)
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
if (idx < 0)
|
|
94
|
+
idx = 0;
|
|
95
|
+
const start = Math.max(0, idx - 60);
|
|
96
|
+
const end = Math.min(markdown.length, idx + 180);
|
|
97
|
+
return markdown.slice(start, end).replace(/\s+/g, ' ').trim();
|
|
98
|
+
}
|
|
46
99
|
// Report the real package version to MCP clients (read from package.json, which
|
|
47
100
|
// ships in the tarball at ../package.json relative to the built dist/index.js),
|
|
48
101
|
// so serverInfo.version never drifts from the published version.
|
|
@@ -67,8 +120,21 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
67
120
|
tools: [
|
|
68
121
|
{
|
|
69
122
|
name: 'list_examples',
|
|
70
|
-
description: '
|
|
71
|
-
inputSchema: {
|
|
123
|
+
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.',
|
|
124
|
+
inputSchema: {
|
|
125
|
+
type: 'object',
|
|
126
|
+
properties: {
|
|
127
|
+
query: {
|
|
128
|
+
type: 'string',
|
|
129
|
+
description: 'Free-text filter over id, title, blurb and category, e.g. "kanban" or "server side".',
|
|
130
|
+
},
|
|
131
|
+
category: {
|
|
132
|
+
type: 'string',
|
|
133
|
+
description: 'Exact category, e.g. "Kanban" or "Inputs". Call with no arguments to see the available categories.',
|
|
134
|
+
},
|
|
135
|
+
limit: { type: 'number', description: 'Max results, default 25, max 100.', default: 25 },
|
|
136
|
+
},
|
|
137
|
+
},
|
|
72
138
|
},
|
|
73
139
|
{
|
|
74
140
|
name: 'get_example_source',
|
|
@@ -81,8 +147,21 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
81
147
|
},
|
|
82
148
|
{
|
|
83
149
|
name: 'list_docs',
|
|
84
|
-
description: '
|
|
85
|
-
inputSchema: {
|
|
150
|
+
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.',
|
|
151
|
+
inputSchema: {
|
|
152
|
+
type: 'object',
|
|
153
|
+
properties: {
|
|
154
|
+
query: {
|
|
155
|
+
type: 'string',
|
|
156
|
+
description: 'Free-text filter over slug, title and section, e.g. "column" or "export".',
|
|
157
|
+
},
|
|
158
|
+
section: {
|
|
159
|
+
type: 'string',
|
|
160
|
+
description: 'Exact section, e.g. "Columns" or "Server data". Call with no arguments to see the available sections.',
|
|
161
|
+
},
|
|
162
|
+
limit: { type: 'number', description: 'Max results, default 30, max 100.', default: 30 },
|
|
163
|
+
},
|
|
164
|
+
},
|
|
86
165
|
},
|
|
87
166
|
{
|
|
88
167
|
name: 'get_doc',
|
|
@@ -95,7 +174,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
95
174
|
},
|
|
96
175
|
{
|
|
97
176
|
name: 'search_docs',
|
|
98
|
-
description: '
|
|
177
|
+
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
178
|
inputSchema: {
|
|
100
179
|
type: 'object',
|
|
101
180
|
properties: {
|
|
@@ -160,9 +239,41 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
160
239
|
if (projectResult)
|
|
161
240
|
return projectResult;
|
|
162
241
|
switch (name) {
|
|
242
|
+
// Returning all 373 demos cost ~31k tokens on the call this tool's own
|
|
243
|
+
// description invites a model to start with. Filtered and capped instead,
|
|
244
|
+
// and a bare call answers with the category index to drill into.
|
|
163
245
|
case 'list_examples': {
|
|
164
|
-
const
|
|
165
|
-
|
|
246
|
+
const a = (args ?? {});
|
|
247
|
+
const limit = Math.max(1, Math.min(100, Number(a.limit ?? 25)));
|
|
248
|
+
const q = String(a.query ?? '').trim().toLowerCase();
|
|
249
|
+
const category = String(a.category ?? '').trim().toLowerCase();
|
|
250
|
+
const pool = examples.filter((e) => {
|
|
251
|
+
if (category && e.category.toLowerCase() !== category)
|
|
252
|
+
return false;
|
|
253
|
+
if (q && !`${e.id} ${e.title} ${e.blurb} ${e.category}`.toLowerCase().includes(q))
|
|
254
|
+
return false;
|
|
255
|
+
return true;
|
|
256
|
+
});
|
|
257
|
+
const shown = pool.slice(0, limit);
|
|
258
|
+
const body = {
|
|
259
|
+
total: pool.length,
|
|
260
|
+
shown: shown.length,
|
|
261
|
+
examples: shown.map((e) => ({
|
|
262
|
+
id: e.id,
|
|
263
|
+
title: e.title,
|
|
264
|
+
category: e.category,
|
|
265
|
+
blurb: trimBlurb(e.blurb),
|
|
266
|
+
})),
|
|
267
|
+
};
|
|
268
|
+
if (!q && !category)
|
|
269
|
+
body.categories = countBy(examples, (e) => e.category);
|
|
270
|
+
if (shown.length < pool.length) {
|
|
271
|
+
body.hint = `Showing ${shown.length} of ${pool.length}. Narrow with \`query\` or \`category\`, or raise \`limit\` (max 100).`;
|
|
272
|
+
}
|
|
273
|
+
if (!pool.length) {
|
|
274
|
+
body.hint = 'No match. Drop `category`, or try a broader `query`.';
|
|
275
|
+
}
|
|
276
|
+
return withDocs(JSON.stringify(body, null, 2));
|
|
166
277
|
}
|
|
167
278
|
case 'get_example_source': {
|
|
168
279
|
const id = String(args?.id ?? '');
|
|
@@ -179,9 +290,36 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
179
290
|
],
|
|
180
291
|
};
|
|
181
292
|
}
|
|
293
|
+
// Same shape as list_examples, for the same reason: all 370 pages was
|
|
294
|
+
// ~12.7k tokens. `path` is dropped from the listing because it is always
|
|
295
|
+
// "docs/<slug>.md" and get_doc takes the slug.
|
|
182
296
|
case 'list_docs': {
|
|
183
|
-
const
|
|
184
|
-
|
|
297
|
+
const a = (args ?? {});
|
|
298
|
+
const limit = Math.max(1, Math.min(100, Number(a.limit ?? 30)));
|
|
299
|
+
const q = String(a.query ?? '').trim().toLowerCase();
|
|
300
|
+
const section = String(a.section ?? '').trim().toLowerCase();
|
|
301
|
+
const pool = docs.filter((d) => {
|
|
302
|
+
if (section && d.section.toLowerCase() !== section)
|
|
303
|
+
return false;
|
|
304
|
+
if (q && !`${d.slug} ${d.title} ${d.section}`.toLowerCase().includes(q))
|
|
305
|
+
return false;
|
|
306
|
+
return true;
|
|
307
|
+
});
|
|
308
|
+
const shown = pool.slice(0, limit);
|
|
309
|
+
const body = {
|
|
310
|
+
total: pool.length,
|
|
311
|
+
shown: shown.length,
|
|
312
|
+
docs: shown.map((d) => ({ slug: d.slug, title: d.title, section: d.section })),
|
|
313
|
+
};
|
|
314
|
+
if (!q && !section)
|
|
315
|
+
body.sections = countBy(docs, (d) => d.section);
|
|
316
|
+
if (shown.length < pool.length) {
|
|
317
|
+
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.`;
|
|
318
|
+
}
|
|
319
|
+
if (!pool.length) {
|
|
320
|
+
body.hint = 'No match. Drop `section`, or try search_docs to search page content instead of titles.';
|
|
321
|
+
}
|
|
322
|
+
return withDocs(JSON.stringify(body, null, 2));
|
|
185
323
|
}
|
|
186
324
|
case 'get_doc': {
|
|
187
325
|
const slug = String(args?.slug ?? '');
|
|
@@ -201,21 +339,71 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
201
339
|
if (!query) {
|
|
202
340
|
return { isError: true, content: [{ type: 'text', text: 'query is required' }] };
|
|
203
341
|
}
|
|
204
|
-
|
|
205
|
-
|
|
342
|
+
// Previously this matched the query only as one contiguous substring and
|
|
343
|
+
// returned hits in directory order, so the canonical page for a topic
|
|
344
|
+
// routinely lost to an incidental mention. Score per doc, rank, and treat
|
|
345
|
+
// the query as terms so word order and joining words stop mattering.
|
|
346
|
+
const phrase = query.toLowerCase();
|
|
347
|
+
const tokens = queryTokens(query);
|
|
348
|
+
const scored = [];
|
|
206
349
|
for (const d of docs) {
|
|
207
|
-
const
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
350
|
+
const title = d.title.toLowerCase();
|
|
351
|
+
const markdown = d.markdown.toLowerCase();
|
|
352
|
+
const headings = (d.markdown.match(/^#{1,6}\s+.*$/gm) ?? []).join('\n').toLowerCase();
|
|
353
|
+
// "help/rows/kanban-board" -> "help rows kanban board". The slug is the
|
|
354
|
+
// strongest canonical signal there is: a page named after the topic is
|
|
355
|
+
// the reference page for it, where a recipe merely mentioning it is not.
|
|
356
|
+
const slugWords = d.slug.toLowerCase().replace(/[/-]/g, ' ');
|
|
357
|
+
let score = 0;
|
|
358
|
+
// Whole-phrase hits are the strongest signal, title strongest of all.
|
|
359
|
+
if (title.includes(phrase))
|
|
360
|
+
score += 100;
|
|
361
|
+
if (slugWords.includes(phrase))
|
|
362
|
+
score += 60;
|
|
363
|
+
if (headings.includes(phrase))
|
|
364
|
+
score += 30;
|
|
365
|
+
if (markdown.includes(phrase))
|
|
366
|
+
score += 20;
|
|
367
|
+
let matched = 0;
|
|
368
|
+
for (const t of tokens) {
|
|
369
|
+
const inTitle = title.includes(t);
|
|
370
|
+
const inHeading = headings.includes(t);
|
|
371
|
+
const count = occurrences(markdown, t);
|
|
372
|
+
if (inTitle || inHeading || count > 0)
|
|
373
|
+
matched += 1;
|
|
374
|
+
if (inTitle)
|
|
375
|
+
score += 25;
|
|
376
|
+
if (slugWords.includes(t))
|
|
377
|
+
score += 10;
|
|
378
|
+
if (inHeading)
|
|
379
|
+
score += 8;
|
|
380
|
+
// Capped so a long page cannot outrank a precise one on bulk alone.
|
|
381
|
+
score += Math.min(count, 5);
|
|
216
382
|
}
|
|
383
|
+
if (score > 0)
|
|
384
|
+
scored.push({ d, score, complete: matched === tokens.length });
|
|
217
385
|
}
|
|
218
|
-
|
|
386
|
+
// Prefer pages containing every term; fall back to partial matches only
|
|
387
|
+
// when nothing covers the whole query.
|
|
388
|
+
const complete = scored.filter((s) => s.complete);
|
|
389
|
+
const ranked = (complete.length ? complete : scored)
|
|
390
|
+
.sort((a, b) => b.score - a.score || a.d.slug.localeCompare(b.d.slug))
|
|
391
|
+
.slice(0, limit);
|
|
392
|
+
const hits = ranked.map((s) => ({
|
|
393
|
+
slug: s.d.slug,
|
|
394
|
+
title: s.d.title,
|
|
395
|
+
section: s.d.section,
|
|
396
|
+
score: s.score,
|
|
397
|
+
excerpt: excerptAround(s.d.markdown, [phrase, ...tokens]),
|
|
398
|
+
}));
|
|
399
|
+
const matchedTotal = complete.length || scored.length;
|
|
400
|
+
return withDocs(JSON.stringify({
|
|
401
|
+
query,
|
|
402
|
+
total: matchedTotal,
|
|
403
|
+
shown: hits.length,
|
|
404
|
+
partial: complete.length === 0 && scored.length > 0 ? true : undefined,
|
|
405
|
+
hits,
|
|
406
|
+
}, null, 2));
|
|
219
407
|
}
|
|
220
408
|
case 'get_api_reference': {
|
|
221
409
|
return withDocs(JSON.stringify(apiReference, null, 2));
|
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"type": "commercial",
|
|
6
6
|
"url": "https://svgrid.com/pricing"
|
|
7
7
|
},
|
|
8
|
-
"version": "2.6.
|
|
8
|
+
"version": "2.6.5",
|
|
9
9
|
"description": "Model Context Protocol (MCP) server for SvGrid. Exposes example sources, docs, and API reference to AI assistants.",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"author": "jQWidgets <sales@jqwidgets.com>",
|
package/server.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "com.svgrid/svgrid",
|
|
4
4
|
"title": "SvGrid",
|
|
5
5
|
"description": "Version-pinned Svelte 5 data grid APIs, 373 demo sources, and SvelteKit app scaffolding.",
|
|
6
|
-
"version": "2.6.
|
|
6
|
+
"version": "2.6.5",
|
|
7
7
|
"websiteUrl": "https://svgrid.com/docs/help/mcp-server/",
|
|
8
8
|
"repository": {
|
|
9
9
|
"url": "https://github.com/sv-grid/sv-grid",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"registryType": "npm",
|
|
16
16
|
"registryBaseUrl": "https://registry.npmjs.org",
|
|
17
17
|
"identifier": "@svgrid/mcp",
|
|
18
|
-
"version": "2.6.
|
|
18
|
+
"version": "2.6.5",
|
|
19
19
|
"runtimeHint": "npx",
|
|
20
20
|
"transport": {
|
|
21
21
|
"type": "stdio"
|