@enfyra/mcp-server 0.1.14 → 0.1.16

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,219 @@
1
+ declare const RUNTIME_ZONES: readonly ["admin_ui", "api_runtime", "flow_runtime", "websocket_runtime", "graphql_runtime", "schema_data", "package_runtime", "storage_file", "auth_security"];
2
+ type RuntimeZone = typeof RUNTIME_ZONES[number];
3
+ type RuntimeRecord = Record<string, any>;
4
+ export declare const RUNTIME_ZONE_DESCRIPTIONS: Record<RuntimeZone, string>;
5
+ export declare function searchRuntimeZone(apiUrl: string, input: any): Promise<{
6
+ action: string;
7
+ zone: string;
8
+ zoneDescription: string;
9
+ query: string;
10
+ resultCount: number;
11
+ results: any[];
12
+ searched: {
13
+ tables: any;
14
+ };
15
+ } | {
16
+ zone: "admin_ui";
17
+ zoneDescription: string;
18
+ action: string;
19
+ extension: {
20
+ id: any;
21
+ name: any;
22
+ extensionId: any;
23
+ type: any;
24
+ isEnabled: any;
25
+ version: any;
26
+ updatedAt: any;
27
+ description: any;
28
+ };
29
+ surface: string;
30
+ menu: {
31
+ id: any;
32
+ label: any;
33
+ path: any;
34
+ icon: any;
35
+ sidebarId: any;
36
+ parent: {
37
+ id: any;
38
+ label: any;
39
+ path: any;
40
+ };
41
+ };
42
+ source: {
43
+ tmpFile: string;
44
+ length: number;
45
+ sha256: string;
46
+ preview: string;
47
+ };
48
+ consumers: any[];
49
+ nextSteps: string[];
50
+ next?: undefined;
51
+ tableName?: undefined;
52
+ id?: undefined;
53
+ record?: undefined;
54
+ sources?: undefined;
55
+ } | {
56
+ action: string;
57
+ zone: "schema_data";
58
+ zoneDescription: string;
59
+ next: {
60
+ tool: string;
61
+ input: {
62
+ tableName: any;
63
+ mode?: undefined;
64
+ zone?: undefined;
65
+ query?: undefined;
66
+ maxResults?: undefined;
67
+ };
68
+ } | {
69
+ tool: string;
70
+ input: {
71
+ mode: string;
72
+ zone: "schema_data";
73
+ query: any;
74
+ maxResults: number;
75
+ tableName?: undefined;
76
+ };
77
+ };
78
+ tableName?: undefined;
79
+ id?: undefined;
80
+ record?: undefined;
81
+ sources?: undefined;
82
+ nextSteps?: undefined;
83
+ } | {
84
+ action: string;
85
+ zone: "api_runtime" | "flow_runtime" | "websocket_runtime" | "graphql_runtime" | "package_runtime" | "storage_file" | "auth_security";
86
+ zoneDescription: string;
87
+ tableName: string;
88
+ id: any;
89
+ record: RuntimeRecord;
90
+ sources: any[];
91
+ nextSteps: string[];
92
+ next?: undefined;
93
+ } | {
94
+ action: string;
95
+ zone: "admin_ui";
96
+ zoneDescription: string;
97
+ results: any[];
98
+ query: string;
99
+ path: string;
100
+ type: any;
101
+ searched: {
102
+ extensions: number;
103
+ menus: number;
104
+ includeDisabled: boolean;
105
+ };
106
+ resultCount: number;
107
+ tokenBudget: {
108
+ estimatedOutputChars: number;
109
+ estimatedOutputTokens: number;
110
+ controls: {
111
+ maxResults: number;
112
+ snippetChars: number;
113
+ maxMatchesPerExtension: number;
114
+ includeSourceArtifact: boolean;
115
+ };
116
+ };
117
+ guidance: string[];
118
+ readErrors?: undefined;
119
+ } | {
120
+ action: string;
121
+ zone: "api_runtime" | "flow_runtime" | "websocket_runtime" | "graphql_runtime" | "package_runtime" | "storage_file" | "auth_security";
122
+ zoneDescription: string;
123
+ query: string;
124
+ path: string;
125
+ resultCount: number;
126
+ results: any[];
127
+ readErrors: any[];
128
+ tokenBudget: {
129
+ estimatedOutputChars: number;
130
+ estimatedOutputTokens: number;
131
+ controls: {
132
+ maxResults: number;
133
+ snippetChars: number;
134
+ includeSourceArtifact: boolean;
135
+ };
136
+ };
137
+ guidance: string[];
138
+ }>;
139
+ export declare function inspectRuntimeZoneLocation(apiUrl: string, input: any): Promise<{
140
+ zone: "admin_ui";
141
+ zoneDescription: string;
142
+ action: string;
143
+ extension: {
144
+ id: any;
145
+ name: any;
146
+ extensionId: any;
147
+ type: any;
148
+ isEnabled: any;
149
+ version: any;
150
+ updatedAt: any;
151
+ description: any;
152
+ };
153
+ surface: string;
154
+ menu: {
155
+ id: any;
156
+ label: any;
157
+ path: any;
158
+ icon: any;
159
+ sidebarId: any;
160
+ parent: {
161
+ id: any;
162
+ label: any;
163
+ path: any;
164
+ };
165
+ };
166
+ source: {
167
+ tmpFile: string;
168
+ length: number;
169
+ sha256: string;
170
+ preview: string;
171
+ };
172
+ consumers: any[];
173
+ nextSteps: string[];
174
+ next?: undefined;
175
+ tableName?: undefined;
176
+ id?: undefined;
177
+ record?: undefined;
178
+ sources?: undefined;
179
+ } | {
180
+ action: string;
181
+ zone: "schema_data";
182
+ zoneDescription: string;
183
+ next: {
184
+ tool: string;
185
+ input: {
186
+ tableName: any;
187
+ mode?: undefined;
188
+ zone?: undefined;
189
+ query?: undefined;
190
+ maxResults?: undefined;
191
+ };
192
+ } | {
193
+ tool: string;
194
+ input: {
195
+ mode: string;
196
+ zone: "schema_data";
197
+ query: any;
198
+ maxResults: number;
199
+ tableName?: undefined;
200
+ };
201
+ };
202
+ tableName?: undefined;
203
+ id?: undefined;
204
+ record?: undefined;
205
+ sources?: undefined;
206
+ nextSteps?: undefined;
207
+ } | {
208
+ action: string;
209
+ zone: "api_runtime" | "flow_runtime" | "websocket_runtime" | "graphql_runtime" | "package_runtime" | "storage_file" | "auth_security";
210
+ zoneDescription: string;
211
+ tableName: string;
212
+ id: any;
213
+ record: RuntimeRecord;
214
+ sources: any[];
215
+ nextSteps: string[];
216
+ next?: undefined;
217
+ }>;
218
+ export declare function registerRuntimeZoneTools(server: any, ENFYRA_API_URL: string): void;
219
+ export {};
@@ -0,0 +1,412 @@
1
+ import { z } from 'zod';
2
+ import { fetchAPI } from './fetch.js';
3
+ import { jsonContent } from './response-format.js';
4
+ import { writeSourceArtifact } from './source-artifacts.js';
5
+ import { inspectExtensionLocation, searchExtensions } from './extension-search-tools.js';
6
+ const RUNTIME_ZONES = [
7
+ 'admin_ui',
8
+ 'api_runtime',
9
+ 'flow_runtime',
10
+ 'websocket_runtime',
11
+ 'graphql_runtime',
12
+ 'schema_data',
13
+ 'package_runtime',
14
+ 'storage_file',
15
+ 'auth_security',
16
+ ];
17
+ const ZONE_TABLES = {
18
+ api_runtime: [
19
+ { tableName: 'enfyra_route', fields: 'id,_id,path,description,isEnabled,mainTable.name,availableMethods.name,publicMethods.name', labelFields: ['path', 'description'], pathFields: ['path'] },
20
+ { tableName: 'enfyra_route_handler', fields: 'id,_id,name,key,sourceCode,scriptLanguage,route.id,route.path,method.name', sourceFields: ['sourceCode'], labelFields: ['name', 'key', 'route.path', 'method.name'], pathFields: ['route.path'] },
21
+ { tableName: 'enfyra_pre_hook', fields: 'id,_id,name,key,sourceCode,scriptLanguage,isGlobal,route.id,route.path,methods.name', sourceFields: ['sourceCode'], labelFields: ['name', 'key', 'route.path'], pathFields: ['route.path'] },
22
+ { tableName: 'enfyra_post_hook', fields: 'id,_id,name,key,sourceCode,scriptLanguage,isGlobal,route.id,route.path,methods.name', sourceFields: ['sourceCode'], labelFields: ['name', 'key', 'route.path'], pathFields: ['route.path'] },
23
+ { tableName: 'enfyra_guard', fields: 'id,_id,name,description,position,isGlobal,isEnabled,route.id,route.path,methods.name', labelFields: ['name', 'description', 'route.path'], pathFields: ['route.path'] },
24
+ { tableName: 'enfyra_guard_rule', fields: 'id,_id,name,field,operator,value,guard.id,guard.name,description,isEnabled', labelFields: ['name', 'field', 'operator', 'value', 'guard.name', 'description'] },
25
+ { tableName: 'enfyra_route_permission', fields: 'id,_id,description,isEnabled,route.id,route.path,role.name,methods.name,allowedUsers.id', labelFields: ['description', 'route.path', 'role.name'], pathFields: ['route.path'] },
26
+ ],
27
+ flow_runtime: [
28
+ { tableName: 'enfyra_flow', fields: 'id,_id,name,description,triggerType,triggerConfig,isEnabled,timeout,maxExecutions', labelFields: ['name', 'description', 'triggerType', 'triggerConfig'] },
29
+ { tableName: 'enfyra_flow_step', fields: 'id,_id,name,key,type,description,sourceCode,condition,config,flow.id,flow.name,nextStep.id,errorStep.id,isEnabled', sourceFields: ['sourceCode', 'condition'], labelFields: ['name', 'key', 'type', 'description', 'flow.name', 'config'] },
30
+ ],
31
+ websocket_runtime: [
32
+ { tableName: 'enfyra_websocket', fields: 'id,_id,path,description,connectionHandlerScript,scriptLanguage,isEnabled', sourceFields: ['connectionHandlerScript'], labelFields: ['path', 'description'], pathFields: ['path'] },
33
+ { tableName: 'enfyra_websocket_event', fields: 'id,_id,eventName,description,handlerScript,scriptLanguage,gateway.id,gateway.path,isEnabled', sourceFields: ['handlerScript'], labelFields: ['eventName', 'description', 'gateway.path'], pathFields: ['gateway.path'] },
34
+ ],
35
+ graphql_runtime: [
36
+ { tableName: 'enfyra_graphql', fields: 'id,_id,name,description,sourceCode,scriptLanguage,table.id,table.name,isEnabled', sourceFields: ['sourceCode'], labelFields: ['name', 'description', 'table.name'] },
37
+ ],
38
+ package_runtime: [
39
+ { tableName: 'enfyra_package', fields: 'id,_id,name,version,type,description,isEnabled,createdAt,updatedAt', labelFields: ['name', 'version', 'type', 'description'] },
40
+ ],
41
+ storage_file: [
42
+ { tableName: 'enfyra_storage_config', fields: 'id,_id,name,provider,bucket,baseUrl,description,isDefault,isEnabled', labelFields: ['name', 'provider', 'bucket', 'baseUrl', 'description'] },
43
+ { tableName: 'enfyra_folder', fields: 'id,_id,name,path,description,parent.id,parent.name,isPublic,createdAt,updatedAt', labelFields: ['name', 'path', 'description'], pathFields: ['path'] },
44
+ { tableName: 'enfyra_file', fields: 'id,_id,fileName,originalName,mimeType,path,url,isPublic,folder.id,folder.name,storage.id,storage.name,createdAt,updatedAt', labelFields: ['fileName', 'originalName', 'mimeType', 'path', 'url', 'folder.name', 'storage.name'], pathFields: ['path', 'url'] },
45
+ { tableName: 'enfyra_file_permission', fields: 'id,_id,file.id,file.fileName,role.name,allowedUsers.id,methods.name,description,isEnabled', labelFields: ['file.fileName', 'role.name', 'description'] },
46
+ ],
47
+ auth_security: [
48
+ { tableName: 'enfyra_role', fields: 'id,_id,name,description,isSystem,createdAt,updatedAt', labelFields: ['name', 'description'] },
49
+ { tableName: 'enfyra_route_permission', fields: 'id,_id,description,isEnabled,route.id,route.path,role.name,methods.name,allowedUsers.id', labelFields: ['description', 'route.path', 'role.name'], pathFields: ['route.path'] },
50
+ { tableName: 'enfyra_field_permission', fields: 'id,_id,description,action,effect,role.name,column.name,relation.propertyName,condition,isEnabled', labelFields: ['description', 'action', 'effect', 'role.name', 'column.name', 'relation.propertyName', 'condition'] },
51
+ { tableName: 'enfyra_guard', fields: 'id,_id,name,description,position,isGlobal,isEnabled,route.id,route.path,methods.name', labelFields: ['name', 'description', 'route.path'], pathFields: ['route.path'] },
52
+ { tableName: 'enfyra_guard_rule', fields: 'id,_id,name,field,operator,value,guard.id,guard.name,description,isEnabled', labelFields: ['name', 'field', 'operator', 'value', 'guard.name', 'description'] },
53
+ { tableName: 'enfyra_oauth_config', fields: 'id,_id,provider,displayName,redirectUri,scopes,isEnabled,description', labelFields: ['provider', 'displayName', 'redirectUri', 'scopes', 'description'] },
54
+ ],
55
+ };
56
+ export const RUNTIME_ZONE_DESCRIPTIONS = {
57
+ admin_ui: 'Admin menu + extension UI records: pages, widgets, global shell extensions, menu chips, account panel entries.',
58
+ api_runtime: 'REST routes, handlers, hooks, guards, guard rules, and route permissions.',
59
+ flow_runtime: 'Flows and flow steps that run background jobs, scheduled tasks, and manual operations.',
60
+ websocket_runtime: 'Socket.IO gateways and event handlers.',
61
+ graphql_runtime: 'GraphQL exposure and custom resolver source.',
62
+ schema_data: 'Tables, columns, relations, column rules, field permissions, and route-backed data shape.',
63
+ package_runtime: 'Installed app/server packages and runtime package availability.',
64
+ storage_file: 'Storage configs, folders, files, public file state, and file permissions.',
65
+ auth_security: 'Roles, route permissions, field permissions, guards, guard rules, and OAuth auth surfaces.',
66
+ };
67
+ function getId(record) {
68
+ return record?.id ?? record?._id ?? null;
69
+ }
70
+ function unwrapData(result) {
71
+ if (Array.isArray(result?.data))
72
+ return result.data;
73
+ if (Array.isArray(result))
74
+ return result;
75
+ return [];
76
+ }
77
+ function normalizeText(value) {
78
+ return String(value ?? '')
79
+ .normalize('NFD')
80
+ .replace(/\p{Diacritic}/gu, '')
81
+ .replace(/\s+/g, ' ')
82
+ .toLowerCase();
83
+ }
84
+ function valueAt(record, path) {
85
+ return path.split('.').reduce((current, key) => {
86
+ if (current == null)
87
+ return undefined;
88
+ if (Array.isArray(current))
89
+ return current.map((item) => item?.[key]).filter((item) => item != null);
90
+ return current[key];
91
+ }, record);
92
+ }
93
+ function flattenValue(value) {
94
+ if (value == null)
95
+ return '';
96
+ if (typeof value === 'string')
97
+ return value;
98
+ if (typeof value === 'number' || typeof value === 'boolean')
99
+ return String(value);
100
+ return JSON.stringify(value);
101
+ }
102
+ function filterQuery(filter) {
103
+ return encodeURIComponent(JSON.stringify(filter));
104
+ }
105
+ function sourceMatches(source, query, snippetChars) {
106
+ const normalizedQuery = normalizeText(query);
107
+ if (!source || !normalizedQuery)
108
+ return null;
109
+ const lines = source.split('\n');
110
+ for (let index = 0; index < lines.length; index += 1) {
111
+ const text = lines.slice(index, Math.min(index + 3, lines.length)).join(' ');
112
+ if (normalizeText(text).includes(normalizedQuery)) {
113
+ return {
114
+ line: index + 1,
115
+ snippet: text.replace(/\s+/g, ' ').trim().slice(0, snippetChars),
116
+ };
117
+ }
118
+ }
119
+ const normalizedSource = normalizeText(source);
120
+ const offset = normalizedSource.indexOf(normalizedQuery);
121
+ if (offset < 0)
122
+ return null;
123
+ return {
124
+ line: null,
125
+ snippet: source.slice(Math.max(0, offset - 60), offset + query.length + 120).replace(/\s+/g, ' ').trim().slice(0, snippetChars),
126
+ };
127
+ }
128
+ async function fetchZoneRecords(apiUrl, table) {
129
+ const result = await fetchAPI(apiUrl, `/${table.tableName}?limit=0&fields=${encodeURIComponent(table.fields)}`).catch((error) => ({ error }));
130
+ if (result?.error)
131
+ return { records: [], error: result.error.message || String(result.error) };
132
+ return { records: unwrapData(result), error: null };
133
+ }
134
+ function matchGenericRecord(table, record, query, snippetChars) {
135
+ const matches = [];
136
+ const normalizedQuery = normalizeText(query);
137
+ for (const field of table.labelFields ?? []) {
138
+ const raw = flattenValue(valueAt(record, field));
139
+ if (!raw || !normalizeText(raw).includes(normalizedQuery))
140
+ continue;
141
+ matches.push({
142
+ field,
143
+ score: 70,
144
+ reason: 'metadata match',
145
+ snippet: raw.slice(0, snippetChars),
146
+ });
147
+ }
148
+ for (const field of table.sourceFields ?? []) {
149
+ const source = String(valueAt(record, field) ?? '');
150
+ const hit = sourceMatches(source, query, snippetChars);
151
+ if (!hit)
152
+ continue;
153
+ matches.push({
154
+ field,
155
+ line: hit.line,
156
+ score: 110,
157
+ reason: 'source match',
158
+ snippet: hit.snippet,
159
+ });
160
+ }
161
+ return matches;
162
+ }
163
+ function summarizeGenericRecord(zone, table, record, matches, includeSourceArtifact) {
164
+ const id = getId(record);
165
+ const sourceField = (table.sourceFields ?? []).find((field) => typeof valueAt(record, field) === 'string' && String(valueAt(record, field)).length > 0);
166
+ const source = sourceField ? String(valueAt(record, sourceField)) : '';
167
+ return {
168
+ zone,
169
+ tableName: table.tableName,
170
+ id,
171
+ label: (table.labelFields ?? []).map((field) => flattenValue(valueAt(record, field))).find(Boolean) || String(id),
172
+ routePath: flattenValue(valueAt(record, 'route.path')) || flattenValue(valueAt(record, 'gateway.path')) || null,
173
+ score: matches.reduce((sum, item) => sum + item.score, 0),
174
+ matches: matches.sort((a, b) => b.score - a.score).slice(0, 3),
175
+ source: source
176
+ ? includeSourceArtifact
177
+ ? (() => {
178
+ const artifact = writeSourceArtifact({ tableName: table.tableName, id: id ?? 'record', fieldName: sourceField, source });
179
+ return { tmpFile: artifact.tmpFile, length: artifact.length, sha256: artifact.sha256 };
180
+ })()
181
+ : { field: sourceField, length: source.length }
182
+ : null,
183
+ nextInspect: {
184
+ tool: 'search_runtime_zone',
185
+ input: { mode: 'inspect', zone, tableName: table.tableName, id },
186
+ },
187
+ };
188
+ }
189
+ async function searchSchemaZone(apiUrl, input) {
190
+ const query = String(input.query ?? '').trim();
191
+ const normalizedQuery = normalizeText(query);
192
+ const maxResults = Math.min(Math.max(Number(input.maxResults ?? 8), 1), 25);
193
+ const metadata = await fetchAPI(apiUrl, '/metadata');
194
+ const tables = Array.isArray(metadata?.data)
195
+ ? metadata.data
196
+ : Array.isArray(metadata)
197
+ ? metadata
198
+ : Object.values(metadata?.data ?? metadata ?? {});
199
+ const results = [];
200
+ for (const table of tables) {
201
+ const tableMatches = [];
202
+ for (const field of ['name', 'alias', 'description']) {
203
+ const raw = flattenValue(table?.[field]);
204
+ if (raw && normalizeText(raw).includes(normalizedQuery)) {
205
+ tableMatches.push({ field, score: 80, reason: 'table metadata match', snippet: raw.slice(0, 180) });
206
+ }
207
+ }
208
+ for (const column of table?.columns ?? []) {
209
+ const raw = flattenValue({ name: column.name, type: column.type, description: column.description });
210
+ if (normalizeText(raw).includes(normalizedQuery)) {
211
+ tableMatches.push({ field: `column.${column.name}`, score: 70, reason: 'column match', snippet: raw.slice(0, 180) });
212
+ }
213
+ }
214
+ for (const relation of table?.relations ?? []) {
215
+ const raw = flattenValue({ propertyName: relation.propertyName, type: relation.type, targetTable: relation.targetTable?.name ?? relation.targetTable, description: relation.description });
216
+ if (normalizeText(raw).includes(normalizedQuery)) {
217
+ tableMatches.push({ field: `relation.${relation.propertyName}`, score: 70, reason: 'relation match', snippet: raw.slice(0, 180) });
218
+ }
219
+ }
220
+ if (!tableMatches.length)
221
+ continue;
222
+ results.push({
223
+ zone: 'schema_data',
224
+ tableName: table.name,
225
+ id: getId(table),
226
+ alias: table.alias,
227
+ score: tableMatches.reduce((sum, item) => sum + item.score, 0),
228
+ matches: tableMatches.sort((a, b) => b.score - a.score).slice(0, 4),
229
+ nextInspect: { tool: 'search_runtime_zone', input: { mode: 'inspect', zone: 'schema_data', tableName: table.name } },
230
+ });
231
+ }
232
+ results.sort((a, b) => b.score - a.score || String(a.tableName).localeCompare(String(b.tableName)));
233
+ return {
234
+ action: 'runtime_zone_searched',
235
+ zone: 'schema_data',
236
+ zoneDescription: RUNTIME_ZONE_DESCRIPTIONS.schema_data,
237
+ query,
238
+ resultCount: results.length,
239
+ results: results.slice(0, maxResults),
240
+ searched: { tables: tables.length },
241
+ };
242
+ }
243
+ export async function searchRuntimeZone(apiUrl, input) {
244
+ if ((input.mode ?? 'search') === 'inspect')
245
+ return inspectRuntimeZoneLocation(apiUrl, input);
246
+ const zone = input.zone;
247
+ if (!RUNTIME_ZONES.includes(zone))
248
+ throw new Error(`Unsupported runtime zone: ${zone}`);
249
+ if (zone === 'admin_ui') {
250
+ const adminResult = await searchExtensions(apiUrl, {
251
+ query: input.query,
252
+ path: input.path,
253
+ type: input.extensionType,
254
+ includeDisabled: input.includeDisabled,
255
+ maxResults: input.maxResults,
256
+ snippetChars: input.snippetChars,
257
+ maxMatchesPerExtension: input.maxMatchesPerRecord,
258
+ includeSourceArtifact: input.includeSourceArtifact,
259
+ });
260
+ return {
261
+ ...adminResult,
262
+ action: 'runtime_zone_searched',
263
+ zone,
264
+ zoneDescription: RUNTIME_ZONE_DESCRIPTIONS.admin_ui,
265
+ results: (adminResult.results ?? []).map((item) => ({
266
+ ...item,
267
+ zone,
268
+ nextInspect: {
269
+ tool: 'search_runtime_zone',
270
+ input: { mode: 'inspect', zone, id: item.id, name: item.name },
271
+ },
272
+ })),
273
+ };
274
+ }
275
+ if (zone === 'schema_data')
276
+ return searchSchemaZone(apiUrl, input);
277
+ const query = String(input.query ?? input.path ?? '').trim();
278
+ if (!query)
279
+ throw new Error('query or path is required for this runtime zone.');
280
+ const path = input.path ? String(input.path).trim() : '';
281
+ const maxResults = Math.min(Math.max(Number(input.maxResults ?? 8), 1), 25);
282
+ const snippetChars = Math.min(Math.max(Number(input.snippetChars ?? 180), 120), 600);
283
+ const includeSourceArtifact = Boolean(input.includeSourceArtifact);
284
+ const tables = ZONE_TABLES[zone];
285
+ const allResults = [];
286
+ const errors = [];
287
+ for (const table of tables) {
288
+ const { records, error } = await fetchZoneRecords(apiUrl, table);
289
+ if (error) {
290
+ errors.push({ tableName: table.tableName, error });
291
+ continue;
292
+ }
293
+ for (const record of records) {
294
+ if (path) {
295
+ const pathHit = (table.pathFields ?? []).some((field) => flattenValue(valueAt(record, field)) === path);
296
+ if (!pathHit)
297
+ continue;
298
+ }
299
+ const matches = matchGenericRecord(table, record, query, snippetChars);
300
+ if (!matches.length && path) {
301
+ matches.push({ field: 'path', score: 120, reason: 'exact path match', snippet: path });
302
+ }
303
+ if (!matches.length)
304
+ continue;
305
+ allResults.push(summarizeGenericRecord(zone, table, record, matches, includeSourceArtifact));
306
+ }
307
+ }
308
+ allResults.sort((a, b) => b.score - a.score || String(a.tableName).localeCompare(String(b.tableName)));
309
+ const results = allResults.slice(0, maxResults);
310
+ return {
311
+ action: 'runtime_zone_searched',
312
+ zone,
313
+ zoneDescription: RUNTIME_ZONE_DESCRIPTIONS[zone],
314
+ query: query || null,
315
+ path: path || null,
316
+ resultCount: allResults.length,
317
+ results,
318
+ readErrors: errors,
319
+ tokenBudget: {
320
+ estimatedOutputChars: JSON.stringify(results).length,
321
+ estimatedOutputTokens: Math.ceil(JSON.stringify(results).length / 4),
322
+ controls: { maxResults, snippetChars, includeSourceArtifact },
323
+ },
324
+ guidance: [
325
+ 'Use the returned nextInspect input on search_runtime_zone(mode=inspect) for the best candidate before editing.',
326
+ 'Use source artifacts only for the selected candidate when snippets are insufficient.',
327
+ 'Use zone-specific write tools after inspection instead of generic CRUD when a business operation tool exists.',
328
+ ],
329
+ };
330
+ }
331
+ export async function inspectRuntimeZoneLocation(apiUrl, input) {
332
+ const zone = input.zone;
333
+ if (!RUNTIME_ZONES.includes(zone))
334
+ throw new Error(`Unsupported runtime zone: ${zone}`);
335
+ if (zone === 'admin_ui') {
336
+ return { ...(await inspectExtensionLocation(apiUrl, input)), zone, zoneDescription: RUNTIME_ZONE_DESCRIPTIONS.admin_ui };
337
+ }
338
+ if (zone === 'schema_data') {
339
+ if (!input.tableName && !input.query)
340
+ throw new Error('tableName or query is required for schema_data inspection.');
341
+ return {
342
+ action: 'runtime_zone_location_inspected',
343
+ zone,
344
+ zoneDescription: RUNTIME_ZONE_DESCRIPTIONS.schema_data,
345
+ next: input.tableName
346
+ ? { tool: 'inspect_table', input: { tableName: input.tableName } }
347
+ : { tool: 'search_runtime_zone', input: { mode: 'search', zone, query: input.query, maxResults: 5 } },
348
+ };
349
+ }
350
+ const tableName = String(input.tableName ?? '');
351
+ if (!tableName)
352
+ throw new Error('tableName is required for non-admin runtime zone inspection. Use search_runtime_zone first and pass nextInspect.input.');
353
+ const table = ZONE_TABLES[zone]?.find((item) => item.tableName === tableName);
354
+ if (!table)
355
+ throw new Error(`Table ${tableName} does not belong to zone ${zone}.`);
356
+ const id = input.id;
357
+ const filter = id != null
358
+ ? { id: { _eq: id } }
359
+ : input.query
360
+ ? null
361
+ : null;
362
+ if (!filter)
363
+ throw new Error('id is required for direct inspection. Use search_runtime_zone for query-based discovery.');
364
+ const result = await fetchAPI(apiUrl, `/${table.tableName}?filter=${filterQuery(filter)}&limit=1&fields=${encodeURIComponent(table.fields)}`);
365
+ const record = unwrapData(result)[0];
366
+ if (!record)
367
+ throw new Error(`Record not found: ${table.tableName} ${id}`);
368
+ const sources = [];
369
+ for (const field of table.sourceFields ?? []) {
370
+ const source = String(valueAt(record, field) ?? '');
371
+ if (!source)
372
+ continue;
373
+ const artifact = writeSourceArtifact({ tableName: table.tableName, id: getId(record) ?? 'record', fieldName: field, source });
374
+ sources.push(artifact);
375
+ }
376
+ return {
377
+ action: 'runtime_zone_location_inspected',
378
+ zone,
379
+ zoneDescription: RUNTIME_ZONE_DESCRIPTIONS[zone],
380
+ tableName: table.tableName,
381
+ id: getId(record),
382
+ record,
383
+ sources,
384
+ nextSteps: [
385
+ 'Use zone-specific edit tools where available.',
386
+ 'For source edits, use get_script_source/patch_script_source/update_script_source when the table is script-backed.',
387
+ 'Re-run search_runtime_zone or the specific inspector after mutation.',
388
+ ],
389
+ };
390
+ }
391
+ export function registerRuntimeZoneTools(server, ENFYRA_API_URL) {
392
+ server.tool('search_runtime_zone', [
393
+ 'Single zone-scoped search/inspect tool for DB-backed Enfyra runtime artifacts.',
394
+ 'Use this before editing anything that lives in the database rather than repo files.',
395
+ 'Choose a zone first so the output stays precise and token-bounded. Use mode=search first, then call this same tool with mode=inspect using nextInspect.input.',
396
+ ].join(' '), {
397
+ mode: z.enum(['search', 'inspect']).optional().default('search').describe('search returns ranked matches. inspect opens one result using tableName/id or admin UI name/path/query.'),
398
+ zone: z.enum(RUNTIME_ZONES).describe('DB-backed runtime zone to search. admin_ui=menu/extensions, api_runtime=routes/handlers/hooks/guards, flow_runtime=flows, websocket_runtime=socket gateways/events, graphql_runtime=GraphQL, schema_data=tables/fields/relations, package_runtime=packages, storage_file=storage/files, auth_security=access/auth.'),
399
+ query: z.string().optional().describe('Visible label, path, event name, flow step key, route path, source-code term, field name, package name, or config keyword.'),
400
+ path: z.string().optional().describe('Exact route/gateway/menu/file path where the zone supports path lookup.'),
401
+ tableName: z.string().optional().describe('Required for mode=inspect on non-admin zones; use nextInspect.input from search results.'),
402
+ id: z.union([z.string(), z.number()]).optional().describe('Record id for mode=inspect; use nextInspect.input from search results.'),
403
+ name: z.string().optional().describe('Admin UI extension name for mode=inspect when zone=admin_ui.'),
404
+ extensionType: z.enum(['page', 'widget', 'global']).optional().describe('Only for admin_ui zone. Narrows extension type.'),
405
+ includeDisabled: z.boolean().optional().default(true).describe('Only for admin_ui zone. Include disabled extensions.'),
406
+ maxResults: z.number().int().min(1).max(25).optional().default(8).describe('Maximum ranked results.'),
407
+ snippetChars: z.number().int().min(120).max(600).optional().default(180).describe('Approximate snippet characters per match.'),
408
+ maxMatchesPerRecord: z.number().int().min(1).max(8).optional().default(2).describe('Only for admin_ui zone. Maximum source matches per extension.'),
409
+ includeSourceArtifact: z.boolean().optional().default(false).describe('Write matched source to /tmp and return compact source artifact metadata. Prefer false unless snippets are insufficient.'),
410
+ }, async (input) => jsonContent(await searchRuntimeZone(ENFYRA_API_URL, input)));
411
+ }
412
+ //# sourceMappingURL=runtime-zone-tools.js.map