@svadmin/mcp 0.4.30 → 0.6.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svadmin/mcp",
3
- "version": "0.4.30",
3
+ "version": "0.6.0",
4
4
  "description": "MCP (Model Context Protocol) server — expose svadmin DataProvider as tools for AI agents",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -20,7 +20,8 @@
20
20
  }
21
21
  },
22
22
  "peerDependencies": {
23
- "@svadmin/core": "^0.69.3"
23
+ "@svadmin/core": "^0.71.0",
24
+ "@svadmin/devtools": "^0.6.0"
24
25
  },
25
26
  "license": "MIT",
26
27
  "publishConfig": {
@@ -40,6 +41,9 @@
40
41
  "peerDependenciesMeta": {
41
42
  "@svadmin/core": {
42
43
  "optional": true
44
+ },
45
+ "@svadmin/devtools": {
46
+ "optional": true
43
47
  }
44
48
  }
45
49
  }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * @svadmin/mcp/governance
3
+ *
4
+ * Pure, filesystem-free rule engine for the SVAdmin Vibe Coding governance
5
+ * checks. The MCP tool and `scripts/check-vibe-governance.ts` both delegate to
6
+ * this module so agents and CI enforce exactly the same rules.
7
+ */
8
+
9
+ export type GovernanceRuleId = 'no-unmanaged-fetch' | 'escape-hatch-discipline';
10
+
11
+ export interface GovernanceSource {
12
+ /** Path used in reports and error messages */
13
+ path: string;
14
+ /** Full source text of the file */
15
+ content: string;
16
+ }
17
+
18
+ export interface GovernanceViolation {
19
+ file: string;
20
+ line: number;
21
+ rule: GovernanceRuleId;
22
+ message: string;
23
+ remediation: string;
24
+ }
25
+
26
+ export interface VibeGovernanceReport {
27
+ passed: boolean;
28
+ totalFilesScanned: number;
29
+ rulesChecked: GovernanceRuleId[];
30
+ violations: GovernanceViolation[];
31
+ summary: string;
32
+ }
33
+
34
+ export interface VibeGovernanceOptions {
35
+ /**
36
+ * Strict mode: a `@vibe-escape` annotation only suppresses a violation when it
37
+ * carries a non-empty reason. Non-strict mode honours any annotation but still
38
+ * reports annotations that are missing a reason.
39
+ */
40
+ strict?: boolean;
41
+ }
42
+
43
+ const ESCAPE_MARKER = /@vibe-escape\b/u;
44
+ const ESCAPE_REASON = /@vibe-escape\s*:\s*(\S.*)$/u;
45
+ const COMMENT_PREFIX = /^(?:\/\/|\/\*|\*)/u;
46
+ const NETWORK_CALL = [/[^.\w]fetch\s*\(/u, /(?:^|[^.\w])axios\s*[.(]/u];
47
+
48
+ function escapeSuppression(lines: string[], strict: boolean): boolean {
49
+ for (const line of lines) {
50
+ if (!ESCAPE_MARKER.test(line)) continue;
51
+ if (!strict) return true;
52
+ if (ESCAPE_REASON.test(line)) return true;
53
+ }
54
+ return false;
55
+ }
56
+
57
+ /**
58
+ * Scans in-memory sources for architectural violations.
59
+ *
60
+ * Rules:
61
+ * 1. `no-unmanaged-fetch` — view code must not call `fetch()` / `axios` directly
62
+ * unless the file documents `// @vibe-escape: <reason>`.
63
+ * 2. `escape-hatch-discipline` — every `@vibe-escape` marker must carry a reason.
64
+ */
65
+ export function scanVibeGovernance(
66
+ sources: GovernanceSource[],
67
+ options: VibeGovernanceOptions = {},
68
+ ): VibeGovernanceReport {
69
+ const strict = options.strict ?? false;
70
+ const violations: GovernanceViolation[] = [];
71
+
72
+ for (const source of sources) {
73
+ const lines = source.content.split(/\r?\n/u);
74
+ const suppressed = escapeSuppression(lines, strict);
75
+
76
+ lines.forEach((line, index) => {
77
+ const lineNumber = index + 1;
78
+ const trimmed = line.trim();
79
+
80
+ if (ESCAPE_MARKER.test(line) && !ESCAPE_REASON.test(line)) {
81
+ violations.push({
82
+ file: source.path,
83
+ line: lineNumber,
84
+ rule: 'escape-hatch-discipline',
85
+ message: `Escape hatch annotation is missing a reason: "${trimmed}"`,
86
+ remediation: 'Use "// @vibe-escape: <reason>" so reviewers and agents can justify the bypass.',
87
+ });
88
+ }
89
+
90
+ if (suppressed) return;
91
+ if (COMMENT_PREFIX.test(trimmed)) return;
92
+ if (ESCAPE_MARKER.test(line)) return;
93
+ if (!NETWORK_CALL.some(pattern => pattern.test(line))) return;
94
+
95
+ violations.push({
96
+ file: source.path,
97
+ line: lineNumber,
98
+ rule: 'no-unmanaged-fetch',
99
+ message: `Direct unmanaged network call detected: "${trimmed}"`,
100
+ remediation:
101
+ 'Use standard DataProvider hooks (useList, useOne, useCreate, useUpdate) or add "// @vibe-escape: <reason>" if an external API call is intended.',
102
+ });
103
+ });
104
+ }
105
+
106
+ const passed = violations.length === 0;
107
+ return {
108
+ passed,
109
+ totalFilesScanned: sources.length,
110
+ rulesChecked: ['no-unmanaged-fetch', 'escape-hatch-discipline'],
111
+ violations,
112
+ summary: passed
113
+ ? `Vibe Governance passed! Scanned ${sources.length} files with zero architectural violations.`
114
+ : `Vibe Governance failed with ${violations.length} violation(s) across ${sources.length} scanned files.`,
115
+ };
116
+ }
package/src/index.ts CHANGED
@@ -25,6 +25,18 @@ import { definedOptions } from '@svadmin/core/options';
25
25
  */
26
26
 
27
27
  import type { DataProvider, ResourceDefinition } from '@svadmin/core';
28
+ import { inferResource } from '@svadmin/core/inferencer';
29
+ import { scanVibeGovernance, type GovernanceSource } from './governance';
30
+ import { inferFromJSONSchema, inferFromDatabaseColumns } from '@svadmin/core/inferencer-schema';
31
+ import {
32
+ buildResourcePatch,
33
+ buildSelfHealingReport,
34
+ type DevtoolsAppDiagnostics,
35
+ type DevtoolsCollector,
36
+ type DevtoolsDiagnostic,
37
+ type DevtoolsSnapshot,
38
+ type ResourcePatchAction,
39
+ } from '@svadmin/devtools';
28
40
 
29
41
  // ─── Types ───────────────────────────────────────────────────
30
42
 
@@ -41,6 +53,14 @@ export interface MCPServerOptions {
41
53
  version?: string;
42
54
  /** When true, only expose read operations (getList, getOne, getMany) */
43
55
  readOnly?: boolean;
56
+ /** Optional DevTools collector instance to expose runtime diagnostics to Agent */
57
+ collector?: DevtoolsCollector;
58
+ /** Optional custom diagnostics provider function */
59
+ getDiagnostics?: () =>
60
+ | DevtoolsAppDiagnostics
61
+ | DevtoolsSnapshot
62
+ | DevtoolsDiagnostic[]
63
+ | Promise<DevtoolsAppDiagnostics | DevtoolsSnapshot | DevtoolsDiagnostic[]>;
44
64
  }
45
65
 
46
66
  /** MCP Tool definition following the Model Context Protocol spec */
@@ -217,6 +237,118 @@ function generateTools(options: MCPServerOptions): MCPTool[] {
217
237
  });
218
238
  }
219
239
 
240
+ // Diagnostics and self-healing tools
241
+ tools.push({
242
+ name: 'svadmin_getDiagnostics',
243
+ description:
244
+ 'Fetch current SVAdmin runtime diagnostics, including health status, configured providers, active resources, and error events.',
245
+ readOnly: true,
246
+ concurrent: true,
247
+ destructive: false,
248
+ inputSchema: {
249
+ type: 'object',
250
+ properties: {},
251
+ },
252
+ });
253
+
254
+ tools.push({
255
+ name: 'svadmin_getSelfHealingPrompt',
256
+ description:
257
+ 'Get an AI-ready self-healing diagnosis report from runtime errors captured by SVAdmin DevTools, with structured root causes and suggested code fixes.',
258
+ readOnly: true,
259
+ concurrent: true,
260
+ destructive: false,
261
+ inputSchema: {
262
+ type: 'object',
263
+ properties: {},
264
+ },
265
+ });
266
+
267
+ tools.push({
268
+ name: 'svadmin_getManifest',
269
+ description:
270
+ 'Get the active SVAdmin application manifest including registered resources, fields, operations, and data provider metadata.',
271
+ readOnly: true,
272
+ concurrent: true,
273
+ destructive: false,
274
+ inputSchema: {
275
+ type: 'object',
276
+ properties: {},
277
+ },
278
+ });
279
+
280
+ tools.push({
281
+ name: 'svadmin_generateResource',
282
+ description:
283
+ 'Zero-hallucination code generator for SVAdmin. Infers ResourceDefinition, TypeBox validation, and complete Svelte 5 AutoTable/AutoForm component snippets from sample records, JSON Schema, or database column definitions.',
284
+ readOnly: true,
285
+ concurrent: true,
286
+ destructive: false,
287
+ inputSchema: {
288
+ type: 'object',
289
+ properties: {
290
+ resource: { type: 'string', description: 'Name of the resource to generate (e.g. "orders", "users")' },
291
+ sourceType: {
292
+ type: 'string',
293
+ enum: ['sampleData', 'jsonSchema', 'databaseColumns'],
294
+ description: 'Type of input payload',
295
+ },
296
+ payload: { type: 'string', description: 'JSON-serialized payload matching the sourceType' },
297
+ idKey: { type: 'string', description: 'Primary key name (defaults to "id")' },
298
+ label: { type: 'string', description: 'Human-readable resource label' },
299
+ },
300
+ required: ['resource', 'sourceType', 'payload'],
301
+ },
302
+ });
303
+
304
+ tools.push({
305
+ name: 'svadmin_applyResourcePatch',
306
+ description:
307
+ 'Applies a structured writeback patch derived from the Visual Inspector (Click-to-Edit), generating verified code snippets and agent prompts to update ResourceDefinitions, column visibility, or field properties.',
308
+ readOnly: true,
309
+ concurrent: true,
310
+ destructive: false,
311
+ inputSchema: {
312
+ type: 'object',
313
+ properties: {
314
+ resource: { type: 'string', description: 'Resource name to patch' },
315
+ action: {
316
+ type: 'string',
317
+ enum: ['updateField', 'toggleColumnVisibility', 'updateColumnTitle', 'updatePageSize', 'reorderColumns'],
318
+ description: 'Patch action to perform',
319
+ },
320
+ field: { type: 'string', description: 'Field name targeted' },
321
+ column: { type: 'string', description: 'Column name targeted' },
322
+ value: { type: 'string', description: 'JSON-serialized or string patch value' },
323
+ targetComponent: { type: 'string', description: 'Component context (e.g. AutoTable, AutoForm)' },
324
+ },
325
+ required: ['resource', 'action'],
326
+ },
327
+ });
328
+
329
+ tools.push({
330
+ name: 'svadmin_verifyGovernance',
331
+ description:
332
+ 'Runs the SVAdmin Vibe Coding architectural governance check on the supplied sources, verifying data provider isolation (no unmanaged fetch/axios) and escape-hatch annotation discipline. Returns the same verdict as `bun run check:vibe-governance`.',
333
+ readOnly: true,
334
+ concurrent: true,
335
+ destructive: false,
336
+ inputSchema: {
337
+ type: 'object',
338
+ properties: {
339
+ files: {
340
+ type: 'string',
341
+ description: 'JSON-serialized array of { path, content } sources to scan (for example the files an agent just edited).',
342
+ },
343
+ strict: {
344
+ type: 'boolean',
345
+ description: 'Require every @vibe-escape annotation to carry a non-empty reason before it suppresses a violation.',
346
+ },
347
+ },
348
+ required: ['files'],
349
+ },
350
+ });
351
+
220
352
  return tools;
221
353
  }
222
354
 
@@ -228,7 +360,15 @@ async function handleToolCall(
228
360
  options: MCPServerOptions,
229
361
  ): Promise<unknown> {
230
362
  const resource = args['resource'] as string;
231
- if (resource && !options.resources.includes(resource)) {
363
+ const dataCrudTools = [
364
+ 'svadmin_getList',
365
+ 'svadmin_getOne',
366
+ 'svadmin_getMany',
367
+ 'svadmin_create',
368
+ 'svadmin_update',
369
+ 'svadmin_delete',
370
+ ];
371
+ if (dataCrudTools.includes(toolName) && resource && !options.resources.includes(resource)) {
232
372
  throw new Error(`Unauthorized resource access: ${resource} is not exposed.`);
233
373
  }
234
374
  const dp = options.dataProvider;
@@ -296,6 +436,149 @@ async function handleToolCall(
296
436
  });
297
437
  }
298
438
 
439
+ case 'svadmin_getDiagnostics': {
440
+ if (options.collector) {
441
+ return options.collector.snapshot();
442
+ }
443
+ if (options.getDiagnostics) {
444
+ return await options.getDiagnostics();
445
+ }
446
+ return {
447
+ status: 'healthy',
448
+ resources: options.resources,
449
+ dataProvider: options.dataProvider.getApiUrl?.() ?? 'configured',
450
+ message: 'No DevTools collector attached. Basic provider info returned.',
451
+ };
452
+ }
453
+
454
+ case 'svadmin_getSelfHealingPrompt': {
455
+ let diags: DevtoolsDiagnostic[] = [];
456
+ if (options.collector) {
457
+ diags = options.collector.snapshot().diagnostics;
458
+ } else if (options.getDiagnostics) {
459
+ const res = await options.getDiagnostics();
460
+ if (Array.isArray(res)) {
461
+ diags = res;
462
+ } else if (res && typeof res === 'object') {
463
+ if ('snapshot' in res && res.snapshot) {
464
+ diags = (res.snapshot as DevtoolsSnapshot).diagnostics;
465
+ } else if ('diagnostics' in res && Array.isArray((res as { diagnostics: unknown }).diagnostics)) {
466
+ diags = (res as { diagnostics: DevtoolsDiagnostic[] }).diagnostics;
467
+ }
468
+ }
469
+ }
470
+ return buildSelfHealingReport(diags);
471
+ }
472
+
473
+ case 'svadmin_getManifest': {
474
+ return {
475
+ resources: options.resourceDefinitions ?? options.resources.map((name) => ({ name })),
476
+ capabilities: {
477
+ apiUrl: options.dataProvider.getApiUrl?.() ?? undefined,
478
+ readOnly: !!options.readOnly,
479
+ hasCollector: !!options.collector,
480
+ },
481
+ };
482
+ }
483
+
484
+ case 'svadmin_generateResource': {
485
+ const resource = args['resource'] as string;
486
+ const sourceType = args['sourceType'] as 'sampleData' | 'jsonSchema' | 'databaseColumns';
487
+ const rawPayload = args['payload'] as string;
488
+ const idKey = (args['idKey'] as string) ?? 'id';
489
+ const label = args['label'] as string | undefined;
490
+
491
+ const parsedPayload = typeof rawPayload === 'string' ? JSON.parse(rawPayload) : rawPayload;
492
+ let result;
493
+
494
+ if (sourceType === 'sampleData') {
495
+ result = inferResource(resource, Array.isArray(parsedPayload) ? parsedPayload : [parsedPayload], definedOptions({
496
+ primaryKey: idKey,
497
+ label,
498
+ }));
499
+ } else if (sourceType === 'jsonSchema') {
500
+ result = inferFromJSONSchema(resource, parsedPayload, definedOptions({
501
+ primaryKey: idKey,
502
+ label,
503
+ }));
504
+ } else if (sourceType === 'databaseColumns') {
505
+ result = inferFromDatabaseColumns(resource, parsedPayload, definedOptions({
506
+ primaryKey: idKey,
507
+ label,
508
+ }));
509
+ } else {
510
+ throw new Error(`Invalid sourceType: ${sourceType}`);
511
+ }
512
+
513
+ return {
514
+ resource: result.resource,
515
+ code: result.code,
516
+ typeboxCode: result.typeboxCode,
517
+ componentCode: result.componentCode,
518
+ review: result.review,
519
+ };
520
+ }
521
+
522
+ case 'svadmin_applyResourcePatch': {
523
+ const resource = args['resource'] as string;
524
+ const action = args['action'] as ResourcePatchAction;
525
+ const field = args['field'] as string | undefined;
526
+ const column = args['column'] as string | undefined;
527
+ const targetComponent = args['targetComponent'] as string | undefined;
528
+ let value = args['value'];
529
+ if (typeof value === 'string') {
530
+ try {
531
+ value = JSON.parse(value);
532
+ } catch {
533
+ // Keep as string
534
+ }
535
+ }
536
+
537
+ return buildResourcePatch({
538
+ resource,
539
+ action,
540
+ value,
541
+ ...definedOptions({
542
+ field,
543
+ column,
544
+ targetComponent,
545
+ }),
546
+ });
547
+ }
548
+
549
+ case 'svadmin_verifyGovernance': {
550
+ const strict = Boolean(args['strict']);
551
+ const rawFiles = args['files'];
552
+ const parsed: unknown = typeof rawFiles === 'string' ? JSON.parse(rawFiles) : rawFiles;
553
+ if (!Array.isArray(parsed) || parsed.length === 0) {
554
+ throw new Error('svadmin_verifyGovernance requires a non-empty files array of { path, content } sources.');
555
+ }
556
+
557
+ const sources: GovernanceSource[] = parsed.map((entry: unknown, index: number) => {
558
+ const candidate = entry as { path?: unknown; content?: unknown } | null;
559
+ if (
560
+ candidate === null ||
561
+ typeof candidate !== 'object' ||
562
+ typeof candidate.path !== 'string' ||
563
+ typeof candidate.content !== 'string'
564
+ ) {
565
+ throw new Error(`svadmin_verifyGovernance: files[${index}] must be an object with string path and content.`);
566
+ }
567
+ return { path: candidate.path, content: candidate.content };
568
+ });
569
+
570
+ const report = scanVibeGovernance(sources, { strict });
571
+ return {
572
+ governancePassed: report.passed,
573
+ status: report.passed ? 'compliant' : 'violations',
574
+ strictMode: strict,
575
+ rulesChecked: report.rulesChecked,
576
+ filesScanned: report.totalFilesScanned,
577
+ violations: report.violations,
578
+ summary: report.summary,
579
+ };
580
+ }
581
+
299
582
  default:
300
583
  throw new Error(`Unknown tool: ${toolName}`);
301
584
  }
@@ -326,6 +609,15 @@ async function handleToolCall(
326
609
  /**
327
610
  * Projects an MCP tool into the standard allowlisted schema while preserving metadata and safety flags.
328
611
  */
612
+ export { scanVibeGovernance } from './governance';
613
+ export type {
614
+ GovernanceRuleId,
615
+ GovernanceSource,
616
+ GovernanceViolation,
617
+ VibeGovernanceOptions,
618
+ VibeGovernanceReport,
619
+ } from './governance';
620
+
329
621
  export function projectMCPToolSchema(tool: MCPTool): MCPTool {
330
622
  return {
331
623
  name: tool.name,