@abinnovision/payloadcms-mcpx 1.0.0-beta.10 → 1.0.0-beta.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.
- package/README.md +72 -7
- package/dist/api-keys/fields.mjs +1 -1
- package/dist/endpoint/{result.mjs → errors.mjs} +4 -21
- package/dist/endpoint/handler.mjs +8 -4
- package/dist/endpoint/index.mjs +4 -0
- package/dist/endpoint/server.mjs +31 -25
- package/dist/index.d.mts +4 -4
- package/dist/index.mjs +4 -3
- package/dist/plugin.mjs +1 -0
- package/dist/{write/draft-guard.d.mts → request.d.mts} +2 -2
- package/dist/request.mjs +8 -0
- package/dist/result.d.mts +13 -0
- package/dist/result.mjs +22 -0
- package/dist/schema/index.mjs +6 -0
- package/dist/schema/pointer.mjs +1 -1
- package/dist/schema/walk.mjs +11 -15
- package/dist/tools/{index.mjs → builtin.mjs} +7 -4
- package/dist/tools/create-document.mjs +18 -12
- package/dist/tools/describe-schema.mjs +7 -6
- package/dist/tools/find-documents.mjs +6 -6
- package/dist/tools/get-document.mjs +6 -5
- package/dist/tools/list-capabilities.mjs +8 -8
- package/dist/tools/patch-document.mjs +18 -17
- package/dist/tools/shared.mjs +31 -11
- package/dist/tools/validate-document.mjs +12 -12
- package/dist/types.d.mts +109 -11
- package/dist/types.mjs +3 -4
- package/dist/write/draft-guard.mjs +2 -6
- package/dist/write/patch.mjs +131 -57
- package/dist/write/publish-blockers.mjs +10 -3
- package/package.json +1 -1
- package/dist/i18n.d.mts +0 -1
- package/dist/options.d.mts +0 -2
- package/dist/schema/lexical.d.mts +0 -1
- package/dist/schema/walk.d.mts +0 -3
- package/dist/tools/target.d.mts +0 -3
- package/dist/tools/types.d.mts +0 -5
- package/dist/write/publish-blockers.d.mts +0 -15
package/README.md
CHANGED
|
@@ -13,8 +13,9 @@ resolved server-side against the real config and the real document, so unknown
|
|
|
13
13
|
fields, misplaced blocks and unusable rich text nodes or node fields are refused with the
|
|
14
14
|
valid alternatives listed, never silently dropped.
|
|
15
15
|
|
|
16
|
-
Writes are RFC 6902 patches that
|
|
17
|
-
human action in the admin panel.
|
|
16
|
+
Writes are RFC 6902 patches that land as drafts, with `allowLiveWrites` as the
|
|
17
|
+
one documented exception; publishing stays a human action in the admin panel.
|
|
18
|
+
Every write returns the publish
|
|
18
19
|
blockers: the validation failures that still prevent a human from publishing
|
|
19
20
|
the draft. Capabilities are declared twice: the plugin config decides what
|
|
20
21
|
can exist, a checkbox on each API key decides what does, and a missing checkbox
|
|
@@ -153,7 +154,7 @@ only the slugs the key may touch.
|
|
|
153
154
|
| `getDocument` | Read one document or a subtree of it. | `collection` + `id` \| `global`, `path?` (JSON pointer), `depth?`, `locale?`, `draft?` |
|
|
154
155
|
| `patchDocument` | Apply RFC 6902 operations to the current draft. | `collection` + `id` \| `global`, `locale`, `patches`, `expectedUpdatedAt?` |
|
|
155
156
|
| `createDocument` | Create a draft from a minimal seed. | `collection`, `locale`, `data` |
|
|
156
|
-
| `validateDocument` | Publish blockers without
|
|
157
|
+
| `validateDocument` | Publish blockers without saving anything. | `collection` + `id` \| `global`, `locale` |
|
|
157
158
|
|
|
158
159
|
Rules the tools enforce and explain in their own descriptions:
|
|
159
160
|
|
|
@@ -266,6 +267,12 @@ Collections with `versions.drafts.validate: true` refuse invalid drafts
|
|
|
266
267
|
outright; those failures come back as `validationErrors`. Both carry pointers,
|
|
267
268
|
restated from the dotted paths Payload reports internally.
|
|
268
269
|
|
|
270
|
+
`publishBlockersUnavailable` marks a check that could not complete, which is
|
|
271
|
+
not the same answer as a document with nothing wrong with it. `validateDocument`
|
|
272
|
+
runs the same traversal without saving anything, so it is not free of side
|
|
273
|
+
effects: field `beforeValidate` and `beforeChange` hooks run, and it carries no
|
|
274
|
+
`readOnlyHint` for that reason.
|
|
275
|
+
|
|
269
276
|
Writes also report `notApplied`: pointers whose value Payload kept unchanged,
|
|
270
277
|
which happens when field-level access denies the update.
|
|
271
278
|
|
|
@@ -297,9 +304,62 @@ const publishQueue = defineMcpxTool({
|
|
|
297
304
|
|
|
298
305
|
Each custom tool gets its own checkbox on every API key, default off.
|
|
299
306
|
|
|
300
|
-
Custom
|
|
301
|
-
|
|
302
|
-
|
|
307
|
+
Custom tools take the same route as the builtins: one `McpxTool` shape, one
|
|
308
|
+
registration loop. Anything a builtin does, a custom tool can do.
|
|
309
|
+
|
|
310
|
+
`handler` receives `scope` alongside `args`, `req` and `extra`. The scope
|
|
311
|
+
carries what the key may touch (`readable`, `writable`, `readableGlobals`,
|
|
312
|
+
`writableGlobals`), the configured locales, the limits in force and the
|
|
313
|
+
exposed collections and globals. `req` is shorthand for `scope.req`.
|
|
314
|
+
|
|
315
|
+
`inputSchema` may be a function of that scope instead of a fixed shape, which
|
|
316
|
+
is how a tool narrows an enum to what the key may read:
|
|
317
|
+
|
|
318
|
+
```ts
|
|
319
|
+
import { defineMcpxTool } from "@abinnovision/payloadcms-mcpx";
|
|
320
|
+
import { z } from "zod";
|
|
321
|
+
|
|
322
|
+
const whichCollection = defineMcpxTool({
|
|
323
|
+
name: "whichCollection",
|
|
324
|
+
description: "Echoes back one of the collections this key may read.",
|
|
325
|
+
isEnabled: (scope) =>
|
|
326
|
+
scope.capabilities.tools["whichCollection"] === true &&
|
|
327
|
+
scope.readable.length > 0,
|
|
328
|
+
inputSchema: (scope) => ({
|
|
329
|
+
collection: z.enum(scope.readable as [string, ...string[]]),
|
|
330
|
+
}),
|
|
331
|
+
handler: ({ args }) => ({
|
|
332
|
+
content: [{ type: "text", text: args.collection }],
|
|
333
|
+
}),
|
|
334
|
+
});
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
`defineMcpxTool` defines every tool, builtin ones included, and infers the
|
|
338
|
+
handler's arguments from the input schema either way: from a fixed shape, or
|
|
339
|
+
from the object literal a per-request shape returns. Above, `args` is
|
|
340
|
+
`{ collection: string }` without being told.
|
|
341
|
+
|
|
342
|
+
Inference reaches as far as the shape's static type. A helper returning
|
|
343
|
+
`z.ZodRawShape` erases that type and leaves `args` as
|
|
344
|
+
`Record<string, unknown>`, so the builtins' shape helpers declare the superset
|
|
345
|
+
they produce instead: which keys a helper emits depends on the key's scope,
|
|
346
|
+
and the declared type states what a handler must cope with across every scope.
|
|
347
|
+
Their arguments stay derived from their schema that way, and cannot drift from
|
|
348
|
+
it. If your own helpers erase, state the arguments as a type argument:
|
|
349
|
+
`defineMcpxTool<Args>({ ... })`.
|
|
350
|
+
|
|
351
|
+
`isEnabled` decides whether the tool is registered for this key at all: a tool
|
|
352
|
+
that is not enabled never appears in `tools/list`. It defaults to the tool's
|
|
353
|
+
own checkbox, which is what the builtins replace to derive their availability
|
|
354
|
+
from the key's collection and global capabilities. Defining it **replaces**
|
|
355
|
+
the checkbox check, so restate `scope.capabilities.tools[name]` when you still
|
|
356
|
+
want it, as above.
|
|
357
|
+
|
|
358
|
+
Every input schema is registered strictly, custom tools included: an unknown
|
|
359
|
+
argument is rejected by name rather than stripped before the handler runs.
|
|
360
|
+
|
|
361
|
+
`jsonResult` and `errorResult` are exported so a custom tool can return
|
|
362
|
+
results shaped like a builtin's.
|
|
303
363
|
|
|
304
364
|
## Options
|
|
305
365
|
|
|
@@ -320,10 +380,15 @@ Builtin tools reject them instead.
|
|
|
320
380
|
| `endpoint.path` | `/mcpx` | Endpoint path below the API route. |
|
|
321
381
|
| `limits.maxLimit` | `25` | Upper bound for `findDocuments.limit`. |
|
|
322
382
|
| `limits.maxDepth` | `1` | Upper bound for `depth` on reads. |
|
|
323
|
-
| `tools` | `[]` | Custom tools.
|
|
383
|
+
| `tools` | `[]` | Custom tools, defined the same way as the builtins. |
|
|
324
384
|
| `auth.resolve` | none | Replace or wrap the default key resolution. |
|
|
325
385
|
| `serverInfo` | package name and version | Reported to MCP clients. |
|
|
326
386
|
|
|
387
|
+
`allowLiveWrites` is the only way an MCP write reaches live content. Where it is
|
|
388
|
+
set, the server instructions and the `patchDocument` and `createDocument`
|
|
389
|
+
descriptions name those slugs for the key in question, so a client is never told
|
|
390
|
+
its writes are drafts while they are not.
|
|
391
|
+
|
|
327
392
|
Misconfiguration (unknown slugs, write on a collection without drafts, upload
|
|
328
393
|
collections exposed for write, tool name collisions) fails at startup with
|
|
329
394
|
`InvalidConfiguration`. Auth collections cannot be exposed at all, read
|
package/dist/api-keys/fields.mjs
CHANGED
|
@@ -107,7 +107,7 @@ const SETUP_GUIDE_FIELD = "setupGuide";
|
|
|
107
107
|
label: global.slug,
|
|
108
108
|
fields: [...global.read ? [checkbox("read", "Describe and read this global.")] : [], ...global.write ? [checkbox("write", "Patch and validate this global's draft.")] : []]
|
|
109
109
|
}));
|
|
110
|
-
const toolCheckboxes = options.tools.map((tool) => checkbox(tool.name, tool.description));
|
|
110
|
+
const toolCheckboxes = options.tools.map((tool) => checkbox(tool.name, typeof tool.description === "string" ? tool.description : tool.name));
|
|
111
111
|
const groups = [
|
|
112
112
|
...collectionGroups.length > 0 ? [{
|
|
113
113
|
name: "collections",
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { errorResult } from "../result.mjs";
|
|
1
2
|
import { pointerFromPayloadPath } from "../schema/walk.mjs";
|
|
3
|
+
import "../schema/index.mjs";
|
|
2
4
|
import { APIError, ValidationError } from "payload";
|
|
3
|
-
//#region src/endpoint/
|
|
5
|
+
//#region src/endpoint/errors.ts
|
|
4
6
|
/**
|
|
5
7
|
* A JSON-RPC error response for failures that happen before the MCP server
|
|
6
8
|
* is involved (auth, method, body parsing).
|
|
@@ -20,25 +22,6 @@ import { APIError, ValidationError } from "payload";
|
|
|
20
22
|
});
|
|
21
23
|
};
|
|
22
24
|
/**
|
|
23
|
-
* A successful tool result carrying `value` as JSON text.
|
|
24
|
-
*/ const jsonResult = (value) => ({ content: [{
|
|
25
|
-
type: "text",
|
|
26
|
-
text: JSON.stringify(value)
|
|
27
|
-
}] });
|
|
28
|
-
/**
|
|
29
|
-
* A failed tool result. `extras` travel alongside the message so the client
|
|
30
|
-
* can act on them (problems, validation errors, the current `updatedAt`).
|
|
31
|
-
*/ const errorResult = (message, extras = {}) => ({
|
|
32
|
-
content: [{
|
|
33
|
-
type: "text",
|
|
34
|
-
text: JSON.stringify({
|
|
35
|
-
error: message,
|
|
36
|
-
...extras
|
|
37
|
-
})
|
|
38
|
-
}],
|
|
39
|
-
isError: true
|
|
40
|
-
});
|
|
41
|
-
/**
|
|
42
25
|
* Maps an exception thrown by a tool to a result the client can read.
|
|
43
26
|
*
|
|
44
27
|
* Payload's public errors keep their message and status; a `ValidationError`
|
|
@@ -62,4 +45,4 @@ import { APIError, ValidationError } from "payload";
|
|
|
62
45
|
return errorResult("Internal error");
|
|
63
46
|
};
|
|
64
47
|
//#endregion
|
|
65
|
-
export {
|
|
48
|
+
export { jsonRpcError, toToolError };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readableGlobalSlugs, readableSlugs, resolveCapabilities, writableGlobalSlugs, writableSlugs } from "../capabilities.mjs";
|
|
2
|
+
import { jsonRpcError } from "./errors.mjs";
|
|
2
3
|
import { resolveApiKeyAuth } from "../auth/resolve.mjs";
|
|
3
|
-
import { jsonRpcError } from "./result.mjs";
|
|
4
4
|
import { createMcpServer } from "./server.mjs";
|
|
5
5
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
6
6
|
//#region src/endpoint/handler.ts
|
|
@@ -8,14 +8,18 @@ const buildScope = (req, options, capabilities) => {
|
|
|
8
8
|
const { localization } = req.payload.config;
|
|
9
9
|
return {
|
|
10
10
|
req,
|
|
11
|
-
options,
|
|
12
11
|
capabilities,
|
|
13
12
|
readable: readableSlugs(capabilities),
|
|
14
13
|
writable: writableSlugs(capabilities),
|
|
15
14
|
readableGlobals: readableGlobalSlugs(capabilities),
|
|
16
15
|
writableGlobals: writableGlobalSlugs(capabilities),
|
|
17
16
|
locales: localization ? localization.localeCodes : null,
|
|
18
|
-
defaultLocale: localization ? localization.defaultLocale : null
|
|
17
|
+
defaultLocale: localization ? localization.defaultLocale : null,
|
|
18
|
+
limits: options.limits,
|
|
19
|
+
exposure: {
|
|
20
|
+
collections: options.collections,
|
|
21
|
+
globals: options.globals
|
|
22
|
+
}
|
|
19
23
|
};
|
|
20
24
|
};
|
|
21
25
|
/**
|
|
@@ -68,7 +72,7 @@ const buildScope = (req, options, capabilities) => {
|
|
|
68
72
|
code: -32600,
|
|
69
73
|
message: "Invalid request: a JSON body is required."
|
|
70
74
|
});
|
|
71
|
-
const server = createMcpServer(buildScope(req, options, capabilities));
|
|
75
|
+
const server = createMcpServer(buildScope(req, options, capabilities), options);
|
|
72
76
|
const transport = new WebStandardStreamableHTTPServerTransport({ enableJsonResponse: true });
|
|
73
77
|
await server.connect(transport);
|
|
74
78
|
const headers = new Headers(req.headers);
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { jsonRpcError, toToolError } from "./errors.mjs";
|
|
2
|
+
import { createMcpServer, isToolEnabled, toolDescription, toolInputSchema } from "./server.mjs";
|
|
3
|
+
import { createMcpxHandler, methodNotAllowed } from "./handler.mjs";
|
|
4
|
+
export { createMcpServer, createMcpxHandler, isToolEnabled, jsonRpcError, methodNotAllowed, toToolError, toolDescription, toolInputSchema };
|
package/dist/endpoint/server.mjs
CHANGED
|
@@ -1,24 +1,37 @@
|
|
|
1
|
-
import { toToolError } from "./
|
|
2
|
-
import {
|
|
1
|
+
import { toToolError } from "./errors.mjs";
|
|
2
|
+
import { draftSentence } from "../tools/shared.mjs";
|
|
3
|
+
import { BUILTIN_TOOLS } from "../tools/builtin.mjs";
|
|
3
4
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
5
|
import { z } from "zod";
|
|
5
6
|
//#region src/endpoint/server.ts
|
|
6
7
|
/**
|
|
7
|
-
* Builds a
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
|
|
8
|
+
* Builds a tool's input schema as a strict object, so an unknown argument is
|
|
9
|
+
* rejected with its name instead of being silently stripped and the tool
|
|
10
|
+
* answering as if it had not been passed. A tool may build its shape from the
|
|
11
|
+
* scope to narrow enums to what the key may touch.
|
|
12
|
+
*/ const toolInputSchema = (tool, scope) => z.strictObject(typeof tool.inputSchema === "function" ? tool.inputSchema(scope) : tool.inputSchema ?? {});
|
|
11
13
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
* A tool's description, which may be built from the scope so it can name the
|
|
15
|
+
* targets this key writes live.
|
|
16
|
+
*/ const toolDescription = (tool, scope) => typeof tool.description === "function" ? tool.description(scope) : tool.description;
|
|
17
|
+
/**
|
|
18
|
+
* Whether the key may call the tool. A tool that does not decide for itself is
|
|
19
|
+
* gated by its own checkbox on the key, which is how the tools from
|
|
20
|
+
* `options.tools` work; the builtins derive it from the key's collection and
|
|
21
|
+
* global capabilities instead.
|
|
22
|
+
*/ const isToolEnabled = (tool, scope) => tool.isEnabled ? tool.isEnabled(scope) : scope.capabilities.tools[tool.name] === true;
|
|
23
|
+
/**
|
|
24
|
+
* Builds the MCP server for one request. Builtin and configured tools take the
|
|
25
|
+
* same route: each is registered against the key's capabilities, so
|
|
26
|
+
* `tools/list` shows exactly what the key may call and every `collection` enum
|
|
27
|
+
* is limited to what it may touch.
|
|
28
|
+
*/ const createMcpServer = (scope, options) => {
|
|
29
|
+
const { req } = scope;
|
|
17
30
|
const { logger } = req.payload;
|
|
18
31
|
const server = new McpServer({
|
|
19
32
|
name: options.serverInfo.name,
|
|
20
33
|
version: options.serverInfo.version
|
|
21
|
-
}, { instructions:
|
|
34
|
+
}, { instructions: `Start with listCapabilities, then describeSchema for the collection or global you work on. ${draftSentence(scope)}` });
|
|
22
35
|
const guarded = (run) => async () => {
|
|
23
36
|
try {
|
|
24
37
|
return await run();
|
|
@@ -26,22 +39,15 @@ import { z } from "zod";
|
|
|
26
39
|
return toToolError(error, logger);
|
|
27
40
|
}
|
|
28
41
|
};
|
|
29
|
-
for (const tool of BUILTIN_TOOLS) {
|
|
30
|
-
if (!tool
|
|
31
|
-
server.registerTool(tool.name, {
|
|
32
|
-
description: tool.description,
|
|
33
|
-
inputSchema: builtinInputSchema(tool, scope),
|
|
34
|
-
annotations: tool.annotations
|
|
35
|
-
}, (args) => guarded(() => tool.handler(args, scope))());
|
|
36
|
-
}
|
|
37
|
-
for (const tool of options.tools) {
|
|
38
|
-
if (capabilities.tools[tool.name] !== true) continue;
|
|
42
|
+
for (const tool of [...BUILTIN_TOOLS, ...options.tools]) {
|
|
43
|
+
if (!isToolEnabled(tool, scope)) continue;
|
|
39
44
|
server.registerTool(tool.name, {
|
|
40
|
-
description: tool
|
|
41
|
-
inputSchema: tool
|
|
45
|
+
description: toolDescription(tool, scope),
|
|
46
|
+
inputSchema: toolInputSchema(tool, scope),
|
|
42
47
|
...tool.annotations ? { annotations: tool.annotations } : {}
|
|
43
48
|
}, (args, extra) => guarded(() => tool.handler({
|
|
44
49
|
args,
|
|
50
|
+
scope,
|
|
45
51
|
req,
|
|
46
52
|
extra
|
|
47
53
|
}))());
|
|
@@ -49,4 +55,4 @@ import { z } from "zod";
|
|
|
49
55
|
return server;
|
|
50
56
|
};
|
|
51
57
|
//#endregion
|
|
52
|
-
export {
|
|
58
|
+
export { createMcpServer, isToolEnabled, toolDescription, toolInputSchema };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { McpxAuthResult, McpxCollectionCapabilities, McpxCollectionOptions, McpxGlobalOptions, McpxPluginOptions, McpxRequestContext, McpxResolvedCapabilities, McpxTool, McpxToolExtra, defineMcpxTool } from "./types.mjs";
|
|
1
|
+
import { McpxAnyTool, McpxAuthResult, McpxCollectionCapabilities, McpxCollectionOptions, McpxExposedEntity, McpxGlobalOptions, McpxPluginOptions, McpxRequestContext, McpxResolvedCapabilities, McpxTool, McpxToolExtra, McpxToolScope, PublishBlocker, defineMcpxTool } from "./types.mjs";
|
|
2
2
|
import { mcpxPlugin } from "./plugin.mjs";
|
|
3
|
-
import { isMcpxRequest } from "./
|
|
4
|
-
import {
|
|
5
|
-
export {
|
|
3
|
+
import { isMcpxRequest } from "./request.mjs";
|
|
4
|
+
import { errorResult, jsonResult } from "./result.mjs";
|
|
5
|
+
export { McpxAnyTool, McpxAuthResult, McpxCollectionCapabilities, McpxCollectionOptions, McpxExposedEntity, McpxGlobalOptions, McpxPluginOptions, McpxRequestContext, McpxResolvedCapabilities, McpxTool, McpxToolExtra, McpxToolScope, PublishBlocker, defineMcpxTool, errorResult, isMcpxRequest, jsonResult, mcpxPlugin };
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { mcpxPlugin } from "./plugin.mjs";
|
|
1
|
+
import { errorResult, jsonResult } from "./result.mjs";
|
|
3
2
|
import { defineMcpxTool } from "./types.mjs";
|
|
4
|
-
|
|
3
|
+
import { isMcpxRequest } from "./request.mjs";
|
|
4
|
+
import { mcpxPlugin } from "./plugin.mjs";
|
|
5
|
+
export { defineMcpxTool, errorResult, isMcpxRequest, jsonResult, mcpxPlugin };
|
package/dist/plugin.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createApiKeysCollection } from "./api-keys/collection.mjs";
|
|
2
2
|
import { createMcpxHandler, methodNotAllowed } from "./endpoint/handler.mjs";
|
|
3
|
+
import "./endpoint/index.mjs";
|
|
3
4
|
import { normalizeOptions } from "./options.mjs";
|
|
4
5
|
import { installDraftGuards, installGlobalDraftGuards } from "./write/draft-guard.mjs";
|
|
5
6
|
import { definePlugin } from "payload";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
//#region src/
|
|
1
|
+
import { PayloadRequest } from "payload";
|
|
2
|
+
//#region src/request.d.ts
|
|
3
3
|
/**
|
|
4
4
|
* Whether a request originated from the MCP endpoint. The endpoint stamps
|
|
5
5
|
* `req.context.mcpx`, which travels into every local API call made with the
|
package/dist/request.mjs
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
//#region src/request.ts
|
|
2
|
+
/**
|
|
3
|
+
* Whether a request originated from the MCP endpoint. The endpoint stamps
|
|
4
|
+
* `req.context.mcpx`, which travels into every local API call made with the
|
|
5
|
+
* same `req`, including those made by custom tools.
|
|
6
|
+
*/ const isMcpxRequest = (req) => req.context.mcpx !== void 0;
|
|
7
|
+
//#endregion
|
|
8
|
+
export { isMcpxRequest };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
//#region src/result.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* A successful tool result carrying `value` as JSON text.
|
|
5
|
+
*/
|
|
6
|
+
declare const jsonResult: (value: unknown) => CallToolResult;
|
|
7
|
+
/**
|
|
8
|
+
* A failed tool result. `extras` travel alongside the message so the client
|
|
9
|
+
* can act on them (problems, validation errors, the current `updatedAt`).
|
|
10
|
+
*/
|
|
11
|
+
declare const errorResult: (message: string, extras?: Record<string, unknown>) => CallToolResult;
|
|
12
|
+
//#endregion
|
|
13
|
+
export { errorResult, jsonResult };
|
package/dist/result.mjs
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
//#region src/result.ts
|
|
2
|
+
/**
|
|
3
|
+
* A successful tool result carrying `value` as JSON text.
|
|
4
|
+
*/ const jsonResult = (value) => ({ content: [{
|
|
5
|
+
type: "text",
|
|
6
|
+
text: JSON.stringify(value)
|
|
7
|
+
}] });
|
|
8
|
+
/**
|
|
9
|
+
* A failed tool result. `extras` travel alongside the message so the client
|
|
10
|
+
* can act on them (problems, validation errors, the current `updatedAt`).
|
|
11
|
+
*/ const errorResult = (message, extras = {}) => ({
|
|
12
|
+
content: [{
|
|
13
|
+
type: "text",
|
|
14
|
+
text: JSON.stringify({
|
|
15
|
+
error: message,
|
|
16
|
+
...extras
|
|
17
|
+
})
|
|
18
|
+
}],
|
|
19
|
+
isError: true
|
|
20
|
+
});
|
|
21
|
+
//#endregion
|
|
22
|
+
export { errorResult, jsonResult };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { allowedNodeTypes, lexicalSubSchema, nodeOptions, subSchemaNodeTypes } from "./lexical.mjs";
|
|
2
|
+
import { JSON_POINTER_PATTERN, RESERVED_FIELD_NAMES, blockOf, blockSlugsOf, describeAddressableFields, describeFields, findBlocksField, findRichTextField, joinPath, pointerFromPayloadPath, splitPath, targetOf } from "./walk.mjs";
|
|
3
|
+
import { nodeDescriber, reachableSchemaPaths } from "./describe.mjs";
|
|
4
|
+
import { resolveDataPointer } from "./pointer.mjs";
|
|
5
|
+
import { validateWriteValue } from "./shape.mjs";
|
|
6
|
+
export { JSON_POINTER_PATTERN, RESERVED_FIELD_NAMES, allowedNodeTypes, blockOf, blockSlugsOf, describeAddressableFields, describeFields, findBlocksField, findRichTextField, joinPath, lexicalSubSchema, nodeDescriber, nodeOptions, pointerFromPayloadPath, reachableSchemaPaths, resolveDataPointer, splitPath, subSchemaNodeTypes, targetOf, validateWriteValue };
|
package/dist/schema/pointer.mjs
CHANGED
|
@@ -43,7 +43,7 @@ const partMatches = (part, segment) => segment !== void 0 && (part === "*" ? isI
|
|
|
43
43
|
if (isSubtreePrefix(descriptors, segments)) return {
|
|
44
44
|
...blockType === void 0 ? {} : { blockType },
|
|
45
45
|
fields,
|
|
46
|
-
prefix: segments
|
|
46
|
+
prefix: segments.map((segment) => isIndexSegment(segment) ? "*" : segment)
|
|
47
47
|
};
|
|
48
48
|
throw new Error(`"${joinPath(segments)}" is not a field here. Available: ${descriptors.map((descriptor) => descriptor.path).join(", ")}`);
|
|
49
49
|
}
|
package/dist/schema/walk.mjs
CHANGED
|
@@ -83,7 +83,7 @@ const withRows = (descriptor, field) => ({
|
|
|
83
83
|
* Whether a descriptor stands for a construct that only holds other fields.
|
|
84
84
|
*
|
|
85
85
|
* These describe a position rather than a value, so everything that resolves a
|
|
86
|
-
* path to something writable skips them; only {@link
|
|
86
|
+
* path to something writable skips them; only {@link nodeDescriber} reports
|
|
87
87
|
* them, to carry what the container itself declares.
|
|
88
88
|
*/ const isContainer = (descriptor) => descriptor.type === "array" || descriptor.type === "group" || descriptor.type === "tab";
|
|
89
89
|
/**
|
|
@@ -133,29 +133,25 @@ const withRows = (descriptor, field) => ({
|
|
|
133
133
|
/**
|
|
134
134
|
* The descriptors that address a value, which is what every walk resolving a
|
|
135
135
|
* path against a document needs. A container describes a position rather than
|
|
136
|
-
* a value, so only {@link
|
|
136
|
+
* a value, so only {@link nodeDescriber} reports one.
|
|
137
137
|
*/ const describeAddressableFields = (fields) => describeFields(fields).filter((descriptor) => !isContainer(descriptor));
|
|
138
138
|
/**
|
|
139
|
-
* Locates the
|
|
140
|
-
*/ const
|
|
139
|
+
* Locates the field of `type` that a resolved descriptor path refers to.
|
|
140
|
+
*/ const findFieldAt = (fields, path, type) => {
|
|
141
141
|
for (const field of fields) {
|
|
142
142
|
if (!("name" in field) || field.name !== path[0]) continue;
|
|
143
|
-
if (field.type ===
|
|
144
|
-
if (field.type === "tab" || field.type === "group") return
|
|
145
|
-
if (field.type === "array" && path[1] === "*") return
|
|
143
|
+
if (field.type === type && path.length === 1) return field;
|
|
144
|
+
if (field.type === "tab" || field.type === "group") return findFieldAt(field.flattenedFields, path.slice(1), type);
|
|
145
|
+
if (field.type === "array" && path[1] === "*") return findFieldAt(field.flattenedFields, path.slice(2), type);
|
|
146
146
|
}
|
|
147
147
|
};
|
|
148
148
|
/**
|
|
149
|
+
* Locates the blocks field that a resolved descriptor path refers to.
|
|
150
|
+
*/ const findBlocksField = (fields, path) => findFieldAt(fields, path, "blocks");
|
|
151
|
+
/**
|
|
149
152
|
* Locates the rich text field that a resolved descriptor path refers to, so
|
|
150
153
|
* its editor can be introspected for the fields its nodes carry.
|
|
151
|
-
*/ const findRichTextField = (fields, path) =>
|
|
152
|
-
for (const field of fields) {
|
|
153
|
-
if (!("name" in field) || field.name !== path[0]) continue;
|
|
154
|
-
if (field.type === "richText" && path.length === 1) return field;
|
|
155
|
-
if (field.type === "tab" || field.type === "group") return findRichTextField(field.flattenedFields, path.slice(1));
|
|
156
|
-
if (field.type === "array" && path[1] === "*") return findRichTextField(field.flattenedFields, path.slice(2));
|
|
157
|
-
}
|
|
158
|
-
};
|
|
154
|
+
*/ const findRichTextField = (fields, path) => findFieldAt(fields, path, "richText");
|
|
159
155
|
const targetOf = (config, ref) => {
|
|
160
156
|
const found = ref.kind === "collection" ? config.collections.find((candidate) => candidate.slug === ref.slug) : config.globals.find((candidate) => candidate.slug === ref.slug);
|
|
161
157
|
if (!found) throw new Error(`Unknown ${ref.kind} "${ref.slug}".`);
|
|
@@ -5,11 +5,14 @@ import { getDocument } from "./get-document.mjs";
|
|
|
5
5
|
import { listCapabilities } from "./list-capabilities.mjs";
|
|
6
6
|
import { patchDocument } from "./patch-document.mjs";
|
|
7
7
|
import { validateDocument } from "./validate-document.mjs";
|
|
8
|
-
//#region src/tools/
|
|
8
|
+
//#region src/tools/builtin.ts
|
|
9
9
|
/**
|
|
10
|
-
* The builtin tools in registration order.
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* The builtin tools in registration order. They are ordinary {@link McpxTool}s
|
|
11
|
+
* that ship with the plugin and register through the same loop as the tools
|
|
12
|
+
* from `options.tools`; only their `isEnabled` differs, deriving from the
|
|
13
|
+
* key's collection and global capabilities rather than a checkbox of their
|
|
14
|
+
* own. The surface is fixed: adding a collection, block or field never
|
|
15
|
+
* changes it.
|
|
13
16
|
*/ const BUILTIN_TOOLS = [
|
|
14
17
|
listCapabilities,
|
|
15
18
|
describeSchema,
|
|
@@ -1,14 +1,19 @@
|
|
|
1
|
-
import { errorResult, jsonResult } from "../
|
|
2
|
-
import { localeOf, localeShape, readTarget, slugEnum } from "./shared.mjs";
|
|
3
|
-
import { resolveTarget } from "./target.mjs";
|
|
1
|
+
import { errorResult, jsonResult } from "../result.mjs";
|
|
4
2
|
import { validateWriteValue } from "../schema/shape.mjs";
|
|
3
|
+
import "../schema/index.mjs";
|
|
4
|
+
import { draftSentence, localeOf, localeShape, readTarget, slugEnum } from "./shared.mjs";
|
|
5
|
+
import { resolveTarget } from "./target.mjs";
|
|
6
|
+
import { defineMcpxTool } from "../types.mjs";
|
|
5
7
|
import { stripRowIds } from "../write/patch.mjs";
|
|
6
8
|
import { collectPublishBlockers } from "../write/publish-blockers.mjs";
|
|
7
9
|
import { z } from "zod";
|
|
8
10
|
//#region src/tools/create-document.ts
|
|
9
|
-
const
|
|
11
|
+
const DESCRIPTION = (scope) => `Creates a new document from a minimal seed. Only the fields describeSchema lists may appear in "data"; unknown keys are refused with the valid siblings, and "id" is Payload's to assign. The document may be incomplete: the response lists "publishBlockers", which patchDocument can then work through, and "publishBlockersUnavailable" when that check itself failed. Use this when no document exists yet; prefer patching an existing draft otherwise.
|
|
12
|
+
|
|
13
|
+
${draftSentence(scope)}`;
|
|
14
|
+
const createDocument = defineMcpxTool({
|
|
10
15
|
name: "createDocument",
|
|
11
|
-
description:
|
|
16
|
+
description: DESCRIPTION,
|
|
12
17
|
annotations: {
|
|
13
18
|
readOnlyHint: false,
|
|
14
19
|
destructiveHint: false,
|
|
@@ -24,22 +29,22 @@ const createDocument = {
|
|
|
24
29
|
}),
|
|
25
30
|
data: z.record(z.string(), z.unknown()).describe("Initial field values, as describeSchema lists them.")
|
|
26
31
|
}),
|
|
27
|
-
handler: async (args, scope) => {
|
|
32
|
+
handler: async ({ args, scope }) => {
|
|
28
33
|
const target = resolveTarget(scope, { collection: args.collection }, "write");
|
|
29
34
|
const { payload } = scope.req;
|
|
30
35
|
const locale = localeOf(scope, args.locale);
|
|
31
|
-
|
|
36
|
+
if ("id" in args.data) return errorResult("Nothing was created.", { problems: ["/id: Payload assigns the id; it cannot be supplied."] });
|
|
32
37
|
const problems = validateWriteValue(payload.config, {
|
|
33
38
|
pointer: "",
|
|
34
39
|
resolution: {
|
|
35
40
|
fields: target.config.flattenedFields,
|
|
36
41
|
prefix: []
|
|
37
42
|
}
|
|
38
|
-
},
|
|
43
|
+
}, args.data);
|
|
39
44
|
if (problems.length > 0) return errorResult("Nothing was created.", { problems });
|
|
40
45
|
const created = await payload.create({
|
|
41
46
|
collection: args.collection,
|
|
42
|
-
data: stripRowIds(
|
|
47
|
+
data: stripRowIds(args.data),
|
|
43
48
|
depth: 0,
|
|
44
49
|
draft: true,
|
|
45
50
|
overrideAccess: false,
|
|
@@ -52,7 +57,7 @@ const createDocument = {
|
|
|
52
57
|
locale,
|
|
53
58
|
privileged: true
|
|
54
59
|
});
|
|
55
|
-
const
|
|
60
|
+
const validation = await collectPublishBlockers(scope.req, {
|
|
56
61
|
doc: saved,
|
|
57
62
|
entity: target
|
|
58
63
|
});
|
|
@@ -60,9 +65,10 @@ const createDocument = {
|
|
|
60
65
|
id: saved["id"],
|
|
61
66
|
status: saved["_status"],
|
|
62
67
|
updatedAt: saved["updatedAt"],
|
|
63
|
-
...
|
|
68
|
+
...validation.blockers.length > 0 ? { publishBlockers: validation.blockers } : {},
|
|
69
|
+
...validation.unavailable ? { publishBlockersUnavailable: true } : {}
|
|
64
70
|
});
|
|
65
71
|
}
|
|
66
|
-
};
|
|
72
|
+
});
|
|
67
73
|
//#endregion
|
|
68
74
|
export { createDocument };
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
+
import { jsonResult } from "../result.mjs";
|
|
1
2
|
import { translatorFor } from "../i18n.mjs";
|
|
2
|
-
import {
|
|
3
|
+
import { nodeDescriber, reachableSchemaPaths } from "../schema/describe.mjs";
|
|
4
|
+
import "../schema/index.mjs";
|
|
3
5
|
import { targetShape } from "./shared.mjs";
|
|
4
6
|
import { refOf, resolveTarget } from "./target.mjs";
|
|
5
|
-
import {
|
|
7
|
+
import { defineMcpxTool } from "../types.mjs";
|
|
6
8
|
import { z } from "zod";
|
|
7
|
-
|
|
8
|
-
const describeSchema = {
|
|
9
|
+
const describeSchema = defineMcpxTool({
|
|
9
10
|
name: "describeSchema",
|
|
10
11
|
description: `Describes the writable shape of a document, one node at a time.
|
|
11
12
|
|
|
@@ -31,7 +32,7 @@ Fields Payload maintains (id, _status, createdAt, updatedAt) are never listed an
|
|
|
31
32
|
paths: z.array(z.string()).optional().describe("Schema paths to describe, e.g. \"/layout/sections/sectionWrapper\". Omit for the collection root."),
|
|
32
33
|
expand: z.boolean().optional().describe("Return every node reachable from the root in one response. Ignores paths.")
|
|
33
34
|
}),
|
|
34
|
-
handler: (args, scope) => {
|
|
35
|
+
handler: ({ args, scope }) => {
|
|
35
36
|
const ref = refOf(resolveTarget(scope, args, "read"));
|
|
36
37
|
const { config } = scope.req.payload;
|
|
37
38
|
const describeNode = nodeDescriber(translatorFor(scope.req.i18n));
|
|
@@ -49,6 +50,6 @@ Fields Payload maintains (id, _status, createdAt, updatedAt) are never listed an
|
|
|
49
50
|
if (expanded?.truncated) nodes.push({ error: `Result truncated after ${String(400)} nodes. Request explicit paths instead.` });
|
|
50
51
|
return Promise.resolve(jsonResult(nodes));
|
|
51
52
|
}
|
|
52
|
-
};
|
|
53
|
+
});
|
|
53
54
|
//#endregion
|
|
54
55
|
export { describeSchema };
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { jsonResult } from "../
|
|
1
|
+
import { jsonResult } from "../result.mjs";
|
|
2
2
|
import { depthShape, localeOf, localeShape, slugEnum } from "./shared.mjs";
|
|
3
3
|
import { resolveTarget } from "./target.mjs";
|
|
4
|
+
import { defineMcpxTool } from "../types.mjs";
|
|
4
5
|
import { z } from "zod";
|
|
5
|
-
|
|
6
|
-
const findDocuments = {
|
|
6
|
+
const findDocuments = defineMcpxTool({
|
|
7
7
|
name: "findDocuments",
|
|
8
8
|
description: `Finds documents in a collection. "where" is a Payload query object, e.g. {"title":{"contains":"home"}} or {"and":[...]}; "select" picks fields, e.g. {"title":true}. Drafts are included by default so unpublished work is visible. Keep depth at 0 unless populated relationships are needed; ids are enough for writes.`,
|
|
9
9
|
annotations: {
|
|
@@ -15,7 +15,7 @@ const findDocuments = {
|
|
|
15
15
|
collection: slugEnum(scope.readable).describe("Collection to search."),
|
|
16
16
|
where: z.record(z.string(), z.unknown()).optional().describe("Payload where query."),
|
|
17
17
|
sort: z.string().optional().describe("Sort field, prefix with \"-\" for descending."),
|
|
18
|
-
limit: z.number().int().min(1).max(scope.
|
|
18
|
+
limit: z.number().int().min(1).max(scope.limits.maxLimit).optional().describe(`Documents per page. Default 10, at most ${String(scope.limits.maxLimit)}.`),
|
|
19
19
|
page: z.number().int().min(1).optional().describe("Page number, from 1."),
|
|
20
20
|
...depthShape(scope),
|
|
21
21
|
select: z.record(z.string(), z.unknown()).optional().describe("Fields to return, e.g. {\"title\":true}."),
|
|
@@ -25,7 +25,7 @@ const findDocuments = {
|
|
|
25
25
|
}),
|
|
26
26
|
draft: z.boolean().optional().describe("Include the latest drafts. Default true.")
|
|
27
27
|
}),
|
|
28
|
-
handler: async (args, scope) => {
|
|
28
|
+
handler: async ({ args, scope }) => {
|
|
29
29
|
resolveTarget(scope, { collection: args.collection }, "read");
|
|
30
30
|
const locale = localeOf(scope, args.locale);
|
|
31
31
|
const result = await scope.req.payload.find({
|
|
@@ -50,6 +50,6 @@ const findDocuments = {
|
|
|
50
50
|
hasNextPage: result.hasNextPage
|
|
51
51
|
});
|
|
52
52
|
}
|
|
53
|
-
};
|
|
53
|
+
});
|
|
54
54
|
//#endregion
|
|
55
55
|
export { findDocuments };
|