payload-mcp-toolkit 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/README.md +100 -237
  2. package/dist/api-keys.js +16 -0
  3. package/dist/api-keys.js.map +1 -1
  4. package/dist/auth-strategy.js +49 -49
  5. package/dist/auth-strategy.js.map +1 -1
  6. package/dist/components/AgentConnectPill.d.ts +3 -0
  7. package/dist/components/AgentConnectPill.js +121 -0
  8. package/dist/components/AgentConnectPill.js.map +1 -0
  9. package/dist/components/CollectionScopesMatrix.js.map +1 -1
  10. package/dist/components/ConsentPermissions.d.ts +10 -0
  11. package/dist/components/ConsentPermissions.js +165 -0
  12. package/dist/components/ConsentPermissions.js.map +1 -0
  13. package/dist/components/GlobalScopesMatrix.js.map +1 -1
  14. package/dist/components/OAuthView.d.ts +4 -0
  15. package/dist/components/OAuthView.js +269 -0
  16. package/dist/components/OAuthView.js.map +1 -0
  17. package/dist/components/ScopesTable.d.ts +28 -0
  18. package/dist/components/ScopesTable.js +165 -130
  19. package/dist/components/ScopesTable.js.map +1 -1
  20. package/dist/components/agentInstructions.d.ts +2 -0
  21. package/dist/components/agentInstructions.js +34 -0
  22. package/dist/components/agentInstructions.js.map +1 -0
  23. package/dist/components/index.d.ts +2 -0
  24. package/dist/components/index.js +2 -0
  25. package/dist/components/index.js.map +1 -1
  26. package/dist/components/oauth.css +111 -0
  27. package/dist/conflict-detection.js +13 -13
  28. package/dist/conflict-detection.js.map +1 -1
  29. package/dist/draft-workflow.js +26 -26
  30. package/dist/draft-workflow.js.map +1 -1
  31. package/dist/endpoint.d.ts +5 -0
  32. package/dist/endpoint.js +21 -13
  33. package/dist/endpoint.js.map +1 -1
  34. package/dist/hash.js +15 -15
  35. package/dist/hash.js.map +1 -1
  36. package/dist/index.d.ts +2 -0
  37. package/dist/index.js +67 -3
  38. package/dist/index.js.map +1 -1
  39. package/dist/introspection.js +47 -47
  40. package/dist/introspection.js.map +1 -1
  41. package/dist/oauth-permissions.d.ts +52 -0
  42. package/dist/oauth-permissions.js +100 -0
  43. package/dist/oauth-permissions.js.map +1 -0
  44. package/dist/oauth-store.d.ts +19 -0
  45. package/dist/oauth-store.js +122 -0
  46. package/dist/oauth-store.js.map +1 -0
  47. package/dist/oauth.d.ts +19 -0
  48. package/dist/oauth.js +523 -0
  49. package/dist/oauth.js.map +1 -0
  50. package/dist/prompts.js +5 -5
  51. package/dist/prompts.js.map +1 -1
  52. package/dist/registry.js +7 -1
  53. package/dist/registry.js.map +1 -1
  54. package/dist/resources.js +10 -10
  55. package/dist/resources.js.map +1 -1
  56. package/dist/scope/audit-log.js +9 -9
  57. package/dist/scope/audit-log.js.map +1 -1
  58. package/dist/scope/policy.js +7 -7
  59. package/dist/scope/policy.js.map +1 -1
  60. package/dist/tools/_helpers.d.ts +10 -0
  61. package/dist/tools/_helpers.js +53 -41
  62. package/dist/tools/_helpers.js.map +1 -1
  63. package/dist/tools/_layout-helpers.js +33 -33
  64. package/dist/tools/_layout-helpers.js.map +1 -1
  65. package/dist/tools/create-document.js +20 -19
  66. package/dist/tools/create-document.js.map +1 -1
  67. package/dist/tools/delete-document.js +10 -9
  68. package/dist/tools/delete-document.js.map +1 -1
  69. package/dist/tools/find-document.js +18 -15
  70. package/dist/tools/find-document.js.map +1 -1
  71. package/dist/tools/find-global.js +13 -13
  72. package/dist/tools/find-global.js.map +1 -1
  73. package/dist/tools/global-versions.js +8 -7
  74. package/dist/tools/global-versions.js.map +1 -1
  75. package/dist/tools/patch-global-layout.js +9 -9
  76. package/dist/tools/patch-global-layout.js.map +1 -1
  77. package/dist/tools/patch-layout.js +15 -14
  78. package/dist/tools/patch-layout.js.map +1 -1
  79. package/dist/tools/publish-draft.js +2 -1
  80. package/dist/tools/publish-draft.js.map +1 -1
  81. package/dist/tools/publish-global-draft.js +4 -4
  82. package/dist/tools/publish-global-draft.js.map +1 -1
  83. package/dist/tools/resolve-reference.js.map +1 -1
  84. package/dist/tools/safe-delete.js +14 -14
  85. package/dist/tools/safe-delete.js.map +1 -1
  86. package/dist/tools/schedule-publish.js +21 -20
  87. package/dist/tools/schedule-publish.js.map +1 -1
  88. package/dist/tools/search-content.js +12 -12
  89. package/dist/tools/search-content.js.map +1 -1
  90. package/dist/tools/update-document.js +6 -5
  91. package/dist/tools/update-document.js.map +1 -1
  92. package/dist/tools/update-global.js +9 -9
  93. package/dist/tools/update-global.js.map +1 -1
  94. package/dist/tools/upload-media.js +2 -1
  95. package/dist/tools/upload-media.js.map +1 -1
  96. package/dist/tools/versions.js +10 -8
  97. package/dist/tools/versions.js.map +1 -1
  98. package/dist/types.d.ts +3 -0
  99. package/dist/types.js.map +1 -1
  100. package/docs/oauth.md +96 -0
  101. package/package.json +7 -2
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/ScopesTable.tsx"],"sourcesContent":["'use client'\r\n\r\nimport * as React from 'react'\r\nimport { CheckboxInput, useField } from '@payloadcms/ui'\r\nimport { toWords } from 'payload/shared'\r\n\r\n/**\r\n * Reusable matrix renderer for \"items × actions\" scope tables.\r\n *\r\n * Used by both CollectionScopesMatrix (collections × 4 actions) and\r\n * GlobalScopesMatrix (globals × 2 actions). The stored shape is\r\n * `Array<{ slug: string; actions: string[] }>` — the parent field name\r\n * (`collectionScopes` vs `globalScopes`) already encodes which axis the\r\n * slugs belong to, so the row payload no longer mirrors that distinction.\r\n */\r\n\r\ninterface RowValue {\r\n slug?: unknown\r\n actions?: unknown\r\n}\r\ntype MatrixValue = RowValue[]\r\n\r\nexport interface ScopesTableProps {\r\n path: string\r\n /** Slugs to render as rows. */\r\n items: string[]\r\n /** Action columns shown to the operator. */\r\n actions: string[]\r\n /** Display label per action (e.g. { read: 'Read', update: 'Update' }). */\r\n actionLabels: Record<string, string>\r\n /** Header label for the leftmost column. */\r\n itemHeader: string\r\n /** Page-level title rendered above the table. */\r\n title: string\r\n /** Helper sentence rendered between title and table. */\r\n description: string\r\n /** Copy shown when `items` is empty. */\r\n emptyMessage: string\r\n}\r\n\r\nfunction rowsToMap(\r\n value: MatrixValue | null | undefined,\r\n allowedActions: Set<string>,\r\n): Map<string, Set<string>> {\r\n const map = new Map<string, Set<string>>()\r\n if (!Array.isArray(value)) return map\r\n for (const row of value) {\r\n if (!row || typeof row.slug !== 'string') continue\r\n const slug = row.slug\r\n const actions = new Set<string>()\r\n if (Array.isArray(row.actions)) {\r\n for (const a of row.actions) {\r\n if (typeof a === 'string' && allowedActions.has(a)) actions.add(a)\r\n }\r\n }\r\n map.set(slug, actions)\r\n }\r\n return map\r\n}\r\n\r\nfunction mapToRows(\r\n map: Map<string, Set<string>>,\r\n items: string[],\r\n actions: string[],\r\n): MatrixValue {\r\n const rows: MatrixValue = []\r\n for (const slug of items) {\r\n const set = map.get(slug)\r\n if (!set || set.size === 0) continue\r\n rows.push({\r\n slug,\r\n actions: actions.filter((a) => set.has(a)),\r\n })\r\n }\r\n return rows\r\n}\r\n\r\nconst cellStyle: React.CSSProperties = {\r\n textAlign: 'center',\r\n padding: '0.5rem 0.5rem',\r\n borderLeft: '1px solid var(--theme-elevation-100)',\r\n verticalAlign: 'middle',\r\n}\r\n\r\nconst headerCellStyle: React.CSSProperties = {\r\n ...cellStyle,\r\n background: 'var(--theme-elevation-50)',\r\n borderBottom: '1px solid var(--theme-elevation-150)',\r\n fontWeight: 600,\r\n}\r\n\r\nconst labelCellStyle: React.CSSProperties = {\r\n textAlign: 'left',\r\n padding: '0.5rem 0.75rem',\r\n verticalAlign: 'middle',\r\n}\r\n\r\nexport function ScopesTable(props: ScopesTableProps): React.ReactElement {\r\n const { value, setValue } = useField<MatrixValue>({ path: props.path, hasRows: false })\r\n const allowedActionsSet = React.useMemo(() => new Set(props.actions), [props.actions])\r\n\r\n const map = React.useMemo(\r\n () => rowsToMap(value, allowedActionsSet),\r\n [value, allowedActionsSet],\r\n )\r\n\r\n const update = React.useCallback(\r\n (mutator: (m: Map<string, Set<string>>) => void) => {\r\n const next = new Map<string, Set<string>>()\r\n for (const [k, v] of map) next.set(k, new Set(v))\r\n mutator(next)\r\n setValue(mapToRows(next, props.items, props.actions))\r\n },\r\n [map, props.items, props.actions, setValue],\r\n )\r\n\r\n const isCellChecked = (slug: string, action: string): boolean =>\r\n !!map.get(slug)?.has(action)\r\n const isRowFull = (slug: string): boolean => {\r\n const set = map.get(slug)\r\n return !!set && props.actions.every((a) => set.has(a))\r\n }\r\n const isRowPartial = (slug: string): boolean => {\r\n const set = map.get(slug)\r\n return !!set && set.size > 0 && set.size < props.actions.length\r\n }\r\n const isColumnFull = (action: string): boolean =>\r\n props.items.length > 0 && props.items.every((s) => map.get(s)?.has(action))\r\n const isColumnPartial = (action: string): boolean => {\r\n if (props.items.length === 0) return false\r\n const checked = props.items.filter((s) => map.get(s)?.has(action)).length\r\n return checked > 0 && checked < props.items.length\r\n }\r\n const allCheckedCount = props.items.reduce((sum, s) => sum + (map.get(s)?.size ?? 0), 0)\r\n const allTotalCount = props.items.length * props.actions.length\r\n const isAllFull = allTotalCount > 0 && allCheckedCount === allTotalCount\r\n const isAllPartial = allCheckedCount > 0 && allCheckedCount < allTotalCount\r\n\r\n const toggleCell = (slug: string, action: string, checked: boolean) =>\r\n update((m) => {\r\n const set = m.get(slug) ?? new Set<string>()\r\n if (checked) set.add(action)\r\n else set.delete(action)\r\n m.set(slug, set)\r\n })\r\n\r\n const toggleRow = (slug: string, checked: boolean) =>\r\n update((m) => {\r\n m.set(slug, checked ? new Set(props.actions) : new Set())\r\n })\r\n\r\n const toggleColumn = (action: string, checked: boolean) =>\r\n update((m) => {\r\n for (const slug of props.items) {\r\n const set = m.get(slug) ?? new Set<string>()\r\n if (checked) set.add(action)\r\n else set.delete(action)\r\n m.set(slug, set)\r\n }\r\n })\r\n\r\n const toggleAll = (checked: boolean) =>\r\n update((m) => {\r\n for (const slug of props.items) m.set(slug, checked ? new Set(props.actions) : new Set())\r\n })\r\n\r\n if (props.items.length === 0) {\r\n return (\r\n <div className=\"field-type\" style={{ padding: '0.5rem 0' }}>\r\n <label className=\"field-label\">{props.title}</label>\r\n <p style={{ color: 'var(--theme-elevation-500)', fontSize: '0.85rem', margin: '0.25rem 0 0' }}>\r\n {props.emptyMessage}\r\n </p>\r\n </div>\r\n )\r\n }\r\n\r\n return (\r\n <div className=\"field-type\" style={{ padding: '0.5rem 0' }}>\r\n <label className=\"field-label\" htmlFor={`${props.path}-matrix`}>\r\n {props.title}\r\n </label>\r\n <p\r\n style={{\r\n color: 'var(--theme-elevation-500)',\r\n fontSize: '0.85rem',\r\n margin: '0.25rem 0 0.75rem',\r\n }}\r\n >\r\n {props.description}\r\n </p>\r\n <table\r\n id={`${props.path}-matrix`}\r\n style={{\r\n width: '100%',\r\n borderCollapse: 'collapse',\r\n fontSize: '0.9rem',\r\n background: 'var(--theme-input-bg)',\r\n border: '1px solid var(--theme-elevation-150)',\r\n borderRadius: '4px',\r\n overflow: 'hidden',\r\n }}\r\n >\r\n <thead>\r\n <tr>\r\n <th style={{ ...headerCellStyle, textAlign: 'left' }}>\r\n <div style={{ display: 'flex', flexDirection: 'column', gap: '0.35rem' }}>\r\n <span>{props.itemHeader}</span>\r\n <CheckboxInput\r\n checked={isAllFull}\r\n partialChecked={isAllPartial && !isAllFull}\r\n onToggle={(e) => toggleAll(e.currentTarget.checked)}\r\n label=\"All\"\r\n />\r\n </div>\r\n </th>\r\n {props.actions.map((action) => (\r\n <th key={action} style={headerCellStyle}>\r\n <div\r\n style={{\r\n display: 'flex',\r\n flexDirection: 'column',\r\n alignItems: 'center',\r\n gap: '0.35rem',\r\n }}\r\n >\r\n <span>{props.actionLabels[action] ?? action}</span>\r\n <CheckboxInput\r\n checked={isColumnFull(action)}\r\n partialChecked={isColumnPartial(action) && !isColumnFull(action)}\r\n onToggle={(e) => toggleColumn(action, e.currentTarget.checked)}\r\n />\r\n </div>\r\n </th>\r\n ))}\r\n </tr>\r\n </thead>\r\n <tbody>\r\n {props.items.map((slug, idx) => {\r\n const niceName = toWords(slug)\r\n return (\r\n <tr\r\n key={slug}\r\n style={{\r\n background:\r\n idx % 2 === 0 ? 'var(--theme-input-bg)' : 'var(--theme-elevation-25)',\r\n }}\r\n >\r\n <td\r\n style={{\r\n ...labelCellStyle,\r\n borderTop: idx === 0 ? 'none' : '1px solid var(--theme-elevation-100)',\r\n }}\r\n >\r\n <div style={{ display: 'flex', alignItems: 'center', gap: '0.6rem' }}>\r\n <CheckboxInput\r\n checked={isRowFull(slug)}\r\n partialChecked={isRowPartial(slug) && !isRowFull(slug)}\r\n onToggle={(e) => toggleRow(slug, e.currentTarget.checked)}\r\n />\r\n <div style={{ display: 'flex', flexDirection: 'column' }}>\r\n <span>{niceName}</span>\r\n <code\r\n style={{\r\n fontSize: '0.75rem',\r\n color: 'var(--theme-elevation-500)',\r\n background: 'transparent',\r\n padding: 0,\r\n }}\r\n >\r\n {slug}\r\n </code>\r\n </div>\r\n </div>\r\n </td>\r\n {props.actions.map((action) => (\r\n <td\r\n key={action}\r\n style={{\r\n ...cellStyle,\r\n borderTop: idx === 0 ? 'none' : '1px solid var(--theme-elevation-100)',\r\n }}\r\n >\r\n <CheckboxInput\r\n checked={isCellChecked(slug, action)}\r\n onToggle={(e) => toggleCell(slug, action, e.currentTarget.checked)}\r\n />\r\n </td>\r\n ))}\r\n </tr>\r\n )\r\n })}\r\n </tbody>\r\n </table>\r\n </div>\r\n )\r\n}\r\n"],"names":["React","CheckboxInput","useField","toWords","rowsToMap","value","allowedActions","map","Map","Array","isArray","row","slug","actions","Set","a","has","add","set","mapToRows","items","rows","get","size","push","filter","cellStyle","textAlign","padding","borderLeft","verticalAlign","headerCellStyle","background","borderBottom","fontWeight","labelCellStyle","ScopesTable","props","setValue","path","hasRows","allowedActionsSet","useMemo","update","useCallback","mutator","next","k","v","isCellChecked","action","isRowFull","every","isRowPartial","length","isColumnFull","s","isColumnPartial","checked","allCheckedCount","reduce","sum","allTotalCount","isAllFull","isAllPartial","toggleCell","m","delete","toggleRow","toggleColumn","toggleAll","div","className","style","label","title","p","color","fontSize","margin","emptyMessage","htmlFor","description","table","id","width","borderCollapse","border","borderRadius","overflow","thead","tr","th","display","flexDirection","gap","span","itemHeader","partialChecked","onToggle","e","currentTarget","alignItems","actionLabels","tbody","idx","niceName","td","borderTop","code"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/components/ScopesTable.tsx"],"sourcesContent":["'use client'\n\nimport * as React from 'react'\nimport { CheckboxInput, useField } from '@payloadcms/ui'\nimport { toWords } from 'payload/shared'\n\n/**\n * Reusable matrix renderer for \"items × actions\" scope tables.\n *\n * Used by both CollectionScopesMatrix (collections × 4 actions) and\n * GlobalScopesMatrix (globals × 2 actions). The stored shape is\n * `Array<{ slug: string; actions: string[] }>` — the parent field name\n * (`collectionScopes` vs `globalScopes`) already encodes which axis the\n * slugs belong to, so the row payload no longer mirrors that distinction.\n */\n\ninterface RowValue {\n slug?: unknown\n actions?: unknown\n}\ntype MatrixValue = RowValue[]\n\nexport interface ScopesMatrixProps {\n /** Rows in the stored `{ slug, actions }[]` shape. */\n value: MatrixValue | null | undefined\n onChange: (rows: MatrixValue) => void\n /** Table id; unique on the page. */\n id: string\n items: string[]\n actions: string[]\n actionLabels: Record<string, string>\n itemHeader: string\n}\n\nexport interface ScopesTableProps {\n path: string\n /** Slugs to render as rows. */\n items: string[]\n /** Action columns shown to the operator. */\n actions: string[]\n /** Display label per action (e.g. { read: 'Read', update: 'Update' }). */\n actionLabels: Record<string, string>\n /** Header label for the leftmost column. */\n itemHeader: string\n /** Page-level title rendered above the table. */\n title: string\n /** Helper sentence rendered between title and table. */\n description: string\n /** Copy shown when `items` is empty. */\n emptyMessage: string\n}\n\nfunction rowsToMap(\n value: MatrixValue | null | undefined,\n allowedActions: Set<string>,\n): Map<string, Set<string>> {\n const map = new Map<string, Set<string>>()\n if (!Array.isArray(value)) return map\n for (const row of value) {\n if (!row || typeof row.slug !== 'string') continue\n const slug = row.slug\n const actions = new Set<string>()\n if (Array.isArray(row.actions)) {\n for (const a of row.actions) {\n if (typeof a === 'string' && allowedActions.has(a)) actions.add(a)\n }\n }\n map.set(slug, actions)\n }\n return map\n}\n\nfunction mapToRows(\n map: Map<string, Set<string>>,\n items: string[],\n actions: string[],\n): MatrixValue {\n const rows: MatrixValue = []\n for (const slug of items) {\n const set = map.get(slug)\n if (!set || set.size === 0) continue\n rows.push({\n slug,\n actions: actions.filter((a) => set.has(a)),\n })\n }\n return rows\n}\n\nconst cellStyle: React.CSSProperties = {\n textAlign: 'center',\n padding: '0.5rem 0.5rem',\n borderLeft: '1px solid var(--theme-elevation-100)',\n verticalAlign: 'middle',\n}\n\nconst headerCellStyle: React.CSSProperties = {\n ...cellStyle,\n background: 'var(--theme-elevation-50)',\n borderBottom: '1px solid var(--theme-elevation-150)',\n fontWeight: 600,\n}\n\n// The matrix shows row names and column headers visually; this gives each checkbox a spoken name too.\nconst hiddenStyle: React.CSSProperties = {\n position: 'absolute', width: 1, height: 1, margin: -1, padding: 0, overflow: 'hidden', clip: 'rect(0 0 0 0)', whiteSpace: 'nowrap', border: 0,\n}\nconst hiddenLabel = (htmlFor: string, text: string) => <label htmlFor={htmlFor} style={hiddenStyle}>{text}</label>\n\nconst labelCellStyle: React.CSSProperties = {\n textAlign: 'left',\n padding: '0.5rem 0.75rem',\n verticalAlign: 'middle',\n}\n\n/** The items × actions checkbox table without form binding. The OAuth consent screen uses it too. */\nexport function ScopesMatrix(props: ScopesMatrixProps): React.ReactElement {\n const { value, onChange: setValue } = props\n const allowedActionsSet = React.useMemo(() => new Set(props.actions), [props.actions])\n\n const map = React.useMemo(\n () => rowsToMap(value, allowedActionsSet),\n [value, allowedActionsSet],\n )\n\n const update = React.useCallback(\n (mutator: (m: Map<string, Set<string>>) => void) => {\n const next = new Map<string, Set<string>>()\n for (const [k, v] of map) next.set(k, new Set(v))\n mutator(next)\n setValue(mapToRows(next, props.items, props.actions))\n },\n [map, props.items, props.actions, setValue],\n )\n\n const isCellChecked = (slug: string, action: string): boolean =>\n !!map.get(slug)?.has(action)\n const isRowFull = (slug: string): boolean => {\n const set = map.get(slug)\n return !!set && props.actions.every((a) => set.has(a))\n }\n const isRowPartial = (slug: string): boolean => {\n const set = map.get(slug)\n return !!set && set.size > 0 && set.size < props.actions.length\n }\n const isColumnFull = (action: string): boolean =>\n props.items.length > 0 && props.items.every((s) => map.get(s)?.has(action))\n const isColumnPartial = (action: string): boolean => {\n if (props.items.length === 0) return false\n const checked = props.items.filter((s) => map.get(s)?.has(action)).length\n return checked > 0 && checked < props.items.length\n }\n const allCheckedCount = props.items.reduce((sum, s) => sum + (map.get(s)?.size ?? 0), 0)\n const allTotalCount = props.items.length * props.actions.length\n const isAllFull = allTotalCount > 0 && allCheckedCount === allTotalCount\n const isAllPartial = allCheckedCount > 0 && allCheckedCount < allTotalCount\n\n const toggleCell = (slug: string, action: string, checked: boolean) =>\n update((m) => {\n const set = m.get(slug) ?? new Set<string>()\n if (checked) set.add(action)\n else set.delete(action)\n m.set(slug, set)\n })\n\n const toggleRow = (slug: string, checked: boolean) =>\n update((m) => {\n m.set(slug, checked ? new Set(props.actions) : new Set())\n })\n\n const toggleColumn = (action: string, checked: boolean) =>\n update((m) => {\n for (const slug of props.items) {\n const set = m.get(slug) ?? new Set<string>()\n if (checked) set.add(action)\n else set.delete(action)\n m.set(slug, set)\n }\n })\n\n const toggleAll = (checked: boolean) =>\n update((m) => {\n for (const slug of props.items) m.set(slug, checked ? new Set(props.actions) : new Set())\n })\n\n return (\n <table\n id={props.id}\n style={{\n width: '100%',\n borderCollapse: 'collapse',\n fontSize: '0.9rem',\n background: 'var(--theme-input-bg)',\n border: '1px solid var(--theme-elevation-150)',\n borderRadius: '4px',\n overflow: 'hidden',\n }}\n >\n <thead>\n <tr>\n <th style={{ ...headerCellStyle, textAlign: 'left' }}>\n <div style={{ display: 'flex', flexDirection: 'column', gap: '0.35rem' }}>\n <span>{props.itemHeader}</span>\n <CheckboxInput\n checked={isAllFull}\n partialChecked={isAllPartial && !isAllFull}\n onToggle={(e) => toggleAll(e.currentTarget.checked)}\n label=\"All\"\n />\n </div>\n </th>\n {props.actions.map((action) => (\n <th key={action} style={headerCellStyle}>\n <div\n style={{\n display: 'flex',\n flexDirection: 'column',\n alignItems: 'center',\n gap: '0.35rem',\n }}\n >\n <span>{props.actionLabels[action] ?? action}</span>\n <CheckboxInput\n checked={isColumnFull(action)}\n id={`${props.id}-col-${action}`}\n Label={hiddenLabel(`${props.id}-col-${action}`, `${props.actionLabels[action] ?? action}: all`)}\n partialChecked={isColumnPartial(action) && !isColumnFull(action)}\n onToggle={(e) => toggleColumn(action, e.currentTarget.checked)}\n />\n </div>\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {props.items.map((slug, idx) => {\n const niceName = toWords(slug)\n return (\n <tr\n key={slug}\n style={{\n background:\n idx % 2 === 0 ? 'var(--theme-input-bg)' : 'var(--theme-elevation-25)',\n }}\n >\n <td\n style={{\n ...labelCellStyle,\n borderTop: idx === 0 ? 'none' : '1px solid var(--theme-elevation-100)',\n }}\n >\n <div style={{ display: 'flex', alignItems: 'center', gap: '0.6rem' }}>\n <CheckboxInput\n checked={isRowFull(slug)}\n id={`${props.id}-row-${idx}`}\n Label={hiddenLabel(`${props.id}-row-${idx}`, `${niceName}: all actions`)}\n partialChecked={isRowPartial(slug) && !isRowFull(slug)}\n onToggle={(e) => toggleRow(slug, e.currentTarget.checked)}\n />\n <div style={{ display: 'flex', flexDirection: 'column' }}>\n <span>{niceName}</span>\n <code\n style={{\n fontSize: '0.75rem',\n color: 'var(--theme-elevation-500)',\n background: 'transparent',\n padding: 0,\n }}\n >\n {slug}\n </code>\n </div>\n </div>\n </td>\n {props.actions.map((action) => (\n <td\n key={action}\n style={{\n ...cellStyle,\n borderTop: idx === 0 ? 'none' : '1px solid var(--theme-elevation-100)',\n }}\n >\n <CheckboxInput\n checked={isCellChecked(slug, action)}\n id={`${props.id}-row-${idx}-${action}`}\n Label={hiddenLabel(`${props.id}-row-${idx}-${action}`, `${niceName}: ${props.actionLabels[action] ?? action}`)}\n onToggle={(e) => toggleCell(slug, action, e.currentTarget.checked)}\n />\n </td>\n ))}\n </tr>\n )\n })}\n </tbody>\n </table>\n )\n}\n\nexport function ScopesTable(props: ScopesTableProps): React.ReactElement {\n const { value, setValue } = useField<MatrixValue>({ path: props.path, hasRows: false })\n\n if (props.items.length === 0) {\n return (\n <div className=\"field-type\" style={{ padding: '0.5rem 0' }}>\n <label className=\"field-label\">{props.title}</label>\n <p style={{ color: 'var(--theme-elevation-500)', fontSize: '0.85rem', margin: '0.25rem 0 0' }}>\n {props.emptyMessage}\n </p>\n </div>\n )\n }\n\n return (\n <div className=\"field-type\" style={{ padding: '0.5rem 0' }}>\n <label className=\"field-label\" htmlFor={`${props.path}-matrix`}>\n {props.title}\n </label>\n <p\n style={{\n color: 'var(--theme-elevation-500)',\n fontSize: '0.85rem',\n margin: '0.25rem 0 0.75rem',\n }}\n >\n {props.description}\n </p>\n <ScopesMatrix\n id={`${props.path}-matrix`}\n value={value}\n onChange={setValue}\n items={props.items}\n actions={props.actions}\n actionLabels={props.actionLabels}\n itemHeader={props.itemHeader}\n />\n </div>\n )\n}\n"],"names":["React","CheckboxInput","useField","toWords","rowsToMap","value","allowedActions","map","Map","Array","isArray","row","slug","actions","Set","a","has","add","set","mapToRows","items","rows","get","size","push","filter","cellStyle","textAlign","padding","borderLeft","verticalAlign","headerCellStyle","background","borderBottom","fontWeight","hiddenStyle","position","width","height","margin","overflow","clip","whiteSpace","border","hiddenLabel","htmlFor","text","label","style","labelCellStyle","ScopesMatrix","props","onChange","setValue","allowedActionsSet","useMemo","update","useCallback","mutator","next","k","v","isCellChecked","action","isRowFull","every","isRowPartial","length","isColumnFull","s","isColumnPartial","checked","allCheckedCount","reduce","sum","allTotalCount","isAllFull","isAllPartial","toggleCell","m","delete","toggleRow","toggleColumn","toggleAll","table","id","borderCollapse","fontSize","borderRadius","thead","tr","th","div","display","flexDirection","gap","span","itemHeader","partialChecked","onToggle","e","currentTarget","alignItems","actionLabels","Label","tbody","idx","niceName","td","borderTop","code","color","ScopesTable","path","hasRows","className","title","p","emptyMessage","description"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ /** Plain-text instructions a user pastes into an AI app (for example as project instructions). */
2
+ export declare function agentInstructions(resource: string, access: 'read-only' | 'editor'): string;
@@ -0,0 +1,34 @@
1
+ /** Plain-text instructions a user pastes into an AI app (for example as project instructions). */ export function agentInstructions(resource, access) {
2
+ const editor = access === 'editor';
3
+ const rules = editor ? [
4
+ 'Start by listing the content you can access. Do not change anything yet.',
5
+ 'Read a document before you change it.',
6
+ 'Before you create or update anything, show me the change and wait for my approval.',
7
+ 'Save changes as drafts where the collection supports drafts. Publish only when I ask.',
8
+ 'After each change, tell me which document changed and what changed.'
9
+ ] : [
10
+ 'Start by listing the content you can access.',
11
+ 'Do not try to change content. If I ask for a change, tell me which document and field to edit.'
12
+ ];
13
+ return [
14
+ `You help me manage the content of ${new URL(resource).host} through its MCP connector.`,
15
+ '',
16
+ 'Connection',
17
+ `- Connector URL: ${resource}`,
18
+ '- Sign-in: OAuth with my website account. No API key or client secret is necessary.',
19
+ '- If you cannot see tools from this connector, stop. Tell me to add a custom connector with the URL above, sign in and approve access.',
20
+ ' - Claude: Settings > Connectors > Add custom connector.',
21
+ ' - ChatGPT (web): turn on Developer mode in Settings, then create an app with this URL and OAuth sign-in.',
22
+ '',
23
+ 'Access',
24
+ editor ? '- You can read content and create or update collection entries. Global settings are read-only.' : '- You can read content only.',
25
+ '- You cannot delete content. My website permissions still apply.',
26
+ `- Access lasts up to 30 days. I can disconnect at ${resource}/oauth/connections.`,
27
+ '',
28
+ 'How to work',
29
+ ...rules.map((rule, index)=>`${index + 1}. ${rule}`),
30
+ `${rules.length + 1}. If a tool call fails because of authorization, tell me to reconnect. Do not retry.`
31
+ ].join('\n');
32
+ }
33
+
34
+ //# sourceMappingURL=agentInstructions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/agentInstructions.ts"],"sourcesContent":["/** Plain-text instructions a user pastes into an AI app (for example as project instructions). */\nexport function agentInstructions(resource: string, access: 'read-only' | 'editor'): string {\n const editor = access === 'editor'\n const rules = editor\n ? [\n 'Start by listing the content you can access. Do not change anything yet.',\n 'Read a document before you change it.',\n 'Before you create or update anything, show me the change and wait for my approval.',\n 'Save changes as drafts where the collection supports drafts. Publish only when I ask.',\n 'After each change, tell me which document changed and what changed.',\n ]\n : [\n 'Start by listing the content you can access.',\n 'Do not try to change content. If I ask for a change, tell me which document and field to edit.',\n ]\n return [\n `You help me manage the content of ${new URL(resource).host} through its MCP connector.`,\n '',\n 'Connection',\n `- Connector URL: ${resource}`,\n '- Sign-in: OAuth with my website account. No API key or client secret is necessary.',\n '- If you cannot see tools from this connector, stop. Tell me to add a custom connector with the URL above, sign in and approve access.',\n ' - Claude: Settings > Connectors > Add custom connector.',\n ' - ChatGPT (web): turn on Developer mode in Settings, then create an app with this URL and OAuth sign-in.',\n '',\n 'Access',\n editor\n ? '- You can read content and create or update collection entries. Global settings are read-only.'\n : '- You can read content only.',\n '- You cannot delete content. My website permissions still apply.',\n `- Access lasts up to 30 days. I can disconnect at ${resource}/oauth/connections.`,\n '',\n 'How to work',\n ...rules.map((rule, index) => `${index + 1}. ${rule}`),\n `${rules.length + 1}. If a tool call fails because of authorization, tell me to reconnect. Do not retry.`,\n ].join('\\n')\n}\n"],"names":["agentInstructions","resource","access","editor","rules","URL","host","map","rule","index","length","join"],"mappings":"AAAA,gGAAgG,GAChG,OAAO,SAASA,kBAAkBC,QAAgB,EAAEC,MAA8B;IAChF,MAAMC,SAASD,WAAW;IAC1B,MAAME,QAAQD,SACV;QACA;QACA;QACA;QACA;QACA;KACD,GACC;QACA;QACA;KACD;IACH,OAAO;QACL,CAAC,kCAAkC,EAAE,IAAIE,IAAIJ,UAAUK,IAAI,CAAC,2BAA2B,CAAC;QACxF;QACA;QACA,CAAC,iBAAiB,EAAEL,UAAU;QAC9B;QACA;QACA;QACA;QACA;QACA;QACAE,SACI,mGACA;QACJ;QACA,CAAC,kDAAkD,EAAEF,SAAS,mBAAmB,CAAC;QAClF;QACA;WACGG,MAAMG,GAAG,CAAC,CAACC,MAAMC,QAAU,GAAGA,QAAQ,EAAE,EAAE,EAAED,MAAM;QACrD,GAAGJ,MAAMM,MAAM,GAAG,EAAE,oFAAoF,CAAC;KAC1G,CAACC,IAAI,CAAC;AACT"}
@@ -1,2 +1,4 @@
1
1
  export { default as CollectionScopesMatrix } from './CollectionScopesMatrix';
2
2
  export { default as GlobalScopesMatrix } from './GlobalScopesMatrix';
3
+ export { AgentConnectPill } from './AgentConnectPill';
4
+ export { OAuthView } from './OAuthView';
@@ -1,4 +1,6 @@
1
1
  export { default as CollectionScopesMatrix } from './CollectionScopesMatrix';
2
2
  export { default as GlobalScopesMatrix } from './GlobalScopesMatrix';
3
+ export { AgentConnectPill } from './AgentConnectPill';
4
+ export { OAuthView } from './OAuthView';
3
5
 
4
6
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/index.ts"],"sourcesContent":["export { default as CollectionScopesMatrix } from './CollectionScopesMatrix'\r\nexport { default as GlobalScopesMatrix } from './GlobalScopesMatrix'\r\n"],"names":["default","CollectionScopesMatrix","GlobalScopesMatrix"],"mappings":"AAAA,SAASA,WAAWC,sBAAsB,QAAQ,2BAA0B;AAC5E,SAASD,WAAWE,kBAAkB,QAAQ,uBAAsB"}
1
+ {"version":3,"sources":["../../src/components/index.ts"],"sourcesContent":["export { default as CollectionScopesMatrix } from './CollectionScopesMatrix'\nexport { default as GlobalScopesMatrix } from './GlobalScopesMatrix'\nexport { AgentConnectPill } from './AgentConnectPill'\nexport { OAuthView } from './OAuthView'\n"],"names":["default","CollectionScopesMatrix","GlobalScopesMatrix","AgentConnectPill","OAuthView"],"mappings":"AAAA,SAASA,WAAWC,sBAAsB,QAAQ,2BAA0B;AAC5E,SAASD,WAAWE,kBAAkB,QAAQ,uBAAsB;AACpE,SAASC,gBAAgB,QAAQ,qBAAoB;AACrD,SAASC,SAAS,QAAQ,cAAa"}
@@ -0,0 +1,111 @@
1
+ .mcp-oauth {
2
+ max-width: 42rem;
3
+ margin: calc(var(--base) * 3) auto;
4
+ color: var(--theme-text);
5
+ font-family: var(--font-body);
6
+ }
7
+
8
+ .mcp-oauth h1 { margin: 0 0 var(--base); }
9
+ .mcp-oauth h2 { margin-top: calc(var(--base) * 2); }
10
+ .mcp-oauth p { line-height: 1.6; }
11
+
12
+ .mcp-oauth__details {
13
+ margin: calc(var(--base) * 1.5) 0;
14
+ padding: var(--base);
15
+ background: var(--theme-elevation-50);
16
+ border: 1px solid var(--theme-elevation-150);
17
+ border-radius: var(--style-radius-m);
18
+ }
19
+
20
+ .mcp-oauth__details dt { font-weight: 600; }
21
+ .mcp-oauth__details dd { margin: calc(var(--base) * .25) 0 var(--base); line-height: 1.6; overflow-wrap: anywhere; }
22
+ .mcp-oauth__details dd:last-child { margin-bottom: 0; }
23
+ .mcp-oauth__actions { display: flex; flex-wrap: wrap; gap: calc(var(--base) * .5); }
24
+ .mcp-oauth__permissions { display: flex; flex-direction: column; gap: var(--base); margin-bottom: calc(var(--base) * 1.5); }
25
+ .mcp-oauth__permissions h3 { margin: var(--base) 0 calc(var(--base) * .5); font-size: 1rem; }
26
+ .mcp-oauth__permissions h3:first-child { margin-top: 0; }
27
+ .mcp-oauth__permissions p { margin: 0 0 calc(var(--base) * .5); color: var(--theme-elevation-600); }
28
+ .mcp-oauth__customize {
29
+ background: var(--theme-elevation-50);
30
+ border: 1px solid var(--theme-elevation-150);
31
+ border-radius: var(--style-radius-m);
32
+ }
33
+ .mcp-oauth__customize > summary {
34
+ display: flex;
35
+ flex-direction: column;
36
+ gap: calc(var(--base) * .15);
37
+ padding: calc(var(--base) * .75) var(--base);
38
+ cursor: pointer;
39
+ list-style: none;
40
+ }
41
+ .mcp-oauth__customize > summary::-webkit-details-marker { display: none; }
42
+ /* The second value is the arrow's alt text: empty, so screen readers skip it. */
43
+ .mcp-oauth__customize > summary strong::after { content: ' ▸'; content: ' ▸' / ''; color: var(--theme-elevation-500); }
44
+ .mcp-oauth__customize[open] > summary strong::after { content: ' ▾'; content: ' ▾' / ''; }
45
+ .mcp-oauth__customize > summary span { color: var(--theme-elevation-600); font-size: .8125rem; }
46
+ .mcp-oauth__customize > summary:focus-visible { outline: var(--accessibility-outline); outline-offset: var(--accessibility-outline-offset); }
47
+ .mcp-oauth__customize[open] > summary { border-bottom: 1px solid var(--theme-elevation-150); }
48
+ .mcp-oauth__customize-body { padding: var(--base); }
49
+ .mcp-oauth__tools { display: grid; grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr)); gap: calc(var(--base) * .5) var(--base); }
50
+ .mcp-oauth__url { display: block; padding: var(--base); background: var(--theme-elevation-50); overflow-wrap: anywhere; }
51
+ .mcp-oauth__instructions {
52
+ margin: 0;
53
+ padding: var(--base);
54
+ background: var(--theme-elevation-50);
55
+ border: 1px solid var(--theme-elevation-150);
56
+ border-radius: var(--style-radius-m);
57
+ font-family: var(--font-mono);
58
+ line-height: 1.6;
59
+ white-space: pre-wrap;
60
+ overflow-wrap: anywhere;
61
+ }
62
+ .mcp-agent-pill {
63
+ display: flex;
64
+ align-items: stretch;
65
+ max-width: 100%;
66
+ margin: calc(var(--base) * .5) 0;
67
+ background: var(--theme-elevation-0);
68
+ border: 1px solid var(--theme-elevation-150);
69
+ border-radius: 999px;
70
+ transition: background-color 120ms ease-out, border-color 120ms ease-out;
71
+ }
72
+ .mcp-agent-pill:hover { background: var(--theme-elevation-50); border-color: var(--theme-elevation-250); }
73
+ .mcp-agent-pill button {
74
+ appearance: none;
75
+ background: none;
76
+ border: 0;
77
+ color: inherit;
78
+ cursor: pointer;
79
+ font: inherit;
80
+ }
81
+ .mcp-agent-pill button:focus-visible { outline: var(--accessibility-outline); outline-offset: var(--accessibility-outline-offset); }
82
+ .mcp-agent-pill__copy {
83
+ position: relative;
84
+ display: flex;
85
+ flex: 1;
86
+ align-items: center;
87
+ gap: calc(var(--base) * .35);
88
+ min-width: 0;
89
+ padding: calc(var(--base) * .35) calc(var(--base) * .2) calc(var(--base) * .35) calc(var(--base) * .6);
90
+ color: var(--theme-elevation-800) !important;
91
+ font-size: .8125rem !important;
92
+ line-height: 1.2;
93
+ border-radius: 999px 0 0 999px;
94
+ }
95
+ .mcp-agent-pill__label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
96
+ .mcp-agent-pill__mark { flex-shrink: 0; width: 14px; height: 14px; }
97
+ .mcp-agent-pill__dismiss {
98
+ display: grid;
99
+ place-items: center;
100
+ flex-shrink: 0;
101
+ padding: 0 calc(var(--base) * .3) 0 calc(var(--base) * .15);
102
+ color: var(--theme-elevation-400) !important;
103
+ border-radius: 0 999px 999px 0;
104
+ }
105
+ .mcp-agent-pill__dismiss:hover { color: var(--theme-elevation-800) !important; }
106
+ .mcp-agent-pill__dismiss svg { width: 16px; height: 16px; }
107
+ @media (prefers-reduced-motion: reduce) { .mcp-agent-pill { transition: none; } }
108
+
109
+ .mcp-oauth__copy { display: flex; flex-wrap: wrap; align-items: center; gap: calc(var(--base) * .5); margin: calc(var(--base) * .5) 0 var(--base); }
110
+ .mcp-oauth__connection { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--base); padding: var(--base) 0; border-top: 1px solid var(--theme-elevation-150); }
111
+ .mcp-oauth__connection p { margin: 0; }
@@ -1,10 +1,10 @@
1
1
  import { API_KEYS_DEFAULT_SLUG } from './api-keys';
2
2
  const UPGRADE_HINT = 'payload-mcp-toolkit v0.4 is the standalone successor to @payloadcms/plugin-mcp. ' + 'Remove the upstream `mcpPlugin(...)` from your `plugins[]` (and the package from your dependencies) before upgrading. ' + 'See README "Upgrading from 0.3.x".';
3
- /**
4
- * Returns true when the supplied plugin reference looks like the upstream
5
- * `mcpPlugin(...)` invocation (function name, source-string sniff, or wrapped
6
- * config probing). Heuristic: covers the three common ways the upstream
7
- * plugin shows up in `plugins[]`.
3
+ /**
4
+ * Returns true when the supplied plugin reference looks like the upstream
5
+ * `mcpPlugin(...)` invocation (function name, source-string sniff, or wrapped
6
+ * config probing). Heuristic: covers the three common ways the upstream
7
+ * plugin shows up in `plugins[]`.
8
8
  */ function looksLikeUpstreamPlugin(plugin) {
9
9
  if (typeof plugin !== 'function') return false;
10
10
  const fn = plugin;
@@ -13,10 +13,10 @@ const UPGRADE_HINT = 'payload-mcp-toolkit v0.4 is the standalone successor to @p
13
13
  const src = fn.toString();
14
14
  return /@payloadcms\/plugin-mcp/.test(src) || /payload-mcp-api-keys/.test(src);
15
15
  }
16
- /**
17
- * Throws if `@payloadcms/plugin-mcp` is also registered in the host config.
18
- * Two MCP plugins racing for the same collection slug produces a confusing
19
- * boot crash inside Payload — this surfaces a clearer migration message.
16
+ /**
17
+ * Throws if `@payloadcms/plugin-mcp` is also registered in the host config.
18
+ * Two MCP plugins racing for the same collection slug produces a confusing
19
+ * boot crash inside Payload — this surfaces a clearer migration message.
20
20
  */ export function assertNoUpstreamPlugin(plugins) {
21
21
  if (!plugins || plugins.length === 0) return;
22
22
  for (const plugin of plugins){
@@ -25,10 +25,10 @@ const UPGRADE_HINT = 'payload-mcp-toolkit v0.4 is the standalone successor to @p
25
25
  }
26
26
  }
27
27
  }
28
- /**
29
- * Throws if a collection with the api-keys slug is already in
30
- * `incomingConfig.collections` from another source. Prevents duplicate-slug
31
- * boot errors and gives the user actionable text.
28
+ /**
29
+ * Throws if a collection with the api-keys slug is already in
30
+ * `incomingConfig.collections` from another source. Prevents duplicate-slug
31
+ * boot errors and gives the user actionable text.
32
32
  */ export function assertNoSlugConflict(collections, apiKeysSlug = API_KEYS_DEFAULT_SLUG) {
33
33
  if (!collections || collections.length === 0) return;
34
34
  for (const c of collections){
@@ -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\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"}
1
+ {"version":3,"sources":["../src/conflict-detection.ts"],"sourcesContent":["import type { CollectionConfig, Plugin } from 'payload'\nimport { API_KEYS_DEFAULT_SLUG } from './api-keys'\n\nconst UPGRADE_HINT =\n 'payload-mcp-toolkit v0.4 is the standalone successor to @payloadcms/plugin-mcp. ' +\n 'Remove the upstream `mcpPlugin(...)` from your `plugins[]` (and the package from your dependencies) before upgrading. ' +\n 'See README \"Upgrading from 0.3.x\".'\n\n/**\n * Returns true when the supplied plugin reference looks like the upstream\n * `mcpPlugin(...)` invocation (function name, source-string sniff, or wrapped\n * config probing). Heuristic: covers the three common ways the upstream\n * plugin shows up in `plugins[]`.\n */\nfunction looksLikeUpstreamPlugin(plugin: unknown): boolean {\n if (typeof plugin !== 'function') return false\n const fn = plugin as (...args: unknown[]) => unknown\n const fnName = (fn.name ?? '').toString()\n if (fnName === 'mcpPlugin' || fnName === 'withMcp') return true\n\n const src = fn.toString()\n return /@payloadcms\\/plugin-mcp/.test(src) || /payload-mcp-api-keys/.test(src)\n}\n\n/**\n * Throws if `@payloadcms/plugin-mcp` is also registered in the host config.\n * Two MCP plugins racing for the same collection slug produces a confusing\n * boot crash inside Payload — this surfaces a clearer migration message.\n */\nexport function assertNoUpstreamPlugin(plugins: Plugin[] | undefined): void {\n if (!plugins || plugins.length === 0) return\n for (const plugin of plugins) {\n if (looksLikeUpstreamPlugin(plugin)) {\n throw new Error(UPGRADE_HINT)\n }\n }\n}\n\n/**\n * Throws if a collection with the api-keys slug is already in\n * `incomingConfig.collections` from another source. Prevents duplicate-slug\n * boot errors and gives the user actionable text.\n */\nexport function assertNoSlugConflict(\n collections: CollectionConfig[] | undefined,\n apiKeysSlug: string = API_KEYS_DEFAULT_SLUG,\n): void {\n if (!collections || collections.length === 0) return\n for (const c of collections) {\n if (c?.slug === apiKeysSlug) {\n throw new Error(\n `payload-mcp-toolkit: a collection with slug \"${apiKeysSlug}\" is already registered. ` +\n 'This is usually the upstream `@payloadcms/plugin-mcp` still being active. ' +\n 'Remove it before upgrading to v0.4, or pass a different slug via `apiKeyCollection.slug`.',\n )\n }\n }\n}\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"}
@@ -1,27 +1,27 @@
1
1
  import { hasCollectionDrafts, hasGlobalDrafts } from './introspection';
2
- /**
3
- * Determines the draft behavior for a collection.
4
- *
5
- * - No drafts configured → 'publish' (raw update allowed; no draft concept)
6
- * - Drafts configured + override given → use override
7
- * - Drafts configured + no override → 'always-draft' (raw update locked)
2
+ /**
3
+ * Determines the draft behavior for a collection.
4
+ *
5
+ * - No drafts configured → 'publish' (raw update allowed; no draft concept)
6
+ * - Drafts configured + override given → use override
7
+ * - Drafts configured + no override → 'always-draft' (raw update locked)
8
8
  */ export function getDraftBehavior(collection, options) {
9
9
  if (!hasCollectionDrafts(collection)) return 'publish';
10
10
  const override = options?.draftBehavior?.[collection.slug];
11
11
  if (override) return override;
12
12
  return 'always-draft';
13
13
  }
14
- /**
15
- * Walks the collection list and returns:
16
- * - `draftCollections`: slugs whose `versions.drafts` is on (or whose
17
- * behavior override is `'always-draft'`).
18
- * - `excluded`: slugs to hide from the MCP surface entirely (the api-keys
19
- * collection itself, anything with `auth: true`, anything in
20
- * `excludeCollections`).
21
- *
22
- * Replaces the v0.3.x `generateMcpCollectionConfigs` shape now that the
23
- * toolkit owns the endpoint and tool dispatch directly — no need to produce
24
- * an upstream `mcpCollections` config object.
14
+ /**
15
+ * Walks the collection list and returns:
16
+ * - `draftCollections`: slugs whose `versions.drafts` is on (or whose
17
+ * behavior override is `'always-draft'`).
18
+ * - `excluded`: slugs to hide from the MCP surface entirely (the api-keys
19
+ * collection itself, anything with `auth: true`, anything in
20
+ * `excludeCollections`).
21
+ *
22
+ * Replaces the v0.3.x `generateMcpCollectionConfigs` shape now that the
23
+ * toolkit owns the endpoint and tool dispatch directly — no need to produce
24
+ * an upstream `mcpCollections` config object.
25
25
  */ export function computeDraftCollections(collections, options = {}) {
26
26
  const draftCollections = new Set();
27
27
  const excluded = new Set([
@@ -43,15 +43,15 @@ import { hasCollectionDrafts, hasGlobalDrafts } from './introspection';
43
43
  excluded
44
44
  };
45
45
  }
46
- /**
47
- * Peer of `computeDraftCollections` for globals. Returns the slugs whose
48
- * draft workflow is on (i.e. `versions.drafts` enabled, with optional
49
- * `'always-publish'` override turning it off), and the set excluded from
50
- * the MCP surface entirely.
51
- *
52
- * Mirrors the collection rule: `versions.drafts: true` defaults to
53
- * `'always-draft'` so raw publish is locked and `updateGlobal` / patch
54
- * routes preserve draft semantics.
46
+ /**
47
+ * Peer of `computeDraftCollections` for globals. Returns the slugs whose
48
+ * draft workflow is on (i.e. `versions.drafts` enabled, with optional
49
+ * `'always-publish'` override turning it off), and the set excluded from
50
+ * the MCP surface entirely.
51
+ *
52
+ * Mirrors the collection rule: `versions.drafts: true` defaults to
53
+ * `'always-draft'` so raw publish is locked and `updateGlobal` / patch
54
+ * routes preserve draft semantics.
55
55
  */ export function computeDraftGlobals(globals, options = {}) {
56
56
  const draftGlobals = new Set();
57
57
  const excluded = new Set(options.excludeGlobals ?? []);
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/draft-workflow.ts"],"sourcesContent":["import type { CollectionConfig, GlobalConfig } from 'payload'\r\nimport { hasCollectionDrafts, hasGlobalDrafts } from './introspection'\r\n\r\ninterface ComputeDraftCollectionsOptions {\r\n /** Per-collection draft behavior overrides */\r\n draftBehavior?: Record<string, 'always-draft' | 'always-publish'>\r\n /** Collection slugs to exclude from MCP entirely */\r\n excludeCollections?: string[]\r\n /** API-keys collection slug — always excluded from the MCP surface */\r\n apiKeysSlug?: string\r\n}\r\n\r\n/**\r\n * Determines the draft behavior for a collection.\r\n *\r\n * - No drafts configured → 'publish' (raw update allowed; no draft concept)\r\n * - Drafts configured + override given → use override\r\n * - Drafts configured + no override → 'always-draft' (raw update locked)\r\n */\r\nexport function getDraftBehavior(\r\n collection: CollectionConfig,\r\n options?: { draftBehavior?: Record<string, 'always-draft' | 'always-publish'> },\r\n): 'always-draft' | 'always-publish' | 'publish' {\r\n if (!hasCollectionDrafts(collection)) return 'publish'\r\n\r\n const override = options?.draftBehavior?.[collection.slug]\r\n if (override) return override\r\n\r\n return 'always-draft'\r\n}\r\n\r\nexport interface DraftCollectionsResult {\r\n /** Slugs of collections that participate in the draft workflow. */\r\n draftCollections: Set<string>\r\n /** Slugs of collections excluded from the MCP surface entirely. */\r\n excluded: Set<string>\r\n}\r\n\r\n/**\r\n * Walks the collection list and returns:\r\n * - `draftCollections`: slugs whose `versions.drafts` is on (or whose\r\n * behavior override is `'always-draft'`).\r\n * - `excluded`: slugs to hide from the MCP surface entirely (the api-keys\r\n * collection itself, anything with `auth: true`, anything in\r\n * `excludeCollections`).\r\n *\r\n * Replaces the v0.3.x `generateMcpCollectionConfigs` shape now that the\r\n * toolkit owns the endpoint and tool dispatch directly — no need to produce\r\n * an upstream `mcpCollections` config object.\r\n */\r\nexport function computeDraftCollections(\r\n collections: CollectionConfig[],\r\n options: ComputeDraftCollectionsOptions = {},\r\n): DraftCollectionsResult {\r\n const draftCollections = new Set<string>()\r\n const excluded = new Set<string>([\r\n options.apiKeysSlug ?? 'payload-mcp-api-keys',\r\n ...(options.excludeCollections ?? []),\r\n ])\r\n\r\n for (const collection of collections) {\r\n if (excluded.has(collection.slug)) continue\r\n\r\n // Auth-enabled collections are users — never expose them via MCP.\r\n if ((collection as { auth?: unknown }).auth) {\r\n excluded.add(collection.slug)\r\n continue\r\n }\r\n\r\n const behavior = getDraftBehavior(collection, options)\r\n if (behavior !== 'publish') draftCollections.add(collection.slug)\r\n }\r\n\r\n return { draftCollections, excluded }\r\n}\r\n\r\ninterface ComputeDraftGlobalsOptions {\r\n /**\r\n * Per-resource draft behavior overrides. Shared with collections: a single\r\n * record may contain both collection slugs and global slugs. Collisions\r\n * are unambiguous in practice — Payload's slug registry forbids a global\r\n * and a collection sharing a slug.\r\n */\r\n draftBehavior?: Record<string, 'always-draft' | 'always-publish'>\r\n /** Global slugs to exclude from the MCP surface entirely */\r\n excludeGlobals?: string[]\r\n}\r\n\r\nexport interface DraftGlobalsResult {\r\n draftGlobals: Set<string>\r\n excluded: Set<string>\r\n}\r\n\r\n/**\r\n * Peer of `computeDraftCollections` for globals. Returns the slugs whose\r\n * draft workflow is on (i.e. `versions.drafts` enabled, with optional\r\n * `'always-publish'` override turning it off), and the set excluded from\r\n * the MCP surface entirely.\r\n *\r\n * Mirrors the collection rule: `versions.drafts: true` defaults to\r\n * `'always-draft'` so raw publish is locked and `updateGlobal` / patch\r\n * routes preserve draft semantics.\r\n */\r\nexport function computeDraftGlobals(\r\n globals: GlobalConfig[],\r\n options: ComputeDraftGlobalsOptions = {},\r\n): DraftGlobalsResult {\r\n const draftGlobals = new Set<string>()\r\n const excluded = new Set<string>(options.excludeGlobals ?? [])\r\n\r\n for (const global of globals) {\r\n if (excluded.has(global.slug)) continue\r\n if (!hasGlobalDrafts(global)) continue\r\n const override = options.draftBehavior?.[global.slug]\r\n if (override === 'always-publish') continue\r\n draftGlobals.add(global.slug)\r\n }\r\n\r\n return { draftGlobals, excluded }\r\n}\r\n"],"names":["hasCollectionDrafts","hasGlobalDrafts","getDraftBehavior","collection","options","override","draftBehavior","slug","computeDraftCollections","collections","draftCollections","Set","excluded","apiKeysSlug","excludeCollections","has","auth","add","behavior","computeDraftGlobals","globals","draftGlobals","excludeGlobals","global"],"mappings":"AACA,SAASA,mBAAmB,EAAEC,eAAe,QAAQ,kBAAiB;AAWtE;;;;;;CAMC,GACD,OAAO,SAASC,iBACdC,UAA4B,EAC5BC,OAA+E;IAE/E,IAAI,CAACJ,oBAAoBG,aAAa,OAAO;IAE7C,MAAME,WAAWD,SAASE,eAAe,CAACH,WAAWI,IAAI,CAAC;IAC1D,IAAIF,UAAU,OAAOA;IAErB,OAAO;AACT;AASA;;;;;;;;;;;CAWC,GACD,OAAO,SAASG,wBACdC,WAA+B,EAC/BL,UAA0C,CAAC,CAAC;IAE5C,MAAMM,mBAAmB,IAAIC;IAC7B,MAAMC,WAAW,IAAID,IAAY;QAC/BP,QAAQS,WAAW,IAAI;WACnBT,QAAQU,kBAAkB,IAAI,EAAE;KACrC;IAED,KAAK,MAAMX,cAAcM,YAAa;QACpC,IAAIG,SAASG,GAAG,CAACZ,WAAWI,IAAI,GAAG;QAEnC,kEAAkE;QAClE,IAAI,AAACJ,WAAkCa,IAAI,EAAE;YAC3CJ,SAASK,GAAG,CAACd,WAAWI,IAAI;YAC5B;QACF;QAEA,MAAMW,WAAWhB,iBAAiBC,YAAYC;QAC9C,IAAIc,aAAa,WAAWR,iBAAiBO,GAAG,CAACd,WAAWI,IAAI;IAClE;IAEA,OAAO;QAAEG;QAAkBE;IAAS;AACtC;AAmBA;;;;;;;;;CASC,GACD,OAAO,SAASO,oBACdC,OAAuB,EACvBhB,UAAsC,CAAC,CAAC;IAExC,MAAMiB,eAAe,IAAIV;IACzB,MAAMC,WAAW,IAAID,IAAYP,QAAQkB,cAAc,IAAI,EAAE;IAE7D,KAAK,MAAMC,UAAUH,QAAS;QAC5B,IAAIR,SAASG,GAAG,CAACQ,OAAOhB,IAAI,GAAG;QAC/B,IAAI,CAACN,gBAAgBsB,SAAS;QAC9B,MAAMlB,WAAWD,QAAQE,aAAa,EAAE,CAACiB,OAAOhB,IAAI,CAAC;QACrD,IAAIF,aAAa,kBAAkB;QACnCgB,aAAaJ,GAAG,CAACM,OAAOhB,IAAI;IAC9B;IAEA,OAAO;QAAEc;QAAcT;IAAS;AAClC"}
1
+ {"version":3,"sources":["../src/draft-workflow.ts"],"sourcesContent":["import type { CollectionConfig, GlobalConfig } from 'payload'\nimport { hasCollectionDrafts, hasGlobalDrafts } from './introspection'\n\ninterface ComputeDraftCollectionsOptions {\n /** Per-collection draft behavior overrides */\n draftBehavior?: Record<string, 'always-draft' | 'always-publish'>\n /** Collection slugs to exclude from MCP entirely */\n excludeCollections?: string[]\n /** API-keys collection slug — always excluded from the MCP surface */\n apiKeysSlug?: string\n}\n\n/**\n * Determines the draft behavior for a collection.\n *\n * - No drafts configured → 'publish' (raw update allowed; no draft concept)\n * - Drafts configured + override given → use override\n * - Drafts configured + no override → 'always-draft' (raw update locked)\n */\nexport function getDraftBehavior(\n collection: CollectionConfig,\n options?: { draftBehavior?: Record<string, 'always-draft' | 'always-publish'> },\n): 'always-draft' | 'always-publish' | 'publish' {\n if (!hasCollectionDrafts(collection)) return 'publish'\n\n const override = options?.draftBehavior?.[collection.slug]\n if (override) return override\n\n return 'always-draft'\n}\n\nexport interface DraftCollectionsResult {\n /** Slugs of collections that participate in the draft workflow. */\n draftCollections: Set<string>\n /** Slugs of collections excluded from the MCP surface entirely. */\n excluded: Set<string>\n}\n\n/**\n * Walks the collection list and returns:\n * - `draftCollections`: slugs whose `versions.drafts` is on (or whose\n * behavior override is `'always-draft'`).\n * - `excluded`: slugs to hide from the MCP surface entirely (the api-keys\n * collection itself, anything with `auth: true`, anything in\n * `excludeCollections`).\n *\n * Replaces the v0.3.x `generateMcpCollectionConfigs` shape now that the\n * toolkit owns the endpoint and tool dispatch directly — no need to produce\n * an upstream `mcpCollections` config object.\n */\nexport function computeDraftCollections(\n collections: CollectionConfig[],\n options: ComputeDraftCollectionsOptions = {},\n): DraftCollectionsResult {\n const draftCollections = new Set<string>()\n const excluded = new Set<string>([\n options.apiKeysSlug ?? 'payload-mcp-api-keys',\n ...(options.excludeCollections ?? []),\n ])\n\n for (const collection of collections) {\n if (excluded.has(collection.slug)) continue\n\n // Auth-enabled collections are users — never expose them via MCP.\n if ((collection as { auth?: unknown }).auth) {\n excluded.add(collection.slug)\n continue\n }\n\n const behavior = getDraftBehavior(collection, options)\n if (behavior !== 'publish') draftCollections.add(collection.slug)\n }\n\n return { draftCollections, excluded }\n}\n\ninterface ComputeDraftGlobalsOptions {\n /**\n * Per-resource draft behavior overrides. Shared with collections: a single\n * record may contain both collection slugs and global slugs. Collisions\n * are unambiguous in practice — Payload's slug registry forbids a global\n * and a collection sharing a slug.\n */\n draftBehavior?: Record<string, 'always-draft' | 'always-publish'>\n /** Global slugs to exclude from the MCP surface entirely */\n excludeGlobals?: string[]\n}\n\nexport interface DraftGlobalsResult {\n draftGlobals: Set<string>\n excluded: Set<string>\n}\n\n/**\n * Peer of `computeDraftCollections` for globals. Returns the slugs whose\n * draft workflow is on (i.e. `versions.drafts` enabled, with optional\n * `'always-publish'` override turning it off), and the set excluded from\n * the MCP surface entirely.\n *\n * Mirrors the collection rule: `versions.drafts: true` defaults to\n * `'always-draft'` so raw publish is locked and `updateGlobal` / patch\n * routes preserve draft semantics.\n */\nexport function computeDraftGlobals(\n globals: GlobalConfig[],\n options: ComputeDraftGlobalsOptions = {},\n): DraftGlobalsResult {\n const draftGlobals = new Set<string>()\n const excluded = new Set<string>(options.excludeGlobals ?? [])\n\n for (const global of globals) {\n if (excluded.has(global.slug)) continue\n if (!hasGlobalDrafts(global)) continue\n const override = options.draftBehavior?.[global.slug]\n if (override === 'always-publish') continue\n draftGlobals.add(global.slug)\n }\n\n return { draftGlobals, excluded }\n}\n"],"names":["hasCollectionDrafts","hasGlobalDrafts","getDraftBehavior","collection","options","override","draftBehavior","slug","computeDraftCollections","collections","draftCollections","Set","excluded","apiKeysSlug","excludeCollections","has","auth","add","behavior","computeDraftGlobals","globals","draftGlobals","excludeGlobals","global"],"mappings":"AACA,SAASA,mBAAmB,EAAEC,eAAe,QAAQ,kBAAiB;AAWtE;;;;;;CAMC,GACD,OAAO,SAASC,iBACdC,UAA4B,EAC5BC,OAA+E;IAE/E,IAAI,CAACJ,oBAAoBG,aAAa,OAAO;IAE7C,MAAME,WAAWD,SAASE,eAAe,CAACH,WAAWI,IAAI,CAAC;IAC1D,IAAIF,UAAU,OAAOA;IAErB,OAAO;AACT;AASA;;;;;;;;;;;CAWC,GACD,OAAO,SAASG,wBACdC,WAA+B,EAC/BL,UAA0C,CAAC,CAAC;IAE5C,MAAMM,mBAAmB,IAAIC;IAC7B,MAAMC,WAAW,IAAID,IAAY;QAC/BP,QAAQS,WAAW,IAAI;WACnBT,QAAQU,kBAAkB,IAAI,EAAE;KACrC;IAED,KAAK,MAAMX,cAAcM,YAAa;QACpC,IAAIG,SAASG,GAAG,CAACZ,WAAWI,IAAI,GAAG;QAEnC,kEAAkE;QAClE,IAAI,AAACJ,WAAkCa,IAAI,EAAE;YAC3CJ,SAASK,GAAG,CAACd,WAAWI,IAAI;YAC5B;QACF;QAEA,MAAMW,WAAWhB,iBAAiBC,YAAYC;QAC9C,IAAIc,aAAa,WAAWR,iBAAiBO,GAAG,CAACd,WAAWI,IAAI;IAClE;IAEA,OAAO;QAAEG;QAAkBE;IAAS;AACtC;AAmBA;;;;;;;;;CASC,GACD,OAAO,SAASO,oBACdC,OAAuB,EACvBhB,UAAsC,CAAC,CAAC;IAExC,MAAMiB,eAAe,IAAIV;IACzB,MAAMC,WAAW,IAAID,IAAYP,QAAQkB,cAAc,IAAI,EAAE;IAE7D,KAAK,MAAMC,UAAUH,QAAS;QAC5B,IAAIR,SAASG,GAAG,CAACQ,OAAOhB,IAAI,GAAG;QAC/B,IAAI,CAACN,gBAAgBsB,SAAS;QAC9B,MAAMlB,WAAWD,QAAQE,aAAa,EAAE,CAACiB,OAAOhB,IAAI,CAAC;QACrD,IAAIF,aAAa,kBAAkB;QACnCgB,aAAaJ,GAAG,CAACM,OAAOhB,IAAI;IAC9B;IAEA,OAAO;QAAEc;QAAcT;IAAS;AAClC"}
@@ -24,6 +24,11 @@ export interface CreateMcpEndpointsOptions {
24
24
  serverURL?: string;
25
25
  /** Forwarded to mcp-handler. Default false. */
26
26
  verboseLogs?: boolean;
27
+ oauth?: {
28
+ authenticate: (req: PayloadRequest) => Promise<boolean>;
29
+ metadataURL: string;
30
+ scope: string;
31
+ };
27
32
  }
28
33
  /**
29
34
  * Builds the POST + GET endpoints for `/api/mcp`. Designed to be pushed into
package/dist/endpoint.js CHANGED
@@ -2,10 +2,10 @@ import { createMcpHandler } from 'mcp-handler';
2
2
  import { getApiKeyContext } from './auth-strategy';
3
3
  export const MCP_BASE_PATH = '/api/mcp';
4
4
  export const MCP_ENDPOINT_PATH = '/mcp';
5
- /**
6
- * `mcp-handler` derives its streamable-HTTP route as `${basePath}/mcp`. To
7
- * land that on `/api/mcp` (the public URL), we pass `/api` here — not the
8
- * full `/api/mcp`, which would route to `/api/mcp/mcp` and 404.
5
+ /**
6
+ * `mcp-handler` derives its streamable-HTTP route as `${basePath}/mcp`. To
7
+ * land that on `/api/mcp` (the public URL), we pass `/api` here — not the
8
+ * full `/api/mcp`, which would route to `/api/mcp/mcp` and 404.
9
9
  */ const MCP_HANDLER_BASE_PATH = '/api';
10
10
  function jsonRpcError(message, code, status) {
11
11
  const body = {
@@ -42,14 +42,19 @@ function isHostAllowed(host, serverURL) {
42
42
  }
43
43
  return host === expected;
44
44
  }
45
- /**
46
- * Builds the POST + GET endpoints for `/api/mcp`. Designed to be pushed into
47
- * `incomingConfig.endpoints` (additive — existing endpoints are preserved).
48
- *
49
- * Pure function; can be unit-tested by invoking the returned handlers with a
50
- * minimally-shaped PayloadRequest.
45
+ /**
46
+ * Builds the POST + GET endpoints for `/api/mcp`. Designed to be pushed into
47
+ * `incomingConfig.endpoints` (additive — existing endpoints are preserved).
48
+ *
49
+ * Pure function; can be unit-tested by invoking the returned handlers with a
50
+ * minimally-shaped PayloadRequest.
51
51
  */ export function createMcpEndpoints(options) {
52
52
  const { buildInitializeServer, allowedOrigins, serverURL, verboseLogs = false } = options;
53
+ function unauthorized() {
54
+ const response = jsonRpcError(options.oauth ? 'Unauthorized: MCP API key or access token required' : 'Unauthorized: MCP API key required', -32001, 401);
55
+ if (options.oauth) response.headers.set('WWW-Authenticate', `Bearer resource_metadata="${options.oauth.metadataURL}", scope="${options.oauth.scope}"`);
56
+ return response;
57
+ }
53
58
  const postHandler = async (req)=>{
54
59
  const headers = req.headers;
55
60
  const origin = headers?.get('origin') ?? null;
@@ -65,8 +70,8 @@ function isHostAllowed(host, serverURL) {
65
70
  // present a recognized MCP API key. We refuse before constructing any
66
71
  // mcp-handler — refusing here prevents tools/list and tool dispatch from
67
72
  // running on unauthenticated requests.
68
- if (!getApiKeyContext(req)) {
69
- return jsonRpcError('Unauthorized: MCP API key required', -32001, 401);
73
+ if (!getApiKeyContext(req) && !await options.oauth?.authenticate(req)) {
74
+ return unauthorized();
70
75
  }
71
76
  if (!req.url) return jsonRpcError('Missing request URL', -32600, 400);
72
77
  const handler = createMcpHandler(buildInitializeServer(req), undefined, {
@@ -85,7 +90,10 @@ function isHostAllowed(host, serverURL) {
85
90
  });
86
91
  return handler(fetchRequest);
87
92
  };
88
- const getHandler = async ()=>{
93
+ const getHandler = async (req)=>{
94
+ if (options.oauth && !isHostAllowed(req.headers?.get('host') ?? null, serverURL)) return jsonRpcError('Invalid host', -32600, 400);
95
+ if (options.oauth && !isOriginAllowed(req.headers?.get('origin') ?? null, allowedOrigins)) return jsonRpcError('Origin not allowed', -32600, 403);
96
+ if (options.oauth && !getApiKeyContext(req) && !await options.oauth.authenticate(req)) return unauthorized();
89
97
  return jsonRpcError('POST required for MCP requests', -32600, 405);
90
98
  };
91
99
  return [
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/endpoint.ts"],"sourcesContent":["import type { Endpoint, PayloadRequest } from 'payload'\r\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\r\nimport { createMcpHandler } from 'mcp-handler'\r\nimport { getApiKeyContext } from './auth-strategy'\r\n\r\nexport const MCP_BASE_PATH = '/api/mcp'\r\nexport const MCP_ENDPOINT_PATH = '/mcp'\r\n/**\r\n * `mcp-handler` derives its streamable-HTTP route as `${basePath}/mcp`. To\r\n * land that on `/api/mcp` (the public URL), we pass `/api` here — not the\r\n * full `/api/mcp`, which would route to `/api/mcp/mcp` and 404.\r\n */\r\nconst MCP_HANDLER_BASE_PATH = '/api'\r\n\r\nexport type InitializeServerForRequest = (\r\n req: PayloadRequest,\r\n) => (server: McpServer) => void | Promise<void>\r\n\r\nexport interface CreateMcpEndpointsOptions {\r\n /**\r\n * Per-request factory that returns the McpServer initializer. Called once\r\n * per POST /api/mcp; the returned callback receives a fresh McpServer\r\n * instance and registers tools/prompts/resources scoped to this request.\r\n */\r\n buildInitializeServer: InitializeServerForRequest\r\n /**\r\n * Origins permitted to send the `Origin` header. Server-to-server callers\r\n * (no Origin) are always allowed. An empty / unset list means \"no browsers\".\r\n * `*` is intentionally not honoured — be explicit.\r\n */\r\n allowedOrigins?: string[]\r\n /**\r\n * The Payload `serverURL`. When set, the request `Host` header must match\r\n * this origin's host (DNS-rebinding mitigation). When unset, host check is\r\n * skipped (useful in dev where the host varies).\r\n */\r\n serverURL?: string\r\n /** Forwarded to mcp-handler. Default false. */\r\n verboseLogs?: boolean\r\n}\r\n\r\ninterface JsonRpcErrorBody {\r\n jsonrpc: '2.0'\r\n error: { code: number; message: string }\r\n id: null\r\n}\r\n\r\nfunction jsonRpcError(message: string, code: number, status: number): Response {\r\n const body: JsonRpcErrorBody = { jsonrpc: '2.0', id: null, error: { code, message } }\r\n return new Response(JSON.stringify(body), {\r\n status,\r\n headers: { 'content-type': 'application/json' },\r\n })\r\n}\r\n\r\nfunction isOriginAllowed(origin: string | null, allowedOrigins: string[] | undefined): boolean {\r\n if (!origin) return true // server-to-server\r\n if (!allowedOrigins || allowedOrigins.length === 0) return false\r\n return allowedOrigins.includes(origin)\r\n}\r\n\r\nfunction isHostAllowed(host: string | null, serverURL: string | undefined): boolean {\r\n if (!serverURL) return true // dev / no serverURL configured\r\n if (!host) return false\r\n let expected: string\r\n try {\r\n expected = new URL(serverURL).host\r\n } catch {\r\n return true // misconfigured serverURL — fail open rather than break boot\r\n }\r\n return host === expected\r\n}\r\n\r\n/**\r\n * Builds the POST + GET endpoints for `/api/mcp`. Designed to be pushed into\r\n * `incomingConfig.endpoints` (additive — existing endpoints are preserved).\r\n *\r\n * Pure function; can be unit-tested by invoking the returned handlers with a\r\n * minimally-shaped PayloadRequest.\r\n */\r\nexport function createMcpEndpoints(options: CreateMcpEndpointsOptions): Endpoint[] {\r\n const { buildInitializeServer, allowedOrigins, serverURL, verboseLogs = false } = options\r\n\r\n const postHandler = async (req: PayloadRequest): Promise<Response> => {\r\n const headers = req.headers as Headers | undefined\r\n const origin = headers?.get('origin') ?? null\r\n const host = headers?.get('host') ?? null\r\n\r\n if (!isHostAllowed(host, serverURL)) {\r\n return jsonRpcError('Invalid host', -32600, 400)\r\n }\r\n if (!isOriginAllowed(origin, allowedOrigins)) {\r\n return jsonRpcError('Origin not allowed', -32600, 403)\r\n }\r\n\r\n // Auth gate. The bearer strategy populates req.user._mcpKey on a valid\r\n // Authorization: Bearer <key>; missing context means the request did not\r\n // present a recognized MCP API key. We refuse before constructing any\r\n // mcp-handler — refusing here prevents tools/list and tool dispatch from\r\n // running on unauthenticated requests.\r\n if (!getApiKeyContext(req)) {\r\n return jsonRpcError('Unauthorized: MCP API key required', -32001, 401)\r\n }\r\n\r\n if (!req.url) return jsonRpcError('Missing request URL', -32600, 400)\r\n\r\n const handler = createMcpHandler(buildInitializeServer(req), undefined, {\r\n basePath: MCP_HANDLER_BASE_PATH,\r\n disableSse: true,\r\n verboseLogs,\r\n })\r\n\r\n const fetchRequest = new Request(req.url, {\r\n method: req.method ?? 'POST',\r\n headers: req.headers as HeadersInit,\r\n body: req.body as BodyInit | null | undefined,\r\n // duplex is required for streaming bodies in newer node fetch\r\n ...({ duplex: 'half' } as Record<string, unknown>),\r\n })\r\n\r\n return handler(fetchRequest)\r\n }\r\n\r\n const getHandler = async (): Promise<Response> => {\r\n return jsonRpcError('POST required for MCP requests', -32600, 405)\r\n }\r\n\r\n return [\r\n { path: MCP_ENDPOINT_PATH, method: 'post', handler: postHandler },\r\n { path: MCP_ENDPOINT_PATH, method: 'get', handler: getHandler },\r\n ]\r\n}\r\n"],"names":["createMcpHandler","getApiKeyContext","MCP_BASE_PATH","MCP_ENDPOINT_PATH","MCP_HANDLER_BASE_PATH","jsonRpcError","message","code","status","body","jsonrpc","id","error","Response","JSON","stringify","headers","isOriginAllowed","origin","allowedOrigins","length","includes","isHostAllowed","host","serverURL","expected","URL","createMcpEndpoints","options","buildInitializeServer","verboseLogs","postHandler","req","get","url","handler","undefined","basePath","disableSse","fetchRequest","Request","method","duplex","getHandler","path"],"mappings":"AAEA,SAASA,gBAAgB,QAAQ,cAAa;AAC9C,SAASC,gBAAgB,QAAQ,kBAAiB;AAElD,OAAO,MAAMC,gBAAgB,WAAU;AACvC,OAAO,MAAMC,oBAAoB,OAAM;AACvC;;;;CAIC,GACD,MAAMC,wBAAwB;AAmC9B,SAASC,aAAaC,OAAe,EAAEC,IAAY,EAAEC,MAAc;IACjE,MAAMC,OAAyB;QAAEC,SAAS;QAAOC,IAAI;QAAMC,OAAO;YAAEL;YAAMD;QAAQ;IAAE;IACpF,OAAO,IAAIO,SAASC,KAAKC,SAAS,CAACN,OAAO;QACxCD;QACAQ,SAAS;YAAE,gBAAgB;QAAmB;IAChD;AACF;AAEA,SAASC,gBAAgBC,MAAqB,EAAEC,cAAoC;IAClF,IAAI,CAACD,QAAQ,OAAO,KAAK,mBAAmB;;IAC5C,IAAI,CAACC,kBAAkBA,eAAeC,MAAM,KAAK,GAAG,OAAO;IAC3D,OAAOD,eAAeE,QAAQ,CAACH;AACjC;AAEA,SAASI,cAAcC,IAAmB,EAAEC,SAA6B;IACvE,IAAI,CAACA,WAAW,OAAO,KAAK,gCAAgC;;IAC5D,IAAI,CAACD,MAAM,OAAO;IAClB,IAAIE;IACJ,IAAI;QACFA,WAAW,IAAIC,IAAIF,WAAWD,IAAI;IACpC,EAAE,OAAM;QACN,OAAO,KAAK,6DAA6D;;IAC3E;IACA,OAAOA,SAASE;AAClB;AAEA;;;;;;CAMC,GACD,OAAO,SAASE,mBAAmBC,OAAkC;IACnE,MAAM,EAAEC,qBAAqB,EAAEV,cAAc,EAAEK,SAAS,EAAEM,cAAc,KAAK,EAAE,GAAGF;IAElF,MAAMG,cAAc,OAAOC;QACzB,MAAMhB,UAAUgB,IAAIhB,OAAO;QAC3B,MAAME,SAASF,SAASiB,IAAI,aAAa;QACzC,MAAMV,OAAOP,SAASiB,IAAI,WAAW;QAErC,IAAI,CAACX,cAAcC,MAAMC,YAAY;YACnC,OAAOnB,aAAa,gBAAgB,CAAC,OAAO;QAC9C;QACA,IAAI,CAACY,gBAAgBC,QAAQC,iBAAiB;YAC5C,OAAOd,aAAa,sBAAsB,CAAC,OAAO;QACpD;QAEA,uEAAuE;QACvE,yEAAyE;QACzE,sEAAsE;QACtE,yEAAyE;QACzE,uCAAuC;QACvC,IAAI,CAACJ,iBAAiB+B,MAAM;YAC1B,OAAO3B,aAAa,sCAAsC,CAAC,OAAO;QACpE;QAEA,IAAI,CAAC2B,IAAIE,GAAG,EAAE,OAAO7B,aAAa,uBAAuB,CAAC,OAAO;QAEjE,MAAM8B,UAAUnC,iBAAiB6B,sBAAsBG,MAAMI,WAAW;YACtEC,UAAUjC;YACVkC,YAAY;YACZR;QACF;QAEA,MAAMS,eAAe,IAAIC,QAAQR,IAAIE,GAAG,EAAE;YACxCO,QAAQT,IAAIS,MAAM,IAAI;YACtBzB,SAASgB,IAAIhB,OAAO;YACpBP,MAAMuB,IAAIvB,IAAI;YACd,8DAA8D;YAC9D,GAAI;gBAAEiC,QAAQ;YAAO,CAAC;QACxB;QAEA,OAAOP,QAAQI;IACjB;IAEA,MAAMI,aAAa;QACjB,OAAOtC,aAAa,kCAAkC,CAAC,OAAO;IAChE;IAEA,OAAO;QACL;YAAEuC,MAAMzC;YAAmBsC,QAAQ;YAAQN,SAASJ;QAAY;QAChE;YAAEa,MAAMzC;YAAmBsC,QAAQ;YAAON,SAASQ;QAAW;KAC/D;AACH"}
1
+ {"version":3,"sources":["../src/endpoint.ts"],"sourcesContent":["import type { Endpoint, PayloadRequest } from 'payload'\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport { createMcpHandler } from 'mcp-handler'\nimport { getApiKeyContext } from './auth-strategy'\n\nexport const MCP_BASE_PATH = '/api/mcp'\nexport const MCP_ENDPOINT_PATH = '/mcp'\n/**\n * `mcp-handler` derives its streamable-HTTP route as `${basePath}/mcp`. To\n * land that on `/api/mcp` (the public URL), we pass `/api` here — not the\n * full `/api/mcp`, which would route to `/api/mcp/mcp` and 404.\n */\nconst MCP_HANDLER_BASE_PATH = '/api'\n\nexport type InitializeServerForRequest = (\n req: PayloadRequest,\n) => (server: McpServer) => void | Promise<void>\n\nexport interface CreateMcpEndpointsOptions {\n /**\n * Per-request factory that returns the McpServer initializer. Called once\n * per POST /api/mcp; the returned callback receives a fresh McpServer\n * instance and registers tools/prompts/resources scoped to this request.\n */\n buildInitializeServer: InitializeServerForRequest\n /**\n * Origins permitted to send the `Origin` header. Server-to-server callers\n * (no Origin) are always allowed. An empty / unset list means \"no browsers\".\n * `*` is intentionally not honoured — be explicit.\n */\n allowedOrigins?: string[]\n /**\n * The Payload `serverURL`. When set, the request `Host` header must match\n * this origin's host (DNS-rebinding mitigation). When unset, host check is\n * skipped (useful in dev where the host varies).\n */\n serverURL?: string\n /** Forwarded to mcp-handler. Default false. */\n verboseLogs?: boolean\n oauth?: {\n authenticate: (req: PayloadRequest) => Promise<boolean>\n metadataURL: string\n scope: string\n }\n}\n\ninterface JsonRpcErrorBody {\n jsonrpc: '2.0'\n error: { code: number; message: string }\n id: null\n}\n\nfunction jsonRpcError(message: string, code: number, status: number): Response {\n const body: JsonRpcErrorBody = { jsonrpc: '2.0', id: null, error: { code, message } }\n return new Response(JSON.stringify(body), {\n status,\n headers: { 'content-type': 'application/json' },\n })\n}\n\nfunction isOriginAllowed(origin: string | null, allowedOrigins: string[] | undefined): boolean {\n if (!origin) return true // server-to-server\n if (!allowedOrigins || allowedOrigins.length === 0) return false\n return allowedOrigins.includes(origin)\n}\n\nfunction isHostAllowed(host: string | null, serverURL: string | undefined): boolean {\n if (!serverURL) return true // dev / no serverURL configured\n if (!host) return false\n let expected: string\n try {\n expected = new URL(serverURL).host\n } catch {\n return true // misconfigured serverURL — fail open rather than break boot\n }\n return host === expected\n}\n\n/**\n * Builds the POST + GET endpoints for `/api/mcp`. Designed to be pushed into\n * `incomingConfig.endpoints` (additive — existing endpoints are preserved).\n *\n * Pure function; can be unit-tested by invoking the returned handlers with a\n * minimally-shaped PayloadRequest.\n */\nexport function createMcpEndpoints(options: CreateMcpEndpointsOptions): Endpoint[] {\n const { buildInitializeServer, allowedOrigins, serverURL, verboseLogs = false } = options\n function unauthorized(): Response {\n const response = jsonRpcError(options.oauth ? 'Unauthorized: MCP API key or access token required' : 'Unauthorized: MCP API key required', -32001, 401)\n if (options.oauth) response.headers.set('WWW-Authenticate', `Bearer resource_metadata=\"${options.oauth.metadataURL}\", scope=\"${options.oauth.scope}\"`)\n return response\n }\n\n const postHandler = async (req: PayloadRequest): Promise<Response> => {\n const headers = req.headers as Headers | undefined\n const origin = headers?.get('origin') ?? null\n const host = headers?.get('host') ?? null\n\n if (!isHostAllowed(host, serverURL)) {\n return jsonRpcError('Invalid host', -32600, 400)\n }\n if (!isOriginAllowed(origin, allowedOrigins)) {\n return jsonRpcError('Origin not allowed', -32600, 403)\n }\n\n // Auth gate. The bearer strategy populates req.user._mcpKey on a valid\n // Authorization: Bearer <key>; missing context means the request did not\n // present a recognized MCP API key. We refuse before constructing any\n // mcp-handler — refusing here prevents tools/list and tool dispatch from\n // running on unauthenticated requests.\n if (!getApiKeyContext(req) && !await options.oauth?.authenticate(req)) {\n return unauthorized()\n }\n\n if (!req.url) return jsonRpcError('Missing request URL', -32600, 400)\n\n const handler = createMcpHandler(buildInitializeServer(req), undefined, {\n basePath: MCP_HANDLER_BASE_PATH,\n disableSse: true,\n verboseLogs,\n })\n\n const fetchRequest = new Request(req.url, {\n method: req.method ?? 'POST',\n headers: req.headers as HeadersInit,\n body: req.body as BodyInit | null | undefined,\n // duplex is required for streaming bodies in newer node fetch\n ...({ duplex: 'half' } as Record<string, unknown>),\n })\n\n return handler(fetchRequest)\n }\n\n const getHandler = async (req: PayloadRequest): Promise<Response> => {\n if (options.oauth && !isHostAllowed(req.headers?.get('host') ?? null, serverURL)) return jsonRpcError('Invalid host', -32600, 400)\n if (options.oauth && !isOriginAllowed(req.headers?.get('origin') ?? null, allowedOrigins)) return jsonRpcError('Origin not allowed', -32600, 403)\n if (options.oauth && !getApiKeyContext(req) && !await options.oauth.authenticate(req)) return unauthorized()\n return jsonRpcError('POST required for MCP requests', -32600, 405)\n }\n\n return [\n { path: MCP_ENDPOINT_PATH, method: 'post', handler: postHandler },\n { path: MCP_ENDPOINT_PATH, method: 'get', handler: getHandler },\n ]\n}\n"],"names":["createMcpHandler","getApiKeyContext","MCP_BASE_PATH","MCP_ENDPOINT_PATH","MCP_HANDLER_BASE_PATH","jsonRpcError","message","code","status","body","jsonrpc","id","error","Response","JSON","stringify","headers","isOriginAllowed","origin","allowedOrigins","length","includes","isHostAllowed","host","serverURL","expected","URL","createMcpEndpoints","options","buildInitializeServer","verboseLogs","unauthorized","response","oauth","set","metadataURL","scope","postHandler","req","get","authenticate","url","handler","undefined","basePath","disableSse","fetchRequest","Request","method","duplex","getHandler","path"],"mappings":"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"}