@svgrid/mcp 2.6.8 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -22
- package/dist/core-tools.d.ts +74 -0
- package/dist/core-tools.js +362 -0
- package/dist/data.js +5 -5
- package/dist/index.d.ts +11 -5
- package/dist/index.js +244 -354
- package/dist/installed.d.ts +33 -0
- package/dist/installed.js +87 -0
- package/dist/preview.d.ts +88 -0
- package/dist/preview.js +211 -0
- package/dist/prompts.d.ts +31 -0
- package/dist/prompts.js +65 -0
- package/dist/resources.d.ts +19 -0
- package/dist/resources.js +84 -0
- package/dist/search.d.ts +11 -0
- package/dist/search.js +28 -0
- package/dist/studio-tools.d.ts +13 -0
- package/dist/studio-tools.js +227 -0
- package/dist/validate.d.ts +40 -0
- package/dist/validate.js +70 -5
- package/package.json +1 -1
- package/server.json +2 -2
package/dist/index.js
CHANGED
|
@@ -2,11 +2,17 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* SvGrid MCP server (stdio).
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* Gives an MCP-capable client accurate, version-pinned answers about SvGrid -
|
|
6
|
+
* the real exported API, the shipped docs, 375 runnable demos - and, uniquely,
|
|
7
|
+
* a way to CHECK generated code against that surface before a user ever sees
|
|
8
|
+
* it.
|
|
9
|
+
*
|
|
10
|
+
* The surface is deliberately small. It was 36 tools, which cost ~4,710 tokens
|
|
11
|
+
* of `tools/list` on every single request; 79% of that was Studio, a commercial
|
|
12
|
+
* feature most sessions never touch. Five always-on tools cover the whole
|
|
13
|
+
* question-and-answer path - find, read, check-and-fix, preview, scaffold -
|
|
14
|
+
* Studio is four more behind an opt-in flag, and the old names all still work - they are just not listed, because listing is what
|
|
15
|
+
* costs tokens and calling an unlisted name costs nothing.
|
|
10
16
|
*
|
|
11
17
|
* Run with:
|
|
12
18
|
* npx @svgrid/mcp
|
|
@@ -14,12 +20,17 @@
|
|
|
14
20
|
import { createRequire } from 'node:module';
|
|
15
21
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
16
22
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
17
|
-
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
23
|
+
import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
24
|
+
import { apiSurface } from './data.js';
|
|
25
|
+
import { CORE_TOOLS, handleCoreTool } from './core-tools.js';
|
|
26
|
+
import { handleProjectTool } from './project-tools.js';
|
|
27
|
+
import { STUDIO_TOOLS, handleStudioTool, studioEnabled } from './studio-tools.js';
|
|
28
|
+
import { listResources, readResource } from './resources.js';
|
|
29
|
+
import { PREVIEW_TOOL, handlePreview, readPreviewResource } from './preview.js';
|
|
30
|
+
import { PROMPTS, getPrompt } from './prompts.js';
|
|
31
|
+
import { installedGrid, versionNote } from './installed.js';
|
|
32
|
+
import { applyFixes, checkSvGridCode } from './validate.js';
|
|
21
33
|
import { compileWithSvelte } from './compile-svelte.js';
|
|
22
|
-
import { rankDocs } from './search.js';
|
|
23
34
|
import { checkLicenseKey, introspectDrizzle, introspectJson, scaffold, summarizeVerify, verifyScaffold, } from '@svgrid/enterprise/studio';
|
|
24
35
|
/**
|
|
25
36
|
* Soft commercial gate. Uses the SAME classifier as the browser
|
|
@@ -36,373 +47,252 @@ function studioNote() {
|
|
|
36
47
|
function errText(message) {
|
|
37
48
|
return { isError: true, content: [{ type: 'text', text: message }] };
|
|
38
49
|
}
|
|
39
|
-
/**
|
|
40
|
-
* Out-of-band guidance footer for reference/navigation responses. Points the
|
|
41
|
-
* model (and, through it, the developer) at the full docs and live demos. Kept
|
|
42
|
-
* OFF the code- and file-emitting tools (get_example_source, scaffold_entity)
|
|
43
|
-
* so nothing marketing-flavored ends up welded into generated source.
|
|
44
|
-
*/
|
|
45
|
-
const DOCS_FOOTER = '\n\nSvGrid reference: full docs & 370+ live demos at https://svgrid.com/docs';
|
|
46
|
-
function withDocs(text) {
|
|
47
|
-
return { content: [{ type: 'text', text: text + DOCS_FOOTER }] };
|
|
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
|
-
}
|
|
71
|
-
// Report the real package version to MCP clients (read from package.json, which
|
|
72
|
-
// ships in the tarball at ../package.json relative to the built dist/index.js),
|
|
73
|
-
// so serverInfo.version never drifts from the published version.
|
|
74
50
|
const pkgVersion = (() => {
|
|
75
51
|
try {
|
|
76
|
-
|
|
52
|
+
const require = createRequire(import.meta.url);
|
|
53
|
+
return require('../package.json').version ?? '0.0.0';
|
|
77
54
|
}
|
|
78
55
|
catch {
|
|
79
56
|
return '0.0.0';
|
|
80
57
|
}
|
|
81
58
|
})();
|
|
82
|
-
const server = new Server({
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
capabilities: {
|
|
87
|
-
tools: {},
|
|
88
|
-
},
|
|
59
|
+
const server = new Server({ name: '@svgrid/mcp', version: pkgVersion }, {
|
|
60
|
+
// Tools alone was half the protocol. The corpus is a natural fit for
|
|
61
|
+
// resources (the user attaches a doc, no tool call needed) and the common
|
|
62
|
+
// tasks are a natural fit for prompts.
|
|
63
|
+
capabilities: { tools: {}, resources: {}, prompts: {} },
|
|
89
64
|
});
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
{
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
type: 'object',
|
|
116
|
-
properties: { id: { type: 'string', description: 'Demo id, e.g. "11-stock-market"' } },
|
|
117
|
-
required: ['id'],
|
|
118
|
-
},
|
|
119
|
-
},
|
|
120
|
-
{
|
|
121
|
-
name: 'list_docs',
|
|
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
|
-
},
|
|
137
|
-
},
|
|
138
|
-
{
|
|
139
|
-
name: 'get_doc',
|
|
140
|
-
description: 'Return the markdown content of a specific documentation page by slug.',
|
|
141
|
-
inputSchema: {
|
|
142
|
-
type: 'object',
|
|
143
|
-
properties: { slug: { type: 'string', description: 'Doc slug, e.g. "getting-started" or "help/columns/column-definitions"' } },
|
|
144
|
-
required: ['slug'],
|
|
145
|
-
},
|
|
146
|
-
},
|
|
147
|
-
{
|
|
148
|
-
name: 'search_docs',
|
|
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.',
|
|
150
|
-
inputSchema: {
|
|
151
|
-
type: 'object',
|
|
152
|
-
properties: {
|
|
153
|
-
query: { type: 'string', description: 'Free-text query, e.g. "row virtualization"' },
|
|
154
|
-
limit: { type: 'number', description: 'Max results, default 10', default: 10 },
|
|
155
|
-
},
|
|
156
|
-
required: ['query'],
|
|
157
|
-
},
|
|
158
|
-
},
|
|
159
|
-
{
|
|
160
|
-
name: 'get_api_reference',
|
|
161
|
-
description: 'Return the curated SvGrid public-API surface, grouped by category (components, headless, scheduler, data ops, export, row models, features, virtualization, accessibility, utilities).',
|
|
162
|
-
inputSchema: { type: 'object', properties: {} },
|
|
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
|
-
},
|
|
179
|
-
{
|
|
180
|
-
name: 'introspect_source',
|
|
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.',
|
|
182
|
-
inputSchema: {
|
|
183
|
-
type: 'object',
|
|
184
|
-
properties: {
|
|
185
|
-
kind: { type: 'string', enum: ['drizzle', 'json'], description: 'Source kind.' },
|
|
186
|
-
source: {
|
|
187
|
-
type: 'string',
|
|
188
|
-
description: 'For kind:"drizzle": the text of a schema file containing a pgTable / sqliteTable / mysqlTable definition.',
|
|
189
|
-
},
|
|
190
|
-
rows: {
|
|
191
|
-
type: 'array',
|
|
192
|
-
description: 'For kind:"json": a non-empty array of sample row objects.',
|
|
193
|
-
items: { type: 'object' },
|
|
194
|
-
},
|
|
195
|
-
name: { type: 'string', description: 'Entity/table name (required for kind:"json").' },
|
|
196
|
-
},
|
|
197
|
-
required: ['kind'],
|
|
198
|
-
},
|
|
199
|
-
},
|
|
200
|
-
{
|
|
201
|
-
name: 'scaffold_entity',
|
|
202
|
-
description: 'SvGrid Studio (commercial): generate runnable SvelteKit files from an EntitySchema - the $lib schema module, a +server.ts API route (createKitHandlers), and a +page.svelte with SvGrid + SvGridEditPanel. Returns files as { path, contents, description }. AFTER writing the files, run the project\'s own svelte-check / tsc to verify they compile, and fix any errors. Generated bodies are wrapped in svgrid:managed markers so regeneration preserves edits outside them.',
|
|
203
|
-
inputSchema: {
|
|
204
|
-
type: 'object',
|
|
205
|
-
properties: {
|
|
206
|
-
schema: {
|
|
207
|
-
type: 'object',
|
|
208
|
-
description: 'The EntitySchema (from introspect_source, optionally edited).',
|
|
209
|
-
},
|
|
210
|
-
route: { type: 'string', description: 'Route segment. Defaults to schema.name.' },
|
|
211
|
-
apiRoute: { type: 'string', description: 'API route. Defaults to /api/{route}.' },
|
|
212
|
-
},
|
|
213
|
-
required: ['schema'],
|
|
214
|
-
},
|
|
65
|
+
// ---- the two tools that are not pure retrieval ----------------------------
|
|
66
|
+
const CHECK_TOOL = {
|
|
67
|
+
name: 'svgrid_check_code',
|
|
68
|
+
title: 'Check and fix SvGrid code',
|
|
69
|
+
description: 'Verify SvGrid code BEFORE handing it to the user. Checks the source against the real exported surface for this version - props, column options, api methods, imports - and compiles it. Reports unknown props, wrong imports and compile errors with the line to fix, AND returns a corrected copy of the file (`fixed`) whenever the correction is exact, with `applied` listing every edit. Nothing else in this server prevents a confidently wrong answer; call it on every component you write, then use `fixed` rather than re-deriving the edits.',
|
|
70
|
+
inputSchema: {
|
|
71
|
+
type: 'object',
|
|
72
|
+
properties: {
|
|
73
|
+
source: { type: 'string', description: 'The file contents to check.' },
|
|
74
|
+
filename: { type: 'string', description: 'Optional filename, used in messages, e.g. "Grid.svelte".' },
|
|
75
|
+
},
|
|
76
|
+
required: ['source'],
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
const SCAFFOLD_TOOL = {
|
|
80
|
+
name: 'svgrid_scaffold',
|
|
81
|
+
title: 'Scaffold a CRUD screen',
|
|
82
|
+
description: 'SvGrid Studio (commercial): turn a data source into runnable SvelteKit files - the $lib schema module, a +server.ts API route, and a +page.svelte with SvGrid + SvGridEditPanel. Accepts a Drizzle schema, sample JSON rows, or an EntitySchema you already have; infers the schema and generates in one call. Set schemaOnly to stop after inference. Generated bodies carry svgrid:managed markers so regeneration preserves your edits outside them.',
|
|
83
|
+
inputSchema: {
|
|
84
|
+
type: 'object',
|
|
85
|
+
properties: {
|
|
86
|
+
from: {
|
|
87
|
+
type: 'string',
|
|
88
|
+
enum: ['drizzle', 'json', 'schema'],
|
|
89
|
+
description: '"drizzle" a schema file, "json" sample rows, or "schema" an EntitySchema.',
|
|
215
90
|
},
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
91
|
+
drizzle: { type: 'string', description: 'For from:"drizzle": the schema source.' },
|
|
92
|
+
rows: { type: 'array', description: 'For from:"json": a non-empty array of sample rows.' },
|
|
93
|
+
name: { type: 'string', description: 'For from:"json": the entity name.' },
|
|
94
|
+
schema: { type: 'object', description: 'For from:"schema": an EntitySchema.', additionalProperties: true },
|
|
95
|
+
route: { type: 'string', description: 'Page route, e.g. "/people".' },
|
|
96
|
+
apiRoute: { type: 'string', description: 'API route, e.g. "/api/people".' },
|
|
97
|
+
schemaOnly: { type: 'boolean', description: 'Return the inferred EntitySchema without generating files.' },
|
|
98
|
+
},
|
|
99
|
+
required: ['from'],
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
function listedTools() {
|
|
103
|
+
const tools = [...CORE_TOOLS, CHECK_TOOL, PREVIEW_TOOL, SCAFFOLD_TOOL];
|
|
104
|
+
if (studioEnabled())
|
|
105
|
+
tools.push(...STUDIO_TOOLS);
|
|
106
|
+
return tools;
|
|
107
|
+
}
|
|
108
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: listedTools() }));
|
|
109
|
+
// ---- resources ------------------------------------------------------------
|
|
110
|
+
server.setRequestHandler(ListResourcesRequestSchema, async (req) => listResources(req.params?.cursor));
|
|
111
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (req) => {
|
|
112
|
+
const found = readPreviewResource(req.params.uri) ?? readResource(req.params.uri);
|
|
113
|
+
if (!found)
|
|
114
|
+
throw new Error(`No SvGrid resource at "${req.params.uri}".`);
|
|
115
|
+
return found;
|
|
221
116
|
});
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const
|
|
226
|
-
if (
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
const match = examples.find((e) => e.id === id);
|
|
268
|
-
if (!match) {
|
|
269
|
-
return {
|
|
270
|
-
isError: true,
|
|
271
|
-
content: [{ type: 'text', text: `No example with id "${id}". Call list_examples for available ids.` }],
|
|
272
|
-
};
|
|
273
|
-
}
|
|
274
|
-
return {
|
|
275
|
-
content: [
|
|
276
|
-
{ type: 'text', text: `// ${match.path}\n// ${match.title} - ${match.blurb}\n\n${match.source}` },
|
|
277
|
-
],
|
|
278
|
-
};
|
|
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.
|
|
283
|
-
case 'list_docs': {
|
|
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.';
|
|
117
|
+
// ---- prompts --------------------------------------------------------------
|
|
118
|
+
server.setRequestHandler(ListPromptsRequestSchema, async () => ({ prompts: PROMPTS }));
|
|
119
|
+
server.setRequestHandler(GetPromptRequestSchema, async (req) => {
|
|
120
|
+
const found = getPrompt(req.params.name, (req.params.arguments ?? {}));
|
|
121
|
+
if (!found)
|
|
122
|
+
throw new Error(`No SvGrid prompt named "${req.params.name}".`);
|
|
123
|
+
return found;
|
|
124
|
+
});
|
|
125
|
+
// ---- tool calls -----------------------------------------------------------
|
|
126
|
+
async function runCheck(args) {
|
|
127
|
+
const source = args.source;
|
|
128
|
+
if (typeof source !== 'string' || !source.trim()) {
|
|
129
|
+
return errText('source (the file contents to check) is required');
|
|
130
|
+
}
|
|
131
|
+
const result = await checkSvGridCode(source, apiSurface, {
|
|
132
|
+
filename: typeof args.filename === 'string' ? args.filename : undefined,
|
|
133
|
+
compile: compileWithSvelte,
|
|
134
|
+
});
|
|
135
|
+
// Reporting the mistake and stopping leaves the caller to re-derive the edit
|
|
136
|
+
// from prose, which is the loop this tool exists to shorten. Where the
|
|
137
|
+
// correction is exact, hand back the corrected file too - and an audit trail,
|
|
138
|
+
// so it can be reviewed rather than trusted. Both omitted when nothing was
|
|
139
|
+
// mechanically fixable, so the common clean case does not grow.
|
|
140
|
+
const { fixed, applied } = applyFixes(source, result.diagnostics);
|
|
141
|
+
// Which version this was actually checked against. A proxy-shaped server
|
|
142
|
+
// answers from one global "latest" and cannot know what the caller has
|
|
143
|
+
// installed; we ship the corpus, so we can say when the two disagree instead
|
|
144
|
+
// of letting a model assert an API the user does not have.
|
|
145
|
+
const version = versionNote(apiSurface.gridVersion);
|
|
146
|
+
const payload = {
|
|
147
|
+
...result,
|
|
148
|
+
...(version ? { version } : {}),
|
|
149
|
+
...(applied.length ? { applied, fixed } : {}),
|
|
150
|
+
};
|
|
151
|
+
// No docs footer: this output is a work list, and a marketing line at the end
|
|
152
|
+
// of it is noise the model has to read past on every iteration.
|
|
153
|
+
return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
|
|
154
|
+
}
|
|
155
|
+
async function runScaffold(args) {
|
|
156
|
+
const from = String(args.from ?? '');
|
|
157
|
+
try {
|
|
158
|
+
let schema;
|
|
159
|
+
if (from === 'drizzle') {
|
|
160
|
+
if (typeof args.drizzle !== 'string' || !args.drizzle.trim()) {
|
|
161
|
+
return errText('drizzle (the schema source) is required for from:"drizzle"');
|
|
308
162
|
}
|
|
309
|
-
|
|
163
|
+
schema = introspectDrizzle(args.drizzle);
|
|
310
164
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
if (!match) {
|
|
315
|
-
return {
|
|
316
|
-
isError: true,
|
|
317
|
-
content: [{ type: 'text', text: `No doc with slug "${slug}". Call list_docs for available slugs.` }],
|
|
318
|
-
};
|
|
165
|
+
else if (from === 'json') {
|
|
166
|
+
if (!Array.isArray(args.rows) || args.rows.length === 0) {
|
|
167
|
+
return errText('rows (a non-empty array) is required for from:"json"');
|
|
319
168
|
}
|
|
320
|
-
|
|
169
|
+
schema = introspectJson(typeof args.name === 'string' ? args.name : 'entity', args.rows);
|
|
321
170
|
}
|
|
322
|
-
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
if (!query) {
|
|
327
|
-
return { isError: true, content: [{ type: 'text', text: 'query is required' }] };
|
|
171
|
+
else if (from === 'schema') {
|
|
172
|
+
const given = args.schema;
|
|
173
|
+
if (!given || !Array.isArray(given.fields) || given.fields.length === 0) {
|
|
174
|
+
return errText('schema (an EntitySchema with a non-empty fields array) is required for from:"schema"');
|
|
328
175
|
}
|
|
329
|
-
|
|
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));
|
|
176
|
+
schema = given;
|
|
333
177
|
}
|
|
334
|
-
|
|
335
|
-
return
|
|
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) }] };
|
|
178
|
+
else {
|
|
179
|
+
return errText('from must be "drizzle", "json" or "schema"');
|
|
349
180
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
try {
|
|
353
|
-
let schema;
|
|
354
|
-
if (a.kind === 'drizzle') {
|
|
355
|
-
if (!a.source)
|
|
356
|
-
return errText('source is required for kind:"drizzle"');
|
|
357
|
-
schema = introspectDrizzle(a.source);
|
|
358
|
-
}
|
|
359
|
-
else if (a.kind === 'json') {
|
|
360
|
-
if (!Array.isArray(a.rows) || a.rows.length === 0) {
|
|
361
|
-
return errText('rows (a non-empty array) is required for kind:"json"');
|
|
362
|
-
}
|
|
363
|
-
schema = introspectJson(a.name ?? 'entity', a.rows);
|
|
364
|
-
}
|
|
365
|
-
else {
|
|
366
|
-
return errText('kind must be "drizzle" or "json"');
|
|
367
|
-
}
|
|
368
|
-
return { content: [{ type: 'text', text: studioNote() + JSON.stringify(schema, null, 2) }] };
|
|
369
|
-
}
|
|
370
|
-
catch (err) {
|
|
371
|
-
return errText(err instanceof Error ? err.message : String(err));
|
|
372
|
-
}
|
|
181
|
+
if (args.schemaOnly === true) {
|
|
182
|
+
return { content: [{ type: 'text', text: studioNote() + JSON.stringify(schema, null, 2) }] };
|
|
373
183
|
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
184
|
+
const { files } = scaffold(schema, {
|
|
185
|
+
route: typeof args.route === 'string' ? args.route : undefined,
|
|
186
|
+
apiRoute: typeof args.apiRoute === 'string' ? args.apiRoute : undefined,
|
|
187
|
+
});
|
|
188
|
+
// Verify loop: compile the generated .svelte before handing files back.
|
|
189
|
+
const verify = await verifyScaffold(files);
|
|
190
|
+
const header = `// ${summarizeVerify(verify)}\n` +
|
|
191
|
+
"// After writing these files, run the project's svelte-check / tsc and fix any errors.\n\n";
|
|
192
|
+
return {
|
|
193
|
+
content: [{ type: 'text', text: studioNote() + header + JSON.stringify({ files, verify }, null, 2) }],
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
catch (err) {
|
|
197
|
+
return errText(err instanceof Error ? err.message : String(err));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* The pre-3.0 tool names.
|
|
202
|
+
*
|
|
203
|
+
* Kept working but NOT listed. `tools/list` is what costs context on every
|
|
204
|
+
* request; `tools/call` accepting a name it did not advertise costs nothing. So
|
|
205
|
+
* anyone with a saved prompt, skill or script written against the old surface
|
|
206
|
+
* keeps working, and nobody pays for the compatibility.
|
|
207
|
+
*/
|
|
208
|
+
const LEGACY = {
|
|
209
|
+
list_examples: (a) => ({ ...a, kind: 'examples', query: a.query ?? a.category ?? '' }),
|
|
210
|
+
list_docs: (a) => ({ ...a, kind: 'docs', query: a.query ?? a.section ?? '' }),
|
|
211
|
+
search_docs: (a) => ({ ...a, kind: 'docs' }),
|
|
212
|
+
get_example_source: (a) => ({ ref: a.id, kind: 'example' }),
|
|
213
|
+
get_doc: (a) => ({ ref: a.slug, kind: 'doc' }),
|
|
214
|
+
get_api_reference: () => ({ ref: 'api', kind: 'api' }),
|
|
215
|
+
};
|
|
216
|
+
const LEGACY_TARGET = {
|
|
217
|
+
list_examples: 'svgrid_search',
|
|
218
|
+
list_docs: 'svgrid_search',
|
|
219
|
+
search_docs: 'svgrid_search',
|
|
220
|
+
get_example_source: 'svgrid_get',
|
|
221
|
+
get_doc: 'svgrid_get',
|
|
222
|
+
get_api_reference: 'svgrid_get',
|
|
223
|
+
};
|
|
224
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
225
|
+
const { name, arguments: rawArgs } = req.params;
|
|
226
|
+
const args = (rawArgs ?? {});
|
|
227
|
+
// Retrieval tools, old names mapped onto the new ones.
|
|
228
|
+
const legacy = LEGACY[name];
|
|
229
|
+
if (legacy) {
|
|
230
|
+
const mapped = handleCoreTool(LEGACY_TARGET[name], legacy(args));
|
|
231
|
+
if (mapped)
|
|
232
|
+
return mapped;
|
|
233
|
+
}
|
|
234
|
+
const core = handleCoreTool(name, args);
|
|
235
|
+
if (core)
|
|
236
|
+
return core;
|
|
237
|
+
if (name === 'svgrid_preview')
|
|
238
|
+
return handlePreview(args);
|
|
239
|
+
if (name === 'svgrid_check_code' || name === 'check_svgrid_code')
|
|
240
|
+
return runCheck(args);
|
|
241
|
+
if (name === 'svgrid_scaffold')
|
|
242
|
+
return runScaffold(args);
|
|
243
|
+
// The two pre-3.0 scaffold tools, expressed as the one that replaced them.
|
|
244
|
+
if (name === 'introspect_source') {
|
|
245
|
+
return runScaffold({
|
|
246
|
+
from: args.kind === 'drizzle' ? 'drizzle' : 'json',
|
|
247
|
+
drizzle: args.source,
|
|
248
|
+
rows: args.rows,
|
|
249
|
+
name: args.name,
|
|
250
|
+
schemaOnly: true,
|
|
251
|
+
});
|
|
397
252
|
}
|
|
253
|
+
if (name === 'scaffold_entity') {
|
|
254
|
+
return runScaffold({ from: 'schema', schema: args.schema, route: args.route, apiRoute: args.apiRoute });
|
|
255
|
+
}
|
|
256
|
+
// Consolidated Studio tools, then the 27 individual pre-3.0 ones, which the
|
|
257
|
+
// project dispatcher still understands.
|
|
258
|
+
const studio = handleStudioTool(name, args);
|
|
259
|
+
if (studio)
|
|
260
|
+
return studio;
|
|
261
|
+
const project = handleProjectTool(name, args);
|
|
262
|
+
if (project)
|
|
263
|
+
return project;
|
|
264
|
+
return {
|
|
265
|
+
isError: true,
|
|
266
|
+
content: [
|
|
267
|
+
{
|
|
268
|
+
type: 'text',
|
|
269
|
+
text: `Unknown tool: ${name}. Available: ${listedTools()
|
|
270
|
+
.map((t) => t.name)
|
|
271
|
+
.join(', ')}` +
|
|
272
|
+
(studioEnabled()
|
|
273
|
+
? ''
|
|
274
|
+
: '. SvGrid Studio tools are off - set SVGRID_MCP_STUDIO=1 or SVGRID_LICENSE_KEY to enable them.'),
|
|
275
|
+
},
|
|
276
|
+
],
|
|
277
|
+
};
|
|
398
278
|
});
|
|
399
279
|
async function main() {
|
|
400
280
|
const transport = new StdioServerTransport();
|
|
401
281
|
await server.connect(transport);
|
|
402
|
-
// The MCP SDK keeps the process alive on the stdio transport, so we just
|
|
403
|
-
//
|
|
404
|
-
//
|
|
405
|
-
|
|
282
|
+
// The MCP SDK keeps the process alive on the stdio transport, so we just log
|
|
283
|
+
// a startup banner to stderr (stdout is reserved for the JSON-RPC protocol)
|
|
284
|
+
// and let the SDK take over.
|
|
285
|
+
// Name both versions up front. A corpus that describes a different grid than
|
|
286
|
+
// the one installed is the quietest way for this server to be confidently
|
|
287
|
+
// wrong, and the banner is where someone would actually notice.
|
|
288
|
+
const found = installedGrid();
|
|
289
|
+
const versions = found && found.version !== apiSurface.gridVersion
|
|
290
|
+
? ` - describes grid ${apiSurface.gridVersion}, but ${found.version} is installed here`
|
|
291
|
+
: ` - grid ${apiSurface.gridVersion}${found ? ' (matches installed)' : ''}`;
|
|
292
|
+
process.stderr.write(`@svgrid/mcp ${pkgVersion} on stdio - ${listedTools().length} tools` +
|
|
293
|
+
(studioEnabled() ? ' (Studio on)' : '') +
|
|
294
|
+
versions +
|
|
295
|
+
'\n');
|
|
406
296
|
}
|
|
407
297
|
main().catch((err) => {
|
|
408
298
|
process.stderr.write(`@svgrid/mcp fatal: ${err?.stack ?? err}\n`);
|