@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 +1 -0
- package/package.json +1 -1
- package/src/lib/source-artifacts.js +82 -0
- package/src/mcp-server-entry.mjs +7 -4
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
|
@@ -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
|
+
}
|
package/src/mcp-server-entry.mjs
CHANGED
|
@@ -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
|
-
|
|
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,
|