@svgrid/mcp 2.6.7 → 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.
@@ -0,0 +1,13 @@
1
+ import { type ProjectTool } from './project-tools.js';
2
+ type ToolResult = {
3
+ content: Array<{
4
+ type: 'text';
5
+ text: string;
6
+ }>;
7
+ isError?: boolean;
8
+ };
9
+ /** Studio is opt-in: an explicit flag, or a licence key that implies intent. */
10
+ export declare function studioEnabled(): boolean;
11
+ export declare const STUDIO_TOOLS: ProjectTool[];
12
+ export declare function handleStudioTool(name: string, args: Record<string, unknown>): ToolResult | undefined;
13
+ export {};
@@ -0,0 +1,227 @@
1
+ /**
2
+ * The Studio surface, as four tools instead of twenty-seven.
3
+ *
4
+ * The old surface was one tool per model mutation - `studio_add_block`,
5
+ * `studio_update_block`, `studio_remove_block`, `studio_move_block`, and ten
6
+ * separate `studio_set_*` tools. That is the API-wrapping antipattern: it made
7
+ * building a five-screen app twenty-odd round trips, and it cost ~3,741 tokens
8
+ * of `tools/list` on EVERY request, to every user, including the ones who only
9
+ * wanted to ask a question about the free grid. Studio was 79% of the payload
10
+ * and most sessions never called it once.
11
+ *
12
+ * These four delegate into the same `handleProjectTool` switch, so none of the
13
+ * behaviour is reimplemented here - only the shape a model sees. `studio_apply`
14
+ * takes a BATCH, which is the real win: a whole screen in one call rather than
15
+ * six.
16
+ *
17
+ * Off unless asked for. Set `SVGRID_MCP_STUDIO=1`, or a valid
18
+ * `SVGRID_LICENSE_KEY` - the tools that need a licence to be useful should not
19
+ * be charged to everyone else's context window.
20
+ */
21
+ import { checkLicenseKey } from '@svgrid/enterprise/studio';
22
+ import { handleProjectTool } from './project-tools.js';
23
+ /** Studio is opt-in: an explicit flag, or a licence key that implies intent. */
24
+ export function studioEnabled() {
25
+ if (process.env.SVGRID_MCP_STUDIO === '1')
26
+ return true;
27
+ return checkLicenseKey(process.env.SVGRID_LICENSE_KEY ?? null).valid;
28
+ }
29
+ const PROJECT_ACTIONS = {
30
+ new: 'studio_new_project',
31
+ load: 'studio_load_project',
32
+ describe: 'studio_describe_project',
33
+ config: 'studio_get_config',
34
+ capabilities: 'studio_capabilities',
35
+ };
36
+ const APPLY_OPS = {
37
+ add_entity: 'studio_add_entity',
38
+ add_screen: 'studio_add_screen',
39
+ add_block: 'studio_add_block',
40
+ add_component: 'studio_add_component',
41
+ update_block: 'studio_update_block',
42
+ remove_block: 'studio_remove_block',
43
+ move_block: 'studio_move_block',
44
+ update_screen: 'studio_update_screen',
45
+ remove_screen: 'studio_remove_screen',
46
+ };
47
+ const SETTINGS = {
48
+ theme: 'studio_set_theme',
49
+ access: 'studio_set_access',
50
+ auth: 'studio_set_auth',
51
+ data_layer: 'studio_set_data_layer',
52
+ tenancy: 'studio_set_tenancy',
53
+ job: 'studio_set_job',
54
+ deploy_target: 'studio_set_deploy_target',
55
+ screen_layout: 'studio_set_screen_layout',
56
+ form_layout: 'studio_set_form_layout',
57
+ field_conditions: 'studio_set_field_conditions',
58
+ entity_source: 'studio_set_entity_source',
59
+ };
60
+ const BUILD_ACTIONS = {
61
+ validate: 'studio_validate',
62
+ generate: 'studio_generate_app',
63
+ };
64
+ const keys = (map) => Object.keys(map).join(' | ');
65
+ export const STUDIO_TOOLS = [
66
+ {
67
+ name: 'studio_project',
68
+ description: 'SvGrid Studio (commercial): open or inspect the project model. `new` starts an empty one, `load` parses a studio.config.json string, `describe` summarises the current project (entities, screens, blocks, ids), `config` returns it as studio.config.json, `capabilities` lists the block kinds, UI components, themes and data sources you can use. Call `capabilities` before `studio_apply` so you use real names.',
69
+ inputSchema: {
70
+ type: 'object',
71
+ properties: {
72
+ action: { type: 'string', enum: Object.keys(PROJECT_ACTIONS), description: keys(PROJECT_ACTIONS) },
73
+ title: { type: 'string', description: 'For action "new": the project title.' },
74
+ config: { type: 'string', description: 'For action "load": a studio.config.json string.' },
75
+ },
76
+ required: ['action'],
77
+ },
78
+ },
79
+ {
80
+ name: 'studio_apply',
81
+ description: 'SvGrid Studio (commercial): change the project model. Takes a BATCH of operations applied in order, so a whole screen is one call rather than six. Each op is { op, ...args } where op is one of: ' +
82
+ keys(APPLY_OPS) +
83
+ '. The args are the same ones the individual operations took (entity, screen, kind, blockId, config, ...). Stops at the first failure and reports which op failed and what already applied. Get ids from studio_project action:"describe".',
84
+ inputSchema: {
85
+ type: 'object',
86
+ properties: {
87
+ ops: {
88
+ type: 'array',
89
+ description: 'Operations to apply in order.',
90
+ items: {
91
+ type: 'object',
92
+ properties: {
93
+ op: { type: 'string', enum: Object.keys(APPLY_OPS), description: 'Which operation.' },
94
+ },
95
+ required: ['op'],
96
+ additionalProperties: true,
97
+ },
98
+ },
99
+ },
100
+ required: ['ops'],
101
+ },
102
+ },
103
+ {
104
+ name: 'studio_configure',
105
+ description: 'SvGrid Studio (commercial): set project-wide options. Pass any combination of: ' +
106
+ keys(SETTINGS) +
107
+ '. Each value is the argument object the individual setting took, e.g. { "theme": { "preset": "ember" }, "auth": { "enabled": true } }. Applied in one call so a full app configuration is one round trip.',
108
+ inputSchema: {
109
+ type: 'object',
110
+ properties: Object.fromEntries(Object.keys(SETTINGS).map((k) => [
111
+ k,
112
+ { type: 'object', description: `Arguments for ${SETTINGS[k]}.`, additionalProperties: true },
113
+ ])),
114
+ required: [],
115
+ },
116
+ },
117
+ {
118
+ name: 'studio_build',
119
+ description: 'SvGrid Studio (commercial): `validate` reports codegen errors and warnings for the current project; `generate` emits the full runnable SvelteKit app - every route, $lib module, package.json and config. Validate first: generate on an invalid project wastes a large response.',
120
+ inputSchema: {
121
+ type: 'object',
122
+ properties: {
123
+ action: { type: 'string', enum: Object.keys(BUILD_ACTIONS), description: keys(BUILD_ACTIONS) },
124
+ },
125
+ required: ['action'],
126
+ },
127
+ },
128
+ ];
129
+ const fail = (message) => ({ isError: true, content: [{ type: 'text', text: message }] });
130
+ /**
131
+ * Catch the shape mistakes that would otherwise surface three calls later.
132
+ *
133
+ * An `EntitySchema` field is `{ field, type }`. Passing `{ name, type }` is the
134
+ * obvious guess and it is accepted silently: the entity stores, the screen
135
+ * builds, and nothing complains until `studio_build generate` fails with
136
+ * "no primary key ... or name a field \`id\`" - on a schema that HAS a field
137
+ * called id. That message is about the `field` property, but it reads as the
138
+ * `name` one, and following its advice reproduces the error exactly. It cost
139
+ * three round trips to diagnose by hand; a model would burn the same and then
140
+ * guess.
141
+ *
142
+ * So it is caught here, at the call that got it wrong, naming the fix.
143
+ */
144
+ function checkOpShape(op, entry) {
145
+ if (op !== 'add_entity')
146
+ return null;
147
+ const schema = entry.schema;
148
+ if (!schema || !Array.isArray(schema.fields))
149
+ return null;
150
+ const misnamed = schema.fields.filter((f) => f && typeof f === 'object' && !('field' in f) && 'name' in f);
151
+ if (!misnamed.length)
152
+ return null;
153
+ const names = misnamed
154
+ .map((f) => f.name)
155
+ .filter((n) => typeof n === 'string')
156
+ .join(', ');
157
+ return (`EntitySchema fields use \`field\`, not \`name\` - rewrite { "name": "id" } as ` +
158
+ `{ "field": "id" } (affected: ${names}). Left as-is this stores fine and then ` +
159
+ `fails at generate time with a message about the primary key.`);
160
+ }
161
+ const isError = (r) => !r || r.isError;
162
+ const textOf = (r) => (r?.content ?? []).map((c) => c.text).join('\n');
163
+ export function handleStudioTool(name, args) {
164
+ if (name === 'studio_project') {
165
+ const action = String(args.action ?? '');
166
+ const target = PROJECT_ACTIONS[action];
167
+ if (!target)
168
+ return fail(`Unknown action "${action}". Use one of: ${keys(PROJECT_ACTIONS)}.`);
169
+ return handleProjectTool(target, args);
170
+ }
171
+ if (name === 'studio_apply') {
172
+ const ops = args.ops;
173
+ if (!Array.isArray(ops) || ops.length === 0) {
174
+ return fail(`ops must be a non-empty array of { op, ... }. Valid ops: ${keys(APPLY_OPS)}.`);
175
+ }
176
+ const applied = [];
177
+ for (const [i, raw] of ops.entries()) {
178
+ const entry = (raw ?? {});
179
+ const op = String(entry.op ?? '');
180
+ const target = APPLY_OPS[op];
181
+ if (!target) {
182
+ return fail(`ops[${i}]: unknown op "${op}". Valid ops: ${keys(APPLY_OPS)}.` +
183
+ (applied.length ? `\nAlready applied: ${applied.join(', ')}.` : ''));
184
+ }
185
+ const shapeError = checkOpShape(op, entry);
186
+ if (shapeError) {
187
+ return fail(`ops[${i}] (${op}): ${shapeError}` +
188
+ (applied.length ? `\nAlready applied: ${applied.join(', ')}.` : '\nNothing was applied.'));
189
+ }
190
+ const result = handleProjectTool(target, entry);
191
+ if (isError(result)) {
192
+ // Say what landed before the failure - the model has to know whether to
193
+ // retry the whole batch or only the tail.
194
+ return fail(`ops[${i}] (${op}) failed: ${textOf(result)}` +
195
+ (applied.length ? `\nAlready applied: ${applied.join(', ')}.` : '\nNothing was applied.'));
196
+ }
197
+ applied.push(`${i}:${op}`);
198
+ }
199
+ const describe = handleProjectTool('studio_describe_project', {});
200
+ return { content: [{ type: 'text', text: `Applied ${applied.length} op(s).\n\n${textOf(describe)}` }] };
201
+ }
202
+ if (name === 'studio_configure') {
203
+ const entries = Object.keys(SETTINGS).filter((k) => args[k] !== undefined);
204
+ if (!entries.length) {
205
+ return fail(`Pass at least one of: ${keys(SETTINGS)}.`);
206
+ }
207
+ const done = [];
208
+ for (const key of entries) {
209
+ const value = (args[key] ?? {});
210
+ const result = handleProjectTool(SETTINGS[key], value);
211
+ if (isError(result)) {
212
+ return fail(`${key} failed: ${textOf(result)}` +
213
+ (done.length ? `\nAlready set: ${done.join(', ')}.` : '\nNothing was set.'));
214
+ }
215
+ done.push(key);
216
+ }
217
+ return { content: [{ type: 'text', text: `Set ${done.join(', ')}.` }] };
218
+ }
219
+ if (name === 'studio_build') {
220
+ const action = String(args.action ?? '');
221
+ const target = BUILD_ACTIONS[action];
222
+ if (!target)
223
+ return fail(`Unknown action "${action}". Use one of: ${keys(BUILD_ACTIONS)}.`);
224
+ return handleProjectTool(target, args);
225
+ }
226
+ return undefined;
227
+ }
@@ -27,6 +27,20 @@ export type Diagnostic = {
27
27
  message: string;
28
28
  /** The concrete edit that fixes it, when there is one. */
29
29
  fix?: string;
30
+ /**
31
+ * A mechanically applicable identifier rename, when the correction is exact.
32
+ *
33
+ * Machine-readable ON PURPOSE. `fix` is prose written for a human or a model
34
+ * to read; parsing it back out to edit code would make the wording
35
+ * load-bearing, and someone would eventually reword it and silently break
36
+ * every rewrite. Set only where the replacement is certain - a known rename
37
+ * or a close-enough spelling guess - and never where the advice is "this has
38
+ * no equivalent, remove it".
39
+ */
40
+ rename?: {
41
+ from: string;
42
+ to: string;
43
+ };
30
44
  /** Doc slug or demo id to read for the full story. */
31
45
  see?: string;
32
46
  };
@@ -89,6 +103,32 @@ export declare function blankOut(src: string): string;
89
103
  */
90
104
  export declare function nearest(word: string, candidates: readonly string[]): string | null;
91
105
  /** Run every static rule. Exported for tests and for hosts that skip compiling. */
106
+ /**
107
+ * Apply the mechanically-safe renames to the source, returning the corrected
108
+ * text and a description of every edit.
109
+ *
110
+ * Reporting a mistake and leaving the caller to re-derive the edit from prose
111
+ * is the loop this tool exists to shorten. But the tool's entire worth is that
112
+ * it never cries wolf, and rewriting raises those stakes: a false positive no
113
+ * longer wastes a turn, it corrupts working code. So the rules here are narrow
114
+ * on purpose.
115
+ *
116
+ * - Only diagnostics carrying a `rename`, which is set only where the
117
+ * replacement is exact.
118
+ * - Word-boundary matches only, so `data` never rewrites `rowData`.
119
+ * - Scoped to the reported LINE. `Diagnostic` has no column, and a whole-file
120
+ * replace would hit identifiers the checker never looked at - a local
121
+ * variable that happens to share a name with a wrong prop, say.
122
+ * - Skipped when the line does not contain the identifier, which means the
123
+ * source has moved on since the check.
124
+ *
125
+ * `applied` is the audit trail; a caller that wants to review before trusting
126
+ * has everything it needs.
127
+ */
128
+ export declare function applyFixes(source: string, diagnostics: readonly Diagnostic[]): {
129
+ fixed: string;
130
+ applied: string[];
131
+ };
92
132
  export declare function checkStatic(source: string, surface: ApiSurface, filename?: string): Diagnostic[];
93
133
  /**
94
134
  * Check a snippet and report what a model should do next. `compile` is the
package/dist/validate.js CHANGED
@@ -348,7 +348,10 @@ function checkImports(ctx) {
348
348
  severity: 'error',
349
349
  line,
350
350
  message: `${pkgName}@${isGrid ? surface.gridVersion : surface.enterpriseVersion} does not export \`${name}\`.`,
351
- fix: guess ? `Did you mean \`${guess}\`?` : 'Call get_api_reference for the exported surface.',
351
+ fix: guess
352
+ ? `Did you mean \`${guess}\`?`
353
+ : 'Call svgrid_get with ref:"api" for the exported surface.',
354
+ rename: guess ? { from: name, to: guess } : undefined,
352
355
  });
353
356
  }
354
357
  }
@@ -470,7 +473,7 @@ function checkGridProps(ctx) {
470
473
  message: `\`on:${evt}\` never fires: SvGrid dispatches no component events, it takes callback props.`,
471
474
  fix: real
472
475
  ? `Use \`${real}={...}\`.`
473
- : `Look for the matching \`on...\` prop - call get_api_reference or read reference/SvGrid.`,
476
+ : `Look for the matching \`on...\` prop - call svgrid_get with ref:"api" or read reference/SvGrid.`,
474
477
  see: 'reference/SvGrid',
475
478
  });
476
479
  continue;
@@ -497,6 +500,9 @@ function checkGridProps(ctx) {
497
500
  line: attr.line,
498
501
  message: `\`${name}\` is not a SvGrid prop.`,
499
502
  fix: renamed ? `Use \`${renamed}\`.` : PROP_RENAME_NOTES[name],
503
+ // An empty target means "there is no equivalent, take it out" - a
504
+ // deletion, not a rename, so it is never applied automatically.
505
+ rename: renamed ? { from: name, to: renamed } : undefined,
500
506
  see: 'reference/SvGrid',
501
507
  });
502
508
  continue;
@@ -518,7 +524,10 @@ function checkGridProps(ctx) {
518
524
  severity: 'error',
519
525
  line: attr.line,
520
526
  message: `\`${name}\` is not a prop of <SvGrid> in @svgrid/grid@${surface.gridVersion}.`,
521
- fix: guess ? `Did you mean \`${guess}\`?` : 'Call get_api_reference, or read the reference/SvGrid doc for the prop list.',
527
+ fix: guess
528
+ ? `Did you mean \`${guess}\`?`
529
+ : 'Call svgrid_search with the prop name, or read the reference/SvGrid doc for the prop list.',
530
+ rename: guess ? { from: name, to: guess } : undefined,
522
531
  see: 'reference/SvGrid',
523
532
  });
524
533
  }
@@ -652,6 +661,7 @@ function checkColumns(ctx) {
652
661
  line,
653
662
  message: `\`${key}\` is not a SvGrid column key.`,
654
663
  fix: renamed ? `Use \`${renamed}\`.` : COLUMN_RENAME_NOTES[key],
664
+ rename: renamed ? { from: key, to: renamed } : undefined,
655
665
  see: 'help/columns/column-definitions',
656
666
  });
657
667
  continue;
@@ -874,13 +884,18 @@ function checkEnterpriseUsage(ctx) {
874
884
  continue;
875
885
  }
876
886
  const hint = API_METHOD_HINTS[method];
877
- const guess = hint ?? nearest(method, all);
887
+ const spelling = nearest(method, all);
888
+ const guess = hint ?? spelling;
878
889
  push(ctx, {
879
890
  rule: 'svgrid/unknown-api-method',
880
891
  severity: 'error',
881
892
  line,
882
893
  message: `The grid API has no \`${method}()\` in @svgrid/grid@${ctx.surface.gridVersion}.`,
883
- fix: guess ? `Use \`${guess}\`.` : 'Call get_api_reference for the api surface.',
894
+ fix: guess ? `Use \`${guess}\`.` : 'Call svgrid_get with ref:"api" for the api surface.',
895
+ // Only the spelling guess is a bare identifier. An API_METHOD_HINTS
896
+ // value is a call expression - `exportData({ format: "xlsx" })` - and
897
+ // substituting that for an identifier would emit `api.exportData({...})(...)`.
898
+ rename: !hint && spelling ? { from: method, to: spelling } : undefined,
884
899
  see: 'reference/SvGrid',
885
900
  });
886
901
  }
@@ -941,6 +956,56 @@ function checkFeatures(ctx) {
941
956
  // Entry point
942
957
  // ---------------------------------------------------------------------------
943
958
  /** Run every static rule. Exported for tests and for hosts that skip compiling. */
959
+ /**
960
+ * Apply the mechanically-safe renames to the source, returning the corrected
961
+ * text and a description of every edit.
962
+ *
963
+ * Reporting a mistake and leaving the caller to re-derive the edit from prose
964
+ * is the loop this tool exists to shorten. But the tool's entire worth is that
965
+ * it never cries wolf, and rewriting raises those stakes: a false positive no
966
+ * longer wastes a turn, it corrupts working code. So the rules here are narrow
967
+ * on purpose.
968
+ *
969
+ * - Only diagnostics carrying a `rename`, which is set only where the
970
+ * replacement is exact.
971
+ * - Word-boundary matches only, so `data` never rewrites `rowData`.
972
+ * - Scoped to the reported LINE. `Diagnostic` has no column, and a whole-file
973
+ * replace would hit identifiers the checker never looked at - a local
974
+ * variable that happens to share a name with a wrong prop, say.
975
+ * - Skipped when the line does not contain the identifier, which means the
976
+ * source has moved on since the check.
977
+ *
978
+ * `applied` is the audit trail; a caller that wants to review before trusting
979
+ * has everything it needs.
980
+ */
981
+ export function applyFixes(source, diagnostics) {
982
+ const lines = source.split('\n');
983
+ const applied = [];
984
+ for (const d of diagnostics) {
985
+ if (!d.rename)
986
+ continue;
987
+ const { from, to } = d.rename;
988
+ if (!from || !to || from === to)
989
+ continue;
990
+ const i = d.line - 1;
991
+ const line = lines[i];
992
+ if (line === undefined)
993
+ continue;
994
+ // Word boundaries via an explicit character class rather than \b, which
995
+ // treats `$` and `-` as boundaries and would match inside `$derived` or a
996
+ // hyphenated attribute.
997
+ const pattern = new RegExp(`(^|[^A-Za-z0-9_$])${escapeRegExp(from)}(?![A-Za-z0-9_$])`, 'g');
998
+ if (!pattern.test(line))
999
+ continue;
1000
+ pattern.lastIndex = 0;
1001
+ lines[i] = line.replace(pattern, (_m, before) => `${before}${to}`);
1002
+ applied.push(`${from} -> ${to} (line ${d.line})`);
1003
+ }
1004
+ return { fixed: lines.join('\n'), applied };
1005
+ }
1006
+ function escapeRegExp(value) {
1007
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1008
+ }
944
1009
  export function checkStatic(source, surface, filename = 'Component.svelte') {
945
1010
  const ctx = {
946
1011
  raw: source,
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.7",
8
+ "version": "3.0.0",
9
9
  "description": "Model Context Protocol server for SvGrid, the Svelte 5 data grid: checks the code your AI writes against the real API surface, plus version-pinned docs, API reference and 373 demo sources.",
10
10
  "license": "MIT",
11
11
  "author": "jQWidgets <sales@jqwidgets.com>",
@@ -33,7 +33,7 @@
33
33
  "@modelcontextprotocol/sdk": "^1.0.4",
34
34
  "svelte": "^5.55.5",
35
35
  "zod": "^3.23.8",
36
- "@svgrid/enterprise": "^2.6.4"
36
+ "@svgrid/enterprise": "^2.7.0"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@types/node": "^22.10.7",
package/server.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "name": "com.svgrid/svgrid",
4
4
  "title": "SvGrid",
5
5
  "description": "Checks AI-written SvGrid code against the real API, plus version-pinned Svelte 5 grid docs.",
6
- "version": "2.6.7",
6
+ "version": "3.0.0",
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.7",
18
+ "version": "3.0.0",
19
19
  "runtimeHint": "npx",
20
20
  "transport": {
21
21
  "type": "stdio"