@abinnovision/payloadcms-mcpx 1.0.0-beta.8 → 1.0.0

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.
Files changed (60) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +331 -194
  3. package/dist/api-keys/collection.mjs +3 -3
  4. package/dist/api-keys/fields.mjs +56 -5
  5. package/dist/api-keys/setup-guide.mjs +56 -0
  6. package/dist/auth/resolve.mjs +5 -7
  7. package/dist/capabilities.mjs +23 -4
  8. package/dist/client/index.d.mts +2 -0
  9. package/dist/client/index.mjs +2 -0
  10. package/dist/client/setup-guide.d.mts +14 -0
  11. package/dist/client/setup-guide.mjs +87 -0
  12. package/dist/endpoint/{result.mjs → errors.mjs} +4 -23
  13. package/dist/endpoint/handler.mjs +11 -5
  14. package/dist/endpoint/index.mjs +4 -0
  15. package/dist/endpoint/server.mjs +18 -26
  16. package/dist/i18n.mjs +4 -15
  17. package/dist/index.d.mts +4 -4
  18. package/dist/index.mjs +4 -3
  19. package/dist/options.mjs +31 -26
  20. package/dist/plugin.mjs +1 -0
  21. package/dist/{write/draft-guard.d.mts → request.d.mts} +2 -2
  22. package/dist/request.mjs +8 -0
  23. package/dist/result.d.mts +11 -0
  24. package/dist/result.mjs +20 -0
  25. package/dist/schema/describe.mjs +3 -15
  26. package/dist/schema/index.mjs +8 -0
  27. package/dist/schema/lexical-pointer.mjs +125 -0
  28. package/dist/schema/lexical.mjs +195 -27
  29. package/dist/schema/outline.mjs +67 -0
  30. package/dist/schema/pointer.mjs +77 -30
  31. package/dist/schema/shape.mjs +133 -51
  32. package/dist/schema/walk.mjs +44 -64
  33. package/dist/tools/{index.mjs → builtin.mjs} +8 -5
  34. package/dist/tools/create-document.mjs +34 -15
  35. package/dist/tools/describe-schema.mjs +21 -7
  36. package/dist/tools/find-documents.mjs +13 -6
  37. package/dist/tools/get-document.mjs +45 -11
  38. package/dist/tools/list-capabilities.mjs +19 -9
  39. package/dist/tools/names.mjs +2 -1
  40. package/dist/tools/patch-document.mjs +32 -21
  41. package/dist/tools/publish-document.mjs +79 -0
  42. package/dist/tools/shared.mjs +84 -32
  43. package/dist/tools/target.mjs +7 -11
  44. package/dist/tools/validate-document.mjs +20 -12
  45. package/dist/types.d.mts +115 -42
  46. package/dist/types.mjs +3 -4
  47. package/dist/version.mjs +1 -1
  48. package/dist/write/draft-guard.mjs +47 -44
  49. package/dist/write/patch.mjs +174 -92
  50. package/dist/write/publish-blockers.mjs +13 -12
  51. package/dist/write/publish-intent.mjs +17 -0
  52. package/dist/write/transaction.mjs +8 -3
  53. package/package.json +24 -9
  54. package/dist/i18n.d.mts +0 -1
  55. package/dist/options.d.mts +0 -2
  56. package/dist/schema/lexical.d.mts +0 -1
  57. package/dist/schema/walk.d.mts +0 -3
  58. package/dist/tools/target.d.mts +0 -3
  59. package/dist/tools/types.d.mts +0 -5
  60. package/dist/write/publish-blockers.d.mts +0 -15
@@ -1,4 +1,4 @@
1
- import { createCapabilityFields, createKeyFields } from "./fields.mjs";
1
+ import { createCapabilityFields, createKeyFields, withSetupGuideTab } from "./fields.mjs";
2
2
  import { generateApiKey, hashApiKey } from "./key.mjs";
3
3
  //#region src/api-keys/collection.ts
4
4
  /**
@@ -36,7 +36,7 @@ import { generateApiKey, hashApiKey } from "./key.mjs";
36
36
  delete: ownKeysOnly
37
37
  },
38
38
  hooks: { beforeChange: [keyBeforeChange] },
39
- fields: [
39
+ fields: withSetupGuideTab([
40
40
  {
41
41
  name: "user",
42
42
  type: "relationship",
@@ -51,7 +51,7 @@ import { generateApiKey, hashApiKey } from "./key.mjs";
51
51
  },
52
52
  ...createKeyFields(),
53
53
  ...createCapabilityFields(options)
54
- ]
54
+ ], options)
55
55
  };
56
56
  };
57
57
  //#endregion
@@ -1,4 +1,4 @@
1
- import { CAPABILITIES_FIELD } from "../capabilities.mjs";
1
+ import { CAPABILITIES_FIELD, canCreate, canPublish, canWrite } from "../capabilities.mjs";
2
2
  //#region src/api-keys/fields.ts
3
3
  const encryptKey = ({ req, value }) => typeof value === "string" ? req.payload.encrypt(value) : value;
4
4
  const decryptKey = ({ req, value }) => {
@@ -15,6 +15,7 @@ const checkbox = (name, description) => ({
15
15
  defaultValue: false,
16
16
  admin: { description }
17
17
  });
18
+ /** Name of the `ui` field the "Connect a client" tab renders. */ const SETUP_GUIDE_FIELD = "setupGuide";
18
19
  /**
19
20
  * Fields every key carries. Key generation and the HMAC index live in the
20
21
  * collection-level `beforeChange` hook (see `collection.ts`), because sibling
@@ -56,24 +57,74 @@ const checkbox = (name, description) => ({
56
57
  }
57
58
  ];
58
59
  /**
60
+ * Wraps the key fields and the setup guide in unnamed tabs, so the wide
61
+ * snippets get the full form width without pushing the key itself out of view.
62
+ * Unnamed on purpose: named tabs would nest the data and move `capabilities`
63
+ * off the document root, which capability resolution reads.
64
+ *
65
+ * The guide tab is conditioned on the update operation. On create there is no
66
+ * key to hand out, and a tab leading to an empty panel is worse than no tab.
67
+ */ const withSetupGuideTab = (keyFields, options) => {
68
+ if (!options.setupGuide) return keyFields;
69
+ return [{
70
+ type: "tabs",
71
+ tabs: [{
72
+ label: "Key",
73
+ fields: keyFields
74
+ }, {
75
+ label: "Connect a client",
76
+ admin: { condition: (_data, _siblingData, { operation }) => operation === "update" },
77
+ fields: [{
78
+ name: SETUP_GUIDE_FIELD,
79
+ type: "ui",
80
+ admin: {
81
+ disableListColumn: true,
82
+ components: { Field: {
83
+ path: "@abinnovision/payloadcms-mcpx/client",
84
+ exportName: "McpxSetupGuide",
85
+ clientProps: { endpointPath: options.endpointPath }
86
+ } }
87
+ }
88
+ }]
89
+ }]
90
+ }];
91
+ };
92
+ const PUBLISH_DESCRIPTION = "Publish the current draft. Changes what the public sees.";
93
+ /**
59
94
  * One checkbox per exposed operation, grouped per collection, per global and
60
95
  * per custom tool. Only operations the plugin config exposes get a checkbox, so
61
96
  * a key can never enable more than the config allows. Everything defaults to
62
97
  * off, which is why a key issued before a capability existed stays closed to it.
98
+ *
99
+ * An entity without versions gets no `publish` checkbox even under
100
+ * `write: "live"`: there is no draft to promote there, the write itself is the
101
+ * live change, and a second checkbox would only make `write` a dead setting.
102
+ *
103
+ * An upload collection gets the same checkboxes as any other, only worded for
104
+ * what `write` reaches there: a document's own fields, never `createDocument`,
105
+ * because the file comes from the admin panel.
63
106
  */ const createCapabilityFields = (options) => {
64
107
  const collectionGroups = options.collections.map((collection) => ({
65
108
  name: collection.fieldName,
66
109
  type: "group",
67
110
  label: collection.slug,
68
- fields: [...collection.read ? [checkbox("read", "Describe, find and read documents.")] : [], ...collection.write ? [checkbox("write", "Create, patch and validate drafts.")] : []]
111
+ fields: [
112
+ ...collection.read ? [checkbox("read", "Describe, find and read documents.")] : [],
113
+ ...canWrite(collection) ? [checkbox("write", canCreate(collection) ? "Create, patch and validate drafts." : "Patch and validate drafts. The file itself is uploaded in the admin panel.")] : [],
114
+ ...canPublish(collection) ? [checkbox("publish", PUBLISH_DESCRIPTION)] : []
115
+ ]
69
116
  }));
70
117
  const globalGroups = options.globals.map((global) => ({
71
118
  name: global.fieldName,
72
119
  type: "group",
73
120
  label: global.slug,
74
- fields: [...global.read ? [checkbox("read", "Describe and read this global.")] : [], ...global.write ? [checkbox("write", "Patch and validate this global's draft.")] : []]
121
+ fields: [
122
+ ...global.read ? [checkbox("read", "Describe and read this global.")] : [],
123
+ ...canWrite(global) ? [checkbox("write", "Patch and validate this global's draft.")] : [],
124
+ ...canPublish(global) ? [checkbox("publish", PUBLISH_DESCRIPTION)] : []
125
+ ]
75
126
  }));
76
- const toolCheckboxes = options.tools.map((tool) => checkbox(tool.name, tool.description));
127
+ const toolCheckboxes = options.tools.map((tool) => checkbox(tool.name, typeof tool.description === "string" ? tool.description : tool.name));
77
128
  const groups = [
78
129
  ...collectionGroups.length > 0 ? [{
79
130
  name: "collections",
@@ -100,4 +151,4 @@ const checkbox = (name, description) => ({
100
151
  }];
101
152
  };
102
153
  //#endregion
103
- export { createCapabilityFields, createKeyFields };
154
+ export { SETUP_GUIDE_FIELD, createCapabilityFields, createKeyFields, withSetupGuideTab };
@@ -0,0 +1,56 @@
1
+ //#region src/api-keys/setup-guide.ts
2
+ /**
3
+ * Stands in for the key in the snippets whenever the real one is unavailable,
4
+ * so the instructions still render and say what is missing.
5
+ */ const KEY_PLACEHOLDER = "<your-key>";
6
+ /**
7
+ * Server name for the client config. MCP clients key their config by this, so
8
+ * it has to survive labels with spaces or punctuation.
9
+ */ const toServerName = (label) => {
10
+ const slug = (label ?? "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
11
+ return slug === "" ? "payload" : slug;
12
+ };
13
+ /**
14
+ * A pure builder, so the admin component holds only rendering and the snippets
15
+ * stay unit-testable.
16
+ */ const buildSetupGuide = (input) => {
17
+ const key = typeof input.apiKey === "string" ? input.apiKey : KEY_PLACEHOLDER;
18
+ const name = toServerName(input.label);
19
+ const url = input.endpointUrl;
20
+ return [
21
+ {
22
+ id: "endpoint",
23
+ title: "Endpoint",
24
+ description: "Streamable HTTP. Point any MCP client at this URL.",
25
+ snippet: url
26
+ },
27
+ {
28
+ id: "header",
29
+ title: "Authorization header",
30
+ description: "The only accepted credential; cookies and JWTs are ignored.",
31
+ snippet: `Authorization: Bearer ${key}`
32
+ },
33
+ {
34
+ id: "claude-code",
35
+ title: "Claude Code",
36
+ snippet: [`claude mcp add --transport http ${name} ${url} \\`, ` --header "Authorization: Bearer ${key}"`].join("\n")
37
+ },
38
+ {
39
+ id: "claude-desktop",
40
+ title: "Claude Desktop",
41
+ description: "Has no direct header support, so it goes through mcp-remote.",
42
+ snippet: JSON.stringify({ mcpServers: { [name]: {
43
+ command: "npx",
44
+ args: [
45
+ "-y",
46
+ "mcp-remote",
47
+ url,
48
+ "--header",
49
+ `Authorization: Bearer ${key}`
50
+ ]
51
+ } } }, null, 2)
52
+ }
53
+ ];
54
+ };
55
+ //#endregion
56
+ export { KEY_PLACEHOLDER, buildSetupGuide, toServerName };
@@ -1,17 +1,15 @@
1
1
  import { hashApiKey } from "../api-keys/key.mjs";
2
2
  //#region src/auth/resolve.ts
3
3
  const BEARER = /^Bearer\s+(\S+)\s*$/i;
4
- /**
5
- * The bearer token of an `Authorization` header, or `null`.
6
- */ const parseBearer = (headers) => {
7
- const header = headers.get("authorization");
8
- if (!header) return null;
9
- return BEARER.exec(header.trim())?.[1] ?? null;
10
- };
11
4
  const relationId = (value) => {
12
5
  if (typeof value === "string" || typeof value === "number") return value;
13
6
  if (typeof value === "object" && value !== null && "id" in value) return value.id;
14
7
  };
8
+ const parseBearer = (headers) => {
9
+ const header = headers.get("authorization");
10
+ if (!header) return null;
11
+ return BEARER.exec(header.trim())?.[1] ?? null;
12
+ };
15
13
  /**
16
14
  * Resolves the bearer key of a request to the user it acts as.
17
15
  *
@@ -1,8 +1,23 @@
1
1
  //#region src/capabilities.ts
2
- /** Name of the capability group on the key document. */ const CAPABILITIES_FIELD = "capabilities";
2
+ /** Group field holding the capability checkboxes on an API key document. */ const CAPABILITIES_FIELD = "capabilities";
3
+ /** Whatever the write lands on; {@link isLiveWrite} tells the two apart. */ const canWrite = (entity) => entity.write !== false;
4
+ /** The config lets MCP change live content and there is a draft to promote. */ const canPublish = (entity) => entity.write === "live" && entity.hasDrafts;
5
+ /**
6
+ * An upload document is a file, and no tool here carries one. Its own fields
7
+ * stay patchable; the first version is made in the admin panel.
8
+ */ const canCreate = (entity) => canWrite(entity) && !entity.isUpload;
9
+ /**
10
+ * With no versions there is no draft to land on, so `write: "live"` permits the
11
+ * write at all and every write is live.
12
+ */ const isLiveWrite = (entity) => entity.write === "live" && !entity.hasDrafts;
3
13
  const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
4
14
  const flag = (group, name) => isRecord(group) && group[name] === true;
5
15
  /**
16
+ * Publishing is an extension of writing, never a capability of its own: a key
17
+ * that may publish may also edit the draft it publishes. Both checkboxes are
18
+ * therefore required, on top of the config exposing publishing at all.
19
+ */ const publishFlag = (entity, group) => canPublish(entity) && flag(group, "write") && flag(group, "publish");
20
+ /**
6
21
  * Capabilities in force for a key: the plugin config decides what can exist,
7
22
  * the key's checkboxes decide what does. A missing checkbox is `false`, so keys
8
23
  * issued before a capability existed stay closed.
@@ -15,7 +30,8 @@ const flag = (group, name) => isRecord(group) && group[name] === true;
15
30
  const group = isRecord(collectionsGroup) ? collectionsGroup[collection.fieldName] : void 0;
16
31
  collections[collection.slug] = {
17
32
  read: collection.read && flag(group, "read"),
18
- write: collection.write && flag(group, "write")
33
+ write: canWrite(collection) && flag(group, "write"),
34
+ publish: publishFlag(collection, group)
19
35
  };
20
36
  }
21
37
  const globals = {};
@@ -23,7 +39,8 @@ const flag = (group, name) => isRecord(group) && group[name] === true;
23
39
  const group = isRecord(globalsGroup) ? globalsGroup[global.fieldName] : void 0;
24
40
  globals[global.slug] = {
25
41
  read: global.read && flag(group, "read"),
26
- write: global.write && flag(group, "write")
42
+ write: canWrite(global) && flag(group, "write"),
43
+ publish: publishFlag(global, group)
27
44
  };
28
45
  }
29
46
  const tools = {};
@@ -37,7 +54,9 @@ const flag = (group, name) => isRecord(group) && group[name] === true;
37
54
  const pick = (entries, operation) => Object.entries(entries).filter(([, value]) => value[operation]).map(([slug]) => slug);
38
55
  const readableSlugs = (capabilities) => pick(capabilities.collections, "read");
39
56
  const writableSlugs = (capabilities) => pick(capabilities.collections, "write");
57
+ const publishableSlugs = (capabilities) => pick(capabilities.collections, "publish");
40
58
  const readableGlobalSlugs = (capabilities) => pick(capabilities.globals, "read");
41
59
  const writableGlobalSlugs = (capabilities) => pick(capabilities.globals, "write");
60
+ const publishableGlobalSlugs = (capabilities) => pick(capabilities.globals, "publish");
42
61
  //#endregion
43
- export { CAPABILITIES_FIELD, readableGlobalSlugs, readableSlugs, resolveCapabilities, writableGlobalSlugs, writableSlugs };
62
+ export { CAPABILITIES_FIELD, canCreate, canPublish, canWrite, isLiveWrite, publishableGlobalSlugs, publishableSlugs, readableGlobalSlugs, readableSlugs, resolveCapabilities, writableGlobalSlugs, writableSlugs };
@@ -0,0 +1,2 @@
1
+ import { McpxSetupGuide } from "./setup-guide.mjs";
2
+ export { McpxSetupGuide };
@@ -0,0 +1,2 @@
1
+ import { McpxSetupGuide } from "./setup-guide.mjs";
2
+ export { McpxSetupGuide };
@@ -0,0 +1,14 @@
1
+ import React from "react";
2
+ //#region src/client/setup-guide.d.ts
3
+ interface McpxSetupGuideProps {
4
+ /** Endpoint path below the API route, from the plugin options. */
5
+ endpointPath: string;
6
+ }
7
+ /**
8
+ * Per-key connection instructions on the API key edit view. Renders nothing
9
+ * until the document is saved, because before that there is no key to hand to
10
+ * a client.
11
+ */
12
+ declare const McpxSetupGuide: React.FC<McpxSetupGuideProps>;
13
+ //#endregion
14
+ export { McpxSetupGuide };
@@ -0,0 +1,87 @@
1
+ "use client";
2
+ import { buildSetupGuide } from "../api-keys/setup-guide.mjs";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
+ import { CopyToClipboard, useConfig, useDocumentInfo, useFormFields } from "@payloadcms/ui";
5
+ import { useEffect, useState } from "react";
6
+ //#region src/client/setup-guide.tsx
7
+ const asString = (value) => typeof value === "string" ? value : void 0;
8
+ /**
9
+ * Reads `serverURL` when the config sets one and falls back to the browser's
10
+ * origin. The fallback has to wait for mount: this component is server-rendered
11
+ * first, where `window` does not exist.
12
+ */ const useOrigin = (serverUrl) => {
13
+ const [origin, setOrigin] = useState(serverUrl);
14
+ useEffect(() => {
15
+ if (serverUrl === "") setOrigin(window.location.origin);
16
+ }, [serverUrl]);
17
+ return origin;
18
+ };
19
+ /**
20
+ * Payload's own theme variables, so the panel follows the admin's light and
21
+ * dark themes without shipping a stylesheet consumers would have to transpile.
22
+ */ const styles = {
23
+ lead: { marginBottom: "calc(var(--base) * 0.75)" },
24
+ section: { marginBottom: "calc(var(--base) * 0.75)" },
25
+ sectionHeader: {
26
+ display: "flex",
27
+ alignItems: "center",
28
+ gap: "calc(var(--base) * 0.25)"
29
+ },
30
+ description: {
31
+ margin: "calc(var(--base) * 0.15) 0",
32
+ color: "var(--theme-elevation-500)"
33
+ },
34
+ snippet: {
35
+ margin: 0,
36
+ padding: "calc(var(--base) * 0.4)",
37
+ background: "var(--theme-elevation-50)",
38
+ border: "1px solid var(--theme-elevation-150)",
39
+ borderRadius: "3px",
40
+ fontFamily: "var(--font-mono)",
41
+ overflowX: "auto",
42
+ whiteSpace: "pre-wrap",
43
+ wordBreak: "break-all"
44
+ }
45
+ };
46
+ /**
47
+ * Per-key connection instructions on the API key edit view. Renders nothing
48
+ * until the document is saved, because before that there is no key to hand to
49
+ * a client.
50
+ */ const McpxSetupGuide = ({ endpointPath }) => {
51
+ const { id } = useDocumentInfo();
52
+ const { config } = useConfig();
53
+ const apiKey = useFormFields(([fields]) => fields["apiKey"]?.value);
54
+ const label = useFormFields(([fields]) => fields["label"]?.value);
55
+ const origin = useOrigin(config.serverURL);
56
+ if (id === void 0) return null;
57
+ const sections = buildSetupGuide({
58
+ endpointUrl: `${origin.replace(/\/+$/, "")}${config.routes.api}${endpointPath}`,
59
+ apiKey: asString(apiKey),
60
+ label: asString(label)
61
+ });
62
+ return /*#__PURE__*/ jsxs("div", {
63
+ className: "field-type",
64
+ children: [/*#__PURE__*/ jsx("p", {
65
+ style: styles.lead,
66
+ children: "Every snippet below contains this key in full. Treat it like a password."
67
+ }), sections.map((section) => /*#__PURE__*/ jsxs("section", {
68
+ style: styles.section,
69
+ children: [
70
+ /*#__PURE__*/ jsxs("div", {
71
+ style: styles.sectionHeader,
72
+ children: [/*#__PURE__*/ jsx("strong", { children: section.title }), /*#__PURE__*/ jsx(CopyToClipboard, { value: section.snippet })]
73
+ }),
74
+ section.description ? /*#__PURE__*/ jsx("p", {
75
+ style: styles.description,
76
+ children: section.description
77
+ }) : null,
78
+ /*#__PURE__*/ jsx("pre", {
79
+ style: styles.snippet,
80
+ children: section.snippet
81
+ })
82
+ ]
83
+ }, section.id))]
84
+ });
85
+ };
86
+ //#endregion
87
+ export { McpxSetupGuide };
@@ -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/result.ts
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,27 +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
- * Maps an exception thrown by a tool to a result the client can read.
43
- *
44
25
  * Payload's public errors keep their message and status; a `ValidationError`
45
26
  * also surfaces its per-field detail, with each field's path restated as a
46
27
  * JSON Pointer so it reads like every other path this plugin reports. Anything
@@ -62,4 +43,4 @@ import { APIError, ValidationError } from "payload";
62
43
  return errorResult("Internal error");
63
44
  };
64
45
  //#endregion
65
- export { errorResult, jsonResult, jsonRpcError, toToolError };
46
+ export { jsonRpcError, toToolError };
@@ -1,6 +1,6 @@
1
- import { readableGlobalSlugs, readableSlugs, resolveCapabilities, writableGlobalSlugs, writableSlugs } from "../capabilities.mjs";
1
+ import { publishableGlobalSlugs, publishableSlugs, 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,20 @@ 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),
14
+ publishable: publishableSlugs(capabilities),
15
15
  readableGlobals: readableGlobalSlugs(capabilities),
16
16
  writableGlobals: writableGlobalSlugs(capabilities),
17
+ publishableGlobals: publishableGlobalSlugs(capabilities),
17
18
  locales: localization ? localization.localeCodes : null,
18
- defaultLocale: localization ? localization.defaultLocale : null
19
+ defaultLocale: localization ? localization.defaultLocale : null,
20
+ limits: options.limits,
21
+ exposure: {
22
+ collections: options.collections,
23
+ globals: options.globals
24
+ }
19
25
  };
20
26
  };
21
27
  /**
@@ -68,7 +74,7 @@ const buildScope = (req, options, capabilities) => {
68
74
  code: -32600,
69
75
  message: "Invalid request: a JSON body is required."
70
76
  });
71
- const server = createMcpServer(buildScope(req, options, capabilities));
77
+ const server = createMcpServer(buildScope(req, options, capabilities), options);
72
78
  const transport = new WebStandardStreamableHTTPServerTransport({ enableJsonResponse: true });
73
79
  await server.connect(transport);
74
80
  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 };
@@ -1,24 +1,23 @@
1
- import { toToolError } from "./result.mjs";
2
- import { BUILTIN_TOOLS } from "../tools/index.mjs";
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
7
+ /** Strict, so an unknown argument is rejected by name rather than stripped. */ const toolInputSchema = (tool, scope) => z.strictObject(typeof tool.inputSchema === "function" ? tool.inputSchema(scope) : tool.inputSchema ?? {});
8
+ /** May be built from the scope, to name the targets this key writes live. */ const toolDescription = (tool, scope) => typeof tool.description === "function" ? tool.description(scope) : tool.description;
9
+ /** A tool that does not decide for itself is gated by its own checkbox. */ const isToolEnabled = (tool, scope) => tool.isEnabled ? tool.isEnabled(scope) : scope.capabilities.tools[tool.name] === true;
6
10
  /**
7
- * Builds a builtin tool's input schema as a strict object, so an unknown
8
- * argument is rejected with its name instead of being silently stripped and
9
- * the tool answering as if it had not been passed.
10
- */ const builtinInputSchema = (tool, scope) => z.strictObject(tool.inputSchema(scope));
11
- /**
12
- * Builds the MCP server for one request. Tools are registered against the
13
- * key's capabilities, so `tools/list` shows exactly what the key may call and
14
- * every `collection` enum is limited to what it may touch.
15
- */ const createMcpServer = (scope) => {
16
- const { req, options, capabilities } = scope;
11
+ * One server per request. Builtin and configured tools take the same route,
12
+ * each registered against the key's capabilities, so `tools/list` shows exactly
13
+ * what the key may call.
14
+ */ const createMcpServer = (scope, options) => {
15
+ const { req } = scope;
17
16
  const { logger } = req.payload;
18
17
  const server = new McpServer({
19
18
  name: options.serverInfo.name,
20
19
  version: options.serverInfo.version
21
- }, { instructions: "Start with listCapabilities, then describeSchema for the collection or global you work on. Writes always land as drafts; a human publishes." });
20
+ }, { instructions: `Start with listCapabilities, then describeSchema for the collection or global you work on. ${draftSentence(scope)}` });
22
21
  const guarded = (run) => async () => {
23
22
  try {
24
23
  return await run();
@@ -26,22 +25,15 @@ import { z } from "zod";
26
25
  return toToolError(error, logger);
27
26
  }
28
27
  };
29
- for (const tool of BUILTIN_TOOLS) {
30
- if (!tool.isEnabled(scope)) continue;
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;
28
+ for (const tool of [...BUILTIN_TOOLS, ...options.tools]) {
29
+ if (!isToolEnabled(tool, scope)) continue;
39
30
  server.registerTool(tool.name, {
40
- description: tool.description,
41
- inputSchema: tool.inputSchema ?? {},
31
+ description: toolDescription(tool, scope),
32
+ inputSchema: toolInputSchema(tool, scope),
42
33
  ...tool.annotations ? { annotations: tool.annotations } : {}
43
34
  }, (args, extra) => guarded(() => tool.handler({
44
35
  args,
36
+ scope,
45
37
  req,
46
38
  extra
47
39
  }))());
@@ -49,4 +41,4 @@ import { z } from "zod";
49
41
  return server;
50
42
  };
51
43
  //#endregion
52
- export { builtinInputSchema, createMcpServer };
44
+ export { createMcpServer, isToolEnabled, toolDescription, toolInputSchema };
package/dist/i18n.mjs CHANGED
@@ -1,11 +1,6 @@
1
1
  //#region src/i18n.ts
2
- /**
3
- * A locale-keyed record, once it is known to hold nothing but strings.
4
- */ const stringRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && Object.values(value).every((entry) => typeof entry === "string") ? value : void 0;
5
- /**
6
- * Picks the entry a language addresses, treating an empty value as absent so
7
- * the chain continues rather than yielding a useless string.
8
- */ const pick = (record, language) => {
2
+ const stringRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && Object.values(value).every((entry) => typeof entry === "string") ? value : void 0;
3
+ /** Treats an empty value as absent, so the fallback chain continues. */ const pick = (record, language) => {
9
4
  for (const code of Array.isArray(language) ? language : [language]) {
10
5
  const entry = record[code];
11
6
  if (entry !== void 0 && entry.trim() !== "") return entry;
@@ -25,14 +20,8 @@
25
20
  if (!record) return;
26
21
  return pick(record, language.language) ?? pick(record, language.fallbackLanguage) ?? Object.values(record).find((entry) => entry.trim() !== "");
27
22
  };
28
- /**
29
- * Binds {@link translateStatic} to a request's language, so a walk that
30
- * resolves many descriptions carries no request of its own.
31
- */ const translatorFor = (i18n) => (value) => translateStatic(value, i18n);
32
- /**
33
- * Translator for callers with no request in hand. Both language keys miss, so
34
- * the chain degrades to the record's first entry.
35
- */ const translateAny = translatorFor({
23
+ /** Bound to one request's language, so a walk carries no request of its own. */ const translatorFor = (i18n) => (value) => translateStatic(value, i18n);
24
+ /** For callers with no request: both keys miss, so the first entry wins. */ const translateAny = translatorFor({
36
25
  fallbackLanguage: "",
37
26
  language: ""
38
27
  });
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, McpxWriteMode, PublishBlocker, defineMcpxTool } from "./types.mjs";
2
2
  import { mcpxPlugin } from "./plugin.mjs";
3
- import { isMcpxRequest } from "./write/draft-guard.mjs";
4
- import { PublishBlocker } from "./write/publish-blockers.mjs";
5
- export { type McpxAuthResult, type McpxCollectionCapabilities, type McpxCollectionOptions, type McpxGlobalOptions, type McpxPluginOptions, type McpxRequestContext, type McpxResolvedCapabilities, type McpxTool, type McpxToolExtra, type PublishBlocker, defineMcpxTool, isMcpxRequest, mcpxPlugin };
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, McpxWriteMode, PublishBlocker, defineMcpxTool, errorResult, isMcpxRequest, jsonResult, mcpxPlugin };
package/dist/index.mjs CHANGED
@@ -1,4 +1,5 @@
1
- import { isMcpxRequest } from "./write/draft-guard.mjs";
2
- import { mcpxPlugin } from "./plugin.mjs";
1
+ import { errorResult, jsonResult } from "./result.mjs";
3
2
  import { defineMcpxTool } from "./types.mjs";
4
- export { defineMcpxTool, isMcpxRequest, mcpxPlugin };
3
+ import { isMcpxRequest } from "./request.mjs";
4
+ import { mcpxPlugin } from "./plugin.mjs";
5
+ export { defineMcpxTool, errorResult, isMcpxRequest, jsonResult, mcpxPlugin };