@svgrid/mcp 2.0.1 → 2.2.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 +20 -0
- package/dist/data.js +392 -101
- package/dist/index.js +24 -8
- package/dist/project-tools.js +391 -0
- package/package.json +6 -2
package/dist/index.js
CHANGED
|
@@ -15,6 +15,7 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
|
15
15
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
16
16
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
17
17
|
import { apiReference, docs, examples } from './data.js';
|
|
18
|
+
import { projectTools, handleProjectTool } from './project-tools.js';
|
|
18
19
|
import { checkLicenseKey, introspectDrizzle, introspectJson, scaffold, summarizeVerify, verifyScaffold, } from '@svgrid/enterprise/studio';
|
|
19
20
|
/**
|
|
20
21
|
* Soft commercial gate. Uses the SAME classifier as the browser
|
|
@@ -31,6 +32,16 @@ function studioNote() {
|
|
|
31
32
|
function errText(message) {
|
|
32
33
|
return { isError: true, content: [{ type: 'text', text: message }] };
|
|
33
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Out-of-band guidance footer for reference/navigation responses. Points the
|
|
37
|
+
* model (and, through it, the developer) at the full docs and live demos. Kept
|
|
38
|
+
* OFF the code- and file-emitting tools (get_example_source, scaffold_entity)
|
|
39
|
+
* so nothing marketing-flavored ends up welded into generated source.
|
|
40
|
+
*/
|
|
41
|
+
const DOCS_FOOTER = '\n\nSvGrid reference: full docs & 280+ live demos at https://svgrid.com/docs';
|
|
42
|
+
function withDocs(text) {
|
|
43
|
+
return { content: [{ type: 'text', text: text + DOCS_FOOTER }] };
|
|
44
|
+
}
|
|
34
45
|
const server = new Server({
|
|
35
46
|
name: '@svgrid/mcp',
|
|
36
47
|
version: '0.1.0',
|
|
@@ -84,7 +95,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
84
95
|
},
|
|
85
96
|
{
|
|
86
97
|
name: 'get_api_reference',
|
|
87
|
-
description: 'Return the curated SvGrid public-API surface, grouped by category (components, headless, row models, features, virtualization, accessibility, utilities).',
|
|
98
|
+
description: 'Return the curated SvGrid public-API surface, grouped by category (components, headless, scheduler, data ops, export, row models, features, virtualization, accessibility, utilities).',
|
|
88
99
|
inputSchema: { type: 'object', properties: {} },
|
|
89
100
|
},
|
|
90
101
|
{
|
|
@@ -124,15 +135,22 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
124
135
|
required: ['schema'],
|
|
125
136
|
},
|
|
126
137
|
},
|
|
138
|
+
// SvGrid Studio "drive the model" tools: build/edit the same validated project
|
|
139
|
+
// model the visual designer uses, then generate the app or export the config.
|
|
140
|
+
...projectTools.map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema })),
|
|
127
141
|
],
|
|
128
142
|
};
|
|
129
143
|
});
|
|
130
144
|
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
131
145
|
const { name, arguments: args } = req.params;
|
|
146
|
+
// Project-model tools (studio_*) are handled by their own dispatcher.
|
|
147
|
+
const projectResult = handleProjectTool(name, (args ?? {}));
|
|
148
|
+
if (projectResult)
|
|
149
|
+
return projectResult;
|
|
132
150
|
switch (name) {
|
|
133
151
|
case 'list_examples': {
|
|
134
152
|
const items = examples.map((e) => ({ id: e.id, title: e.title, blurb: e.blurb, path: e.path }));
|
|
135
|
-
return
|
|
153
|
+
return withDocs(JSON.stringify(items, null, 2));
|
|
136
154
|
}
|
|
137
155
|
case 'get_example_source': {
|
|
138
156
|
const id = String(args?.id ?? '');
|
|
@@ -151,7 +169,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
151
169
|
}
|
|
152
170
|
case 'list_docs': {
|
|
153
171
|
const items = docs.map((d) => ({ slug: d.slug, title: d.title, path: d.path }));
|
|
154
|
-
return
|
|
172
|
+
return withDocs(JSON.stringify(items, null, 2));
|
|
155
173
|
}
|
|
156
174
|
case 'get_doc': {
|
|
157
175
|
const slug = String(args?.slug ?? '');
|
|
@@ -162,7 +180,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
162
180
|
content: [{ type: 'text', text: `No doc with slug "${slug}". Call list_docs for available slugs.` }],
|
|
163
181
|
};
|
|
164
182
|
}
|
|
165
|
-
return
|
|
183
|
+
return withDocs(match.markdown);
|
|
166
184
|
}
|
|
167
185
|
case 'search_docs': {
|
|
168
186
|
const a = (args ?? {});
|
|
@@ -185,12 +203,10 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
185
203
|
break;
|
|
186
204
|
}
|
|
187
205
|
}
|
|
188
|
-
return {
|
|
189
|
-
content: [{ type: 'text', text: JSON.stringify({ query, total: hits.length, hits }, null, 2) }],
|
|
190
|
-
};
|
|
206
|
+
return withDocs(JSON.stringify({ query, total: hits.length, hits }, null, 2));
|
|
191
207
|
}
|
|
192
208
|
case 'get_api_reference': {
|
|
193
|
-
return
|
|
209
|
+
return withDocs(JSON.stringify(apiReference, null, 2));
|
|
194
210
|
}
|
|
195
211
|
case 'introspect_source': {
|
|
196
212
|
const a = (args ?? {});
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SvGrid Studio "drive the model" MCP tools.
|
|
3
|
+
*
|
|
4
|
+
* These expose the SAME pure, validated project model the visual designer edits
|
|
5
|
+
* (a `StudioProject`), so an AI agent can build/edit a data app structurally -
|
|
6
|
+
* add entities, screens, blocks, components, wire data sources, theme, auth, the
|
|
7
|
+
* data layer, deploy target - then generate the runnable SvelteKit app or export
|
|
8
|
+
* the `studio.config.json` the designer can Load. Every edit runs through the
|
|
9
|
+
* model's own functions (and `validateProject`), so an agent can't produce an
|
|
10
|
+
* invalid project the way free-text codegen can.
|
|
11
|
+
*
|
|
12
|
+
* The server holds one in-memory "current project" per session; `studio_get_config`
|
|
13
|
+
* / `studio_generate_app` are the outputs.
|
|
14
|
+
*/
|
|
15
|
+
import { createProject, parseProject, serializeProject, validateProject, addEntity, addScreen, addFreestandingScreen, addBlock, addComponentBlock, setEntityDataSource, setTheme, setAuth, setDataLayer, setDeployTarget, introspectDrizzle, introspectJson, flattenBlocks, blockPalette, UI_COMPONENT_REGISTRY, uiComponentSpec, studioThemes, emitStudioAppBundle, checkLicenseKey, entityDataSource, } from '@svgrid/enterprise/studio';
|
|
16
|
+
// ---- session state --------------------------------------------------------
|
|
17
|
+
let project = null;
|
|
18
|
+
function requireProject() {
|
|
19
|
+
if (!project)
|
|
20
|
+
throw new Error('No project loaded. Call studio_new_project or studio_load_project first.');
|
|
21
|
+
return project;
|
|
22
|
+
}
|
|
23
|
+
function ok(text) {
|
|
24
|
+
return { content: [{ type: 'text', text }] };
|
|
25
|
+
}
|
|
26
|
+
function fail(text) {
|
|
27
|
+
return { isError: true, content: [{ type: 'text', text }] };
|
|
28
|
+
}
|
|
29
|
+
function studioNote() {
|
|
30
|
+
return checkLicenseKey(process.env.SVGRID_LICENSE_KEY ?? null).valid
|
|
31
|
+
? ''
|
|
32
|
+
: '// SvGrid Studio is a commercial feature. Set SVGRID_LICENSE_KEY in your MCP server\n// config env for licensed use. https://svgrid.com/pricing\n\n';
|
|
33
|
+
}
|
|
34
|
+
/** A short confirmation + the current validation status, after a mutation. */
|
|
35
|
+
function confirm(headline) {
|
|
36
|
+
const issues = validateProject(project);
|
|
37
|
+
const errs = issues.filter((i) => i.level === 'error');
|
|
38
|
+
const warns = issues.filter((i) => i.level !== 'error');
|
|
39
|
+
const tail = issues.length
|
|
40
|
+
? `\n\nValidation: ${errs.length} error(s), ${warns.length} warning(s):\n` + issues.map((i) => ` - [${i.level}] ${i.message}`).join('\n')
|
|
41
|
+
: '\n\nValidation: clean.';
|
|
42
|
+
return ok(headline + tail);
|
|
43
|
+
}
|
|
44
|
+
/** A human-readable description of the current model (for the agent to reason on). */
|
|
45
|
+
function describe(p) {
|
|
46
|
+
const lines = [];
|
|
47
|
+
lines.push(`Project: ${JSON.stringify(p.title)}`);
|
|
48
|
+
lines.push(`Default data source: ${p.dataSource}`);
|
|
49
|
+
lines.push(`Entities (${p.entities.length}):`);
|
|
50
|
+
for (const e of p.entities) {
|
|
51
|
+
const src = entityDataSource(p, e.name);
|
|
52
|
+
const srcLabel = src.kind === 'sql' ? `sql/${src.dialect ?? 'postgres'}` : src.kind;
|
|
53
|
+
lines.push(` - ${e.name} [${e.fields.map((f) => f.field).join(', ')}] (source: ${srcLabel})`);
|
|
54
|
+
}
|
|
55
|
+
lines.push(`Screens (${p.screens.length}):`);
|
|
56
|
+
for (const s of p.screens) {
|
|
57
|
+
const kinds = flattenBlocks(s.blocks).map((b) => (b.config.kind === 'component' ? `component:${b.config.component}` : b.config.kind));
|
|
58
|
+
lines.push(` - id=${s.id} route=/${s.route} title=${JSON.stringify(s.title)}${s.entity ? ` entity=${s.entity}` : ' (freestanding)'} blocks=[${kinds.join(', ')}]`);
|
|
59
|
+
}
|
|
60
|
+
const t = p.theme;
|
|
61
|
+
lines.push(`Theme: ${t?.preset ?? 'default'} (${t?.mode ?? 'light'})${t?.accent ? ` accent=${t.accent}` : ''}`);
|
|
62
|
+
if (p.access?.enabled)
|
|
63
|
+
lines.push(`Access (RBAC): on, roles=[${p.access.roles.map((r) => r.role).join(', ')}], default=${p.access.defaultRole ?? p.access.roles[0]?.role}`);
|
|
64
|
+
if (p.auth?.enabled) {
|
|
65
|
+
const feats = [p.auth.register && 'register', p.auth.userAdmin && 'user-admin', p.auth.twoFactor && '2FA', p.auth.email && 'email', p.auth.oauth?.length && `oauth:${p.auth.oauth.join('+')}`].filter(Boolean);
|
|
66
|
+
lines.push(`Auth: on${feats.length ? ` (${feats.join(', ')})` : ''}`);
|
|
67
|
+
}
|
|
68
|
+
if (p.dataLayer === 'drizzle')
|
|
69
|
+
lines.push('Data layer: Drizzle (typed schema + migrations)');
|
|
70
|
+
if (p.deploy)
|
|
71
|
+
lines.push(`Deploy target: ${p.deploy}`);
|
|
72
|
+
return lines.join('\n');
|
|
73
|
+
}
|
|
74
|
+
/** Resolve an EntitySchema from either an explicit schema or an introspection request. */
|
|
75
|
+
function resolveSchema(a) {
|
|
76
|
+
if (a.schema && typeof a.schema === 'object')
|
|
77
|
+
return a.schema;
|
|
78
|
+
if (a.kind === 'drizzle') {
|
|
79
|
+
if (!a.source)
|
|
80
|
+
throw new Error('source (the Drizzle schema file text) is required for kind:"drizzle"');
|
|
81
|
+
return introspectDrizzle(a.source);
|
|
82
|
+
}
|
|
83
|
+
if (a.kind === 'json') {
|
|
84
|
+
if (!Array.isArray(a.rows) || a.rows.length === 0)
|
|
85
|
+
throw new Error('rows (a non-empty array of sample objects) is required for kind:"json"');
|
|
86
|
+
return introspectJson(a.name ?? 'entity', a.rows);
|
|
87
|
+
}
|
|
88
|
+
throw new Error('Provide either `schema` (an EntitySchema) or an introspection request (`kind`: "drizzle"|"json").');
|
|
89
|
+
}
|
|
90
|
+
// ---- tool catalogue -------------------------------------------------------
|
|
91
|
+
export const projectTools = [
|
|
92
|
+
{
|
|
93
|
+
name: 'studio_new_project',
|
|
94
|
+
description: 'Start a NEW, empty SvGrid Studio project (the model the visual designer edits). Add entities/screens next. Replaces any project currently in this session.',
|
|
95
|
+
inputSchema: { type: 'object', properties: { title: { type: 'string', description: 'App title.' } } },
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: 'studio_load_project',
|
|
99
|
+
description: 'Load an existing project from a studio.config.json string (e.g. one exported earlier or shipped with a generated app). Validates it. Use this to continue editing an app the designer produced.',
|
|
100
|
+
inputSchema: { type: 'object', properties: { config: { type: 'string', description: 'The studio.config.json contents.' } }, required: ['config'] },
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: 'studio_describe_project',
|
|
104
|
+
description: 'Return a human-readable summary of the CURRENT project: entities (+ fields + data source), screens (+ blocks, with ids), theme, RBAC, auth, data layer, deploy target. Call this to see state before editing.',
|
|
105
|
+
inputSchema: { type: 'object', properties: {} },
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
name: 'studio_get_config',
|
|
109
|
+
description: 'Return the current project model as a studio.config.json string. Write it to `studio.config.json` and the visual designer can Load it (round-trip).',
|
|
110
|
+
inputSchema: { type: 'object', properties: {} },
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: 'studio_capabilities',
|
|
114
|
+
description: 'List what can be added: block kinds (grid/chart/kpi/board/...), UI component keys (button/badge/timeline/...), theme presets, data-source kinds, and deploy targets.',
|
|
115
|
+
inputSchema: { type: 'object', properties: {} },
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: 'studio_add_entity',
|
|
119
|
+
description: 'Add an entity (a table/model) + its default screen. Pass either `schema` (an EntitySchema, e.g. from introspect_source) OR an introspection request (`kind`:"drizzle" with `source`, or `kind`:"json" with `rows` + `name`).',
|
|
120
|
+
inputSchema: {
|
|
121
|
+
type: 'object',
|
|
122
|
+
properties: {
|
|
123
|
+
schema: { type: 'object', description: 'An EntitySchema.' },
|
|
124
|
+
kind: { type: 'string', enum: ['drizzle', 'json'], description: 'Introspect a source instead of passing a schema.' },
|
|
125
|
+
source: { type: 'string', description: 'For kind:"drizzle": the schema file text.' },
|
|
126
|
+
rows: { type: 'array', items: { type: 'object' }, description: 'For kind:"json": sample rows.' },
|
|
127
|
+
name: { type: 'string', description: 'For kind:"json": the entity name.' },
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: 'studio_add_screen',
|
|
133
|
+
description: 'Add a screen. With `entity`, adds an entity-bound screen (default grid). Without it, adds a freestanding page (needs `title`).',
|
|
134
|
+
inputSchema: {
|
|
135
|
+
type: 'object',
|
|
136
|
+
properties: {
|
|
137
|
+
entity: { type: 'string', description: 'Entity name to bind (omit for a freestanding page).' },
|
|
138
|
+
title: { type: 'string', description: 'Title (required for a freestanding page).' },
|
|
139
|
+
route: { type: 'string', description: 'Route segment (freestanding only).' },
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: 'studio_add_block',
|
|
145
|
+
description: 'Add a data block to a screen. `kind` is one of the palette kinds (grid, chart, kpi, gauge, tree, tabs, accordion, pivot, board, calendar, detail, master-detail, filter, record, lookup, dashboard). Use studio_describe_project for screen ids.',
|
|
146
|
+
inputSchema: {
|
|
147
|
+
type: 'object',
|
|
148
|
+
properties: { screenId: { type: 'string' }, kind: { type: 'string', description: 'Block kind.' } },
|
|
149
|
+
required: ['screenId', 'kind'],
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: 'studio_add_component',
|
|
154
|
+
description: 'Add a UI component block to a screen (button, badge, alert, card, stat, timeline, sparkline, chip, ...). `props` overrides the registry defaults. See studio_capabilities for component keys.',
|
|
155
|
+
inputSchema: {
|
|
156
|
+
type: 'object',
|
|
157
|
+
properties: { screenId: { type: 'string' }, component: { type: 'string', description: 'Registry component key.' }, props: { type: 'object', description: 'Prop overrides (incl. _content for text).' } },
|
|
158
|
+
required: ['screenId', 'component'],
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
name: 'studio_set_entity_source',
|
|
163
|
+
description: 'Bind an entity to a data source. `source` is an EntityDataSource, e.g. { "kind": "sql", "table": "customers", "dialect": "postgres" } | { "kind": "memory" } | { "kind": "pglite", "table": "..." } | { "kind": "supabase", ... } | { "kind": "rest", ... }.',
|
|
164
|
+
inputSchema: {
|
|
165
|
+
type: 'object',
|
|
166
|
+
properties: { entity: { type: 'string' }, source: { type: 'object', description: 'The EntityDataSource.' } },
|
|
167
|
+
required: ['entity', 'source'],
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
name: 'studio_set_theme',
|
|
172
|
+
description: 'Set the theme preset + mode + accent. Presets come from studio_capabilities.',
|
|
173
|
+
inputSchema: {
|
|
174
|
+
type: 'object',
|
|
175
|
+
properties: { preset: { type: 'string' }, mode: { type: 'string', enum: ['light', 'dark'] }, accent: { type: 'string', description: 'Hex accent color.' } },
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
name: 'studio_set_access',
|
|
180
|
+
description: 'Configure role-based access control (RBAC). `roles` is an array of { role, screens: "*"|string[], actions: "*"|("create"|"update"|"delete")[] }.',
|
|
181
|
+
inputSchema: {
|
|
182
|
+
type: 'object',
|
|
183
|
+
properties: {
|
|
184
|
+
enabled: { type: 'boolean' },
|
|
185
|
+
roles: { type: 'array', items: { type: 'object' } },
|
|
186
|
+
defaultRole: { type: 'string' },
|
|
187
|
+
},
|
|
188
|
+
required: ['enabled'],
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
name: 'studio_set_auth',
|
|
193
|
+
description: 'Configure the authentication starter. Options: enabled, protect, register, userAdmin, twoFactor, email, oauth (["github","google","oidc"]). register/userAdmin/oauth/2FA need the Drizzle data layer + a SQL entity; userAdmin also needs RBAC.',
|
|
194
|
+
inputSchema: {
|
|
195
|
+
type: 'object',
|
|
196
|
+
properties: {
|
|
197
|
+
enabled: { type: 'boolean' },
|
|
198
|
+
protect: { type: 'boolean' },
|
|
199
|
+
register: { type: 'boolean' },
|
|
200
|
+
userAdmin: { type: 'boolean' },
|
|
201
|
+
twoFactor: { type: 'boolean' },
|
|
202
|
+
email: { type: 'boolean' },
|
|
203
|
+
oauth: { type: 'array', items: { type: 'string', enum: ['github', 'google', 'oidc'] } },
|
|
204
|
+
},
|
|
205
|
+
required: ['enabled'],
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
name: 'studio_set_data_layer',
|
|
210
|
+
description: 'Turn the typed Drizzle data layer (schema.ts + typed repos + drizzle-kit migrations) on or off. Applies to SQL-bound entities.',
|
|
211
|
+
inputSchema: { type: 'object', properties: { enabled: { type: 'boolean' } }, required: ['enabled'] },
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
name: 'studio_set_deploy_target',
|
|
215
|
+
description: 'Set the deploy target: auto | vercel | netlify | cloudflare | node. Picks the SvelteKit adapter + emits provider config + a CI/CD pipeline.',
|
|
216
|
+
inputSchema: { type: 'object', properties: { target: { type: 'string', enum: ['auto', 'vercel', 'netlify', 'cloudflare', 'node'] } }, required: ['target'] },
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
name: 'studio_validate',
|
|
220
|
+
description: 'Validate the current project. Returns any errors (block codegen) + warnings.',
|
|
221
|
+
inputSchema: { type: 'object', properties: {} },
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
name: 'studio_generate_app',
|
|
225
|
+
description: 'Generate the full runnable SvelteKit app from the current model - every file (routes, $lib, package.json, auth, data layer, CI/CD, studio.config.json). Returns [{ path, contents }]. Write them, then run the project\'s svelte-check to verify.',
|
|
226
|
+
inputSchema: { type: 'object', properties: {} },
|
|
227
|
+
},
|
|
228
|
+
];
|
|
229
|
+
// ---- dispatch -------------------------------------------------------------
|
|
230
|
+
/** Handle a studio_* project tool. Returns undefined if `name` isn't one of ours. */
|
|
231
|
+
export function handleProjectTool(name, args) {
|
|
232
|
+
try {
|
|
233
|
+
switch (name) {
|
|
234
|
+
case 'studio_new_project': {
|
|
235
|
+
project = createProject([], { title: typeof args.title === 'string' ? args.title : 'My Studio App' });
|
|
236
|
+
return ok(`New project "${project.title}" created (0 entities). Add entities with studio_add_entity.`);
|
|
237
|
+
}
|
|
238
|
+
case 'studio_load_project': {
|
|
239
|
+
if (typeof args.config !== 'string')
|
|
240
|
+
return fail('config (a studio.config.json string) is required.');
|
|
241
|
+
project = parseProject(args.config);
|
|
242
|
+
return ok('Project loaded.\n\n' + describe(project));
|
|
243
|
+
}
|
|
244
|
+
case 'studio_describe_project':
|
|
245
|
+
return ok(describe(requireProject()));
|
|
246
|
+
case 'studio_get_config':
|
|
247
|
+
return ok(serializeProject(requireProject()));
|
|
248
|
+
case 'studio_capabilities':
|
|
249
|
+
return ok(JSON.stringify({
|
|
250
|
+
blockKinds: blockPalette.map((b) => ({ kind: b.kind, label: b.label, needs: b.needs })),
|
|
251
|
+
components: UI_COMPONENT_REGISTRY.map((c) => ({ key: c.key, label: c.label, category: c.category })),
|
|
252
|
+
themePresets: studioThemes.map((t) => ({ id: t.id, name: t.name })),
|
|
253
|
+
dataSourceKinds: ['memory', 'sql', 'supabase', 'rest', 'pglite'],
|
|
254
|
+
deployTargets: ['auto', 'vercel', 'netlify', 'cloudflare', 'node'],
|
|
255
|
+
}, null, 2));
|
|
256
|
+
case 'studio_add_entity': {
|
|
257
|
+
const p = requireProject();
|
|
258
|
+
const schema = resolveSchema(args);
|
|
259
|
+
project = addEntity(p, schema);
|
|
260
|
+
const screen = project.screens.find((s) => s.entity === schema.name);
|
|
261
|
+
return confirm(`Added entity "${schema.name}" (${schema.fields.length} fields) + screen id=${screen?.id ?? '?'}.`);
|
|
262
|
+
}
|
|
263
|
+
case 'studio_add_screen': {
|
|
264
|
+
const p = requireProject();
|
|
265
|
+
const before = new Set(p.screens.map((s) => s.id));
|
|
266
|
+
if (typeof args.entity === 'string' && args.entity) {
|
|
267
|
+
project = addScreen(p, args.entity);
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
if (typeof args.title !== 'string' || !args.title)
|
|
271
|
+
return fail('A freestanding screen needs a `title` (or pass `entity`).');
|
|
272
|
+
project = addFreestandingScreen(p, { title: args.title, route: typeof args.route === 'string' ? args.route : undefined });
|
|
273
|
+
}
|
|
274
|
+
if (project === p)
|
|
275
|
+
return fail(`Could not add the screen (unknown entity "${String(args.entity)}"?).`);
|
|
276
|
+
const added = project.screens.find((s) => !before.has(s.id));
|
|
277
|
+
return confirm(`Added screen id=${added?.id ?? '?'} (/${added?.route ?? '?'}).`);
|
|
278
|
+
}
|
|
279
|
+
case 'studio_add_block': {
|
|
280
|
+
const p = requireProject();
|
|
281
|
+
const screenId = String(args.screenId ?? '');
|
|
282
|
+
const kind = String(args.kind ?? '');
|
|
283
|
+
const screen = p.screens.find((s) => s.id === screenId);
|
|
284
|
+
if (!screen)
|
|
285
|
+
return fail(`No screen id=${screenId}. Call studio_describe_project.`);
|
|
286
|
+
const before = new Set(flattenBlocks(screen.blocks).map((b) => b.id));
|
|
287
|
+
project = addBlock(p, screenId, kind);
|
|
288
|
+
const now = project.screens.find((s) => s.id === screenId);
|
|
289
|
+
const added = flattenBlocks(now.blocks).find((b) => !before.has(b.id));
|
|
290
|
+
if (!added)
|
|
291
|
+
return fail(`Could not add a "${kind}" block (needs a bound entity, or unknown kind).`);
|
|
292
|
+
return confirm(`Added ${kind} block id=${added.id} to screen ${screenId}.`);
|
|
293
|
+
}
|
|
294
|
+
case 'studio_add_component': {
|
|
295
|
+
const p = requireProject();
|
|
296
|
+
const screenId = String(args.screenId ?? '');
|
|
297
|
+
const component = String(args.component ?? '');
|
|
298
|
+
if (!p.screens.some((s) => s.id === screenId))
|
|
299
|
+
return fail(`No screen id=${screenId}.`);
|
|
300
|
+
const spec = uiComponentSpec(component);
|
|
301
|
+
if (!spec)
|
|
302
|
+
return fail(`Unknown component "${component}". See studio_capabilities.`);
|
|
303
|
+
const defaults = {};
|
|
304
|
+
for (const pr of spec.props)
|
|
305
|
+
if (pr.default != null)
|
|
306
|
+
defaults[pr.key] = pr.default;
|
|
307
|
+
if (spec.hasContent)
|
|
308
|
+
defaults._content = spec.contentDefault ?? spec.label;
|
|
309
|
+
const merged = { ...defaults, ...(args.props ?? {}) };
|
|
310
|
+
project = addComponentBlock(p, screenId, component, merged);
|
|
311
|
+
return confirm(`Added component "${component}" to screen ${screenId}.`);
|
|
312
|
+
}
|
|
313
|
+
case 'studio_set_entity_source': {
|
|
314
|
+
const p = requireProject();
|
|
315
|
+
const entity = String(args.entity ?? '');
|
|
316
|
+
if (!p.entities.some((e) => e.name === entity))
|
|
317
|
+
return fail(`No entity "${entity}".`);
|
|
318
|
+
if (!args.source || typeof args.source !== 'object')
|
|
319
|
+
return fail('source (an EntityDataSource object) is required.');
|
|
320
|
+
project = setEntityDataSource(p, entity, args.source);
|
|
321
|
+
return confirm(`Bound "${entity}" to ${args.source.kind} source.`);
|
|
322
|
+
}
|
|
323
|
+
case 'studio_set_theme': {
|
|
324
|
+
const p = requireProject();
|
|
325
|
+
const mode = args.mode === 'dark' ? 'dark' : args.mode === 'light' ? 'light' : undefined;
|
|
326
|
+
const theme = {
|
|
327
|
+
...(p.theme ?? {}),
|
|
328
|
+
...(typeof args.preset === 'string' ? { preset: args.preset } : {}),
|
|
329
|
+
...(mode ? { mode } : {}),
|
|
330
|
+
...(typeof args.accent === 'string' ? { accent: args.accent } : {}),
|
|
331
|
+
};
|
|
332
|
+
project = setTheme(p, theme);
|
|
333
|
+
return confirm(`Theme set: preset=${theme.preset ?? 'default'} mode=${theme.mode ?? 'light'}.`);
|
|
334
|
+
}
|
|
335
|
+
case 'studio_set_access': {
|
|
336
|
+
const p = requireProject();
|
|
337
|
+
if (!args.enabled) {
|
|
338
|
+
const { access: _drop, ...rest } = p;
|
|
339
|
+
project = rest;
|
|
340
|
+
return confirm('RBAC disabled.');
|
|
341
|
+
}
|
|
342
|
+
const roles = Array.isArray(args.roles) && args.roles.length ? args.roles : [{ role: 'admin', screens: '*', actions: '*' }, { role: 'viewer', screens: '*', actions: [] }];
|
|
343
|
+
project = { ...p, access: { enabled: true, roles, ...(typeof args.defaultRole === 'string' ? { defaultRole: args.defaultRole } : {}) } };
|
|
344
|
+
return confirm(`RBAC enabled with roles [${roles.map((r) => r.role).join(', ')}].`);
|
|
345
|
+
}
|
|
346
|
+
case 'studio_set_auth': {
|
|
347
|
+
const p = requireProject();
|
|
348
|
+
project = setAuth(p, {
|
|
349
|
+
enabled: args.enabled !== false,
|
|
350
|
+
...(typeof args.protect === 'boolean' ? { protect: args.protect } : {}),
|
|
351
|
+
...(typeof args.register === 'boolean' ? { register: args.register } : {}),
|
|
352
|
+
...(typeof args.userAdmin === 'boolean' ? { userAdmin: args.userAdmin } : {}),
|
|
353
|
+
...(typeof args.twoFactor === 'boolean' ? { twoFactor: args.twoFactor } : {}),
|
|
354
|
+
...(typeof args.email === 'boolean' ? { email: args.email } : {}),
|
|
355
|
+
...(Array.isArray(args.oauth) ? { oauth: args.oauth } : {}),
|
|
356
|
+
});
|
|
357
|
+
return confirm(project.auth?.enabled ? 'Auth enabled.' : 'Auth disabled.');
|
|
358
|
+
}
|
|
359
|
+
case 'studio_set_data_layer': {
|
|
360
|
+
project = setDataLayer(requireProject(), args.enabled !== false);
|
|
361
|
+
return confirm(project.dataLayer === 'drizzle' ? 'Drizzle data layer enabled.' : 'Data layer disabled.');
|
|
362
|
+
}
|
|
363
|
+
case 'studio_set_deploy_target': {
|
|
364
|
+
const t = String(args.target ?? 'auto');
|
|
365
|
+
project = setDeployTarget(requireProject(), t);
|
|
366
|
+
return confirm(`Deploy target: ${t}.`);
|
|
367
|
+
}
|
|
368
|
+
case 'studio_validate': {
|
|
369
|
+
const issues = validateProject(requireProject());
|
|
370
|
+
if (!issues.length)
|
|
371
|
+
return ok('Valid: no errors or warnings.');
|
|
372
|
+
return ok(issues.map((i) => `[${i.level}] ${i.message}`).join('\n'));
|
|
373
|
+
}
|
|
374
|
+
case 'studio_generate_app': {
|
|
375
|
+
const p = requireProject();
|
|
376
|
+
if (p.entities.length === 0)
|
|
377
|
+
return fail('Add at least one entity before generating (studio_add_entity).');
|
|
378
|
+
const errs = validateProject(p).filter((i) => i.level === 'error');
|
|
379
|
+
if (errs.length)
|
|
380
|
+
return fail('Fix these errors first:\n' + errs.map((e) => ' - ' + e.message).join('\n'));
|
|
381
|
+
const files = emitStudioAppBundle(p);
|
|
382
|
+
return ok(studioNote() + `// ${files.length} files. Write them all, then run svelte-check.\n\n` + JSON.stringify(files.map((f) => ({ path: f.path, contents: f.contents })), null, 2));
|
|
383
|
+
}
|
|
384
|
+
default:
|
|
385
|
+
return undefined;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
catch (err) {
|
|
389
|
+
return fail(err instanceof Error ? err.message : String(err));
|
|
390
|
+
}
|
|
391
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@svgrid/mcp",
|
|
3
|
-
"
|
|
3
|
+
"funding": {
|
|
4
|
+
"type": "commercial",
|
|
5
|
+
"url": "https://svgrid.com/pricing"
|
|
6
|
+
},
|
|
7
|
+
"version": "2.2.0",
|
|
4
8
|
"description": "Model Context Protocol (MCP) server for SvGrid. Exposes example sources, docs, and API reference to AI assistants.",
|
|
5
9
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
10
|
"author": "jQWidgets <sales@jqwidgets.com>",
|
|
@@ -26,7 +30,7 @@
|
|
|
26
30
|
"dependencies": {
|
|
27
31
|
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
28
32
|
"zod": "^3.23.8",
|
|
29
|
-
"@svgrid/enterprise": "^2.0
|
|
33
|
+
"@svgrid/enterprise": "^2.2.0"
|
|
30
34
|
},
|
|
31
35
|
"devDependencies": {
|
|
32
36
|
"@types/node": "^22.10.7",
|