@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/dist/index.js CHANGED
@@ -2,11 +2,17 @@
2
2
  /**
3
3
  * SvGrid MCP server (stdio).
4
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.
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 { apiReference, apiSurface, docs, examples } from './data.js';
19
- import { projectTools, handleProjectTool } from './project-tools.js';
20
- import { checkSvGridCode } from './validate.js';
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
- return createRequire(import.meta.url)('../package.json').version;
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
- name: '@svgrid/mcp',
84
- version: pkgVersion,
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
- server.setRequestHandler(ListToolsRequestSchema, async () => {
91
- return {
92
- tools: [
93
- {
94
- name: 'list_examples',
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
- },
110
- },
111
- {
112
- name: 'get_example_source',
113
- description: 'Return the full .svelte source of a specific demo by id (e.g. "11-stock-market"). The source is what a user would copy into their project as-is.',
114
- inputSchema: {
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
- // SvGrid Studio "drive the model" tools: build/edit the same validated project
217
- // model the visual designer uses, then generate the app or export the config.
218
- ...projectTools.map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema })),
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
- server.setRequestHandler(CallToolRequestSchema, async (req) => {
223
- const { name, arguments: args } = req.params;
224
- // Project-model tools (studio_*) are handled by their own dispatcher.
225
- const projectResult = handleProjectTool(name, (args ?? {}));
226
- if (projectResult)
227
- return projectResult;
228
- switch (name) {
229
- // Returning all 375 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.
232
- case 'list_examples': {
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));
264
- }
265
- case 'get_example_source': {
266
- const id = String(args?.id ?? '');
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
- return withDocs(JSON.stringify(body, null, 2));
163
+ schema = introspectDrizzle(args.drizzle);
310
164
  }
311
- case 'get_doc': {
312
- const slug = String(args?.slug ?? '');
313
- const match = docs.find((d) => d.slug === slug);
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
- return withDocs(match.markdown);
169
+ schema = introspectJson(typeof args.name === 'string' ? args.name : 'entity', args.rows);
321
170
  }
322
- case 'search_docs': {
323
- const a = (args ?? {});
324
- const query = String(a.query ?? '').trim();
325
- const limit = Math.max(1, Math.min(50, Number(a.limit ?? 10)));
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
- // 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));
176
+ schema = given;
333
177
  }
334
- case 'get_api_reference': {
335
- return withDocs(JSON.stringify(apiReference, null, 2));
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
- case 'introspect_source': {
351
- const a = (args ?? {});
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
- case 'scaffold_entity': {
375
- const a = (args ?? {});
376
- if (!a.schema || !Array.isArray(a.schema.fields) || a.schema.fields.length === 0) {
377
- return errText('schema (an EntitySchema with a non-empty fields array) is required');
378
- }
379
- try {
380
- const { files } = scaffold(a.schema, { route: a.route, apiRoute: a.apiRoute });
381
- // Verify loop: compile the generated .svelte before handing files back.
382
- const verify = await verifyScaffold(files);
383
- const header = `// ${summarizeVerify(verify)}\n// After writing these files, run the project's svelte-check / tsc and fix any errors.\n\n`;
384
- return {
385
- content: [{ type: 'text', text: studioNote() + header + JSON.stringify({ files, verify }, null, 2) }],
386
- };
387
- }
388
- catch (err) {
389
- return errText(err instanceof Error ? err.message : String(err));
390
- }
391
- }
392
- default:
393
- return {
394
- isError: true,
395
- content: [{ type: 'text', text: `Unknown tool: ${name}` }],
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
- // log a startup banner to stderr (stdout is reserved for the JSON-RPC
404
- // protocol) and let the SDK take over.
405
- process.stderr.write('@svgrid/mcp started on stdio\n');
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`);