@enfyra/mcp-server 0.0.106 → 0.0.107
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
|
@@ -188,7 +188,7 @@ The MCP server includes safety guards for LLM callers:
|
|
|
188
188
|
- `validate_extension_code` checks Enfyra admin extension code through `/enfyra_extension/preview` without saving.
|
|
189
189
|
- `compiledCode` is generated from `sourceCode` and may differ textually because macros are expanded; the MCP server never accepts hand-written `compiledCode`.
|
|
190
190
|
- 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.
|
|
191
|
-
- Relation tools reject physical FK/junction names.
|
|
191
|
+
- Relation tools reject physical FK/junction names and resolve table ids from exact table names or aliases before schema mutation.
|
|
192
192
|
- Generated code should use relation property names such as `conversation`, `sender`, and `member` instead of physical FK fields such as `conversationId`, `senderId`, or `memberId`.
|
|
193
193
|
- Custom route tools reject `mainTableId` unless the route is the canonical table route.
|
|
194
194
|
- Platform operation tools such as `api_endpoint_workflow`, `create_api_endpoint`, `enable_route`, `disable_route`, `delete_route`, `public_route_methods`, `add_route_methods`, `set_table_graphql`, `ensure_guard`, `ensure_field_permission`, `ensure_column_rule`, `ensure_websocket_event`, `ensure_script_flow_step`, `ensure_menu`, `ensure_page_extension`, `ensure_global_extension`, and `ensure_widget_extension` resolve metadata ids and validate code before saving.
|
|
@@ -197,7 +197,7 @@ The MCP server includes safety guards for LLM callers:
|
|
|
197
197
|
|
|
198
198
|
## Query Notes
|
|
199
199
|
|
|
200
|
-
Use explicit `fields` in read tools. Include mode is the default, such as `fields=id,email`. Any excluded field switches that scope to exclude mode: `fields=-compiledCode` returns all readable fields except `compiledCode`, and `fields=id,-compiledCode` still means all except `compiledCode`. Dotted exclusions such as `fields=-owner.avatar` work for relation fields when the relation exists in metadata. Every list/query call must pass either `limit` for a bounded page or `all: true` for a complete list. When a caller needs every matching row, pass `all: true` to `query_table` or `
|
|
200
|
+
Use explicit `fields` in read tools. Include mode is the default, such as `fields=id,email`. Any excluded field switches that scope to exclude mode: `fields=-compiledCode` returns all readable fields except `compiledCode`, and `fields=id,-compiledCode` still means all except `compiledCode`. Dotted exclusions such as `fields=-owner.avatar` work for relation fields when the relation exists in metadata. Every list/query call must pass either `limit` for a bounded page or `all: true` for a complete list. When a caller needs every matching row, pass `all: true` to `query_table`, `get_all_routes`, or `get_all_tables`; the tool should not choose an arbitrary page size like 30 or 50.
|
|
201
201
|
|
|
202
202
|
## Enfyra URL Pattern
|
|
203
203
|
|
package/package.json
CHANGED
|
@@ -60,7 +60,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
|
|
|
60
60
|
'',
|
|
61
61
|
'### Direct HTTP Mapping',
|
|
62
62
|
'- Route-backed table CRUD is REST: `GET /<table>?...`, `POST /<table>`, `PATCH /<table>/<id>`, `DELETE /<table>/<id>`. There is no `GET /<table>/<id>`; use a filtered list with `limit=1` or `find_one_record`.',
|
|
63
|
-
'- REST route lifecycle is controlled by `enfyra_route.isEnabled`: disabled routes are not registered at runtime and return 404. Use `enable_route`/`disable_route` instead of raw route PATCH. REST public access is controlled by route `publicMethods`; otherwise direct HTTP needs Bearer JWT plus route permissions. GraphQL requires Bearer auth and table GraphQL enablement.',
|
|
63
|
+
'- REST route lifecycle is controlled by `enfyra_route.isEnabled`: disabled routes are not registered at runtime and return 404. Use `enable_route`/`disable_route` instead of raw route PATCH. REST public access is controlled by route `publicMethods`; otherwise direct HTTP needs Bearer JWT plus route permissions. GraphQL table data requires Bearer auth and table GraphQL enablement; anonymous root/schema probes may still return a 200 without exposing table data.',
|
|
64
64
|
'',
|
|
65
65
|
'When the user asks for details, fetch only the relevant live context or example category instead of relying on broad memorized rules.',
|
|
66
66
|
].join('\n');
|
|
@@ -871,8 +871,8 @@ async function runApiEndpointWorkflow(apiUrl, opts) {
|
|
|
871
871
|
nextSteps,
|
|
872
872
|
cleanupHints: latestState.endpoint.routeId
|
|
873
873
|
? [
|
|
874
|
-
`
|
|
875
|
-
`
|
|
874
|
+
`Use delete_route({ routeId: ${JSON.stringify(latestState.endpoint.routeId)}, confirm: false }) to preview route-owned handlers, hooks, guards, and permissions before cleanup.`,
|
|
875
|
+
`Then call delete_route({ routeId: ${JSON.stringify(latestState.endpoint.routeId)}, expectedPath: ${JSON.stringify(latestState.endpoint.path)}, confirm: true }) when the route contract is no longer needed.`,
|
|
876
876
|
]
|
|
877
877
|
: [],
|
|
878
878
|
};
|
package/src/lib/table-tools.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import { fetchAPI } from './fetch.js';
|
|
6
|
+
import { jsonContent } from './response-format.js';
|
|
6
7
|
|
|
7
8
|
let schemaQueue = Promise.resolve();
|
|
8
9
|
|
|
@@ -43,6 +44,19 @@ export function resolveTableFromMetadataByName(metadata, tableName) {
|
|
|
43
44
|
.find((table) => table?.name === tableName || table?.alias === tableName) || null;
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
export function resolveTableIdentifierFromMetadata(metadata, tableRef, label = 'table') {
|
|
48
|
+
const resolvedTable = normalizeTablesFromMetadata(metadata)
|
|
49
|
+
.find((table) => (
|
|
50
|
+
String(getId(table)) === String(tableRef) ||
|
|
51
|
+
table?.name === tableRef ||
|
|
52
|
+
table?.alias === tableRef
|
|
53
|
+
));
|
|
54
|
+
if (!resolvedTable) {
|
|
55
|
+
throw new Error(`${label} "${tableRef}" was not found in metadata. Pass an existing table id, name, or alias from get_all_tables/inspect_table.`);
|
|
56
|
+
}
|
|
57
|
+
return getId(resolvedTable);
|
|
58
|
+
}
|
|
59
|
+
|
|
46
60
|
/**
|
|
47
61
|
* Helper: fetch table with full columns and relations.
|
|
48
62
|
* Dynamic enfyra_table relation fields can be paginated/truncated, so schema
|
|
@@ -147,6 +161,14 @@ export function normalizeRelationForTablePatch(relation) {
|
|
|
147
161
|
return normalized;
|
|
148
162
|
}
|
|
149
163
|
|
|
164
|
+
function assertNoForbiddenRelationKeys(args) {
|
|
165
|
+
for (const key of FORBIDDEN_RELATION_KEYS) {
|
|
166
|
+
if (Object.prototype.hasOwnProperty.call(args, key)) {
|
|
167
|
+
throw new Error(`create_relation must not include physical column field "${key}". Use sourceTableId/targetTableId and relation propertyName only; Enfyra derives FK and junction columns.`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
150
172
|
export function sanitizeExistingRelationForTablePatch(relation) {
|
|
151
173
|
const {
|
|
152
174
|
fkCol,
|
|
@@ -307,27 +329,32 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
307
329
|
});
|
|
308
330
|
}
|
|
309
331
|
|
|
310
|
-
async function appendRelationToTable(
|
|
332
|
+
async function appendRelationToTable(args) {
|
|
311
333
|
return withSchemaQueue(async () => {
|
|
312
|
-
|
|
334
|
+
assertNoForbiddenRelationKeys(args);
|
|
335
|
+
const { sourceTableId, targetTableId, type, propertyName, inversePropertyName, mappedBy, isNullable, onDelete, description } = args;
|
|
336
|
+
const metadata = await fetchAPI(ENFYRA_API_URL, '/metadata');
|
|
337
|
+
const resolvedSourceTableId = resolveTableIdentifierFromMetadata(metadata, sourceTableId, 'sourceTableId');
|
|
338
|
+
const resolvedTargetTableId = resolveTableIdentifierFromMetadata(metadata, targetTableId, 'targetTableId');
|
|
339
|
+
const tableData = await fetchTableWithDetails(ENFYRA_API_URL, resolvedSourceTableId);
|
|
313
340
|
if (!tableData) {
|
|
314
|
-
return { content: [{ type: 'text', text: `Error: Table
|
|
341
|
+
return { content: [{ type: 'text', text: `Error: Table ${sourceTableId} not found.` }] };
|
|
315
342
|
}
|
|
316
343
|
const existingRelations = (tableData.relations || []).map(sanitizeExistingRelationForTablePatch);
|
|
317
344
|
const beforeIds = existingRelations.map((relation) => String(getId(relation))).filter((id) => id !== 'null');
|
|
318
|
-
const newRelation = { targetTable:
|
|
345
|
+
const newRelation = { targetTable: resolvedTargetTableId, type, propertyName };
|
|
319
346
|
if (inversePropertyName !== undefined) newRelation.inversePropertyName = inversePropertyName || null;
|
|
320
347
|
if (mappedBy !== undefined) newRelation.mappedBy = mappedBy;
|
|
321
348
|
if (isNullable !== undefined) newRelation.isNullable = isNullable;
|
|
322
349
|
if (onDelete !== undefined) newRelation.onDelete = onDelete;
|
|
323
350
|
if (description !== undefined) newRelation.description = description;
|
|
324
|
-
const result = await patchTableAutoConfirm(ENFYRA_API_URL,
|
|
325
|
-
await verifyRelationCascade(ENFYRA_API_URL,
|
|
351
|
+
const result = await patchTableAutoConfirm(ENFYRA_API_URL, resolvedSourceTableId, { relations: [...existingRelations, newRelation] });
|
|
352
|
+
await verifyRelationCascade(ENFYRA_API_URL, resolvedSourceTableId, beforeIds, {
|
|
326
353
|
action: 'create',
|
|
327
354
|
propertyName,
|
|
328
355
|
});
|
|
329
356
|
return {
|
|
330
|
-
content: [{ type: 'text', text: `Relation created: ${propertyName} (${type}) from table ${
|
|
357
|
+
content: [{ type: 'text', text: `Relation created: ${propertyName} (${type}) from table ${resolvedSourceTableId} → ${resolvedTargetTableId}.\n\nFull result:\n${JSON.stringify(result, null, 2)}` }],
|
|
331
358
|
};
|
|
332
359
|
});
|
|
333
360
|
}
|
|
@@ -434,8 +461,8 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
434
461
|
};
|
|
435
462
|
|
|
436
463
|
const relationCreateSchema = {
|
|
437
|
-
sourceTableId: z.string().describe('Source table
|
|
438
|
-
targetTableId: z.string().describe('Target table
|
|
464
|
+
sourceTableId: z.string().describe('Source table id, exact table name, or alias. For many-to-one, this is the table that owns the relation property.'),
|
|
465
|
+
targetTableId: z.string().describe('Target table id, exact table name, or alias. MCP resolves names/aliases to ids before mutation.'),
|
|
439
466
|
type: z.enum(['many-to-one', 'one-to-many', 'one-to-one', 'many-to-many']).describe('Relation type.'),
|
|
440
467
|
propertyName: z.string().describe('Property name on source table (e.g., "customer", "items").'),
|
|
441
468
|
inversePropertyName: z.string().optional().describe('Property name on target table for bidirectional relation (e.g., "orders"). Omit unless the reverse field is truly needed.'),
|
|
@@ -443,6 +470,13 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
443
470
|
isNullable: z.boolean().optional().default(true).describe('Whether the relation is nullable.'),
|
|
444
471
|
onDelete: z.enum(['CASCADE', 'SET NULL', 'RESTRICT']).optional().default('SET NULL').describe('On delete behavior.'),
|
|
445
472
|
description: z.string().optional().describe('Relation description.'),
|
|
473
|
+
fkCol: z.never().optional().describe('Forbidden. Use propertyName only; Enfyra derives FK columns.'),
|
|
474
|
+
fkColumn: z.never().optional().describe('Forbidden. Use propertyName only; Enfyra derives FK columns.'),
|
|
475
|
+
foreignKeyColumn: z.never().optional().describe('Forbidden. Use propertyName only; Enfyra derives FK columns.'),
|
|
476
|
+
sourceColumn: z.never().optional().describe('Forbidden. Use propertyName only; Enfyra derives FK columns.'),
|
|
477
|
+
targetColumn: z.never().optional().describe('Forbidden. Use propertyName only; Enfyra derives FK columns.'),
|
|
478
|
+
junctionSourceColumn: z.never().optional().describe('Forbidden. Use relation property names only; Enfyra derives junction columns.'),
|
|
479
|
+
junctionTargetColumn: z.never().optional().describe('Forbidden. Use relation property names only; Enfyra derives junction columns.'),
|
|
446
480
|
};
|
|
447
481
|
|
|
448
482
|
const columnDeleteSchema = {
|
|
@@ -461,13 +495,45 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
461
495
|
|
|
462
496
|
server.tool(
|
|
463
497
|
'get_all_tables',
|
|
464
|
-
'
|
|
465
|
-
{
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
498
|
+
'List table definitions from metadata. Every call must pass either limit or all=true. Use search to narrow by table name or alias.',
|
|
499
|
+
{
|
|
500
|
+
limit: z.number().int().positive().optional().describe('Maximum tables returned after search. Required unless all=true.'),
|
|
501
|
+
all: z.boolean().optional().describe('Return all matched tables. Use this when a complete table list is required.'),
|
|
502
|
+
search: z.string().optional().describe('Optional table name, alias, or description substring filter.'),
|
|
503
|
+
},
|
|
504
|
+
async ({ limit, all, search }) => {
|
|
505
|
+
if (!all && limit === undefined) {
|
|
506
|
+
throw new Error('get_all_tables requires either limit or all=true. Do not invent arbitrary limits for complete table lists; use all=true.');
|
|
507
|
+
}
|
|
508
|
+
const metadata = await fetchAPI(ENFYRA_API_URL, '/metadata');
|
|
509
|
+
const needle = search?.trim().toLowerCase();
|
|
510
|
+
const tables = normalizeTablesFromMetadata(metadata)
|
|
511
|
+
.map((table) => ({
|
|
512
|
+
id: getId(table),
|
|
513
|
+
name: table.name ?? null,
|
|
514
|
+
alias: table.alias ?? null,
|
|
515
|
+
description: table.description ?? null,
|
|
516
|
+
isSingleRecord: table.isSingleRecord ?? null,
|
|
517
|
+
columnCount: Array.isArray(table.columns) ? table.columns.length : null,
|
|
518
|
+
relationCount: Array.isArray(table.relations) ? table.relations.length : null,
|
|
519
|
+
routeBacked: Boolean(table.route || table.routeId || table.path),
|
|
520
|
+
}))
|
|
521
|
+
.filter((table) => {
|
|
522
|
+
if (!needle) return true;
|
|
523
|
+
return [table.name, table.alias, table.description]
|
|
524
|
+
.some((value) => String(value || '').toLowerCase().includes(needle));
|
|
525
|
+
});
|
|
526
|
+
const returnedTables = all ? tables : tables.slice(0, limit);
|
|
527
|
+
return jsonContent({
|
|
528
|
+
action: 'get_all_tables',
|
|
529
|
+
totalTableCount: normalizeTablesFromMetadata(metadata).length,
|
|
530
|
+
matchedTableCount: tables.length,
|
|
531
|
+
returnedTableCount: returnedTables.length,
|
|
532
|
+
all: Boolean(all),
|
|
533
|
+
search: search || null,
|
|
534
|
+
tables: returnedTables,
|
|
535
|
+
detailHint: 'Use inspect_table with a table id/name for columns, relations, indexes, routes, permissions, and GraphQL state.',
|
|
536
|
+
});
|
|
471
537
|
}
|
|
472
538
|
);
|
|
473
539
|
|
|
@@ -558,7 +624,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
558
624
|
alias: z.string().optional().describe('New table alias.'),
|
|
559
625
|
description: z.string().optional().describe('New description.'),
|
|
560
626
|
isSingleRecord: z.boolean().optional().describe('Set to true for single-record table (e.g., settings/config).'),
|
|
561
|
-
graphqlEnabled: z.boolean().optional().describe('Enable or disable GraphQL for this table by syncing enfyra_graphql.isEnabled. GraphQL still requires Bearer auth.'),
|
|
627
|
+
graphqlEnabled: z.boolean().optional().describe('Enable or disable GraphQL for this table by syncing enfyra_graphql.isEnabled. GraphQL table data still requires Bearer auth; anonymous root or schema probes may return 200.'),
|
|
562
628
|
indexes: z.string().optional().describe('Complete JSON array of logical index field groups to store on enfyra_table.indexes. Each group can be ["fieldA","fieldB"] or {"value":["fieldA","fieldB"]}. Omit to preserve current indexes; pass [] to clear.'),
|
|
563
629
|
uniques: z.string().optional().describe('Complete JSON array of logical unique field groups to store on enfyra_table.uniques. Each group can be ["fieldA","fieldB"] or {"value":["fieldA","fieldB"]}. Omit to preserve current uniques; pass [] to clear.'),
|
|
564
630
|
},
|
|
@@ -740,6 +806,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
740
806
|
'create_relation',
|
|
741
807
|
[
|
|
742
808
|
'Create a relation between two tables (many-to-one, one-to-many, one-to-one, many-to-many).',
|
|
809
|
+
'sourceTableId and targetTableId may be table ids, exact table names, or aliases; MCP resolves them from metadata before mutation.',
|
|
743
810
|
'For many-to-one: a physical FK column is created on the source table. For one-to-many: the FK is on the target (inverse relation). This physical FK is derived by Enfyra and hidden from app schema/forms.',
|
|
744
811
|
'Never ask the user for physical FK column names and never send fkCol/fkColumn/foreignKeyColumn/sourceColumn/targetColumn/junction*Column. The public API uses relation propertyName only.',
|
|
745
812
|
'Run sequentially — DB migration locks per operation.',
|
|
@@ -754,6 +821,7 @@ export function registerTableTools(server, ENFYRA_API_URL) {
|
|
|
754
821
|
'add_relation',
|
|
755
822
|
[
|
|
756
823
|
'Alias for create_relation. Add a relation through the canonical enfyra_table cascade.',
|
|
824
|
+
'sourceTableId and targetTableId may be table ids, exact table names, or aliases.',
|
|
757
825
|
'Use relation propertyName only; never provide physical FK or junction column names.',
|
|
758
826
|
'Run schema changes sequentially — migration locks DB per operation.',
|
|
759
827
|
].join(' '),
|
package/src/mcp-server-entry.mjs
CHANGED
|
@@ -64,7 +64,7 @@ const CAPABILITY_AREAS = [
|
|
|
64
64
|
{
|
|
65
65
|
area: 'GraphQL',
|
|
66
66
|
tables: ['enfyra_graphql'],
|
|
67
|
-
workflow: 'Enable per table through enfyra_graphql or update_table graphqlEnabled. GraphQL requires Bearer auth.',
|
|
67
|
+
workflow: 'Enable per table through enfyra_graphql or update_table graphqlEnabled. GraphQL table data requires Bearer auth; anonymous root or schema probes may return 200 without exposing table data.',
|
|
68
68
|
},
|
|
69
69
|
{
|
|
70
70
|
area: 'Files and storage',
|
|
@@ -746,7 +746,7 @@ server.tool(
|
|
|
746
746
|
endpoint: `${ENFYRA_API_URL.replace(/\/$/, '')}/graphql`,
|
|
747
747
|
schemaEndpoint: `${ENFYRA_API_URL.replace(/\/$/, '')}/graphql-schema`,
|
|
748
748
|
enablement: 'A table appears in GraphQL when enfyra_graphql has an enabled row for that table. REST route availableMethods does not enable GraphQL.',
|
|
749
|
-
auth: 'GraphQL
|
|
749
|
+
auth: 'GraphQL table data requires Authorization: Bearer <accessToken>; REST publicMethods do not make GraphQL table data anonymous. Anonymous root/schema probes may still return 200.',
|
|
750
750
|
management: routeTables.has('enfyra_graphql')
|
|
751
751
|
? 'Use update_table graphqlEnabled or create/update records on enfyra_graphql, then reload_graphql if needed.'
|
|
752
752
|
: 'Use update_table graphqlEnabled, then reload_graphql if needed.',
|
|
@@ -914,7 +914,7 @@ server.tool(
|
|
|
914
914
|
: 'SQL commonly uses id; Mongo uses _id. Use table metadata primary column when available.',
|
|
915
915
|
relationNames: 'API relation operations use relation propertyName, not physical FK column names.',
|
|
916
916
|
relationCascadeFkContract: 'When creating relations through create_table/create_relation/enfyra_table PATCH, never provide fkCol/fkColumn/foreignKeyColumn/sourceColumn/targetColumn/junction*Column. These are physical implementation details derived by Enfyra and hidden from app schema/forms.',
|
|
917
|
-
graphql: 'GraphQL query args also accept filter/sort/page/limit
|
|
917
|
+
graphql: 'GraphQL query args also accept filter/sort/page/limit. Table data requires Bearer auth and table enablement via enfyra_graphql; anonymous root/schema probes may still return 200.',
|
|
918
918
|
},
|
|
919
919
|
table: tableName
|
|
920
920
|
? {
|
|
@@ -1052,7 +1052,7 @@ server.tool(
|
|
|
1052
1052
|
graphqlResolver: {
|
|
1053
1053
|
runs: 'Generated GraphQL resolver delegates to dynamic repo/query services.',
|
|
1054
1054
|
data: ['GraphQL request context', 'Bearer auth user', 'dynamic repositories'],
|
|
1055
|
-
caveat: 'REST publicMethods do not make GraphQL anonymous.',
|
|
1055
|
+
caveat: 'REST publicMethods do not make GraphQL table data anonymous.',
|
|
1056
1056
|
},
|
|
1057
1057
|
extensionVueSfc: {
|
|
1058
1058
|
runs: 'Frontend extension code, not server sandbox.',
|
|
@@ -1095,7 +1095,7 @@ server.tool(
|
|
|
1095
1095
|
'Auth: publicMethods on a route can allow a method without Bearer; otherwise JWT + routePermissions — see server instructions.',
|
|
1096
1096
|
'If path might differ from table name, use get_all_routes before asserting a URL.',
|
|
1097
1097
|
'Same mapping as MCP tool → HTTP: query_table=GET /table?..., create_record=POST /table, update_record=PATCH /table/id, delete_record=DELETE /table/id.',
|
|
1098
|
-
'GraphQL: see graphqlHttpUrl / graphqlSchemaUrl in response; enable per table via enfyra_graphql/update_table graphqlEnabled and send Bearer auth.',
|
|
1098
|
+
'GraphQL: see graphqlHttpUrl / graphqlSchemaUrl in response; enable per table via enfyra_graphql/update_table graphqlEnabled and send Bearer auth for table data queries. Anonymous root/schema probes may still return 200.',
|
|
1099
1099
|
].join(' '),
|
|
1100
1100
|
{},
|
|
1101
1101
|
async () => {
|
|
@@ -1113,7 +1113,7 @@ server.tool(
|
|
|
1113
1113
|
},
|
|
1114
1114
|
auth: {
|
|
1115
1115
|
publicMethods: 'If the HTTP method is public for that route, no Bearer required; else Bearer JWT and routePermissions apply.',
|
|
1116
|
-
graphql: 'GraphQL
|
|
1116
|
+
graphql: 'GraphQL table data requires Bearer auth; route publicMethods do not make GraphQL table data anonymous. Anonymous root/schema probes may still return 200.',
|
|
1117
1117
|
mcp: 'This server uses admin credentials from env for tools (fetchAPI).',
|
|
1118
1118
|
},
|
|
1119
1119
|
pathResolution: 'Confirm route path with get_all_routes or metadata — path may not equal table name.',
|