@enfyra/mcp-server 0.1.1 → 0.1.2

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 CHANGED
@@ -257,6 +257,7 @@ The MCP server includes safety guards for LLM callers:
257
257
  - `validate_extension_code` checks Enfyra admin extension code through `/enfyra_extension/preview` without saving.
258
258
  - Dynamic script guidance distinguishes secure repositories (`@REPOS.main`, `@REPOS.secure.<table>`) from trusted internal repositories (`@REPOS.<table>`), and tells agents not to return raw trusted records to users.
259
259
  - `compiledCode` is generated from `sourceCode` and may differ textually because macros are expanded; the MCP server never accepts hand-written `compiledCode`.
260
+ - Long source/code values in read responses are written to `/tmp/enfyra-mcp-sources` and returned as length/hash/preview/tmpFile metadata so LLM callers can inspect full source from the file path without truncating tool output.
260
261
  - JSON responses include `compressionStats` with estimated token savings. Arrays of objects are converted to columnar form only when the compact shape is smaller than raw JSON.
261
262
  - Relation tools reject physical FK/junction names and resolve table ids from exact table names or aliases before schema mutation.
262
263
  - Generated code should use relation property names such as `conversation`, `sender`, and `member` instead of physical FK fields such as `conversationId`, `senderId`, or `memberId`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enfyra/mcp-server",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "MCP server for Enfyra - manage Enfyra instances from MCP-compatible coding tools",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,82 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { mkdirSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { tmpdir } from 'node:os';
5
+
6
+ const DEFAULT_PREVIEW_CHARS = 1200;
7
+ const DEFAULT_INLINE_LIMIT = 1400;
8
+ const SOURCE_FIELD_NAMES = new Set([
9
+ 'sourceCode',
10
+ 'code',
11
+ 'compiledCode',
12
+ 'handlerScript',
13
+ 'connectionHandlerScript',
14
+ ]);
15
+
16
+ function sha256(value) {
17
+ return createHash('sha256').update(String(value), 'utf8').digest('hex');
18
+ }
19
+
20
+ function safePart(value) {
21
+ const source = String(value || 'source').trim();
22
+ return source.replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 80) || 'source';
23
+ }
24
+
25
+ function extensionForField(fieldName) {
26
+ if (fieldName === 'code') return '.vue';
27
+ return '.js';
28
+ }
29
+
30
+ export function writeSourceArtifact({ tableName, id, fieldName, source }) {
31
+ const hash = sha256(source);
32
+ const dir = join(tmpdir(), 'enfyra-mcp-sources');
33
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
34
+ const fileName = [
35
+ safePart(tableName),
36
+ safePart(id),
37
+ safePart(fieldName),
38
+ hash.slice(0, 12),
39
+ ].join('-') + extensionForField(fieldName);
40
+ const path = join(dir, fileName);
41
+ writeFileSync(path, source, { mode: 0o600 });
42
+ return {
43
+ tmpFile: path,
44
+ length: source.length,
45
+ sha256: hash,
46
+ preview: source.length > DEFAULT_PREVIEW_CHARS
47
+ ? `${source.slice(0, DEFAULT_PREVIEW_CHARS)}...`
48
+ : source,
49
+ };
50
+ }
51
+
52
+ export function compactSourceField({ tableName, id, fieldName, source, alwaysWrite = false }) {
53
+ if (typeof source !== 'string') return source;
54
+ if (!alwaysWrite && source.length <= DEFAULT_INLINE_LIMIT) return source;
55
+ return writeSourceArtifact({ tableName, id, fieldName, source });
56
+ }
57
+
58
+ export function compactSourceFields(value, { tableName, idField = 'id', alwaysWrite = false } = {}) {
59
+ if (Array.isArray(value)) {
60
+ return value.map((item) => compactSourceFields(item, { tableName, idField, alwaysWrite }));
61
+ }
62
+ if (!value || typeof value !== 'object') return value;
63
+
64
+ const recordId = value[idField] ?? value._id ?? value.id ?? 'record';
65
+ const out = {};
66
+ for (const [key, fieldValue] of Object.entries(value)) {
67
+ if (SOURCE_FIELD_NAMES.has(key) && typeof fieldValue === 'string') {
68
+ out[key] = compactSourceField({
69
+ tableName,
70
+ id: recordId,
71
+ fieldName: key,
72
+ source: fieldValue,
73
+ alwaysWrite,
74
+ });
75
+ } else if (fieldValue && typeof fieldValue === 'object') {
76
+ out[key] = compactSourceFields(fieldValue, { tableName, idField, alwaysWrite });
77
+ } else {
78
+ out[key] = fieldValue;
79
+ }
80
+ }
81
+ return out;
82
+ }
@@ -33,6 +33,7 @@ import {
33
33
  } from './lib/required-knowledge.js';
34
34
  import { validateMainTableRoutePath } from './lib/route-guards.js';
35
35
  import { installColumnarToolFormatter, jsonContent } from './lib/response-format.js';
36
+ import { compactSourceFields, writeSourceArtifact } from './lib/source-artifacts.js';
36
37
  import {
37
38
  findRoutePermission,
38
39
  mergeMethodNames,
@@ -1357,7 +1358,7 @@ server.tool('query_table', 'Query any route-backed table. Response is minimal un
1357
1358
  },
1358
1359
  minimalDefaultApplied: !(fields && fields.length > 0),
1359
1360
  meta: result?.meta,
1360
- data: result?.data || [],
1361
+ data: compactSourceFields(result?.data || [], { tableName }),
1361
1362
  detailHint: fields && fields.length > 0
1362
1363
  ? undefined
1363
1364
  : 'Only the primary key was returned because fields was omitted. Re-run query_table with explicit fields for details, or use inspect_table to find valid field names.',
@@ -1438,7 +1439,7 @@ server.tool(
1438
1439
  tableName,
1439
1440
  primaryKey,
1440
1441
  fields: selectedFields,
1441
- data: one,
1442
+ data: compactSourceFields(one, { tableName }),
1442
1443
  detailHint: fields && fields.length > 0 ? undefined : 'Only the primary key was returned. Pass fields for details.',
1443
1444
  }, null, 2) }] };
1444
1445
  }
@@ -1456,7 +1457,7 @@ server.tool(
1456
1457
  return { content: [{ type: 'text', text: JSON.stringify({
1457
1458
  tableName,
1458
1459
  fields: selectedFields,
1459
- data: result.data?.[0] || null,
1460
+ data: compactSourceFields(result.data?.[0] || null, { tableName }),
1460
1461
  detailHint: fields && fields.length > 0 ? undefined : 'Only the primary key was returned. Pass fields for details.',
1461
1462
  }, null, 2) }] };
1462
1463
  },
@@ -1515,12 +1516,14 @@ server.tool(
1515
1516
  },
1516
1517
  async ({ tableName, id }) => {
1517
1518
  const { primaryKey, record, sourceField, sourceCode } = await fetchScriptRecord(tableName, id);
1519
+ const sourceArtifact = writeSourceArtifact({ tableName, id, fieldName: sourceField, source: sourceCode });
1518
1520
  return { content: [{ type: 'text', text: JSON.stringify({
1519
1521
  tableName,
1520
1522
  id,
1521
1523
  primaryKey,
1522
1524
  sourceField,
1523
- sourceCode,
1525
+ sourceFile: sourceArtifact.tmpFile,
1526
+ sourcePreview: sourceArtifact.preview,
1524
1527
  sourceLength: sourceCode.length,
1525
1528
  sourceSha256: sha256(sourceCode),
1526
1529
  scriptLanguage: record.scriptLanguage || record.language || null,