@oneentry/mcp-platform-server 0.1.10 → 0.1.12

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.
@@ -48,7 +48,12 @@ const LIST_EXTRAS_NOTE = 'The curated example is ONE attribute as it appears in
48
48
  '"additionalFields" is a separate marker-keyed list of fields and holds no option extras, ' +
49
49
  'and an "image" key on the option itself is stored and read by nothing. Option extras are ' +
50
50
  'also absent from entity reads — a site gets them from the attribute definition, not from ' +
51
- 'the product.';
51
+ 'the product. The form read by marker now returns "multiselect" too, beside listTitles, ' +
52
+ 'validators and settings, so a site can tell a multi-select field from a single-select one. ' +
53
+ 'The VALUE an entity stores for such an attribute is an array of option objects, ' +
54
+ '[{ "value": "<optionId>", "title": "<label>" }] — a bare string or a list of strings ' +
55
+ 'answers 400, but ONLY when the attribute key is named list_id<id>; under any other key the ' +
56
+ 'malformed value is accepted with 200. A radioButton value is the bare option-id string.';
52
57
  export const OPERATION_NOTES = {
53
58
  AdminProductsController_findAll: {
54
59
  readOnly: true,
@@ -185,38 +190,63 @@ export const OPERATION_NOTES = {
185
190
  },
186
191
  AdminMenusController_updatePosition: {
187
192
  example: {
188
- position: { leftObjectId: null, rightObjectId: null },
193
+ position: {
194
+ leftObjectId: 459,
195
+ leftObjectType: 'menu-custom-item',
196
+ rightObjectId: 4,
197
+ rightObjectType: 'menu-page',
198
+ },
189
199
  newParentId: 12,
190
200
  newParentType: 'custom',
191
201
  },
192
202
  note: '"position" is required even when the order does not matter, and it must be an object: a ' +
193
203
  'body without it answers 400 naming the field, and an empty object is not enough — send ' +
194
204
  '{ leftObjectId: null, rightObjectId: null }. Both re-parenting and sibling order apply, ' +
195
- 'and a public read returns the items in that order. Neighbours are addressed by the id of ' +
196
- 'the item, not of its position, and page items and custom items are numbered separately: ' +
197
- 'in a mixed row name each neighbour with "leftObjectType" and "rightObjectType", or a ' +
198
- 'neighbour of the other kind is not found and the item lands at the edge of the list. ' +
199
- '"newParentType" says which kind the new parent is; omit it and the number is looked up ' +
200
- 'among pages first, which is not necessarily the parent you meant.',
205
+ 'and a public read returns the items in that order the order is reproducible, for a row ' +
206
+ 'of pages and for a mixed row alike. Neighbours are addressed by the id of the item, not ' +
207
+ 'of its position, and page items and custom items are numbered separately, so in a mixed ' +
208
+ 'row each neighbour is named with its kind. The body then carries TWO different ' +
209
+ 'vocabularies for "kind", and that is where this operation is lost: ' +
210
+ '"leftObjectType" / "rightObjectType" take the object type of the position record ' +
211
+ '"menu-page" or "menu-custom-item" — while "newParentType" takes "page" or "custom". ' +
212
+ 'Sending "page" or "custom" as a neighbour kind is not refused: the neighbour is not ' +
213
+ 'resolved, the moved item\'s rank is reset to the middle of the range and the answer is ' +
214
+ 'still 200, so the resulting order can look intended and be arbitrary. Omit the neighbour ' +
215
+ 'kinds only in a row of one kind — then each neighbour is read as the kind of the item ' +
216
+ 'being moved. Omit "newParentType" and the number is looked up among pages first, which ' +
217
+ 'is not necessarily the parent you meant. Omitting "position" while changing the parent ' +
218
+ 'leaves the order alone.',
201
219
  verifyWith: {
202
220
  opId: 'AdminMenusController_findOne',
203
- check: 'the parent of the item you moved, its parentType, and the order of its siblings',
204
- why: 'a neighbour or parent resolved to the other kind answers success and moves it elsewhere',
221
+ check: 'the lexorank of the item you moved against its two neighbours\', and its parent ' +
222
+ 'and parentType',
223
+ why: 'an unresolved neighbour answers 200 and resets the rank to the middle of the range, ' +
224
+ 'which the order alone can still show as the one you asked for',
205
225
  },
206
226
  },
207
227
  AdminMenusController_updateCustomItemPosition: {
208
228
  example: {
209
- position: { leftObjectId: null, rightObjectId: null },
229
+ position: {
230
+ leftObjectId: 3,
231
+ leftObjectType: 'menu-page',
232
+ rightObjectId: 4,
233
+ rightObjectType: 'menu-page',
234
+ },
210
235
  newParentId: 12,
211
236
  newParentType: 'custom',
212
237
  },
213
238
  note: 'Same body as AdminMenusController_updatePosition: "position" is required and must be an ' +
214
239
  'object — { leftObjectId: null, rightObjectId: null } when the order does not matter — and ' +
215
- 'the kind of every neighbour and of the new parent is named alongside its number.',
240
+ 'the kind of every neighbour and of the new parent is named alongside its number, in the ' +
241
+ 'two vocabularies that body uses: "menu-page" / "menu-custom-item" for a neighbour, ' +
242
+ '"page" / "custom" for the new parent. A custom item does move in among pages this way; ' +
243
+ 'a neighbour kind the instance does not know is not refused but resets the rank.',
216
244
  verifyWith: {
217
245
  opId: 'AdminMenusController_findOne',
218
- check: 'the parent of the item you moved, its parentType, and the order of its siblings',
219
- why: 'a neighbour or parent resolved to the other kind answers success and moves it elsewhere',
246
+ check: 'the lexorank of the item you moved against its two neighbours\', and its parent ' +
247
+ 'and parentType',
248
+ why: 'an unresolved neighbour answers 200 and resets the rank to the middle of the range, ' +
249
+ 'which the order alone can still show as the one you asked for',
220
250
  },
221
251
  },
222
252
  AdminFormsController_create: {
@@ -288,16 +318,32 @@ export const OPERATION_NOTES = {
288
318
  'attribute of type text uses (htmlValue, mdValue, plainValue, params) are each ' +
289
319
  'rejected, the last two with a message that names no field. On a rating form the score ' +
290
320
  'attribute must be marked isRatingValue, or the ' +
291
- 'form is invalid with "Form must have a rating marker". This route also treats one ' +
292
- 'authenticated author as one submission per entity, so it rejects the second review of a ' +
293
- 'product with "You have already rated": a bulk import of visitor reviews cannot go ' +
294
- 'through the Admin API and has to use the visitor route, which this server does not call.',
321
+ 'form is invalid with "Form must have a rating marker". The one-rating-per-entity rule ' +
322
+ 'counts only submissions that CARRY A SCORE: a second scored submission answers 400 "You ' +
323
+ 'have already rated this entity", while a scoreless one a shop reply, a helpfulness ' +
324
+ 'note is accepted against an entity that is already rated, and naming the review in ' +
325
+ '"replayTo" stores it as that review\'s child (the extended read returns parentId and ' +
326
+ 'depth 1). A bulk import of scored visitor reviews still needs one fingerprint per ' +
327
+ 'review and has to use the visitor route, which this server does not call.',
295
328
  verifyWith: {
296
329
  opId: 'AdminFormDataController_findByFormMarker',
297
330
  check: 'the submission you sent',
298
331
  why: 'the stored submission is the only proof the binding it names is the intended one',
299
332
  },
300
333
  },
334
+ AdminFormDataController_update: {
335
+ note: 'A submission of a rating form is updatable — this used to answer 404 "Form has incorrect ' +
336
+ 'type rating". The update rewrites the submission\'s content only: the original author ' +
337
+ '(userIdentifier, isUserAdmin) and the fingerprint the one-rating-per-entity rule keys on ' +
338
+ 'are both preserved, so correcting a review neither reassigns it to whoever edited it nor ' +
339
+ 'frees that visitor to score the entity a second time. The types still refused here are ' +
340
+ 'order, sign_in_up and collection.',
341
+ verifyWith: {
342
+ opId: 'AdminFormDataController_findByFormMarker',
343
+ check: 'userIdentifier and the values you changed, with isExtended',
344
+ why: 'the point of the operation is that the author survives it, and only the read shows that',
345
+ },
346
+ },
301
347
  AdminPagesController_update: {
302
348
  note: 'Omitting parentId does not leave the parent alone — it moves the page to the root and ' +
303
349
  'decrements the former parent\'s childrenCount. Read the page first and send parentId ' +
@@ -335,7 +381,16 @@ export const OPERATION_NOTES = {
335
381
  'an unsupported one either. There is no content module among them, so "notify when this ' +
336
382
  'page changes" is not an events task: say so rather than looking for a body that gets ' +
337
383
  'through. Whether an event that fired actually reached anyone is a separate read — ' +
338
- 'AdminEventsController_findEmailLogs.',
384
+ 'AdminEventsController_findEmailLogs. For an event on the forms module (moduleId 2), ' +
385
+ '"formType" is an enum — registration | send_code | change_password | submit_data — and ' +
386
+ 'anything else answers 400 "formType must be a valid enum value", a message that does NOT ' +
387
+ 'list the four. "forms.emails" is what decides where the mail goes: a list of objects, ' +
388
+ 'each either { "attr": "<form field marker>" } to take the address from the submission or ' +
389
+ '{ "plain": "<address>" } for a fixed one. While it is empty the event looks fully ' +
390
+ 'configured and sends nothing, leaving a "skipped" row reading no-recipient-configured. ' +
391
+ '"forms.mode" is any_data | reply | status, "forms.status" applies when mode is status, ' +
392
+ '"formIdentifier" binds the event to a form, and "formEmailFieldIdentifier" is legacy and ' +
393
+ 'is not read. Every event bound to a form fires, so a duplicate doubles the mail.',
339
394
  verifyWith: {
340
395
  opId: 'AdminEventsController_findOne',
341
396
  check: 'moduleId and localizeInfos.<locale>.title',
@@ -347,7 +402,10 @@ export const OPERATION_NOTES = {
347
402
  'localizeInfos.<locale> as "title", "subject" and "template", with "push" for the push ' +
348
403
  'channel. Nothing about the message belongs under "mailing". Supported modules are ' +
349
404
  'catalog, forms, orders, users, payments and discounts; any other moduleId answers 400, ' +
350
- 'checked against the merged result so a partial update cannot slip past it.',
405
+ 'checked against the merged result so a partial update cannot slip past it. On a forms ' +
406
+ 'event the recipient lives in "forms.emails" — entries of { "attr": "<marker>" } or ' +
407
+ '{ "plain": "<address>" } — and "formType" is the enum registration | send_code | ' +
408
+ 'change_password | submit_data. See AdminEventsController_create for the whole shape.',
351
409
  example: {
352
410
  localizeInfos: {
353
411
  en_US: {
@@ -404,6 +462,29 @@ export const OPERATION_NOTES = {
404
462
  'it. A product status can be set here with "statusId", or for many products in one call ' +
405
463
  'with AdminProductsController_setStatusForProducts.',
406
464
  },
465
+ AdminUsersController_update: {
466
+ note: 'groupIds is a full replacement: send every group the user should end up in, [] to clear ' +
467
+ 'them, or omit the field to leave membership alone. formData and notificationData are ' +
468
+ 'written from whatever the body carries, so omitting them replaces them with empty ' +
469
+ 'values — read the user first and echo them back. One attribute of the sign-in form ' +
470
+ 'carries the flag isLogin, and its value is not an ordinary profile field: it is the ' +
471
+ 'credential the person signs in with, so writing it here moves the credential too. No ' +
472
+ 'form is named for that — the form of the user\'s own sign-in provider is used. A value ' +
473
+ 'counts as a change only when it differs from what the profile already holds under that ' +
474
+ 'marker, so echoing the field back untouched is never refused. A real change answers 400 ' +
475
+ 'when the value is blank, when another user already has it (removed users included), ' +
476
+ 'when the body carries the marker under several locales with different values, or when ' +
477
+ 'the account came from a social network, where the credential belongs to the network. ' +
478
+ 'Every one of those refusals happens before the first write, so neither the profile nor ' +
479
+ 'group membership changes even when the same call also sends groupIds. The value is ' +
480
+ 'stored exactly as sent rather than escaped.',
481
+ verifyWith: {
482
+ opId: 'AdminUsersController_findOne',
483
+ check: 'groupIds and the sign-in field in formData',
484
+ why: 'an omitted groupIds or formData is applied as "leave alone" and "clear" respectively, ' +
485
+ 'and a sign-in value that was refused leaves the whole call unapplied',
486
+ },
487
+ },
407
488
  AdminBlocksController_update: {
408
489
  note: 'Omitting blockPages detaches the block from every page it was on. Read the block first ' +
409
490
  'and send its current page list back unless you mean to change it.',
@@ -427,7 +508,21 @@ export const OPERATION_NOTES = {
427
508
  AdminProductsController_countByCategoryMarker: { readOnly: true },
428
509
  AdminUsersController_findAllByConditions: { readOnly: true },
429
510
  AdminUsersController_search: { readOnly: true },
430
- AdminFormDataController_findByFormMarker: { readOnly: true },
511
+ AdminFormDataController_findByFormMarker: {
512
+ readOnly: true,
513
+ note: 'The only listing of submissions: the flat route answers 405, and there is no GET form of ' +
514
+ 'this one either, because the filter travels in the body. Two filters narrow it and they ' +
515
+ 'are not interchangeable. entityIdentifier picks one entity — send the same value the ' +
516
+ 'submission was made with as moduleEntityIdentifier (a page identifier, a product id, a ' +
517
+ 'login for a user or an admin); an integer value is matched against the entity id as ' +
518
+ 'well, so an id taken from a listing keeps working. formModuleConfigId narrows to one ' +
519
+ 'module binding and cannot separate the entities of one form, because a form has a single ' +
520
+ 'config per module; a config id belonging to another form returns no rows, so an empty ' +
521
+ 'answer there means the id is foreign rather than that nothing was submitted. The rest of ' +
522
+ 'the filter body is parentId (null for root-level entries, and a non-integer answers 400), ' +
523
+ 'dateFrom, dateTo, status, userIdentifier and isAdmin. Both listing modes already return ' +
524
+ 'entityIdentifier, entityId and formModuleConfigId; isExtended adds the comment fields.',
525
+ },
431
526
  AdminBlocksController_findCartComplementProductsByBody: { readOnly: true },
432
527
  AdminBlocksController_findCartSimilarProductsByBody: { readOnly: true },
433
528
  AdminBlocksController_findWishlistSimilarProductsByBody: { readOnly: true },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oneentry/mcp-platform-server",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "description": "MCP server that lets an AI agent operate the OneEntry Admin API, grounded in the project's own rules",
5
5
  "license": "MIT",
6
6
  "type": "module",