payload-mcp-toolkit 0.7.5 → 0.8.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.
package/dist/api-keys.js CHANGED
@@ -18,18 +18,18 @@ const PRESET_OPTIONS = [
18
18
  }
19
19
  ];
20
20
  const isCustomPreset = (data)=>!!data && typeof data === 'object' && data.preset === 'custom';
21
- /**
22
- * Builds the `payload-mcp-api-keys` collection used by the v0.4 standalone
23
- * plugin. Reuses Payload's built-in `useAPIKey: true` so the underlying
24
- * `apiKey` / `apiKeyIndex` columns match what `@payloadcms/plugin-mcp`
25
- * v0.3.x wrote — existing rows authenticate without re-issue.
26
- *
27
- * Layout:
28
- * - Main column: name, description, preset, scopes matrix (custom only),
29
- * tools collapsible (custom only).
30
- * - Sidebar: user relationship, key prefix, expiresAt, revokedAt,
31
- * lastUsedAt — identity + lifecycle metadata kept out of the
32
- * scope-editing flow.
21
+ /**
22
+ * Builds the `payload-mcp-api-keys` collection used by the v0.4 standalone
23
+ * plugin. Reuses Payload's built-in `useAPIKey: true` so the underlying
24
+ * `apiKey` / `apiKeyIndex` columns match what `@payloadcms/plugin-mcp`
25
+ * v0.3.x wrote — existing rows authenticate without re-issue.
26
+ *
27
+ * Layout:
28
+ * - Main column: name, description, preset, scopes matrix (custom only),
29
+ * tools collapsible (custom only).
30
+ * - Sidebar: user relationship, key prefix, expiresAt, revokedAt,
31
+ * lastUsedAt — identity + lifecycle metadata kept out of the
32
+ * scope-editing flow.
33
33
  */ export function createApiKeysCollection(options) {
34
34
  if (!options || !options.userCollection) {
35
35
  throw new Error('createApiKeysCollection: `userCollection` is required (slug of the user collection that owns API keys).');
@@ -183,7 +183,11 @@ const isCustomPreset = (data)=>!!data && typeof data === 'object' && data.preset
183
183
  // an explicit null/empty in `data` over originalDoc.
184
184
  const readField = (key)=>key in d ? d[key] : orig[key];
185
185
  const isNonEmptyArray = (v)=>Array.isArray(v) && v.length > 0;
186
- const isEmptyArray = (v)=>Array.isArray(v) && v.length === 0;
186
+ // Null, undefined, or `[]` — the three shapes that mean "the user
187
+ // expressed no tool restriction". Anything else (a bare string, a
188
+ // number) is malformed and must reach Payload's validator rather than
189
+ // being silently read as "no restriction".
190
+ const isUnset = (v)=>v === null || v === undefined || Array.isArray(v) && v.length === 0;
187
191
  const OVERRIDE_AXES = [
188
192
  'collectionScopes',
189
193
  'globalScopes',
@@ -203,14 +207,18 @@ const isCustomPreset = (data)=>!!data && typeof data === 'object' && data.preset
203
207
  // store `toolAllow:[]`, which `composeScopes` honours as deny-all on
204
208
  // the tools axis and rejects every call.
205
209
  //
206
- // Resolve the mismatch by coercing an empty `toolAllow` to null
207
- // whenever the key carries any concrete resource scope (collection
208
- // or global entries). The fresh-Custom-key sentinel in
209
- // `composeScopes` still covers the "no scopes at all" case
210
- // (everything null → deny-all), so users who genuinely want
211
- // deny-all do not regress.
210
+ // Resolve the mismatch by coercing a non-populated `toolAllow` to
211
+ // null whenever the key carries any concrete resource scope
212
+ // (collection or global entries). "Non-populated" covers both `[]`
213
+ // (what the admin form sends) and a missing key (what a key created
214
+ // through the Local API sends) — Payload reads an unset hasMany
215
+ // select back as `[]` either way, so without this a scripted key
216
+ // with collection scopes and no tool list would deny every tool.
217
+ // The fresh-Custom-key sentinel in `composeScopes` still covers the
218
+ // "no scopes at all" case (everything null → deny-all), so users who
219
+ // genuinely want deny-all do not regress.
212
220
  const hasResourceScope = isNonEmptyArray(readField('collectionScopes')) || isNonEmptyArray(readField('globalScopes'));
213
- if (hasResourceScope && isEmptyArray(readField('toolAllow'))) {
221
+ if (hasResourceScope && isUnset(readField('toolAllow'))) {
214
222
  d.toolAllow = null;
215
223
  }
216
224
  return data;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/api-keys.ts"],"sourcesContent":["import type { CollectionBeforeValidateHook, CollectionConfig, Field } from 'payload'\r\n\r\nexport const API_KEYS_DEFAULT_SLUG = 'payload-mcp-api-keys'\r\n\r\nexport interface CreateApiKeysCollectionOptions {\r\n /**\r\n * Collection slug. Defaults to `payload-mcp-api-keys` for zero-touch\r\n * compatibility with rows created by `@payloadcms/plugin-mcp` v0.3.x.\r\n */\r\n slug?: string\r\n /**\r\n * Slug of the user collection that API keys link to. Required.\r\n */\r\n userCollection: string\r\n /**\r\n * Collection slugs offered to the collection-scopes matrix component.\r\n * Snapshotted at plugin-init time from the host Payload config; adding a\r\n * collection requires a dev-server restart for it to surface in the\r\n * admin UI.\r\n */\r\n availableCollections: string[]\r\n /**\r\n * Tool names offered as options for the `toolAllow` / `toolDeny` selects.\r\n * Sourced from the toolkit's registered tools at plugin init.\r\n */\r\n availableTools: string[]\r\n /**\r\n * Global slugs offered to the global-scopes matrix component. Optional —\r\n * direct callers of the factory that pre-date globals support continue\r\n * to work; sites with no globals get an empty array and the second\r\n * matrix table is not rendered.\r\n */\r\n availableGlobals?: string[]\r\n}\r\n\r\nconst PRESET_OPTIONS = [\r\n { label: 'Read-only', value: 'read-only' },\r\n { label: 'Editor (read + create + update)', value: 'editor' },\r\n { label: 'Admin (all actions)', value: 'admin' },\r\n { label: 'Custom (use overrides below)', value: 'custom' },\r\n] as const\r\n\r\nconst isCustomPreset = (data: unknown): boolean =>\r\n !!data && typeof data === 'object' && (data as { preset?: unknown }).preset === 'custom'\r\n\r\n/**\r\n * Builds the `payload-mcp-api-keys` collection used by the v0.4 standalone\r\n * plugin. Reuses Payload's built-in `useAPIKey: true` so the underlying\r\n * `apiKey` / `apiKeyIndex` columns match what `@payloadcms/plugin-mcp`\r\n * v0.3.x wrote — existing rows authenticate without re-issue.\r\n *\r\n * Layout:\r\n * - Main column: name, description, preset, scopes matrix (custom only),\r\n * tools collapsible (custom only).\r\n * - Sidebar: user relationship, key prefix, expiresAt, revokedAt,\r\n * lastUsedAt — identity + lifecycle metadata kept out of the\r\n * scope-editing flow.\r\n */\r\nexport function createApiKeysCollection(\r\n options: CreateApiKeysCollectionOptions,\r\n): CollectionConfig {\r\n if (!options || !options.userCollection) {\r\n throw new Error(\r\n 'createApiKeysCollection: `userCollection` is required (slug of the user collection that owns API keys).',\r\n )\r\n }\r\n if (!Array.isArray(options.availableCollections)) {\r\n throw new Error(\r\n 'createApiKeysCollection: `availableCollections` is required (slugs of collections that scope overrides may target).',\r\n )\r\n }\r\n if (!Array.isArray(options.availableTools)) {\r\n throw new Error(\r\n 'createApiKeysCollection: `availableTools` is required (names of registered MCP tools).',\r\n )\r\n }\r\n\r\n const slug = options.slug ?? API_KEYS_DEFAULT_SLUG\r\n const toolOptions = options.availableTools.map((t) => ({ label: t, value: t }))\r\n const availableGlobals = Array.isArray(options.availableGlobals)\r\n ? options.availableGlobals\r\n : []\r\n\r\n const presetField: Field = {\r\n name: 'preset',\r\n type: 'select',\r\n required: true,\r\n defaultValue: 'custom',\r\n options: PRESET_OPTIONS as unknown as { label: string; value: string }[],\r\n admin: {\r\n description:\r\n 'Role preset. \"Custom\" unlocks the per-collection matrix and the tool overrides below. ' +\r\n 'Switching away from Custom CLEARS every override on save (collectionScopes, globalScopes, ' +\r\n 'toolAllow, toolDeny); switching back to Custom starts from a fresh deny-all baseline, so ' +\r\n 'reconfigure the matrices and tool lists before saving.',\r\n },\r\n }\r\n\r\n // Stored shape: Array<{ slug: string; actions: ('read'|'create'|'update'|'delete')[] }>\r\n // The default Payload UI for an `array` would force users to add rows\r\n // one at a time; the custom matrix component renders all available\r\n // collections at once with a checkbox grid (rows × actions).\r\n //\r\n // `availableCollections` is forwarded via `clientProps` — Payload v3's\r\n // sanctioned escape hatch for serializable static data that the client\r\n // component needs at render time.\r\n const collectionScopesField: Field = {\r\n name: 'collectionScopes',\r\n type: 'json',\r\n admin: {\r\n condition: isCustomPreset,\r\n components: {\r\n Field: {\r\n path: 'payload-mcp-toolkit/client',\r\n exportName: 'CollectionScopesMatrix',\r\n clientProps: {\r\n availableCollections: options.availableCollections,\r\n },\r\n },\r\n },\r\n },\r\n }\r\n\r\n // Mirrors `collectionScopes` exactly — one additive JSONB column with a\r\n // default of `'[]'`, default-rendered by `GlobalScopesMatrix`. Hidden\r\n // under non-custom presets. Stored shape:\r\n // Array<{ slug: string; actions: ('read'|'update')[] }>\r\n // No `availableGlobals.length > 0` gate: `ScopesTable` renders its own\r\n // empty-state message when zero items are passed, so the field surfaces\r\n // under Custom regardless of host config, matching the collection variant.\r\n const globalScopesField: Field = {\r\n name: 'globalScopes',\r\n type: 'json',\r\n admin: {\r\n condition: isCustomPreset,\r\n components: {\r\n Field: {\r\n path: 'payload-mcp-toolkit/client',\r\n exportName: 'GlobalScopesMatrix',\r\n clientProps: {\r\n availableGlobals,\r\n },\r\n },\r\n },\r\n },\r\n }\r\n\r\n const toolsCollapsible: Field = {\r\n type: 'collapsible',\r\n label: 'Tool overrides',\r\n admin: {\r\n condition: isCustomPreset,\r\n description:\r\n 'Per-tool whitelist / blacklist. Layered on top of preset and collection scopes.',\r\n initCollapsed: true,\r\n },\r\n fields: [\r\n {\r\n name: 'toolAllow',\r\n type: 'select',\r\n hasMany: true,\r\n options: toolOptions,\r\n admin: {\r\n description:\r\n 'If set, only these tools are callable with this key. Leave empty to allow any tool ' +\r\n 'the collection or global scopes permit. Under the Custom preset, an empty list is ' +\r\n 'treated as deny-all ONLY when no collection or global scopes are set (the fresh- ' +\r\n 'Custom-key sentinel); when collection or global scopes are populated, an empty list ' +\r\n 'collapses to \"no tool restriction\" so the resource scopes alone determine what is ' +\r\n 'callable — to deny every tool while keeping resource scopes, enumerate them in ' +\r\n 'toolDeny instead. Preset-mode keys created via the REST API with an empty list are ' +\r\n 'coerced to \"no restriction\".',\r\n },\r\n },\r\n {\r\n name: 'toolDeny',\r\n type: 'select',\r\n hasMany: true,\r\n options: toolOptions,\r\n admin: {\r\n description: 'These tools are blocked regardless of any other scope.',\r\n },\r\n },\r\n ],\r\n }\r\n\r\n return {\r\n slug,\r\n admin: {\r\n group: 'MCP',\r\n useAsTitle: 'name',\r\n description:\r\n 'API keys for MCP clients. Scopes control which collections and tools each key can access.',\r\n defaultColumns: ['name', 'user', 'keyPrefix', 'preset', 'lastUsedAt', 'expiresAt', 'revokedAt'],\r\n },\r\n auth: {\r\n disableLocalStrategy: true,\r\n useAPIKey: true,\r\n },\r\n hooks: {\r\n beforeValidate: [\r\n (({ data, originalDoc }) => {\r\n // The override fields (collectionScopes, globalScopes, toolAllow,\r\n // toolDeny) are conditionally rendered only under the Custom preset\r\n // (`condition: isCustomPreset`). Under any other preset they are\r\n // hidden in the admin UI, which means two things:\r\n //\r\n // 1. The admin form omits hidden fields from its payload on save,\r\n // so `data` only carries the visible fields — we can't\r\n // \"collapse the empty array we see in `data`\" because we never\r\n // see it at all. The stale value lives on `originalDoc`.\r\n // 2. A Custom→Admin switch silently keeps the prior\r\n // `toolAllow:[...]` / `collectionScopes:[...]`, and\r\n // `composeScopes` then emits a scope gate that rejects calls\r\n // the user clearly intended to allow.\r\n //\r\n // Fix: when the preset is non-Custom, explicitly write `null` into\r\n // `data` for every override axis (regardless of what `data` carries\r\n // or what originalDoc holds). Payload persists nulls, so the stale\r\n // values are erased on every save. The Custom-preset branch below\r\n // keeps the explicit-empty-means-deny semantic intact.\r\n if (!data) return data\r\n const d = data as Record<string, unknown>\r\n const orig = (originalDoc ?? {}) as Record<string, unknown>\r\n const preset = d.preset ?? orig.preset\r\n\r\n // `readField` falls through to originalDoc when `data` omits the\r\n // key entirely (admin form skipping hidden fields), but honours\r\n // an explicit null/empty in `data` over originalDoc.\r\n const readField = (key: string): unknown =>\r\n key in d ? d[key] : orig[key]\r\n const isNonEmptyArray = (v: unknown): boolean =>\r\n Array.isArray(v) && v.length > 0\r\n const isEmptyArray = (v: unknown): boolean =>\r\n Array.isArray(v) && v.length === 0\r\n\r\n const OVERRIDE_AXES = [\r\n 'collectionScopes',\r\n 'globalScopes',\r\n 'toolAllow',\r\n 'toolDeny',\r\n ] as const\r\n\r\n if (preset !== 'custom') {\r\n for (const axis of OVERRIDE_AXES) d[axis] = null\r\n return data\r\n }\r\n\r\n // Custom preset: the Tools collapsible is labelled as an *override*\r\n // layered on top of collection / global scopes, and its description\r\n // says \"Leave empty to allow any tool the collection scopes permit.\"\r\n // Payload's hasMany-select default of `[]` would otherwise turn the\r\n // Tools section into a mandatory whitelist — a user who configures\r\n // collection scopes and never opens the collapsible would silently\r\n // store `toolAllow:[]`, which `composeScopes` honours as deny-all on\r\n // the tools axis and rejects every call.\r\n //\r\n // Resolve the mismatch by coercing an empty `toolAllow` to null\r\n // whenever the key carries any concrete resource scope (collection\r\n // or global entries). The fresh-Custom-key sentinel in\r\n // `composeScopes` still covers the \"no scopes at all\" case\r\n // (everything null → deny-all), so users who genuinely want\r\n // deny-all do not regress.\r\n const hasResourceScope =\r\n isNonEmptyArray(readField('collectionScopes')) ||\r\n isNonEmptyArray(readField('globalScopes'))\r\n if (hasResourceScope && isEmptyArray(readField('toolAllow'))) {\r\n d.toolAllow = null\r\n }\r\n return data\r\n }) as CollectionBeforeValidateHook,\r\n ],\r\n },\r\n labels: {\r\n plural: 'API Keys',\r\n singular: 'API Key',\r\n },\r\n fields: [\r\n // Main column.\r\n {\r\n name: 'name',\r\n type: 'text',\r\n required: true,\r\n admin: { description: 'Human label for this key (e.g. \"Editorial team — Claude Desktop\").' },\r\n },\r\n {\r\n name: 'description',\r\n type: 'textarea',\r\n admin: { description: 'Optional notes about the purpose of this key.' },\r\n },\r\n presetField,\r\n collectionScopesField,\r\n globalScopesField,\r\n toolsCollapsible,\r\n\r\n // Sidebar — identity + lifecycle.\r\n {\r\n name: 'user',\r\n type: 'relationship',\r\n relationTo: options.userCollection,\r\n required: true,\r\n admin: {\r\n position: 'sidebar',\r\n description:\r\n 'The user this key authenticates as. Tool calls use this user for access checks on target collections.',\r\n },\r\n },\r\n {\r\n name: 'keyPrefix',\r\n type: 'text',\r\n index: true,\r\n admin: {\r\n position: 'sidebar',\r\n readOnly: true,\r\n description:\r\n 'First 8 characters of the API key — used in audit logs to identify the key without exposing the full secret.',\r\n },\r\n hooks: {\r\n beforeChange: [\r\n ({ data, originalDoc, value }) => {\r\n if (typeof value === 'string' && value.length > 0) return value\r\n const incomingKey = (data as { apiKey?: unknown } | undefined)?.apiKey\r\n if (typeof incomingKey === 'string' && incomingKey.length >= 8) {\r\n return incomingKey.slice(0, 8)\r\n }\r\n const existing = (originalDoc as { keyPrefix?: unknown } | undefined)?.keyPrefix\r\n return typeof existing === 'string' ? existing : undefined\r\n },\r\n ],\r\n },\r\n },\r\n {\r\n name: 'expiresAt',\r\n type: 'date',\r\n admin: {\r\n position: 'sidebar',\r\n description: 'Optional expiry. Requests authenticated with an expired key are rejected.',\r\n },\r\n },\r\n {\r\n name: 'revokedAt',\r\n type: 'date',\r\n admin: {\r\n position: 'sidebar',\r\n description: 'Set to revoke a key. Revoked keys are rejected at auth time.',\r\n },\r\n },\r\n {\r\n name: 'lastUsedAt',\r\n type: 'date',\r\n admin: {\r\n position: 'sidebar',\r\n readOnly: true,\r\n description:\r\n 'Updated on each successful authentication. Fire-and-forget; not on the request hot path.',\r\n },\r\n },\r\n ],\r\n }\r\n}\r\n"],"names":["API_KEYS_DEFAULT_SLUG","PRESET_OPTIONS","label","value","isCustomPreset","data","preset","createApiKeysCollection","options","userCollection","Error","Array","isArray","availableCollections","availableTools","slug","toolOptions","map","t","availableGlobals","presetField","name","type","required","defaultValue","admin","description","collectionScopesField","condition","components","Field","path","exportName","clientProps","globalScopesField","toolsCollapsible","initCollapsed","fields","hasMany","group","useAsTitle","defaultColumns","auth","disableLocalStrategy","useAPIKey","hooks","beforeValidate","originalDoc","d","orig","readField","key","isNonEmptyArray","v","length","isEmptyArray","OVERRIDE_AXES","axis","hasResourceScope","toolAllow","labels","plural","singular","relationTo","position","index","readOnly","beforeChange","incomingKey","apiKey","slice","existing","keyPrefix","undefined"],"mappings":"AAEA,OAAO,MAAMA,wBAAwB,uBAAsB;AAiC3D,MAAMC,iBAAiB;IACrB;QAAEC,OAAO;QAAaC,OAAO;IAAY;IACzC;QAAED,OAAO;QAAmCC,OAAO;IAAS;IAC5D;QAAED,OAAO;QAAuBC,OAAO;IAAQ;IAC/C;QAAED,OAAO;QAAgCC,OAAO;IAAS;CAC1D;AAED,MAAMC,iBAAiB,CAACC,OACtB,CAAC,CAACA,QAAQ,OAAOA,SAAS,YAAY,AAACA,KAA8BC,MAAM,KAAK;AAElF;;;;;;;;;;;;CAYC,GACD,OAAO,SAASC,wBACdC,OAAuC;IAEvC,IAAI,CAACA,WAAW,CAACA,QAAQC,cAAc,EAAE;QACvC,MAAM,IAAIC,MACR;IAEJ;IACA,IAAI,CAACC,MAAMC,OAAO,CAACJ,QAAQK,oBAAoB,GAAG;QAChD,MAAM,IAAIH,MACR;IAEJ;IACA,IAAI,CAACC,MAAMC,OAAO,CAACJ,QAAQM,cAAc,GAAG;QAC1C,MAAM,IAAIJ,MACR;IAEJ;IAEA,MAAMK,OAAOP,QAAQO,IAAI,IAAIf;IAC7B,MAAMgB,cAAcR,QAAQM,cAAc,CAACG,GAAG,CAAC,CAACC,IAAO,CAAA;YAAEhB,OAAOgB;YAAGf,OAAOe;QAAE,CAAA;IAC5E,MAAMC,mBAAmBR,MAAMC,OAAO,CAACJ,QAAQW,gBAAgB,IAC3DX,QAAQW,gBAAgB,GACxB,EAAE;IAEN,MAAMC,cAAqB;QACzBC,MAAM;QACNC,MAAM;QACNC,UAAU;QACVC,cAAc;QACdhB,SAASP;QACTwB,OAAO;YACLC,aACE,2FACA,+FACA,8FACA;QACJ;IACF;IAEA,wFAAwF;IACxF,sEAAsE;IACtE,mEAAmE;IACnE,6DAA6D;IAC7D,EAAE;IACF,uEAAuE;IACvE,uEAAuE;IACvE,kCAAkC;IAClC,MAAMC,wBAA+B;QACnCN,MAAM;QACNC,MAAM;QACNG,OAAO;YACLG,WAAWxB;YACXyB,YAAY;gBACVC,OAAO;oBACLC,MAAM;oBACNC,YAAY;oBACZC,aAAa;wBACXpB,sBAAsBL,QAAQK,oBAAoB;oBACpD;gBACF;YACF;QACF;IACF;IAEA,wEAAwE;IACxE,sEAAsE;IACtE,0CAA0C;IAC1C,0DAA0D;IAC1D,uEAAuE;IACvE,wEAAwE;IACxE,2EAA2E;IAC3E,MAAMqB,oBAA2B;QAC/Bb,MAAM;QACNC,MAAM;QACNG,OAAO;YACLG,WAAWxB;YACXyB,YAAY;gBACVC,OAAO;oBACLC,MAAM;oBACNC,YAAY;oBACZC,aAAa;wBACXd;oBACF;gBACF;YACF;QACF;IACF;IAEA,MAAMgB,mBAA0B;QAC9Bb,MAAM;QACNpB,OAAO;QACPuB,OAAO;YACLG,WAAWxB;YACXsB,aACE;YACFU,eAAe;QACjB;QACAC,QAAQ;YACN;gBACEhB,MAAM;gBACNC,MAAM;gBACNgB,SAAS;gBACT9B,SAASQ;gBACTS,OAAO;oBACLC,aACE,wFACA,uFACA,sFACA,yFACA,uFACA,oFACA,wFACA;gBACJ;YACF;YACA;gBACEL,MAAM;gBACNC,MAAM;gBACNgB,SAAS;gBACT9B,SAASQ;gBACTS,OAAO;oBACLC,aAAa;gBACf;YACF;SACD;IACH;IAEA,OAAO;QACLX;QACAU,OAAO;YACLc,OAAO;YACPC,YAAY;YACZd,aACE;YACFe,gBAAgB;gBAAC;gBAAQ;gBAAQ;gBAAa;gBAAU;gBAAc;gBAAa;aAAY;QACjG;QACAC,MAAM;YACJC,sBAAsB;YACtBC,WAAW;QACb;QACAC,OAAO;YACLC,gBAAgB;gBACb,CAAC,EAAEzC,IAAI,EAAE0C,WAAW,EAAE;oBACrB,kEAAkE;oBAClE,oEAAoE;oBACpE,iEAAiE;oBACjE,kDAAkD;oBAClD,EAAE;oBACF,oEAAoE;oBACpE,4DAA4D;oBAC5D,oEAAoE;oBACpE,8DAA8D;oBAC9D,sDAAsD;oBACtD,yDAAyD;oBACzD,kEAAkE;oBAClE,2CAA2C;oBAC3C,EAAE;oBACF,mEAAmE;oBACnE,oEAAoE;oBACpE,mEAAmE;oBACnE,kEAAkE;oBAClE,uDAAuD;oBACvD,IAAI,CAAC1C,MAAM,OAAOA;oBAClB,MAAM2C,IAAI3C;oBACV,MAAM4C,OAAQF,eAAe,CAAC;oBAC9B,MAAMzC,SAAS0C,EAAE1C,MAAM,IAAI2C,KAAK3C,MAAM;oBAEtC,iEAAiE;oBACjE,gEAAgE;oBAChE,qDAAqD;oBACrD,MAAM4C,YAAY,CAACC,MACjBA,OAAOH,IAAIA,CAAC,CAACG,IAAI,GAAGF,IAAI,CAACE,IAAI;oBAC/B,MAAMC,kBAAkB,CAACC,IACvB1C,MAAMC,OAAO,CAACyC,MAAMA,EAAEC,MAAM,GAAG;oBACjC,MAAMC,eAAe,CAACF,IACpB1C,MAAMC,OAAO,CAACyC,MAAMA,EAAEC,MAAM,KAAK;oBAEnC,MAAME,gBAAgB;wBACpB;wBACA;wBACA;wBACA;qBACD;oBAED,IAAIlD,WAAW,UAAU;wBACvB,KAAK,MAAMmD,QAAQD,cAAeR,CAAC,CAACS,KAAK,GAAG;wBAC5C,OAAOpD;oBACT;oBAEA,oEAAoE;oBACpE,oEAAoE;oBACpE,qEAAqE;oBACrE,oEAAoE;oBACpE,mEAAmE;oBACnE,mEAAmE;oBACnE,qEAAqE;oBACrE,yCAAyC;oBACzC,EAAE;oBACF,gEAAgE;oBAChE,mEAAmE;oBACnE,uDAAuD;oBACvD,2DAA2D;oBAC3D,4DAA4D;oBAC5D,2BAA2B;oBAC3B,MAAMqD,mBACJN,gBAAgBF,UAAU,wBAC1BE,gBAAgBF,UAAU;oBAC5B,IAAIQ,oBAAoBH,aAAaL,UAAU,eAAe;wBAC5DF,EAAEW,SAAS,GAAG;oBAChB;oBACA,OAAOtD;gBACT;aACD;QACH;QACAuD,QAAQ;YACNC,QAAQ;YACRC,UAAU;QACZ;QACAzB,QAAQ;YACN,eAAe;YACf;gBACEhB,MAAM;gBACNC,MAAM;gBACNC,UAAU;gBACVE,OAAO;oBAAEC,aAAa;gBAAqE;YAC7F;YACA;gBACEL,MAAM;gBACNC,MAAM;gBACNG,OAAO;oBAAEC,aAAa;gBAAgD;YACxE;YACAN;YACAO;YACAO;YACAC;YAEA,kCAAkC;YAClC;gBACEd,MAAM;gBACNC,MAAM;gBACNyC,YAAYvD,QAAQC,cAAc;gBAClCc,UAAU;gBACVE,OAAO;oBACLuC,UAAU;oBACVtC,aACE;gBACJ;YACF;YACA;gBACEL,MAAM;gBACNC,MAAM;gBACN2C,OAAO;gBACPxC,OAAO;oBACLuC,UAAU;oBACVE,UAAU;oBACVxC,aACE;gBACJ;gBACAmB,OAAO;oBACLsB,cAAc;wBACZ,CAAC,EAAE9D,IAAI,EAAE0C,WAAW,EAAE5C,KAAK,EAAE;4BAC3B,IAAI,OAAOA,UAAU,YAAYA,MAAMmD,MAAM,GAAG,GAAG,OAAOnD;4BAC1D,MAAMiE,cAAe/D,MAA2CgE;4BAChE,IAAI,OAAOD,gBAAgB,YAAYA,YAAYd,MAAM,IAAI,GAAG;gCAC9D,OAAOc,YAAYE,KAAK,CAAC,GAAG;4BAC9B;4BACA,MAAMC,WAAYxB,aAAqDyB;4BACvE,OAAO,OAAOD,aAAa,WAAWA,WAAWE;wBACnD;qBACD;gBACH;YACF;YACA;gBACEpD,MAAM;gBACNC,MAAM;gBACNG,OAAO;oBACLuC,UAAU;oBACVtC,aAAa;gBACf;YACF;YACA;gBACEL,MAAM;gBACNC,MAAM;gBACNG,OAAO;oBACLuC,UAAU;oBACVtC,aAAa;gBACf;YACF;YACA;gBACEL,MAAM;gBACNC,MAAM;gBACNG,OAAO;oBACLuC,UAAU;oBACVE,UAAU;oBACVxC,aACE;gBACJ;YACF;SACD;IACH;AACF"}
1
+ {"version":3,"sources":["../src/api-keys.ts"],"sourcesContent":["import type { CollectionBeforeValidateHook, CollectionConfig, Field } from 'payload'\n\nexport const API_KEYS_DEFAULT_SLUG = 'payload-mcp-api-keys'\n\nexport interface CreateApiKeysCollectionOptions {\n /**\n * Collection slug. Defaults to `payload-mcp-api-keys` for zero-touch\n * compatibility with rows created by `@payloadcms/plugin-mcp` v0.3.x.\n */\n slug?: string\n /**\n * Slug of the user collection that API keys link to. Required.\n */\n userCollection: string\n /**\n * Collection slugs offered to the collection-scopes matrix component.\n * Snapshotted at plugin-init time from the host Payload config; adding a\n * collection requires a dev-server restart for it to surface in the\n * admin UI.\n */\n availableCollections: string[]\n /**\n * Tool names offered as options for the `toolAllow` / `toolDeny` selects.\n * Sourced from the toolkit's registered tools at plugin init.\n */\n availableTools: string[]\n /**\n * Global slugs offered to the global-scopes matrix component. Optional —\n * direct callers of the factory that pre-date globals support continue\n * to work; sites with no globals get an empty array and the second\n * matrix table is not rendered.\n */\n availableGlobals?: string[]\n}\n\nconst PRESET_OPTIONS = [\n { label: 'Read-only', value: 'read-only' },\n { label: 'Editor (read + create + update)', value: 'editor' },\n { label: 'Admin (all actions)', value: 'admin' },\n { label: 'Custom (use overrides below)', value: 'custom' },\n] as const\n\nconst isCustomPreset = (data: unknown): boolean =>\n !!data && typeof data === 'object' && (data as { preset?: unknown }).preset === 'custom'\n\n/**\n * Builds the `payload-mcp-api-keys` collection used by the v0.4 standalone\n * plugin. Reuses Payload's built-in `useAPIKey: true` so the underlying\n * `apiKey` / `apiKeyIndex` columns match what `@payloadcms/plugin-mcp`\n * v0.3.x wrote — existing rows authenticate without re-issue.\n *\n * Layout:\n * - Main column: name, description, preset, scopes matrix (custom only),\n * tools collapsible (custom only).\n * - Sidebar: user relationship, key prefix, expiresAt, revokedAt,\n * lastUsedAt — identity + lifecycle metadata kept out of the\n * scope-editing flow.\n */\nexport function createApiKeysCollection(\n options: CreateApiKeysCollectionOptions,\n): CollectionConfig {\n if (!options || !options.userCollection) {\n throw new Error(\n 'createApiKeysCollection: `userCollection` is required (slug of the user collection that owns API keys).',\n )\n }\n if (!Array.isArray(options.availableCollections)) {\n throw new Error(\n 'createApiKeysCollection: `availableCollections` is required (slugs of collections that scope overrides may target).',\n )\n }\n if (!Array.isArray(options.availableTools)) {\n throw new Error(\n 'createApiKeysCollection: `availableTools` is required (names of registered MCP tools).',\n )\n }\n\n const slug = options.slug ?? API_KEYS_DEFAULT_SLUG\n const toolOptions = options.availableTools.map((t) => ({ label: t, value: t }))\n const availableGlobals = Array.isArray(options.availableGlobals)\n ? options.availableGlobals\n : []\n\n const presetField: Field = {\n name: 'preset',\n type: 'select',\n required: true,\n defaultValue: 'custom',\n options: PRESET_OPTIONS as unknown as { label: string; value: string }[],\n admin: {\n description:\n 'Role preset. \"Custom\" unlocks the per-collection matrix and the tool overrides below. ' +\n 'Switching away from Custom CLEARS every override on save (collectionScopes, globalScopes, ' +\n 'toolAllow, toolDeny); switching back to Custom starts from a fresh deny-all baseline, so ' +\n 'reconfigure the matrices and tool lists before saving.',\n },\n }\n\n // Stored shape: Array<{ slug: string; actions: ('read'|'create'|'update'|'delete')[] }>\n // The default Payload UI for an `array` would force users to add rows\n // one at a time; the custom matrix component renders all available\n // collections at once with a checkbox grid (rows × actions).\n //\n // `availableCollections` is forwarded via `clientProps` — Payload v3's\n // sanctioned escape hatch for serializable static data that the client\n // component needs at render time.\n const collectionScopesField: Field = {\n name: 'collectionScopes',\n type: 'json',\n admin: {\n condition: isCustomPreset,\n components: {\n Field: {\n path: 'payload-mcp-toolkit/client',\n exportName: 'CollectionScopesMatrix',\n clientProps: {\n availableCollections: options.availableCollections,\n },\n },\n },\n },\n }\n\n // Mirrors `collectionScopes` exactly — one additive JSONB column with a\n // default of `'[]'`, default-rendered by `GlobalScopesMatrix`. Hidden\n // under non-custom presets. Stored shape:\n // Array<{ slug: string; actions: ('read'|'update')[] }>\n // No `availableGlobals.length > 0` gate: `ScopesTable` renders its own\n // empty-state message when zero items are passed, so the field surfaces\n // under Custom regardless of host config, matching the collection variant.\n const globalScopesField: Field = {\n name: 'globalScopes',\n type: 'json',\n admin: {\n condition: isCustomPreset,\n components: {\n Field: {\n path: 'payload-mcp-toolkit/client',\n exportName: 'GlobalScopesMatrix',\n clientProps: {\n availableGlobals,\n },\n },\n },\n },\n }\n\n const toolsCollapsible: Field = {\n type: 'collapsible',\n label: 'Tool overrides',\n admin: {\n condition: isCustomPreset,\n description:\n 'Per-tool whitelist / blacklist. Layered on top of preset and collection scopes.',\n initCollapsed: true,\n },\n fields: [\n {\n name: 'toolAllow',\n type: 'select',\n hasMany: true,\n options: toolOptions,\n admin: {\n description:\n 'If set, only these tools are callable with this key. Leave empty to allow any tool ' +\n 'the collection or global scopes permit. Under the Custom preset, an empty list is ' +\n 'treated as deny-all ONLY when no collection or global scopes are set (the fresh- ' +\n 'Custom-key sentinel); when collection or global scopes are populated, an empty list ' +\n 'collapses to \"no tool restriction\" so the resource scopes alone determine what is ' +\n 'callable — to deny every tool while keeping resource scopes, enumerate them in ' +\n 'toolDeny instead. Preset-mode keys created via the REST API with an empty list are ' +\n 'coerced to \"no restriction\".',\n },\n },\n {\n name: 'toolDeny',\n type: 'select',\n hasMany: true,\n options: toolOptions,\n admin: {\n description: 'These tools are blocked regardless of any other scope.',\n },\n },\n ],\n }\n\n return {\n slug,\n admin: {\n group: 'MCP',\n useAsTitle: 'name',\n description:\n 'API keys for MCP clients. Scopes control which collections and tools each key can access.',\n defaultColumns: ['name', 'user', 'keyPrefix', 'preset', 'lastUsedAt', 'expiresAt', 'revokedAt'],\n },\n auth: {\n disableLocalStrategy: true,\n useAPIKey: true,\n },\n hooks: {\n beforeValidate: [\n (({ data, originalDoc }) => {\n // The override fields (collectionScopes, globalScopes, toolAllow,\n // toolDeny) are conditionally rendered only under the Custom preset\n // (`condition: isCustomPreset`). Under any other preset they are\n // hidden in the admin UI, which means two things:\n //\n // 1. The admin form omits hidden fields from its payload on save,\n // so `data` only carries the visible fields — we can't\n // \"collapse the empty array we see in `data`\" because we never\n // see it at all. The stale value lives on `originalDoc`.\n // 2. A Custom→Admin switch silently keeps the prior\n // `toolAllow:[...]` / `collectionScopes:[...]`, and\n // `composeScopes` then emits a scope gate that rejects calls\n // the user clearly intended to allow.\n //\n // Fix: when the preset is non-Custom, explicitly write `null` into\n // `data` for every override axis (regardless of what `data` carries\n // or what originalDoc holds). Payload persists nulls, so the stale\n // values are erased on every save. The Custom-preset branch below\n // keeps the explicit-empty-means-deny semantic intact.\n if (!data) return data\n const d = data as Record<string, unknown>\n const orig = (originalDoc ?? {}) as Record<string, unknown>\n const preset = d.preset ?? orig.preset\n\n // `readField` falls through to originalDoc when `data` omits the\n // key entirely (admin form skipping hidden fields), but honours\n // an explicit null/empty in `data` over originalDoc.\n const readField = (key: string): unknown =>\n key in d ? d[key] : orig[key]\n const isNonEmptyArray = (v: unknown): boolean =>\n Array.isArray(v) && v.length > 0\n // Null, undefined, or `[]` — the three shapes that mean \"the user\n // expressed no tool restriction\". Anything else (a bare string, a\n // number) is malformed and must reach Payload's validator rather than\n // being silently read as \"no restriction\".\n const isUnset = (v: unknown): boolean =>\n v === null || v === undefined || (Array.isArray(v) && v.length === 0)\n\n const OVERRIDE_AXES = [\n 'collectionScopes',\n 'globalScopes',\n 'toolAllow',\n 'toolDeny',\n ] as const\n\n if (preset !== 'custom') {\n for (const axis of OVERRIDE_AXES) d[axis] = null\n return data\n }\n\n // Custom preset: the Tools collapsible is labelled as an *override*\n // layered on top of collection / global scopes, and its description\n // says \"Leave empty to allow any tool the collection scopes permit.\"\n // Payload's hasMany-select default of `[]` would otherwise turn the\n // Tools section into a mandatory whitelist — a user who configures\n // collection scopes and never opens the collapsible would silently\n // store `toolAllow:[]`, which `composeScopes` honours as deny-all on\n // the tools axis and rejects every call.\n //\n // Resolve the mismatch by coercing a non-populated `toolAllow` to\n // null whenever the key carries any concrete resource scope\n // (collection or global entries). \"Non-populated\" covers both `[]`\n // (what the admin form sends) and a missing key (what a key created\n // through the Local API sends) — Payload reads an unset hasMany\n // select back as `[]` either way, so without this a scripted key\n // with collection scopes and no tool list would deny every tool.\n // The fresh-Custom-key sentinel in `composeScopes` still covers the\n // \"no scopes at all\" case (everything null → deny-all), so users who\n // genuinely want deny-all do not regress.\n const hasResourceScope =\n isNonEmptyArray(readField('collectionScopes')) ||\n isNonEmptyArray(readField('globalScopes'))\n if (hasResourceScope && isUnset(readField('toolAllow'))) {\n d.toolAllow = null\n }\n return data\n }) as CollectionBeforeValidateHook,\n ],\n },\n labels: {\n plural: 'API Keys',\n singular: 'API Key',\n },\n fields: [\n // Main column.\n {\n name: 'name',\n type: 'text',\n required: true,\n admin: { description: 'Human label for this key (e.g. \"Editorial team — Claude Desktop\").' },\n },\n {\n name: 'description',\n type: 'textarea',\n admin: { description: 'Optional notes about the purpose of this key.' },\n },\n presetField,\n collectionScopesField,\n globalScopesField,\n toolsCollapsible,\n\n // Sidebar — identity + lifecycle.\n {\n name: 'user',\n type: 'relationship',\n relationTo: options.userCollection,\n required: true,\n admin: {\n position: 'sidebar',\n description:\n 'The user this key authenticates as. Tool calls use this user for access checks on target collections.',\n },\n },\n {\n name: 'keyPrefix',\n type: 'text',\n index: true,\n admin: {\n position: 'sidebar',\n readOnly: true,\n description:\n 'First 8 characters of the API key — used in audit logs to identify the key without exposing the full secret.',\n },\n hooks: {\n beforeChange: [\n ({ data, originalDoc, value }) => {\n if (typeof value === 'string' && value.length > 0) return value\n const incomingKey = (data as { apiKey?: unknown } | undefined)?.apiKey\n if (typeof incomingKey === 'string' && incomingKey.length >= 8) {\n return incomingKey.slice(0, 8)\n }\n const existing = (originalDoc as { keyPrefix?: unknown } | undefined)?.keyPrefix\n return typeof existing === 'string' ? existing : undefined\n },\n ],\n },\n },\n {\n name: 'expiresAt',\n type: 'date',\n admin: {\n position: 'sidebar',\n description: 'Optional expiry. Requests authenticated with an expired key are rejected.',\n },\n },\n {\n name: 'revokedAt',\n type: 'date',\n admin: {\n position: 'sidebar',\n description: 'Set to revoke a key. Revoked keys are rejected at auth time.',\n },\n },\n {\n name: 'lastUsedAt',\n type: 'date',\n admin: {\n position: 'sidebar',\n readOnly: true,\n description:\n 'Updated on each successful authentication. Fire-and-forget; not on the request hot path.',\n },\n },\n ],\n }\n}\n"],"names":["API_KEYS_DEFAULT_SLUG","PRESET_OPTIONS","label","value","isCustomPreset","data","preset","createApiKeysCollection","options","userCollection","Error","Array","isArray","availableCollections","availableTools","slug","toolOptions","map","t","availableGlobals","presetField","name","type","required","defaultValue","admin","description","collectionScopesField","condition","components","Field","path","exportName","clientProps","globalScopesField","toolsCollapsible","initCollapsed","fields","hasMany","group","useAsTitle","defaultColumns","auth","disableLocalStrategy","useAPIKey","hooks","beforeValidate","originalDoc","d","orig","readField","key","isNonEmptyArray","v","length","isUnset","undefined","OVERRIDE_AXES","axis","hasResourceScope","toolAllow","labels","plural","singular","relationTo","position","index","readOnly","beforeChange","incomingKey","apiKey","slice","existing","keyPrefix"],"mappings":"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"}
@@ -11,3 +11,15 @@ export declare function assertNoUpstreamPlugin(plugins: Plugin[] | undefined): v
11
11
  * boot errors and gives the user actionable text.
12
12
  */
13
13
  export declare function assertNoSlugConflict(collections: CollectionConfig[] | undefined, apiKeysSlug?: string): void;
14
+ /**
15
+ * Throws if a custom tool reuses a built-in tool's name.
16
+ *
17
+ * Registering two tools under one name is silently last-wins inside the MCP
18
+ * SDK, which turns a typo into a built-in tool quietly disappearing from
19
+ * `tools/list`. Fail at boot instead, and name both sides in the message.
20
+ */
21
+ export declare function assertNoToolNameConflict(builtIn: Array<{
22
+ name: string;
23
+ }>, custom: Array<{
24
+ name: string;
25
+ }> | undefined): void;
@@ -37,5 +37,25 @@ const UPGRADE_HINT = 'payload-mcp-toolkit v0.4 is the standalone successor to @p
37
37
  }
38
38
  }
39
39
  }
40
+ /**
41
+ * Throws if a custom tool reuses a built-in tool's name.
42
+ *
43
+ * Registering two tools under one name is silently last-wins inside the MCP
44
+ * SDK, which turns a typo into a built-in tool quietly disappearing from
45
+ * `tools/list`. Fail at boot instead, and name both sides in the message.
46
+ */ export function assertNoToolNameConflict(builtIn, custom) {
47
+ if (!custom || custom.length === 0) return;
48
+ const taken = new Set(builtIn.map((t)=>t.name));
49
+ const seen = new Set();
50
+ for (const tool of custom){
51
+ if (taken.has(tool.name)) {
52
+ throw new Error(`payload-mcp-toolkit: customTools entry "${tool.name}" reuses a built-in tool name. ` + 'Rename it — a duplicate name would shadow the built-in tool at registration time.');
53
+ }
54
+ if (seen.has(tool.name)) {
55
+ throw new Error(`payload-mcp-toolkit: customTools contains two entries named "${tool.name}". Tool names must be unique.`);
56
+ }
57
+ seen.add(tool.name);
58
+ }
59
+ }
40
60
 
41
61
  //# sourceMappingURL=conflict-detection.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/conflict-detection.ts"],"sourcesContent":["import type { CollectionConfig, Plugin } from 'payload'\r\nimport { API_KEYS_DEFAULT_SLUG } from './api-keys'\r\n\r\nconst UPGRADE_HINT =\r\n 'payload-mcp-toolkit v0.4 is the standalone successor to @payloadcms/plugin-mcp. ' +\r\n 'Remove the upstream `mcpPlugin(...)` from your `plugins[]` (and the package from your dependencies) before upgrading. ' +\r\n 'See README \"Upgrading from 0.3.x\".'\r\n\r\n/**\r\n * Returns true when the supplied plugin reference looks like the upstream\r\n * `mcpPlugin(...)` invocation (function name, source-string sniff, or wrapped\r\n * config probing). Heuristic: covers the three common ways the upstream\r\n * plugin shows up in `plugins[]`.\r\n */\r\nfunction looksLikeUpstreamPlugin(plugin: unknown): boolean {\r\n if (typeof plugin !== 'function') return false\r\n const fn = plugin as (...args: unknown[]) => unknown\r\n const fnName = (fn.name ?? '').toString()\r\n if (fnName === 'mcpPlugin' || fnName === 'withMcp') return true\r\n\r\n const src = fn.toString()\r\n return /@payloadcms\\/plugin-mcp/.test(src) || /payload-mcp-api-keys/.test(src)\r\n}\r\n\r\n/**\r\n * Throws if `@payloadcms/plugin-mcp` is also registered in the host config.\r\n * Two MCP plugins racing for the same collection slug produces a confusing\r\n * boot crash inside Payload — this surfaces a clearer migration message.\r\n */\r\nexport function assertNoUpstreamPlugin(plugins: Plugin[] | undefined): void {\r\n if (!plugins || plugins.length === 0) return\r\n for (const plugin of plugins) {\r\n if (looksLikeUpstreamPlugin(plugin)) {\r\n throw new Error(UPGRADE_HINT)\r\n }\r\n }\r\n}\r\n\r\n/**\r\n * Throws if a collection with the api-keys slug is already in\r\n * `incomingConfig.collections` from another source. Prevents duplicate-slug\r\n * boot errors and gives the user actionable text.\r\n */\r\nexport function assertNoSlugConflict(\r\n collections: CollectionConfig[] | undefined,\r\n apiKeysSlug: string = API_KEYS_DEFAULT_SLUG,\r\n): void {\r\n if (!collections || collections.length === 0) return\r\n for (const c of collections) {\r\n if (c?.slug === apiKeysSlug) {\r\n throw new Error(\r\n `payload-mcp-toolkit: a collection with slug \"${apiKeysSlug}\" is already registered. ` +\r\n 'This is usually the upstream `@payloadcms/plugin-mcp` still being active. ' +\r\n 'Remove it before upgrading to v0.4, or pass a different slug via `apiKeyCollection.slug`.',\r\n )\r\n }\r\n }\r\n}\r\n"],"names":["API_KEYS_DEFAULT_SLUG","UPGRADE_HINT","looksLikeUpstreamPlugin","plugin","fn","fnName","name","toString","src","test","assertNoUpstreamPlugin","plugins","length","Error","assertNoSlugConflict","collections","apiKeysSlug","c","slug"],"mappings":"AACA,SAASA,qBAAqB,QAAQ,aAAY;AAElD,MAAMC,eACJ,qFACA,2HACA;AAEF;;;;;CAKC,GACD,SAASC,wBAAwBC,MAAe;IAC9C,IAAI,OAAOA,WAAW,YAAY,OAAO;IACzC,MAAMC,KAAKD;IACX,MAAME,SAAS,AAACD,CAAAA,GAAGE,IAAI,IAAI,EAAC,EAAGC,QAAQ;IACvC,IAAIF,WAAW,eAAeA,WAAW,WAAW,OAAO;IAE3D,MAAMG,MAAMJ,GAAGG,QAAQ;IACvB,OAAO,0BAA0BE,IAAI,CAACD,QAAQ,uBAAuBC,IAAI,CAACD;AAC5E;AAEA;;;;CAIC,GACD,OAAO,SAASE,uBAAuBC,OAA6B;IAClE,IAAI,CAACA,WAAWA,QAAQC,MAAM,KAAK,GAAG;IACtC,KAAK,MAAMT,UAAUQ,QAAS;QAC5B,IAAIT,wBAAwBC,SAAS;YACnC,MAAM,IAAIU,MAAMZ;QAClB;IACF;AACF;AAEA;;;;CAIC,GACD,OAAO,SAASa,qBACdC,WAA2C,EAC3CC,cAAsBhB,qBAAqB;IAE3C,IAAI,CAACe,eAAeA,YAAYH,MAAM,KAAK,GAAG;IAC9C,KAAK,MAAMK,KAAKF,YAAa;QAC3B,IAAIE,GAAGC,SAASF,aAAa;YAC3B,MAAM,IAAIH,MACR,CAAC,6CAA6C,EAAEG,YAAY,yBAAyB,CAAC,GACpF,+EACA;QAEN;IACF;AACF"}
1
+ {"version":3,"sources":["../src/conflict-detection.ts"],"sourcesContent":["import type { CollectionConfig, Plugin } from 'payload'\r\nimport { API_KEYS_DEFAULT_SLUG } from './api-keys'\r\n\r\nconst UPGRADE_HINT =\r\n 'payload-mcp-toolkit v0.4 is the standalone successor to @payloadcms/plugin-mcp. ' +\r\n 'Remove the upstream `mcpPlugin(...)` from your `plugins[]` (and the package from your dependencies) before upgrading. ' +\r\n 'See README \"Upgrading from 0.3.x\".'\r\n\r\n/**\r\n * Returns true when the supplied plugin reference looks like the upstream\r\n * `mcpPlugin(...)` invocation (function name, source-string sniff, or wrapped\r\n * config probing). Heuristic: covers the three common ways the upstream\r\n * plugin shows up in `plugins[]`.\r\n */\r\nfunction looksLikeUpstreamPlugin(plugin: unknown): boolean {\r\n if (typeof plugin !== 'function') return false\r\n const fn = plugin as (...args: unknown[]) => unknown\r\n const fnName = (fn.name ?? '').toString()\r\n if (fnName === 'mcpPlugin' || fnName === 'withMcp') return true\r\n\r\n const src = fn.toString()\r\n return /@payloadcms\\/plugin-mcp/.test(src) || /payload-mcp-api-keys/.test(src)\r\n}\r\n\r\n/**\r\n * Throws if `@payloadcms/plugin-mcp` is also registered in the host config.\r\n * Two MCP plugins racing for the same collection slug produces a confusing\r\n * boot crash inside Payload — this surfaces a clearer migration message.\r\n */\r\nexport function assertNoUpstreamPlugin(plugins: Plugin[] | undefined): void {\r\n if (!plugins || plugins.length === 0) return\r\n for (const plugin of plugins) {\r\n if (looksLikeUpstreamPlugin(plugin)) {\r\n throw new Error(UPGRADE_HINT)\r\n }\r\n }\r\n}\r\n\r\n/**\r\n * Throws if a collection with the api-keys slug is already in\r\n * `incomingConfig.collections` from another source. Prevents duplicate-slug\r\n * boot errors and gives the user actionable text.\r\n */\r\nexport function assertNoSlugConflict(\r\n collections: CollectionConfig[] | undefined,\r\n apiKeysSlug: string = API_KEYS_DEFAULT_SLUG,\r\n): void {\r\n if (!collections || collections.length === 0) return\r\n for (const c of collections) {\r\n if (c?.slug === apiKeysSlug) {\r\n throw new Error(\r\n `payload-mcp-toolkit: a collection with slug \"${apiKeysSlug}\" is already registered. ` +\r\n 'This is usually the upstream `@payloadcms/plugin-mcp` still being active. ' +\r\n 'Remove it before upgrading to v0.4, or pass a different slug via `apiKeyCollection.slug`.',\r\n )\r\n }\r\n }\r\n}\r\n\n/**\n * Throws if a custom tool reuses a built-in tool's name.\n *\n * Registering two tools under one name is silently last-wins inside the MCP\n * SDK, which turns a typo into a built-in tool quietly disappearing from\n * `tools/list`. Fail at boot instead, and name both sides in the message.\n */\nexport function assertNoToolNameConflict(\n builtIn: Array<{ name: string }>,\n custom: Array<{ name: string }> | undefined,\n): void {\n if (!custom || custom.length === 0) return\n const taken = new Set(builtIn.map((t) => t.name))\n const seen = new Set<string>()\n for (const tool of custom) {\n if (taken.has(tool.name)) {\n throw new Error(\n `payload-mcp-toolkit: customTools entry \"${tool.name}\" reuses a built-in tool name. ` +\n 'Rename it — a duplicate name would shadow the built-in tool at registration time.',\n )\n }\n if (seen.has(tool.name)) {\n throw new Error(\n `payload-mcp-toolkit: customTools contains two entries named \"${tool.name}\". Tool names must be unique.`,\n )\n }\n seen.add(tool.name)\n }\n}\n"],"names":["API_KEYS_DEFAULT_SLUG","UPGRADE_HINT","looksLikeUpstreamPlugin","plugin","fn","fnName","name","toString","src","test","assertNoUpstreamPlugin","plugins","length","Error","assertNoSlugConflict","collections","apiKeysSlug","c","slug","assertNoToolNameConflict","builtIn","custom","taken","Set","map","t","seen","tool","has","add"],"mappings":"AACA,SAASA,qBAAqB,QAAQ,aAAY;AAElD,MAAMC,eACJ,qFACA,2HACA;AAEF;;;;;CAKC,GACD,SAASC,wBAAwBC,MAAe;IAC9C,IAAI,OAAOA,WAAW,YAAY,OAAO;IACzC,MAAMC,KAAKD;IACX,MAAME,SAAS,AAACD,CAAAA,GAAGE,IAAI,IAAI,EAAC,EAAGC,QAAQ;IACvC,IAAIF,WAAW,eAAeA,WAAW,WAAW,OAAO;IAE3D,MAAMG,MAAMJ,GAAGG,QAAQ;IACvB,OAAO,0BAA0BE,IAAI,CAACD,QAAQ,uBAAuBC,IAAI,CAACD;AAC5E;AAEA;;;;CAIC,GACD,OAAO,SAASE,uBAAuBC,OAA6B;IAClE,IAAI,CAACA,WAAWA,QAAQC,MAAM,KAAK,GAAG;IACtC,KAAK,MAAMT,UAAUQ,QAAS;QAC5B,IAAIT,wBAAwBC,SAAS;YACnC,MAAM,IAAIU,MAAMZ;QAClB;IACF;AACF;AAEA;;;;CAIC,GACD,OAAO,SAASa,qBACdC,WAA2C,EAC3CC,cAAsBhB,qBAAqB;IAE3C,IAAI,CAACe,eAAeA,YAAYH,MAAM,KAAK,GAAG;IAC9C,KAAK,MAAMK,KAAKF,YAAa;QAC3B,IAAIE,GAAGC,SAASF,aAAa;YAC3B,MAAM,IAAIH,MACR,CAAC,6CAA6C,EAAEG,YAAY,yBAAyB,CAAC,GACpF,+EACA;QAEN;IACF;AACF;AAEA;;;;;;CAMC,GACD,OAAO,SAASG,yBACdC,OAAgC,EAChCC,MAA2C;IAE3C,IAAI,CAACA,UAAUA,OAAOT,MAAM,KAAK,GAAG;IACpC,MAAMU,QAAQ,IAAIC,IAAIH,QAAQI,GAAG,CAAC,CAACC,IAAMA,EAAEnB,IAAI;IAC/C,MAAMoB,OAAO,IAAIH;IACjB,KAAK,MAAMI,QAAQN,OAAQ;QACzB,IAAIC,MAAMM,GAAG,CAACD,KAAKrB,IAAI,GAAG;YACxB,MAAM,IAAIO,MACR,CAAC,wCAAwC,EAAEc,KAAKrB,IAAI,CAAC,+BAA+B,CAAC,GACnF;QAEN;QACA,IAAIoB,KAAKE,GAAG,CAACD,KAAKrB,IAAI,GAAG;YACvB,MAAM,IAAIO,MACR,CAAC,6DAA6D,EAAEc,KAAKrB,IAAI,CAAC,6BAA6B,CAAC;QAE5G;QACAoB,KAAKG,GAAG,CAACF,KAAKrB,IAAI;IACpB;AACF"}
package/dist/index.d.ts CHANGED
@@ -16,4 +16,8 @@ import type { ContentToolkitOptions } from './types';
16
16
  * See `ContentToolkitOptions` for the (entirely optional) escape hatches.
17
17
  */
18
18
  export declare function mcpToolkitPlugin(options?: ContentToolkitOptions): Plugin;
19
+ export { jsonResponse, textResponse } from './tools/_helpers';
20
+ export type { ToolFactoryOutput } from './registry';
21
+ export type { ToolRouting, ResourceKind } from './scope/policy';
22
+ export type { McpTextResponse } from './tools/_helpers';
19
23
  export type { ContentToolkitOptions, DomainPrompt, CollectionSchema, GlobalSchema, BlockCatalog, BlockSchema, BlockNestingMap, BlockNestingEdge, RelationshipEdge, FieldSchema, CollectionAction, GlobalAction, KeyScopes, ScopePreset, } from './types';
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import { createApiKeysCollection, API_KEYS_DEFAULT_SLUG } from './api-keys';
6
6
  import { createBearerStrategy } from './auth-strategy';
7
7
  import { createMcpEndpoints } from './endpoint';
8
8
  import { createInitializeServer } from './registry';
9
- import { assertNoSlugConflict, assertNoUpstreamPlugin } from './conflict-detection';
9
+ import { assertNoSlugConflict, assertNoToolNameConflict, assertNoUpstreamPlugin } from './conflict-detection';
10
10
  import { createCreateDocumentTool } from './tools/create-document';
11
11
  import { createDeleteDocumentTool } from './tools/delete-document';
12
12
  import { createFindDocumentTool } from './tools/find-document';
@@ -24,29 +24,29 @@ import { createSearchContentTool } from './tools/search-content';
24
24
  import { createUpdateDocumentTool } from './tools/update-document';
25
25
  import { createUploadMediaTool } from './tools/upload-media';
26
26
  import { createListVersionsTool, createRestoreVersionTool } from './tools/versions';
27
- /**
28
- * Resolves the user collection slug for API-key linkage.
29
- *
30
- * Resolution order: explicit `options.apiKeyCollection.userCollection` →
31
- * explicit `options.userCollection` → `incomingConfig.admin.user` →
32
- * 'users' as a last resort.
27
+ /**
28
+ * Resolves the user collection slug for API-key linkage.
29
+ *
30
+ * Resolution order: explicit `options.apiKeyCollection.userCollection` →
31
+ * explicit `options.userCollection` → `incomingConfig.admin.user` →
32
+ * 'users' as a last resort.
33
33
  */ function resolveUserCollection(options, incomingConfig) {
34
34
  return options.apiKeyCollection?.userCollection ?? options.userCollection ?? incomingConfig.admin?.user ?? 'users';
35
35
  }
36
- /**
37
- * payload-mcp-toolkit — standalone Payload v3 MCP plugin.
38
- *
39
- * Owns the `/api/mcp` endpoint, the `payload-mcp-api-keys` collection,
40
- * bearer authentication via Payload's `auth.strategies` extension point,
41
- * and the per-tool scope check. Upstream `@payloadcms/plugin-mcp` is no
42
- * longer required (and is incompatible — see `assertNoUpstreamPlugin`).
43
- *
44
- * Zero-config usage:
45
- * ```ts
46
- * plugins: [mcpToolkitPlugin()]
47
- * ```
48
- *
49
- * See `ContentToolkitOptions` for the (entirely optional) escape hatches.
36
+ /**
37
+ * payload-mcp-toolkit — standalone Payload v3 MCP plugin.
38
+ *
39
+ * Owns the `/api/mcp` endpoint, the `payload-mcp-api-keys` collection,
40
+ * bearer authentication via Payload's `auth.strategies` extension point,
41
+ * and the per-tool scope check. Upstream `@payloadcms/plugin-mcp` is no
42
+ * longer required (and is incompatible — see `assertNoUpstreamPlugin`).
43
+ *
44
+ * Zero-config usage:
45
+ * ```ts
46
+ * plugins: [mcpToolkitPlugin()]
47
+ * ```
48
+ *
49
+ * See `ContentToolkitOptions` for the (entirely optional) escape hatches.
50
50
  */ export function mcpToolkitPlugin(options = {}) {
51
51
  return (incomingConfig)=>{
52
52
  const apiKeysSlug = options.apiKeyCollection?.slug ?? API_KEYS_DEFAULT_SLUG;
@@ -141,6 +141,10 @@ import { createListVersionsTool, createRestoreVersionTool } from './tools/versio
141
141
  const restoreGlobalVersion = createRestoreGlobalVersionTool(draftGlobals);
142
142
  if (restoreGlobalVersion) tools.push(restoreGlobalVersion);
143
143
  }
144
+ // Host-supplied tools go on the end, so a built-in never loses its slot in
145
+ // tools/list. They share the wrapper: scope check, mcp stamp, audit log.
146
+ assertNoToolNameConflict(tools, options.customTools);
147
+ if (options.customTools?.length) tools.push(...options.customTools);
144
148
  // Build the per-request initializer that mcp-handler invokes.
145
149
  const buildInitializeServer = createInitializeServer({
146
150
  tools,
@@ -208,5 +212,8 @@ import { createListVersionsTool, createRestoreVersionTool } from './tools/versio
208
212
  };
209
213
  };
210
214
  }
215
+ // Runtime helpers for building custom tools — the same envelope builders the
216
+ // built-in tools use, so a host tool returns the identical result shape.
217
+ export { jsonResponse, textResponse } from './tools/_helpers';
211
218
 
212
219
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { Block, CollectionConfig, Config, GlobalConfig, Plugin } from 'payload'\r\nimport type { ContentToolkitOptions, GlobalSchema } from './types'\r\nimport {\r\n introspectCollections,\r\n introspectGlobals,\r\n introspectBlocks,\r\n buildBlockNestingMap,\r\n buildRelationshipGraph,\r\n} from './introspection'\r\nimport { generatePrompts } from './prompts'\r\nimport { generateResources } from './resources'\r\nimport { computeDraftCollections, computeDraftGlobals } from './draft-workflow'\r\nimport { createApiKeysCollection, API_KEYS_DEFAULT_SLUG } from './api-keys'\r\nimport { createBearerStrategy } from './auth-strategy'\r\nimport { createMcpEndpoints } from './endpoint'\r\nimport {\r\n createInitializeServer,\r\n type ToolFactoryOutput,\r\n} from './registry'\r\nimport { assertNoSlugConflict, assertNoUpstreamPlugin } from './conflict-detection'\r\nimport { createCreateDocumentTool } from './tools/create-document'\r\nimport { createDeleteDocumentTool } from './tools/delete-document'\r\nimport { createFindDocumentTool } from './tools/find-document'\r\nimport { createFindGlobalTool } from './tools/find-global'\r\nimport { createUpdateGlobalTool } from './tools/update-global'\r\nimport { createPatchGlobalLayoutTool } from './tools/patch-global-layout'\r\nimport { createPublishGlobalDraftTool } from './tools/publish-global-draft'\r\nimport {\r\n createListGlobalVersionsTool,\r\n createRestoreGlobalVersionTool,\r\n} from './tools/global-versions'\r\nimport { createPatchLayoutTool } from './tools/patch-layout'\r\nimport { createPublishDraftTool } from './tools/publish-draft'\r\nimport { createResolveReferenceTool } from './tools/resolve-reference'\r\nimport { createSafeDeleteTool } from './tools/safe-delete'\r\nimport { createSchedulePublishTool } from './tools/schedule-publish'\r\nimport { createSearchContentTool } from './tools/search-content'\r\nimport { createUpdateDocumentTool } from './tools/update-document'\r\nimport { createUploadMediaTool } from './tools/upload-media'\r\nimport { createListVersionsTool, createRestoreVersionTool } from './tools/versions'\r\n\r\n/**\r\n * Resolves the user collection slug for API-key linkage.\r\n *\r\n * Resolution order: explicit `options.apiKeyCollection.userCollection` →\r\n * explicit `options.userCollection` → `incomingConfig.admin.user` →\r\n * 'users' as a last resort.\r\n */\r\nfunction resolveUserCollection(\r\n options: ContentToolkitOptions,\r\n incomingConfig: Config,\r\n): string {\r\n return (\r\n options.apiKeyCollection?.userCollection ??\r\n options.userCollection ??\r\n (incomingConfig.admin?.user as string | undefined) ??\r\n 'users'\r\n )\r\n}\r\n\r\n/**\r\n * payload-mcp-toolkit — standalone Payload v3 MCP plugin.\r\n *\r\n * Owns the `/api/mcp` endpoint, the `payload-mcp-api-keys` collection,\r\n * bearer authentication via Payload's `auth.strategies` extension point,\r\n * and the per-tool scope check. Upstream `@payloadcms/plugin-mcp` is no\r\n * longer required (and is incompatible — see `assertNoUpstreamPlugin`).\r\n *\r\n * Zero-config usage:\r\n * ```ts\r\n * plugins: [mcpToolkitPlugin()]\r\n * ```\r\n *\r\n * See `ContentToolkitOptions` for the (entirely optional) escape hatches.\r\n */\r\nexport function mcpToolkitPlugin(options: ContentToolkitOptions = {}): Plugin {\r\n return (incomingConfig: Config): Config => {\r\n const apiKeysSlug = options.apiKeyCollection?.slug ?? API_KEYS_DEFAULT_SLUG\r\n\r\n // Conflict detection — fail fast with actionable messages.\r\n assertNoUpstreamPlugin(incomingConfig.plugins)\r\n assertNoSlugConflict(incomingConfig.collections as CollectionConfig[] | undefined, apiKeysSlug)\r\n\r\n const collections = (incomingConfig.collections ?? []) as CollectionConfig[]\r\n const globals = (incomingConfig.globals ?? []) as GlobalConfig[]\r\n const allBlocks = (incomingConfig.blocks ?? []) as Block[]\r\n\r\n const collectionSchemas = introspectCollections(collections)\r\n const globalSchemas = introspectGlobals(globals)\r\n const blockCatalog = introspectBlocks(allBlocks)\r\n const relationships = buildRelationshipGraph(collectionSchemas)\r\n\r\n const previewSiteUrl = options.preview?.disabled\r\n ? undefined\r\n : options.preview?.siteUrl ??\r\n incomingConfig.serverURL ??\r\n process.env.NEXT_PUBLIC_SERVER_URL ??\r\n process.env.SITE_URL\r\n const previewDisabled = options.preview?.disabled === true\r\n\r\n const { draftCollections, excluded } = computeDraftCollections(collections, {\r\n draftBehavior: options.draftBehavior,\r\n excludeCollections: options.exclude?.collections,\r\n apiKeysSlug,\r\n })\r\n\r\n const { draftGlobals, excluded: excludedGlobals } = computeDraftGlobals(globals, {\r\n draftBehavior: options.draftBehavior,\r\n excludeGlobals: options.exclude?.globals,\r\n })\r\n\r\n // Build blockNesting from exclusion-filtered inputs so excluded\r\n // collections/globals never appear in patchLayout/patchGlobalLayout slug\r\n // enums or in the `blocks://nesting` resource body.\r\n const exposedCollectionsForNesting = collections.filter((c) => !excluded.has(c.slug))\r\n const exposedGlobalsForNesting = globals.filter((g) => !excludedGlobals.has(g.slug))\r\n const blockNesting = buildBlockNestingMap(\r\n exposedCollectionsForNesting,\r\n exposedGlobalsForNesting,\r\n allBlocks,\r\n )\r\n\r\n // Build a slug → CollectionConfig map for tools that need access to\r\n // collection-level admin config (preview functions, etc).\r\n const collectionsBySlug = new Map<string, CollectionConfig>()\r\n for (const c of collections) {\r\n if (!excluded.has(c.slug)) collectionsBySlug.set(c.slug, c)\r\n }\r\n\r\n // Schemas-without-excluded view, used by the polymorphic tool factories\r\n // so excluded collections don't appear in tool descriptions.\r\n const exposedSchemas = new Map<string, ReturnType<typeof introspectCollections> extends Map<string, infer V> ? V : never>()\r\n for (const [slug, schema] of collectionSchemas) {\r\n if (!excluded.has(slug)) exposedSchemas.set(slug, schema)\r\n }\r\n\r\n // Same shape for globals — excluded slugs are stripped at registration\r\n // time so they never reach Zod input enums, the globals://schema\r\n // resource, or availableGlobals on the admin matrix. composeScopes\r\n // stays exclusion-unaware (mirrors the collection mechanism).\r\n const exposedGlobalSchemas = new Map<string, GlobalSchema>()\r\n const globalsBySlug = new Map<string, GlobalConfig>()\r\n for (const [slug, schema] of globalSchemas) {\r\n if (excludedGlobals.has(slug)) continue\r\n exposedGlobalSchemas.set(slug, schema)\r\n }\r\n for (const g of globals) {\r\n if (!excludedGlobals.has(g.slug)) globalsBySlug.set(g.slug, g)\r\n }\r\n\r\n const prompts = generatePrompts(\r\n exposedSchemas,\r\n blockCatalog,\r\n blockNesting,\r\n relationships,\r\n options.domainPrompts,\r\n )\r\n const resources = generateResources(\r\n exposedSchemas,\r\n blockCatalog,\r\n blockNesting,\r\n relationships,\r\n exposedGlobalSchemas,\r\n )\r\n\r\n const searchableCollections = new Map<string, string[]>()\r\n for (const [slug, schema] of exposedSchemas) {\r\n if (schema.searchableFields.length > 0) {\r\n searchableCollections.set(slug, schema.searchableFields)\r\n }\r\n }\r\n\r\n const tools: ToolFactoryOutput[] = [\r\n createCreateDocumentTool(exposedSchemas, draftCollections),\r\n createDeleteDocumentTool(exposedSchemas),\r\n createFindDocumentTool(\r\n exposedSchemas,\r\n draftCollections,\r\n collectionsBySlug,\r\n previewSiteUrl,\r\n previewDisabled,\r\n ),\r\n createPatchLayoutTool(blockCatalog, blockNesting, draftCollections),\r\n createPublishDraftTool(draftCollections),\r\n createResolveReferenceTool(searchableCollections),\r\n createSafeDeleteTool(relationships),\r\n createSearchContentTool(exposedSchemas),\r\n createUpdateDocumentTool(exposedSchemas, draftCollections),\r\n createUploadMediaTool({\r\n maxFileSize: options.mediaUpload?.maxFileSize,\r\n collectionSlug: options.mediaUpload?.collectionSlug,\r\n }),\r\n createListVersionsTool(draftCollections),\r\n createRestoreVersionTool(draftCollections),\r\n ]\r\n\r\n const schedulePublish = createSchedulePublishTool(exposedSchemas, draftCollections)\r\n if (schedulePublish) tools.push(schedulePublish)\r\n\r\n // Global tools — registered only when at least one global is exposed.\r\n if (exposedGlobalSchemas.size > 0) {\r\n tools.push(\r\n createFindGlobalTool(\r\n exposedGlobalSchemas,\r\n draftGlobals,\r\n globalsBySlug,\r\n previewSiteUrl,\r\n previewDisabled,\r\n ),\r\n createUpdateGlobalTool(exposedGlobalSchemas, draftGlobals),\r\n )\r\n\r\n const patchGlobalLayout = createPatchGlobalLayoutTool(blockCatalog, blockNesting, draftGlobals)\r\n if (patchGlobalLayout) tools.push(patchGlobalLayout)\r\n\r\n const publishGlobalDraft = createPublishGlobalDraftTool(draftGlobals)\r\n if (publishGlobalDraft) tools.push(publishGlobalDraft)\r\n\r\n const listGlobalVersions = createListGlobalVersionsTool(draftGlobals)\r\n if (listGlobalVersions) tools.push(listGlobalVersions)\r\n\r\n const restoreGlobalVersion = createRestoreGlobalVersionTool(draftGlobals)\r\n if (restoreGlobalVersion) tools.push(restoreGlobalVersion)\r\n }\r\n\r\n // Build the per-request initializer that mcp-handler invokes.\r\n const buildInitializeServer = createInitializeServer({\r\n tools,\r\n prompts: prompts as never,\r\n resources: resources as never,\r\n })\r\n\r\n // Attach API-keys collection. Available collections / tools are\r\n // snapshotted now so the admin UI's scope dropdowns reflect the host\r\n // config at boot time. Adding a collection requires a dev restart for\r\n // it to surface as a scope option.\r\n const userCollection = resolveUserCollection(options, incomingConfig)\r\n const availableCollections = collections\r\n .map((c) => c.slug)\r\n .filter((s) => s !== apiKeysSlug)\r\n const availableGlobals = [...exposedGlobalSchemas.keys()]\r\n const availableTools = tools.map((t) => t.name)\r\n const apiKeysCollection = createApiKeysCollection({\r\n slug: apiKeysSlug,\r\n userCollection,\r\n availableCollections,\r\n availableGlobals,\r\n availableTools,\r\n })\r\n const updatedCollections: CollectionConfig[] = [...collections, apiKeysCollection]\r\n\r\n // Attach the bearer strategy to the user collection's auth config.\r\n const bearerStrategy = createBearerStrategy({\r\n collectionSlug: apiKeysSlug,\r\n userCollection,\r\n })\r\n const collectionsWithStrategy = updatedCollections.map((c) => {\r\n if (c.slug !== userCollection) return c\r\n const existingAuth = c.auth\r\n if (!existingAuth) return c\r\n const authConfig =\r\n typeof existingAuth === 'object' && existingAuth !== null\r\n ? { ...existingAuth }\r\n : { useAPIKey: existingAuth === true ? false : false }\r\n const existingStrategies = Array.isArray((authConfig as { strategies?: unknown[] }).strategies)\r\n ? ((authConfig as { strategies: unknown[] }).strategies as unknown[])\r\n : []\r\n ;(authConfig as { strategies: unknown[] }).strategies = [\r\n ...existingStrategies,\r\n bearerStrategy,\r\n ]\r\n return { ...c, auth: authConfig } as CollectionConfig\r\n })\r\n\r\n // Attach the MCP endpoints additively to the host config.\r\n const mcpEndpoints = createMcpEndpoints({\r\n buildInitializeServer,\r\n allowedOrigins: options.auth?.allowedOrigins,\r\n serverURL: incomingConfig.serverURL,\r\n })\r\n\r\n return {\r\n ...incomingConfig,\r\n collections: collectionsWithStrategy,\r\n endpoints: [...(incomingConfig.endpoints ?? []), ...mcpEndpoints],\r\n }\r\n }\r\n}\r\n\r\nexport type {\r\n ContentToolkitOptions,\r\n DomainPrompt,\r\n CollectionSchema,\r\n GlobalSchema,\r\n BlockCatalog,\r\n BlockSchema,\r\n BlockNestingMap,\r\n BlockNestingEdge,\r\n RelationshipEdge,\r\n FieldSchema,\r\n CollectionAction,\r\n GlobalAction,\r\n KeyScopes,\r\n ScopePreset,\r\n} from './types'\r\n"],"names":["introspectCollections","introspectGlobals","introspectBlocks","buildBlockNestingMap","buildRelationshipGraph","generatePrompts","generateResources","computeDraftCollections","computeDraftGlobals","createApiKeysCollection","API_KEYS_DEFAULT_SLUG","createBearerStrategy","createMcpEndpoints","createInitializeServer","assertNoSlugConflict","assertNoUpstreamPlugin","createCreateDocumentTool","createDeleteDocumentTool","createFindDocumentTool","createFindGlobalTool","createUpdateGlobalTool","createPatchGlobalLayoutTool","createPublishGlobalDraftTool","createListGlobalVersionsTool","createRestoreGlobalVersionTool","createPatchLayoutTool","createPublishDraftTool","createResolveReferenceTool","createSafeDeleteTool","createSchedulePublishTool","createSearchContentTool","createUpdateDocumentTool","createUploadMediaTool","createListVersionsTool","createRestoreVersionTool","resolveUserCollection","options","incomingConfig","apiKeyCollection","userCollection","admin","user","mcpToolkitPlugin","apiKeysSlug","slug","plugins","collections","globals","allBlocks","blocks","collectionSchemas","globalSchemas","blockCatalog","relationships","previewSiteUrl","preview","disabled","undefined","siteUrl","serverURL","process","env","NEXT_PUBLIC_SERVER_URL","SITE_URL","previewDisabled","draftCollections","excluded","draftBehavior","excludeCollections","exclude","draftGlobals","excludedGlobals","excludeGlobals","exposedCollectionsForNesting","filter","c","has","exposedGlobalsForNesting","g","blockNesting","collectionsBySlug","Map","set","exposedSchemas","schema","exposedGlobalSchemas","globalsBySlug","prompts","domainPrompts","resources","searchableCollections","searchableFields","length","tools","maxFileSize","mediaUpload","collectionSlug","schedulePublish","push","size","patchGlobalLayout","publishGlobalDraft","listGlobalVersions","restoreGlobalVersion","buildInitializeServer","availableCollections","map","s","availableGlobals","keys","availableTools","t","name","apiKeysCollection","updatedCollections","bearerStrategy","collectionsWithStrategy","existingAuth","auth","authConfig","useAPIKey","existingStrategies","Array","isArray","strategies","mcpEndpoints","allowedOrigins","endpoints"],"mappings":"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"}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { Block, CollectionConfig, Config, GlobalConfig, Plugin } from 'payload'\nimport type { ContentToolkitOptions, GlobalSchema } from './types'\nimport {\n introspectCollections,\n introspectGlobals,\n introspectBlocks,\n buildBlockNestingMap,\n buildRelationshipGraph,\n} from './introspection'\nimport { generatePrompts } from './prompts'\nimport { generateResources } from './resources'\nimport { computeDraftCollections, computeDraftGlobals } from './draft-workflow'\nimport { createApiKeysCollection, API_KEYS_DEFAULT_SLUG } from './api-keys'\nimport { createBearerStrategy } from './auth-strategy'\nimport { createMcpEndpoints } from './endpoint'\nimport {\n createInitializeServer,\n type ToolFactoryOutput,\n} from './registry'\nimport {\n assertNoSlugConflict,\n assertNoToolNameConflict,\n assertNoUpstreamPlugin,\n} from './conflict-detection'\nimport { createCreateDocumentTool } from './tools/create-document'\nimport { createDeleteDocumentTool } from './tools/delete-document'\nimport { createFindDocumentTool } from './tools/find-document'\nimport { createFindGlobalTool } from './tools/find-global'\nimport { createUpdateGlobalTool } from './tools/update-global'\nimport { createPatchGlobalLayoutTool } from './tools/patch-global-layout'\nimport { createPublishGlobalDraftTool } from './tools/publish-global-draft'\nimport {\n createListGlobalVersionsTool,\n createRestoreGlobalVersionTool,\n} from './tools/global-versions'\nimport { createPatchLayoutTool } from './tools/patch-layout'\nimport { createPublishDraftTool } from './tools/publish-draft'\nimport { createResolveReferenceTool } from './tools/resolve-reference'\nimport { createSafeDeleteTool } from './tools/safe-delete'\nimport { createSchedulePublishTool } from './tools/schedule-publish'\nimport { createSearchContentTool } from './tools/search-content'\nimport { createUpdateDocumentTool } from './tools/update-document'\nimport { createUploadMediaTool } from './tools/upload-media'\nimport { createListVersionsTool, createRestoreVersionTool } from './tools/versions'\n\n/**\n * Resolves the user collection slug for API-key linkage.\n *\n * Resolution order: explicit `options.apiKeyCollection.userCollection` →\n * explicit `options.userCollection` → `incomingConfig.admin.user` →\n * 'users' as a last resort.\n */\nfunction resolveUserCollection(\n options: ContentToolkitOptions,\n incomingConfig: Config,\n): string {\n return (\n options.apiKeyCollection?.userCollection ??\n options.userCollection ??\n (incomingConfig.admin?.user as string | undefined) ??\n 'users'\n )\n}\n\n/**\n * payload-mcp-toolkit — standalone Payload v3 MCP plugin.\n *\n * Owns the `/api/mcp` endpoint, the `payload-mcp-api-keys` collection,\n * bearer authentication via Payload's `auth.strategies` extension point,\n * and the per-tool scope check. Upstream `@payloadcms/plugin-mcp` is no\n * longer required (and is incompatible — see `assertNoUpstreamPlugin`).\n *\n * Zero-config usage:\n * ```ts\n * plugins: [mcpToolkitPlugin()]\n * ```\n *\n * See `ContentToolkitOptions` for the (entirely optional) escape hatches.\n */\nexport function mcpToolkitPlugin(options: ContentToolkitOptions = {}): Plugin {\n return (incomingConfig: Config): Config => {\n const apiKeysSlug = options.apiKeyCollection?.slug ?? API_KEYS_DEFAULT_SLUG\n\n // Conflict detection — fail fast with actionable messages.\n assertNoUpstreamPlugin(incomingConfig.plugins)\n assertNoSlugConflict(incomingConfig.collections as CollectionConfig[] | undefined, apiKeysSlug)\n\n const collections = (incomingConfig.collections ?? []) as CollectionConfig[]\n const globals = (incomingConfig.globals ?? []) as GlobalConfig[]\n const allBlocks = (incomingConfig.blocks ?? []) as Block[]\n\n const collectionSchemas = introspectCollections(collections)\n const globalSchemas = introspectGlobals(globals)\n const blockCatalog = introspectBlocks(allBlocks)\n const relationships = buildRelationshipGraph(collectionSchemas)\n\n const previewSiteUrl = options.preview?.disabled\n ? undefined\n : options.preview?.siteUrl ??\n incomingConfig.serverURL ??\n process.env.NEXT_PUBLIC_SERVER_URL ??\n process.env.SITE_URL\n const previewDisabled = options.preview?.disabled === true\n\n const { draftCollections, excluded } = computeDraftCollections(collections, {\n draftBehavior: options.draftBehavior,\n excludeCollections: options.exclude?.collections,\n apiKeysSlug,\n })\n\n const { draftGlobals, excluded: excludedGlobals } = computeDraftGlobals(globals, {\n draftBehavior: options.draftBehavior,\n excludeGlobals: options.exclude?.globals,\n })\n\n // Build blockNesting from exclusion-filtered inputs so excluded\n // collections/globals never appear in patchLayout/patchGlobalLayout slug\n // enums or in the `blocks://nesting` resource body.\n const exposedCollectionsForNesting = collections.filter((c) => !excluded.has(c.slug))\n const exposedGlobalsForNesting = globals.filter((g) => !excludedGlobals.has(g.slug))\n const blockNesting = buildBlockNestingMap(\n exposedCollectionsForNesting,\n exposedGlobalsForNesting,\n allBlocks,\n )\n\n // Build a slug → CollectionConfig map for tools that need access to\n // collection-level admin config (preview functions, etc).\n const collectionsBySlug = new Map<string, CollectionConfig>()\n for (const c of collections) {\n if (!excluded.has(c.slug)) collectionsBySlug.set(c.slug, c)\n }\n\n // Schemas-without-excluded view, used by the polymorphic tool factories\n // so excluded collections don't appear in tool descriptions.\n const exposedSchemas = new Map<string, ReturnType<typeof introspectCollections> extends Map<string, infer V> ? V : never>()\n for (const [slug, schema] of collectionSchemas) {\n if (!excluded.has(slug)) exposedSchemas.set(slug, schema)\n }\n\n // Same shape for globals — excluded slugs are stripped at registration\n // time so they never reach Zod input enums, the globals://schema\n // resource, or availableGlobals on the admin matrix. composeScopes\n // stays exclusion-unaware (mirrors the collection mechanism).\n const exposedGlobalSchemas = new Map<string, GlobalSchema>()\n const globalsBySlug = new Map<string, GlobalConfig>()\n for (const [slug, schema] of globalSchemas) {\n if (excludedGlobals.has(slug)) continue\n exposedGlobalSchemas.set(slug, schema)\n }\n for (const g of globals) {\n if (!excludedGlobals.has(g.slug)) globalsBySlug.set(g.slug, g)\n }\n\n const prompts = generatePrompts(\n exposedSchemas,\n blockCatalog,\n blockNesting,\n relationships,\n options.domainPrompts,\n )\n const resources = generateResources(\n exposedSchemas,\n blockCatalog,\n blockNesting,\n relationships,\n exposedGlobalSchemas,\n )\n\n const searchableCollections = new Map<string, string[]>()\n for (const [slug, schema] of exposedSchemas) {\n if (schema.searchableFields.length > 0) {\n searchableCollections.set(slug, schema.searchableFields)\n }\n }\n\n const tools: ToolFactoryOutput[] = [\n createCreateDocumentTool(exposedSchemas, draftCollections),\n createDeleteDocumentTool(exposedSchemas),\n createFindDocumentTool(\n exposedSchemas,\n draftCollections,\n collectionsBySlug,\n previewSiteUrl,\n previewDisabled,\n ),\n createPatchLayoutTool(blockCatalog, blockNesting, draftCollections),\n createPublishDraftTool(draftCollections),\n createResolveReferenceTool(searchableCollections),\n createSafeDeleteTool(relationships),\n createSearchContentTool(exposedSchemas),\n createUpdateDocumentTool(exposedSchemas, draftCollections),\n createUploadMediaTool({\n maxFileSize: options.mediaUpload?.maxFileSize,\n collectionSlug: options.mediaUpload?.collectionSlug,\n }),\n createListVersionsTool(draftCollections),\n createRestoreVersionTool(draftCollections),\n ]\n\n const schedulePublish = createSchedulePublishTool(exposedSchemas, draftCollections)\n if (schedulePublish) tools.push(schedulePublish)\n\n // Global tools — registered only when at least one global is exposed.\n if (exposedGlobalSchemas.size > 0) {\n tools.push(\n createFindGlobalTool(\n exposedGlobalSchemas,\n draftGlobals,\n globalsBySlug,\n previewSiteUrl,\n previewDisabled,\n ),\n createUpdateGlobalTool(exposedGlobalSchemas, draftGlobals),\n )\n\n const patchGlobalLayout = createPatchGlobalLayoutTool(blockCatalog, blockNesting, draftGlobals)\n if (patchGlobalLayout) tools.push(patchGlobalLayout)\n\n const publishGlobalDraft = createPublishGlobalDraftTool(draftGlobals)\n if (publishGlobalDraft) tools.push(publishGlobalDraft)\n\n const listGlobalVersions = createListGlobalVersionsTool(draftGlobals)\n if (listGlobalVersions) tools.push(listGlobalVersions)\n\n const restoreGlobalVersion = createRestoreGlobalVersionTool(draftGlobals)\n if (restoreGlobalVersion) tools.push(restoreGlobalVersion)\n }\n\n // Host-supplied tools go on the end, so a built-in never loses its slot in\n // tools/list. They share the wrapper: scope check, mcp stamp, audit log.\n assertNoToolNameConflict(tools, options.customTools)\n if (options.customTools?.length) tools.push(...options.customTools)\n\n // Build the per-request initializer that mcp-handler invokes.\n const buildInitializeServer = createInitializeServer({\n tools,\n prompts: prompts as never,\n resources: resources as never,\n })\n\n // Attach API-keys collection. Available collections / tools are\n // snapshotted now so the admin UI's scope dropdowns reflect the host\n // config at boot time. Adding a collection requires a dev restart for\n // it to surface as a scope option.\n const userCollection = resolveUserCollection(options, incomingConfig)\n const availableCollections = collections\n .map((c) => c.slug)\n .filter((s) => s !== apiKeysSlug)\n const availableGlobals = [...exposedGlobalSchemas.keys()]\n const availableTools = tools.map((t) => t.name)\n const apiKeysCollection = createApiKeysCollection({\n slug: apiKeysSlug,\n userCollection,\n availableCollections,\n availableGlobals,\n availableTools,\n })\n const updatedCollections: CollectionConfig[] = [...collections, apiKeysCollection]\n\n // Attach the bearer strategy to the user collection's auth config.\n const bearerStrategy = createBearerStrategy({\n collectionSlug: apiKeysSlug,\n userCollection,\n })\n const collectionsWithStrategy = updatedCollections.map((c) => {\n if (c.slug !== userCollection) return c\n const existingAuth = c.auth\n if (!existingAuth) return c\n const authConfig =\n typeof existingAuth === 'object' && existingAuth !== null\n ? { ...existingAuth }\n : { useAPIKey: existingAuth === true ? false : false }\n const existingStrategies = Array.isArray((authConfig as { strategies?: unknown[] }).strategies)\n ? ((authConfig as { strategies: unknown[] }).strategies as unknown[])\n : []\n ;(authConfig as { strategies: unknown[] }).strategies = [\n ...existingStrategies,\n bearerStrategy,\n ]\n return { ...c, auth: authConfig } as CollectionConfig\n })\n\n // Attach the MCP endpoints additively to the host config.\n const mcpEndpoints = createMcpEndpoints({\n buildInitializeServer,\n allowedOrigins: options.auth?.allowedOrigins,\n serverURL: incomingConfig.serverURL,\n })\n\n return {\n ...incomingConfig,\n collections: collectionsWithStrategy,\n endpoints: [...(incomingConfig.endpoints ?? []), ...mcpEndpoints],\n }\n }\n}\n\n// Runtime helpers for building custom tools — the same envelope builders the\n// built-in tools use, so a host tool returns the identical result shape.\nexport { jsonResponse, textResponse } from './tools/_helpers'\n\nexport type { ToolFactoryOutput } from './registry'\nexport type { ToolRouting, ResourceKind } from './scope/policy'\nexport type { McpTextResponse } from './tools/_helpers'\n\nexport type {\n ContentToolkitOptions,\n DomainPrompt,\n CollectionSchema,\n GlobalSchema,\n BlockCatalog,\n BlockSchema,\n BlockNestingMap,\n BlockNestingEdge,\n RelationshipEdge,\n FieldSchema,\n CollectionAction,\n GlobalAction,\n KeyScopes,\n ScopePreset,\n} from './types'\n"],"names":["introspectCollections","introspectGlobals","introspectBlocks","buildBlockNestingMap","buildRelationshipGraph","generatePrompts","generateResources","computeDraftCollections","computeDraftGlobals","createApiKeysCollection","API_KEYS_DEFAULT_SLUG","createBearerStrategy","createMcpEndpoints","createInitializeServer","assertNoSlugConflict","assertNoToolNameConflict","assertNoUpstreamPlugin","createCreateDocumentTool","createDeleteDocumentTool","createFindDocumentTool","createFindGlobalTool","createUpdateGlobalTool","createPatchGlobalLayoutTool","createPublishGlobalDraftTool","createListGlobalVersionsTool","createRestoreGlobalVersionTool","createPatchLayoutTool","createPublishDraftTool","createResolveReferenceTool","createSafeDeleteTool","createSchedulePublishTool","createSearchContentTool","createUpdateDocumentTool","createUploadMediaTool","createListVersionsTool","createRestoreVersionTool","resolveUserCollection","options","incomingConfig","apiKeyCollection","userCollection","admin","user","mcpToolkitPlugin","apiKeysSlug","slug","plugins","collections","globals","allBlocks","blocks","collectionSchemas","globalSchemas","blockCatalog","relationships","previewSiteUrl","preview","disabled","undefined","siteUrl","serverURL","process","env","NEXT_PUBLIC_SERVER_URL","SITE_URL","previewDisabled","draftCollections","excluded","draftBehavior","excludeCollections","exclude","draftGlobals","excludedGlobals","excludeGlobals","exposedCollectionsForNesting","filter","c","has","exposedGlobalsForNesting","g","blockNesting","collectionsBySlug","Map","set","exposedSchemas","schema","exposedGlobalSchemas","globalsBySlug","prompts","domainPrompts","resources","searchableCollections","searchableFields","length","tools","maxFileSize","mediaUpload","collectionSlug","schedulePublish","push","size","patchGlobalLayout","publishGlobalDraft","listGlobalVersions","restoreGlobalVersion","customTools","buildInitializeServer","availableCollections","map","s","availableGlobals","keys","availableTools","t","name","apiKeysCollection","updatedCollections","bearerStrategy","collectionsWithStrategy","existingAuth","auth","authConfig","useAPIKey","existingStrategies","Array","isArray","strategies","mcpEndpoints","allowedOrigins","endpoints","jsonResponse","textResponse"],"mappings":"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"}
package/dist/registry.js CHANGED
@@ -3,11 +3,11 @@ import { getApiKeyContext } from './auth-strategy';
3
3
  import { stampMcpContext } from './tools/_helpers';
4
4
  import { assertScopeAllows, buildRoutingTables, buildScopeChecker } from './scope/policy';
5
5
  import { extractDataKeys, getRequestId, makeSafeLog, summariseArgs } from './scope/audit-log';
6
- /**
7
- * Returns the raw `{ name: ZodType }` shape for either a raw shape or a
8
- * `z.object({...})` instance. The MCP SDK's `registerTool` expects the raw
9
- * shape under `inputSchema`; passing a ZodObject silently registers an
10
- * empty schema and breaks args validation.
6
+ /**
7
+ * Returns the raw `{ name: ZodType }` shape for either a raw shape or a
8
+ * `z.object({...})` instance. The MCP SDK's `registerTool` expects the raw
9
+ * shape under `inputSchema`; passing a ZodObject silently registers an
10
+ * empty schema and breaks args validation.
11
11
  */ function toZodShape(parameters) {
12
12
  if (parameters instanceof ZodObject) {
13
13
  return parameters.shape;
@@ -27,17 +27,17 @@ function scopeRejectionResult(reason) {
27
27
  isError: true
28
28
  };
29
29
  }
30
- /**
31
- * Builds the per-request initializer. mcp-handler invokes this once per
32
- * `tools/call` / `tools/list` JSON-RPC request, passing a fresh McpServer.
33
- *
34
- * Each tool handler is wrapped to:
35
- * 1. Read the API-key context populated by the bearer auth strategy.
36
- * 2. Reject the call (as an `isError: true` result, not a JSON-RPC error)
37
- * when scopes deny it — per the MCP spec, tool-execution failures
38
- * surface in the result envelope so the LLM can self-correct.
39
- * 3. Stamp `req.context.source = 'mcp'` for downstream hooks.
40
- * 4. Emit a structured audit log entry on every success / failure.
30
+ /**
31
+ * Builds the per-request initializer. mcp-handler invokes this once per
32
+ * `tools/call` / `tools/list` JSON-RPC request, passing a fresh McpServer.
33
+ *
34
+ * Each tool handler is wrapped to:
35
+ * 1. Read the API-key context populated by the bearer auth strategy.
36
+ * 2. Reject the call (as an `isError: true` result, not a JSON-RPC error)
37
+ * when scopes deny it — per the MCP spec, tool-execution failures
38
+ * surface in the result envelope so the LLM can self-correct.
39
+ * 3. Stamp `req.context.source = 'mcp'` for downstream hooks.
40
+ * 4. Emit a structured audit log entry on every success / failure.
41
41
  */ export function createInitializeServer(options) {
42
42
  const { tools, prompts = [], resources = [] } = options;
43
43
  const tables = buildRoutingTables(tools);
@@ -74,7 +74,12 @@ function scopeRejectionResult(reason) {
74
74
  stampMcpContext(req);
75
75
  try {
76
76
  const result = await tool.handler(args, req, extra);
77
- safeLog('info', {
77
+ // A handler may report failure in the result envelope instead of
78
+ // throwing — the MCP spec's own way of letting the model self-correct.
79
+ // Logging that as a success would hide every such failure from
80
+ // monitoring.
81
+ const reportedError = result?.isError === true;
82
+ safeLog(reportedError ? 'warn' : 'info', {
78
83
  event: 'mcp.tool_call',
79
84
  keyId: keyCtx?.keyId,
80
85
  keyPrefix: keyCtx?.keyPrefix,
@@ -82,11 +87,14 @@ function scopeRejectionResult(reason) {
82
87
  targetSlug,
83
88
  targetKind,
84
89
  dataKeys,
85
- success: true,
86
- isError: false,
90
+ success: !reportedError,
91
+ isError: reportedError,
87
92
  durationMs: Date.now() - start,
88
- requestId
89
- }, `[payload-mcp-toolkit] Tool call: ${tool.name}`);
93
+ requestId,
94
+ ...reportedError ? {
95
+ errorClass: 'HandlerReportedError'
96
+ } : {}
97
+ }, `[payload-mcp-toolkit] Tool call${reportedError ? ' reported an error' : ''}: ${tool.name}`);
90
98
  return result;
91
99
  } catch (err) {
92
100
  const errorClass = err instanceof Error ? err.name : 'UnknownError';
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/registry.ts"],"sourcesContent":["import type { PayloadRequest } from 'payload'\r\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\r\nimport { ZodObject, type ZodTypeAny } from 'zod'\r\nimport { getApiKeyContext } from './auth-strategy'\r\nimport type { InitializeServerForRequest } from './endpoint'\r\nimport { stampMcpContext, type McpTextResponse } from './tools/_helpers'\r\nimport {\r\n assertScopeAllows,\r\n buildRoutingTables,\r\n buildScopeChecker,\r\n type ResourceKind,\r\n type RoutingTables,\r\n type ScopeChecker,\r\n type ScopeDecision,\r\n type ToolRouting,\r\n} from './scope/policy'\r\nimport { extractDataKeys, getRequestId, makeSafeLog, summariseArgs } from './scope/audit-log'\r\n\r\n// ─── Tool / Prompt / Resource shapes ──────────────────────────────────\r\n\r\nexport interface ToolFactoryOutput {\r\n name: string\r\n description: string\r\n /**\r\n * Either a raw Zod shape (`{ key: ZodType }`) or a `z.object({...})`\r\n * instance. The registry normalises both before registering with the SDK.\r\n */\r\n parameters: Record<string, ZodTypeAny> | ZodObject<Record<string, ZodTypeAny>>\r\n handler: (\r\n args: Record<string, unknown>,\r\n req: PayloadRequest,\r\n extra: unknown,\r\n ) => Promise<McpTextResponse> | McpTextResponse\r\n routing: ToolRouting\r\n}\r\n\r\n/**\r\n * Returns the raw `{ name: ZodType }` shape for either a raw shape or a\r\n * `z.object({...})` instance. The MCP SDK's `registerTool` expects the raw\r\n * shape under `inputSchema`; passing a ZodObject silently registers an\r\n * empty schema and breaks args validation.\r\n */\r\nfunction toZodShape(\r\n parameters: Record<string, ZodTypeAny> | ZodObject<Record<string, ZodTypeAny>>,\r\n): Record<string, ZodTypeAny> {\r\n if (parameters instanceof ZodObject) {\r\n return parameters.shape as Record<string, ZodTypeAny>\r\n }\r\n return parameters\r\n}\r\n\r\nexport interface PromptFactoryOutput {\r\n name: string\r\n title?: string\r\n description?: string\r\n argsSchema?: Record<string, ZodTypeAny>\r\n handler: (args: unknown, req: PayloadRequest, extra: unknown) => unknown\r\n}\r\n\r\nexport interface ResourceFactoryOutput {\r\n name: string\r\n uri: string\r\n title?: string\r\n description?: string\r\n mimeType?: string\r\n handler: (args: unknown, req: PayloadRequest, extra: unknown) => unknown\r\n}\r\n\r\n// Re-export scope-routing primitives so existing callers keep working.\r\nexport {\r\n buildScopeChecker,\r\n type ResourceKind,\r\n type RoutingTables,\r\n type ScopeChecker,\r\n type ScopeDecision,\r\n type ToolRouting,\r\n}\r\n\r\n// ─── Initializer factory ─────────────────────────────────────────────\r\n\r\nexport interface CreateInitializeServerOptions {\r\n tools: ToolFactoryOutput[]\r\n prompts?: PromptFactoryOutput[]\r\n resources?: ResourceFactoryOutput[]\r\n}\r\n\r\ninterface ScopeRejectionResult {\r\n content: Array<{ type: 'text'; text: string }>\r\n isError: true\r\n}\r\n\r\nfunction scopeRejectionResult(reason: string): ScopeRejectionResult {\r\n return {\r\n content: [{ type: 'text', text: `Scope rejection: ${reason}` }],\r\n isError: true,\r\n }\r\n}\r\n\r\n/**\r\n * Builds the per-request initializer. mcp-handler invokes this once per\r\n * `tools/call` / `tools/list` JSON-RPC request, passing a fresh McpServer.\r\n *\r\n * Each tool handler is wrapped to:\r\n * 1. Read the API-key context populated by the bearer auth strategy.\r\n * 2. Reject the call (as an `isError: true` result, not a JSON-RPC error)\r\n * when scopes deny it — per the MCP spec, tool-execution failures\r\n * surface in the result envelope so the LLM can self-correct.\r\n * 3. Stamp `req.context.source = 'mcp'` for downstream hooks.\r\n * 4. Emit a structured audit log entry on every success / failure.\r\n */\r\nexport function createInitializeServer(\r\n options: CreateInitializeServerOptions,\r\n): InitializeServerForRequest {\r\n const { tools, prompts = [], resources = [] } = options\r\n const tables: RoutingTables = buildRoutingTables(tools)\r\n\r\n return (req: PayloadRequest) => (server: McpServer) => {\r\n const logger = req.payload?.logger\r\n const requestId = getRequestId(req)\r\n const safeLog = makeSafeLog(logger)\r\n\r\n for (const tool of tools) {\r\n const resourceKind = tables.toolKind.get(tool.name) ?? null\r\n const wrapped = async (\r\n args: Record<string, unknown>,\r\n extra: unknown,\r\n ): Promise<unknown> => {\r\n const start = Date.now()\r\n const keyCtx = getApiKeyContext(req)\r\n const targetSlug =\r\n typeof args.collection === 'string'\r\n ? args.collection\r\n : typeof args.slug === 'string'\r\n ? args.slug\r\n : undefined\r\n const targetKind = resourceKind ?? undefined\r\n const dataKeys = extractDataKeys(args)\r\n\r\n const decision = assertScopeAllows(\r\n keyCtx?.scopes ?? null,\r\n tool.name,\r\n targetSlug,\r\n tables,\r\n )\r\n\r\n if (!decision.allowed) {\r\n safeLog(\r\n 'warn',\r\n {\r\n event: 'mcp.tool_call',\r\n keyId: keyCtx?.keyId,\r\n keyPrefix: keyCtx?.keyPrefix,\r\n tool: tool.name,\r\n targetSlug,\r\n targetKind,\r\n dataKeys,\r\n success: false,\r\n isError: true,\r\n durationMs: Date.now() - start,\r\n requestId,\r\n errorClass: 'ScopeRejection',\r\n },\r\n `[payload-mcp-toolkit] Scope-rejected tool call: ${tool.name}`,\r\n )\r\n return scopeRejectionResult(decision.reason ?? 'denied')\r\n }\r\n\r\n stampMcpContext(req)\r\n\r\n try {\r\n const result = await tool.handler(args, req, extra)\r\n safeLog(\r\n 'info',\r\n {\r\n event: 'mcp.tool_call',\r\n keyId: keyCtx?.keyId,\r\n keyPrefix: keyCtx?.keyPrefix,\r\n tool: tool.name,\r\n targetSlug,\r\n targetKind,\r\n dataKeys,\r\n success: true,\r\n isError: false,\r\n durationMs: Date.now() - start,\r\n requestId,\r\n },\r\n `[payload-mcp-toolkit] Tool call: ${tool.name}`,\r\n )\r\n return result\r\n } catch (err) {\r\n const errorClass = err instanceof Error ? err.name : 'UnknownError'\r\n const message = err instanceof Error ? err.message : String(err)\r\n safeLog(\r\n 'error',\r\n {\r\n event: 'mcp.tool_call',\r\n err,\r\n keyId: keyCtx?.keyId,\r\n keyPrefix: keyCtx?.keyPrefix,\r\n tool: tool.name,\r\n targetSlug,\r\n targetKind,\r\n dataKeys,\r\n argsSummary: summariseArgs(args),\r\n success: false,\r\n isError: true,\r\n durationMs: Date.now() - start,\r\n requestId,\r\n errorClass,\r\n },\r\n `[payload-mcp-toolkit] Tool call failed: ${tool.name}`,\r\n )\r\n return {\r\n content: [{ type: 'text', text: `Error: ${message}` }],\r\n isError: true,\r\n }\r\n }\r\n }\r\n\r\n server.registerTool(\r\n tool.name,\r\n {\r\n description: tool.description,\r\n inputSchema: toZodShape(tool.parameters),\r\n },\r\n wrapped as never,\r\n )\r\n }\r\n\r\n for (const prompt of prompts) {\r\n const wrapped = async (args: unknown, extra: unknown) => {\r\n try {\r\n return await prompt.handler(args, req, extra)\r\n } catch (err) {\r\n logger?.error?.(\r\n { event: 'mcp.prompt', err, prompt: prompt.name, requestId },\r\n `[payload-mcp-toolkit] Prompt failed: ${prompt.name}`,\r\n )\r\n throw err\r\n }\r\n }\r\n server.registerPrompt(\r\n prompt.name,\r\n {\r\n title: prompt.title,\r\n description: prompt.description,\r\n argsSchema: prompt.argsSchema as never,\r\n },\r\n wrapped as never,\r\n )\r\n }\r\n\r\n for (const resource of resources) {\r\n const wrapped = async (args: unknown, extra: unknown) => {\r\n try {\r\n return await resource.handler(args, req, extra)\r\n } catch (err) {\r\n logger?.error?.(\r\n { event: 'mcp.resource', err, resource: resource.name, requestId },\r\n `[payload-mcp-toolkit] Resource read failed: ${resource.name}`,\r\n )\r\n throw err\r\n }\r\n }\r\n server.registerResource(\r\n resource.name,\r\n resource.uri as never,\r\n {\r\n title: resource.title,\r\n description: resource.description,\r\n mimeType: resource.mimeType,\r\n },\r\n wrapped as never,\r\n )\r\n }\r\n }\r\n}\r\n"],"names":["ZodObject","getApiKeyContext","stampMcpContext","assertScopeAllows","buildRoutingTables","buildScopeChecker","extractDataKeys","getRequestId","makeSafeLog","summariseArgs","toZodShape","parameters","shape","scopeRejectionResult","reason","content","type","text","isError","createInitializeServer","options","tools","prompts","resources","tables","req","server","logger","payload","requestId","safeLog","tool","resourceKind","toolKind","get","name","wrapped","args","extra","start","Date","now","keyCtx","targetSlug","collection","slug","undefined","targetKind","dataKeys","decision","scopes","allowed","event","keyId","keyPrefix","success","durationMs","errorClass","result","handler","err","Error","message","String","argsSummary","registerTool","description","inputSchema","prompt","error","registerPrompt","title","argsSchema","resource","registerResource","uri","mimeType"],"mappings":"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"}
1
+ {"version":3,"sources":["../src/registry.ts"],"sourcesContent":["import type { PayloadRequest } from 'payload'\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport { ZodObject, type ZodTypeAny } from 'zod'\nimport { getApiKeyContext } from './auth-strategy'\nimport type { InitializeServerForRequest } from './endpoint'\nimport { stampMcpContext, type McpTextResponse } from './tools/_helpers'\nimport {\n assertScopeAllows,\n buildRoutingTables,\n buildScopeChecker,\n type ResourceKind,\n type RoutingTables,\n type ScopeChecker,\n type ScopeDecision,\n type ToolRouting,\n} from './scope/policy'\nimport { extractDataKeys, getRequestId, makeSafeLog, summariseArgs } from './scope/audit-log'\n\n// ─── Tool / Prompt / Resource shapes ──────────────────────────────────\n\nexport interface ToolFactoryOutput {\n name: string\n description: string\n /**\n * Either a raw Zod shape (`{ key: ZodType }`) or a `z.object({...})`\n * instance. The registry normalises both before registering with the SDK.\n */\n parameters: Record<string, ZodTypeAny> | ZodObject<Record<string, ZodTypeAny>>\n handler: (\n args: Record<string, unknown>,\n req: PayloadRequest,\n extra: unknown,\n ) => Promise<McpTextResponse> | McpTextResponse\n routing: ToolRouting\n}\n\n/**\n * Returns the raw `{ name: ZodType }` shape for either a raw shape or a\n * `z.object({...})` instance. The MCP SDK's `registerTool` expects the raw\n * shape under `inputSchema`; passing a ZodObject silently registers an\n * empty schema and breaks args validation.\n */\nfunction toZodShape(\n parameters: Record<string, ZodTypeAny> | ZodObject<Record<string, ZodTypeAny>>,\n): Record<string, ZodTypeAny> {\n if (parameters instanceof ZodObject) {\n return parameters.shape as Record<string, ZodTypeAny>\n }\n return parameters\n}\n\nexport interface PromptFactoryOutput {\n name: string\n title?: string\n description?: string\n argsSchema?: Record<string, ZodTypeAny>\n handler: (args: unknown, req: PayloadRequest, extra: unknown) => unknown\n}\n\nexport interface ResourceFactoryOutput {\n name: string\n uri: string\n title?: string\n description?: string\n mimeType?: string\n handler: (args: unknown, req: PayloadRequest, extra: unknown) => unknown\n}\n\n// Re-export scope-routing primitives so existing callers keep working.\nexport {\n buildScopeChecker,\n type ResourceKind,\n type RoutingTables,\n type ScopeChecker,\n type ScopeDecision,\n type ToolRouting,\n}\n\n// ─── Initializer factory ─────────────────────────────────────────────\n\nexport interface CreateInitializeServerOptions {\n tools: ToolFactoryOutput[]\n prompts?: PromptFactoryOutput[]\n resources?: ResourceFactoryOutput[]\n}\n\ninterface ScopeRejectionResult {\n content: Array<{ type: 'text'; text: string }>\n isError: true\n}\n\nfunction scopeRejectionResult(reason: string): ScopeRejectionResult {\n return {\n content: [{ type: 'text', text: `Scope rejection: ${reason}` }],\n isError: true,\n }\n}\n\n/**\n * Builds the per-request initializer. mcp-handler invokes this once per\n * `tools/call` / `tools/list` JSON-RPC request, passing a fresh McpServer.\n *\n * Each tool handler is wrapped to:\n * 1. Read the API-key context populated by the bearer auth strategy.\n * 2. Reject the call (as an `isError: true` result, not a JSON-RPC error)\n * when scopes deny it — per the MCP spec, tool-execution failures\n * surface in the result envelope so the LLM can self-correct.\n * 3. Stamp `req.context.source = 'mcp'` for downstream hooks.\n * 4. Emit a structured audit log entry on every success / failure.\n */\nexport function createInitializeServer(\n options: CreateInitializeServerOptions,\n): InitializeServerForRequest {\n const { tools, prompts = [], resources = [] } = options\n const tables: RoutingTables = buildRoutingTables(tools)\n\n return (req: PayloadRequest) => (server: McpServer) => {\n const logger = req.payload?.logger\n const requestId = getRequestId(req)\n const safeLog = makeSafeLog(logger)\n\n for (const tool of tools) {\n const resourceKind = tables.toolKind.get(tool.name) ?? null\n const wrapped = async (\n args: Record<string, unknown>,\n extra: unknown,\n ): Promise<unknown> => {\n const start = Date.now()\n const keyCtx = getApiKeyContext(req)\n const targetSlug =\n typeof args.collection === 'string'\n ? args.collection\n : typeof args.slug === 'string'\n ? args.slug\n : undefined\n const targetKind = resourceKind ?? undefined\n const dataKeys = extractDataKeys(args)\n\n const decision = assertScopeAllows(\n keyCtx?.scopes ?? null,\n tool.name,\n targetSlug,\n tables,\n )\n\n if (!decision.allowed) {\n safeLog(\n 'warn',\n {\n event: 'mcp.tool_call',\n keyId: keyCtx?.keyId,\n keyPrefix: keyCtx?.keyPrefix,\n tool: tool.name,\n targetSlug,\n targetKind,\n dataKeys,\n success: false,\n isError: true,\n durationMs: Date.now() - start,\n requestId,\n errorClass: 'ScopeRejection',\n },\n `[payload-mcp-toolkit] Scope-rejected tool call: ${tool.name}`,\n )\n return scopeRejectionResult(decision.reason ?? 'denied')\n }\n\n stampMcpContext(req)\n\n try {\n const result = await tool.handler(args, req, extra)\n // A handler may report failure in the result envelope instead of\n // throwing — the MCP spec's own way of letting the model self-correct.\n // Logging that as a success would hide every such failure from\n // monitoring.\n const reportedError = (result as { isError?: unknown })?.isError === true\n safeLog(\n reportedError ? 'warn' : 'info',\n {\n event: 'mcp.tool_call',\n keyId: keyCtx?.keyId,\n keyPrefix: keyCtx?.keyPrefix,\n tool: tool.name,\n targetSlug,\n targetKind,\n dataKeys,\n success: !reportedError,\n isError: reportedError,\n durationMs: Date.now() - start,\n requestId,\n ...(reportedError ? { errorClass: 'HandlerReportedError' } : {}),\n },\n `[payload-mcp-toolkit] Tool call${reportedError ? ' reported an error' : ''}: ${tool.name}`,\n )\n return result\n } catch (err) {\n const errorClass = err instanceof Error ? err.name : 'UnknownError'\n const message = err instanceof Error ? err.message : String(err)\n safeLog(\n 'error',\n {\n event: 'mcp.tool_call',\n err,\n keyId: keyCtx?.keyId,\n keyPrefix: keyCtx?.keyPrefix,\n tool: tool.name,\n targetSlug,\n targetKind,\n dataKeys,\n argsSummary: summariseArgs(args),\n success: false,\n isError: true,\n durationMs: Date.now() - start,\n requestId,\n errorClass,\n },\n `[payload-mcp-toolkit] Tool call failed: ${tool.name}`,\n )\n return {\n content: [{ type: 'text', text: `Error: ${message}` }],\n isError: true,\n }\n }\n }\n\n server.registerTool(\n tool.name,\n {\n description: tool.description,\n inputSchema: toZodShape(tool.parameters),\n },\n wrapped as never,\n )\n }\n\n for (const prompt of prompts) {\n const wrapped = async (args: unknown, extra: unknown) => {\n try {\n return await prompt.handler(args, req, extra)\n } catch (err) {\n logger?.error?.(\n { event: 'mcp.prompt', err, prompt: prompt.name, requestId },\n `[payload-mcp-toolkit] Prompt failed: ${prompt.name}`,\n )\n throw err\n }\n }\n server.registerPrompt(\n prompt.name,\n {\n title: prompt.title,\n description: prompt.description,\n argsSchema: prompt.argsSchema as never,\n },\n wrapped as never,\n )\n }\n\n for (const resource of resources) {\n const wrapped = async (args: unknown, extra: unknown) => {\n try {\n return await resource.handler(args, req, extra)\n } catch (err) {\n logger?.error?.(\n { event: 'mcp.resource', err, resource: resource.name, requestId },\n `[payload-mcp-toolkit] Resource read failed: ${resource.name}`,\n )\n throw err\n }\n }\n server.registerResource(\n resource.name,\n resource.uri as never,\n {\n title: resource.title,\n description: resource.description,\n mimeType: resource.mimeType,\n },\n wrapped as never,\n )\n }\n }\n}\n"],"names":["ZodObject","getApiKeyContext","stampMcpContext","assertScopeAllows","buildRoutingTables","buildScopeChecker","extractDataKeys","getRequestId","makeSafeLog","summariseArgs","toZodShape","parameters","shape","scopeRejectionResult","reason","content","type","text","isError","createInitializeServer","options","tools","prompts","resources","tables","req","server","logger","payload","requestId","safeLog","tool","resourceKind","toolKind","get","name","wrapped","args","extra","start","Date","now","keyCtx","targetSlug","collection","slug","undefined","targetKind","dataKeys","decision","scopes","allowed","event","keyId","keyPrefix","success","durationMs","errorClass","result","handler","reportedError","err","Error","message","String","argsSummary","registerTool","description","inputSchema","prompt","error","registerPrompt","title","argsSchema","resource","registerResource","uri","mimeType"],"mappings":"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"}