@oneentry/mcp-platform-server 0.1.8 → 0.1.10
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/data/permissions.json +0 -1
- package/dist/api/build-catalog.js +13 -0
- package/dist/api/client.d.ts +2 -0
- package/dist/api/client.js +28 -8
- package/dist/api/operation-notes.js +28 -6
- package/dist/tools/api-discovery.js +4 -3
- package/knowledge/operating-rules.md +1 -1
- package/package.json +1 -1
package/data/permissions.json
CHANGED
|
@@ -110,7 +110,6 @@
|
|
|
110
110
|
"AdminMenusController_updateCustomItemPosition": "menu.items.changePositions",
|
|
111
111
|
"AdminModulesController_create": "settings.modules.create",
|
|
112
112
|
"AdminModulesController_update": "settings.modules.update",
|
|
113
|
-
"AdminModulesController_updateConfig": "settings.modules.update",
|
|
114
113
|
"AdminModulesController_remove": "settings.modules.delete",
|
|
115
114
|
"AdminModulesController_setAdminVisibility": "settings.modules.switching",
|
|
116
115
|
"AdminModulesController_updateModulePosition": "settings.modules.changePositions",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
+
import { phantomPathParams } from './client.js';
|
|
2
3
|
import { capSchema, normalizeSchema } from './normalize-schema.js';
|
|
3
4
|
import { ALWAYS_CONFIRM_PREFIXES, OPERATION_NOTES } from './operation-notes.js';
|
|
4
5
|
const API_PREFIX = '/api/admin';
|
|
@@ -167,6 +168,7 @@ export const buildCatalog = (params) => {
|
|
|
167
168
|
const withoutOpId = [];
|
|
168
169
|
const unresolvedBodies = [];
|
|
169
170
|
const contradictoryExamples = [];
|
|
171
|
+
const undeliverableParams = [];
|
|
170
172
|
for (const [fullPath, pathItem] of Object.entries(swagger['paths'])) {
|
|
171
173
|
if (!isRecord(pathItem)) {
|
|
172
174
|
continue;
|
|
@@ -225,6 +227,10 @@ export const buildCatalog = (params) => {
|
|
|
225
227
|
entry.searchText = [opId, method.toUpperCase(), path, tag, summary, permission ?? '']
|
|
226
228
|
.join(' ')
|
|
227
229
|
.toLowerCase();
|
|
230
|
+
const phantom = phantomPathParams(entry);
|
|
231
|
+
if (phantom.length > 0) {
|
|
232
|
+
undeliverableParams.push(`${opId} (${phantom.join(', ')} on ${path})`);
|
|
233
|
+
}
|
|
228
234
|
operations.push(entry);
|
|
229
235
|
}
|
|
230
236
|
}
|
|
@@ -250,6 +256,13 @@ export const buildCatalog = (params) => {
|
|
|
250
256
|
'Those fields are marked "x-example-mismatch" and listed by cms_api_describe — ' +
|
|
251
257
|
'copy the example, not the type. Report it against the CMS.');
|
|
252
258
|
}
|
|
259
|
+
if (undeliverableParams.length > 0) {
|
|
260
|
+
warnings.push(`${String(undeliverableParams.length)} operation(s) declare a path parameter that their ` +
|
|
261
|
+
`own path does not contain (${undeliverableParams.slice(0, 5).join(', ')}). ` +
|
|
262
|
+
'The value has nowhere to go, so this server refuses those calls instead of sending ' +
|
|
263
|
+
'the request without it. cms_api_describe marks them "notExecutable" and names a ' +
|
|
264
|
+
'working route where one is known. Report it against the CMS.');
|
|
265
|
+
}
|
|
253
266
|
const noted = Object.keys(OPERATION_NOTES);
|
|
254
267
|
const notedButAbsent = noted.filter((opId) => !operations.some((operation) => operation.opId === opId));
|
|
255
268
|
if (notedButAbsent.length > 0 && notedButAbsent.length <= noted.length / 2) {
|
package/dist/api/client.d.ts
CHANGED
|
@@ -22,6 +22,8 @@ export declare class RequestBuildError extends Error {
|
|
|
22
22
|
}
|
|
23
23
|
export declare const normalizeBody: (body: unknown) => unknown;
|
|
24
24
|
export declare const unsupportedBodyFormat: (operation: Operation) => string | undefined;
|
|
25
|
+
export declare const phantomPathParams: (operation: Operation) => string[];
|
|
26
|
+
export declare const undeliverablePathParams: (operation: Operation) => string | undefined;
|
|
25
27
|
export declare const buildUrl: (baseUrl: string, operation: Operation, args: CallArgs) => string;
|
|
26
28
|
export declare class AdminApiClient {
|
|
27
29
|
private readonly baseUrl;
|
package/dist/api/client.js
CHANGED
|
@@ -31,7 +31,26 @@ export const unsupportedBodyFormat = (operation) => {
|
|
|
31
31
|
}
|
|
32
32
|
return `${base} Report it to the human and use another route to that instance.`;
|
|
33
33
|
};
|
|
34
|
+
export const phantomPathParams = (operation) => operation.params
|
|
35
|
+
.filter((param) => param.location === 'path' && !operation.path.includes(`{${param.name}}`))
|
|
36
|
+
.map((param) => param.name);
|
|
37
|
+
export const undeliverablePathParams = (operation) => {
|
|
38
|
+
const phantom = phantomPathParams(operation);
|
|
39
|
+
if (phantom.length === 0) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
return (`Operation ${operation.opId} declares ${phantom.map((name) => `"${name}"`).join(', ')} as a ` +
|
|
43
|
+
`path parameter, but its path ${operation.path} has no place to put it. The value cannot ` +
|
|
44
|
+
'reach the API, so the request would go out without it and do something other than what ' +
|
|
45
|
+
'you asked. The call is not executable through this server — find the operation that ' +
|
|
46
|
+
'takes the same values where the API really expects them with cms_api_search, and read ' +
|
|
47
|
+
'"note" in cms_api_describe for a route that has been verified.');
|
|
48
|
+
};
|
|
34
49
|
export const buildUrl = (baseUrl, operation, args) => {
|
|
50
|
+
const undeliverable = undeliverablePathParams(operation);
|
|
51
|
+
if (undeliverable !== undefined) {
|
|
52
|
+
throw new RequestBuildError(undeliverable);
|
|
53
|
+
}
|
|
35
54
|
let path = operation.path;
|
|
36
55
|
const provided = args.path ?? {};
|
|
37
56
|
for (const param of operation.params) {
|
|
@@ -94,11 +113,11 @@ const readBody = async (response) => {
|
|
|
94
113
|
return text.slice(0, 2000);
|
|
95
114
|
}
|
|
96
115
|
};
|
|
97
|
-
const isLoginPage = (
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
return
|
|
116
|
+
const isLoginPage = (body) => typeof body === 'string' && body.trimStart().startsWith('<');
|
|
117
|
+
const describeResponse = (response, body) => {
|
|
118
|
+
const contentType = response.headers.get('content-type') ?? 'no content type';
|
|
119
|
+
const text = typeof body === 'string' ? body : JSON.stringify(body ?? null);
|
|
120
|
+
return `${contentType}, body starts with "${text.trimStart().slice(0, 80).replace(/\s+/g, ' ')}"`;
|
|
102
121
|
};
|
|
103
122
|
const extractMessage = (body, fallback) => {
|
|
104
123
|
if (typeof body === 'string') {
|
|
@@ -163,17 +182,18 @@ export class AdminApiClient {
|
|
|
163
182
|
response = await attempt(await this.tokens.refresh());
|
|
164
183
|
}
|
|
165
184
|
body = await readBody(response);
|
|
166
|
-
if (response.ok && isLoginPage(
|
|
185
|
+
if (response.ok && isLoginPage(body)) {
|
|
167
186
|
response = await attempt(await this.tokens.refresh());
|
|
168
187
|
body = await readBody(response);
|
|
169
|
-
if (response.ok && isLoginPage(
|
|
188
|
+
if (response.ok && isLoginPage(body)) {
|
|
170
189
|
return {
|
|
171
190
|
ok: false,
|
|
172
191
|
error: {
|
|
173
192
|
status: response.status,
|
|
174
193
|
message: `${operation.method.toUpperCase()} ${url} answered ${String(response.status)} with ` +
|
|
175
194
|
'the instance login page instead of a result: the admin session is not valid and ' +
|
|
176
|
-
're-authenticating did not change that. Nothing was written by this call.'
|
|
195
|
+
're-authenticating did not change that. Nothing was written by this call. ' +
|
|
196
|
+
`The response was ${describeResponse(response, body)}.`,
|
|
177
197
|
hint: 'Check the credentials this server runs with, then retry. Treat every write since ' +
|
|
178
198
|
'the last verified read as unconfirmed and read those entities back.',
|
|
179
199
|
},
|
|
@@ -57,6 +57,16 @@ export const OPERATION_NOTES = {
|
|
|
57
57
|
'A body of { limit, offset, langCode } answers 400 complaining about langCode, because ' +
|
|
58
58
|
'the validator is describing the query parameter you did not send.',
|
|
59
59
|
},
|
|
60
|
+
AdminProductsController_removeMany: {
|
|
61
|
+
note: 'Not executable through this server: the document declares "ids" as a path parameter ' +
|
|
62
|
+
'while the path is /products, so the value has nowhere to go and the request would go ' +
|
|
63
|
+
'out as a bare DELETE /products. The verified way to delete a chosen set is the ' +
|
|
64
|
+
'operation on DELETE /api/admin/products/category/{pageId} with a body of ' +
|
|
65
|
+
'{ "ids": [ … ] } — find it with cms_api_search { "query": "products category delete" }. ' +
|
|
66
|
+
'The same route with the ids in the query string answers 500 instead, so send them in ' +
|
|
67
|
+
'the body. Deleting products one by one with AdminProductsController_remove is the other ' +
|
|
68
|
+
'route and needs no category.',
|
|
69
|
+
},
|
|
60
70
|
AdminProductsController_setStatusForProducts: {
|
|
61
71
|
note: 'The status id goes in "statusId". The older field name "id" is accepted for the same ' +
|
|
62
72
|
'value, and a body carrying neither answers 400 rather than writing an empty status. ' +
|
|
@@ -223,7 +233,14 @@ export const OPERATION_NOTES = {
|
|
|
223
233
|
note: '"formModuleConfigs" is a full replacement list, not a patch. Omitting it deletes every ' +
|
|
224
234
|
'module binding of the form AND the submissions recorded against those bindings, and the ' +
|
|
225
235
|
'call still answers 200 true. Read the form first and send its current formModuleConfigs ' +
|
|
226
|
-
'back unless changing them is the point.
|
|
236
|
+
'back unless changing them is the point. An existing binding is matched by its own "id" ' +
|
|
237
|
+
'and by nothing else: an entry carrying the same formId, moduleId and entityIdentifiers ' +
|
|
238
|
+
'but no id is created as a NEW binding, the previous one is deleted, and the submissions ' +
|
|
239
|
+
'counted against it stop being counted — the count route for the form goes to zero. ' +
|
|
240
|
+
'Worse, a public read of the entity keeps naming the previous binding for as long as its ' +
|
|
241
|
+
'answer is cached, so a site submitting against it answers 400 "Incorrect formIdentifier ' +
|
|
242
|
+
'for provided config" after the edit rather than during it. Echo formModuleConfigs[].id ' +
|
|
243
|
+
'back on every entry you are keeping. This is also the only operation that creates a ' +
|
|
227
244
|
'binding: an entry with formId, moduleId and either isGlobal or entityIdentifiers. ' +
|
|
228
245
|
'The rating behaviour of a review form lives in that same entry and has no operation of ' +
|
|
229
246
|
'its own: isRating, ratingCalculation, maxRatingScale, allowHalfRatings, allowRerating ' +
|
|
@@ -247,8 +264,9 @@ export const OPERATION_NOTES = {
|
|
|
247
264
|
},
|
|
248
265
|
verifyWith: {
|
|
249
266
|
opId: 'AdminFormsController_findOne',
|
|
250
|
-
check: 'formModuleConfigs',
|
|
251
|
-
why: 'an omitted formModuleConfigs is applied as "unbind everything", submissions included'
|
|
267
|
+
check: 'formModuleConfigs[].id',
|
|
268
|
+
why: 'an omitted formModuleConfigs is applied as "unbind everything", submissions included, ' +
|
|
269
|
+
'and a binding sent without its id comes back with a new one, which is the same loss',
|
|
252
270
|
},
|
|
253
271
|
},
|
|
254
272
|
AdminFormDataController_create: {
|
|
@@ -263,9 +281,13 @@ export const OPERATION_NOTES = {
|
|
|
263
281
|
'not mean the config is optional. A form with type null fails earlier with "Form has ' +
|
|
264
282
|
'incorrect type". Field validation is wired into the Content API only: through the Admin ' +
|
|
265
283
|
'API a missing required field is stored rather than rejected, so a submission accepted ' +
|
|
266
|
-
'here does not prove a visitor\'s submission would pass. A field of type text takes ' +
|
|
267
|
-
'
|
|
268
|
-
'
|
|
284
|
+
'here does not prove a visitor\'s submission would pass. A field of type text takes a ' +
|
|
285
|
+
'list of ONE object carrying EXACTLY ONE of plainValue, htmlValue or mdValue and no ' +
|
|
286
|
+
'other key: [{ "plainValue": "…" }]. A bare string, a list of strings, ' +
|
|
287
|
+
'[{ "value": … }], two of the three keys together and the four-key shape an entity ' +
|
|
288
|
+
'attribute of type text uses (htmlValue, mdValue, plainValue, params) are each ' +
|
|
289
|
+
'rejected, the last two with a message that names no field. On a rating form the score ' +
|
|
290
|
+
'attribute must be marked isRatingValue, or the ' +
|
|
269
291
|
'form is invalid with "Form must have a rating marker". This route also treats one ' +
|
|
270
292
|
'authenticated author as one submission per entity, so it rejects the second review of a ' +
|
|
271
293
|
'product with "You have already rated": a bulk import of visitor reviews cannot go ' +
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { unsupportedBodyFormat } from '../api/client.js';
|
|
2
|
+
import { undeliverablePathParams, unsupportedBodyFormat } from '../api/client.js';
|
|
3
3
|
import { errorResult, jsonResult } from './result.js';
|
|
4
4
|
const sampleValue = (schema, example) => {
|
|
5
5
|
if (example !== undefined) {
|
|
@@ -110,7 +110,7 @@ export const registerApiDiscovery = (server, deps) => {
|
|
|
110
110
|
: 'Operation ids come from cms_api_search — do not construct them by hand.',
|
|
111
111
|
});
|
|
112
112
|
}
|
|
113
|
-
const formatDenial = unsupportedBodyFormat(operation);
|
|
113
|
+
const formatDenial = unsupportedBodyFormat(operation) ?? undeliverablePathParams(operation);
|
|
114
114
|
const properties = operation.body?.schema.properties;
|
|
115
115
|
const loose = properties
|
|
116
116
|
? Object.entries(properties)
|
|
@@ -170,7 +170,8 @@ export const registerApiDiscovery = (server, deps) => {
|
|
|
170
170
|
next: formatDenial
|
|
171
171
|
? operation.body?.contentType === 'multipart/form-data'
|
|
172
172
|
? 'Do not call this with cms_api_call — upload with cms_upload_file or cms_import_file_from_url.'
|
|
173
|
-
: 'Do not call this operation
|
|
173
|
+
: 'Do not call this operation. Take the route named in "notExecutable" or in ' +
|
|
174
|
+
'"note" above, and report it as unavailable through MCP when there is none.'
|
|
174
175
|
: operation.risk === 'read'
|
|
175
176
|
? 'Call it with cms_api_call, copying the "example" above.'
|
|
176
177
|
: operation.verifyWith
|
|
@@ -51,7 +51,7 @@ Ordering is a lexorank **string** on parent-scoped Admin operations and a **numb
|
|
|
51
51
|
|
|
52
52
|
## A read straight after a write can lag
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
An **admin** read by id shows your write immediately; admin lists and **every public read**, one entity included, lag by seconds. So re-read after a pause and **never repeat the write** — that makes a duplicate somebody cleans up by hand. Never swallow a failed read into an empty result either: the next run then recreates everything.
|
|
55
55
|
|
|
56
56
|
## A 200 means accepted not applied
|
|
57
57
|
|
package/package.json
CHANGED