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/prompts.ts"],"sourcesContent":["import type {\r\n BlockCatalog,\r\n BlockNestingMap,\r\n CollectionSchema,\r\n DomainPrompt,\r\n RelationshipEdge,\r\n} from './types'\r\n\r\n/**\r\n * Generate MCP prompts that teach the AI about the content model.\r\n *\r\n * Auto-generates content model overview, block composition guide, and\r\n * draft workflow guide. User-supplied domain prompts are appended.\r\n */\r\nexport function generatePrompts(\r\n schemas: Map<string, CollectionSchema>,\r\n catalog: BlockCatalog,\r\n nesting: BlockNestingMap,\r\n relationships: RelationshipEdge[],\r\n domainPrompts?: DomainPrompt[],\r\n) {\r\n const prompts = [\r\n buildContentModelOverview(schemas, relationships),\r\n buildBlockCompositionGuide(catalog, nesting),\r\n buildDraftWorkflowGuide(schemas),\r\n ]\r\n\r\n if (domainPrompts?.length) {\r\n for (const dp of domainPrompts) {\r\n prompts.push({\r\n name: dp.name,\r\n title: dp.title,\r\n description: dp.description,\r\n handler() {\r\n return {\r\n messages: [\r\n {\r\n content: { type: 'text' as const, text: dp.content },\r\n role: 'user' as const,\r\n },\r\n ],\r\n }\r\n },\r\n })\r\n }\r\n }\r\n\r\n return prompts\r\n}\r\n\r\n// ─── Prompt builders ──────────────────────────────────────────────\r\n\r\nfunction buildContentModelOverview(\r\n schemas: Map<string, CollectionSchema>,\r\n relationships: RelationshipEdge[],\r\n) {\r\n return {\r\n name: 'contentModelOverview',\r\n title: 'Content Model Overview',\r\n description:\r\n 'Describes every collection in the CMS — its purpose, fields, and relationships to other collections.',\r\n handler() {\r\n const lines: string[] = ['# Content Model Overview', '']\r\n\r\n for (const [slug, schema] of schemas) {\r\n lines.push(`## Collection: ${slug}`)\r\n lines.push(`Draft support: ${schema.hasDrafts ? 'yes' : 'no'}`)\r\n lines.push(`Live preview: ${schema.hasLivePreview ? 'yes' : 'no'}`)\r\n lines.push('')\r\n\r\n lines.push('### Fields')\r\n for (const field of schema.fields) {\r\n const parts = [`- **${field.name}** (${field.type})`]\r\n if (field.required) parts.push(' *required*')\r\n if (field.hasMany) parts.push(' hasMany')\r\n if (field.relationTo) {\r\n const targets = Array.isArray(field.relationTo)\r\n ? field.relationTo.join(', ')\r\n : field.relationTo\r\n parts.push(` → ${targets}`)\r\n }\r\n if (field.options?.length) {\r\n const vals = field.options.map((o) => o.value).join(', ')\r\n parts.push(` [${vals}]`)\r\n }\r\n if (field.maxRows) parts.push(` maxRows: ${field.maxRows}`)\r\n lines.push(parts.join(''))\r\n }\r\n lines.push('')\r\n\r\n const collRels = relationships.filter((r) => r.fromCollection === slug)\r\n if (collRels.length > 0) {\r\n lines.push('### Relationships')\r\n for (const rel of collRels) {\r\n const targets = Array.isArray(rel.toCollection)\r\n ? rel.toCollection.join(', ')\r\n : rel.toCollection\r\n lines.push(`- ${rel.fieldName} → ${targets}${rel.hasMany ? ' (hasMany)' : ''}`)\r\n }\r\n lines.push('')\r\n }\r\n }\r\n\r\n return {\r\n messages: [\r\n {\r\n content: { type: 'text' as const, text: lines.join('\\n') },\r\n role: 'user' as const,\r\n },\r\n ],\r\n }\r\n },\r\n }\r\n}\r\n\r\nfunction buildBlockCompositionGuide(catalog: BlockCatalog, nesting: BlockNestingMap) {\r\n return {\r\n name: 'blockCompositionGuide',\r\n title: 'Block Composition Guide',\r\n description:\r\n 'Lists every block type, its fields, and which slugs each blocks-typed field accepts. Use this with the relationshipGraph and collectionSchema resources to compose layouts at any depth.',\r\n handler() {\r\n const lines: string[] = [\r\n '# Block Composition Guide',\r\n '',\r\n 'Every block has a `blockType` discriminator plus its own fields. A block may include one or more `blocks`-typed fields that nest other blocks. The accepted slugs per field are listed below — recurse into the same map for deeper nesting.',\r\n '',\r\n ]\r\n\r\n lines.push('## Where blocks can nest')\r\n lines.push('')\r\n const collectionEdges = nesting.filter((e) => e.ownerType === 'collection')\r\n const blockEdges = nesting.filter((e) => e.ownerType === 'block')\r\n\r\n if (collectionEdges.length > 0) {\r\n lines.push('### In collections')\r\n for (const edge of collectionEdges) {\r\n const cap = edge.maxRows ? ` (max ${edge.maxRows})` : ''\r\n lines.push(\r\n `- \\`${edge.owner}.${edge.fieldPath}\\`${cap} accepts: ${edge.acceptedBlockSlugs.join(', ') || '(none)'}`,\r\n )\r\n }\r\n lines.push('')\r\n }\r\n\r\n if (blockEdges.length > 0) {\r\n lines.push('### In blocks (nested composition)')\r\n for (const edge of blockEdges) {\r\n const cap = edge.maxRows ? ` (max ${edge.maxRows})` : ''\r\n lines.push(\r\n `- block \\`${edge.owner}\\` field \\`${edge.fieldPath}\\`${cap} accepts: ${edge.acceptedBlockSlugs.join(', ') || '(none)'}`,\r\n )\r\n }\r\n lines.push('')\r\n }\r\n\r\n lines.push('## Block fields')\r\n lines.push('')\r\n for (const block of catalog.blocks) {\r\n lines.push(`### ${block.slug}`)\r\n if (block.fields.length === 0) {\r\n lines.push('(no fields)')\r\n } else {\r\n for (const f of block.fields) {\r\n const parts = [`- ${f.name} (${f.type})`]\r\n if (f.required) parts.push(' *required*')\r\n if (f.options?.length) {\r\n parts.push(` [${f.options.map((o) => o.value).join(', ')}]`)\r\n }\r\n if (f.relationTo) {\r\n const targets = Array.isArray(f.relationTo)\r\n ? f.relationTo.join(', ')\r\n : f.relationTo\r\n parts.push(` → ${targets}`)\r\n }\r\n lines.push(parts.join(''))\r\n }\r\n }\r\n lines.push('')\r\n }\r\n\r\n return {\r\n messages: [\r\n {\r\n content: { type: 'text' as const, text: lines.join('\\n') },\r\n role: 'user' as const,\r\n },\r\n ],\r\n }\r\n },\r\n }\r\n}\r\n\r\nfunction buildDraftWorkflowGuide(schemas: Map<string, CollectionSchema>) {\r\n return {\r\n name: 'draftWorkflowGuide',\r\n title: 'Draft Workflow Guide',\r\n description:\r\n 'Explains which collections support drafts, how to create drafts, review them, and publish.',\r\n handler() {\r\n const draftCollections: string[] = []\r\n const publishCollections: string[] = []\r\n\r\n for (const [slug, schema] of schemas) {\r\n if (schema.hasDrafts) {\r\n draftCollections.push(slug)\r\n } else {\r\n publishCollections.push(slug)\r\n }\r\n }\r\n\r\n const lines: string[] = ['# Draft Workflow Guide', '', '## Collections with draft support', '']\r\n\r\n if (draftCollections.length === 0) {\r\n lines.push('No collections have draft support enabled.')\r\n } else {\r\n for (const slug of draftCollections) {\r\n lines.push(`- **${slug}**`)\r\n }\r\n lines.push('')\r\n lines.push('### How drafts work')\r\n lines.push(\r\n '1. When you create or update a document in a draft-enabled collection, set `_status: \"draft\"` to keep it unpublished.',\r\n )\r\n lines.push(\r\n '2. Draft documents are only visible via preview URLs or the admin panel — they are not public.',\r\n )\r\n lines.push(\r\n '3. To publish a draft, use the `publishDraft` tool (raw `update` is locked on always-draft collections).',\r\n )\r\n lines.push('4. You can review a draft via its preview URL before publishing.')\r\n }\r\n\r\n lines.push('')\r\n lines.push('## Collections without draft support')\r\n lines.push('')\r\n\r\n if (publishCollections.length === 0) {\r\n lines.push('All collections support drafts.')\r\n } else {\r\n for (const slug of publishCollections) {\r\n lines.push(`- **${slug}** — changes are published immediately`)\r\n }\r\n }\r\n\r\n return {\r\n messages: [\r\n {\r\n content: { type: 'text' as const, text: lines.join('\\n') },\r\n role: 'user' as const,\r\n },\r\n ],\r\n }\r\n },\r\n }\r\n}\r\n"],"names":["generatePrompts","schemas","catalog","nesting","relationships","domainPrompts","prompts","buildContentModelOverview","buildBlockCompositionGuide","buildDraftWorkflowGuide","length","dp","push","name","title","description","handler","messages","content","type","text","role","lines","slug","schema","hasDrafts","hasLivePreview","field","fields","parts","required","hasMany","relationTo","targets","Array","isArray","join","options","vals","map","o","value","maxRows","collRels","filter","r","fromCollection","rel","toCollection","fieldName","collectionEdges","e","ownerType","blockEdges","edge","cap","owner","fieldPath","acceptedBlockSlugs","block","blocks","f","draftCollections","publishCollections"],"mappings":"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"}
1
+ {"version":3,"sources":["../src/prompts.ts"],"sourcesContent":["import type {\n BlockCatalog,\n BlockNestingMap,\n CollectionSchema,\n DomainPrompt,\n RelationshipEdge,\n} from './types'\n\n/**\n * Generate MCP prompts that teach the AI about the content model.\n *\n * Auto-generates content model overview, block composition guide, and\n * draft workflow guide. User-supplied domain prompts are appended.\n */\nexport function generatePrompts(\n schemas: Map<string, CollectionSchema>,\n catalog: BlockCatalog,\n nesting: BlockNestingMap,\n relationships: RelationshipEdge[],\n domainPrompts?: DomainPrompt[],\n) {\n const prompts = [\n buildContentModelOverview(schemas, relationships),\n buildBlockCompositionGuide(catalog, nesting),\n buildDraftWorkflowGuide(schemas),\n ]\n\n if (domainPrompts?.length) {\n for (const dp of domainPrompts) {\n prompts.push({\n name: dp.name,\n title: dp.title,\n description: dp.description,\n handler() {\n return {\n messages: [\n {\n content: { type: 'text' as const, text: dp.content },\n role: 'user' as const,\n },\n ],\n }\n },\n })\n }\n }\n\n return prompts\n}\n\n// ─── Prompt builders ──────────────────────────────────────────────\n\nfunction buildContentModelOverview(\n schemas: Map<string, CollectionSchema>,\n relationships: RelationshipEdge[],\n) {\n return {\n name: 'contentModelOverview',\n title: 'Content Model Overview',\n description:\n 'Describes every collection in the CMS — its purpose, fields, and relationships to other collections.',\n handler() {\n const lines: string[] = ['# Content Model Overview', '']\n\n for (const [slug, schema] of schemas) {\n lines.push(`## Collection: ${slug}`)\n lines.push(`Draft support: ${schema.hasDrafts ? 'yes' : 'no'}`)\n lines.push(`Live preview: ${schema.hasLivePreview ? 'yes' : 'no'}`)\n lines.push('')\n\n lines.push('### Fields')\n for (const field of schema.fields) {\n const parts = [`- **${field.name}** (${field.type})`]\n if (field.required) parts.push(' *required*')\n if (field.hasMany) parts.push(' hasMany')\n if (field.relationTo) {\n const targets = Array.isArray(field.relationTo)\n ? field.relationTo.join(', ')\n : field.relationTo\n parts.push(` → ${targets}`)\n }\n if (field.options?.length) {\n const vals = field.options.map((o) => o.value).join(', ')\n parts.push(` [${vals}]`)\n }\n if (field.maxRows) parts.push(` maxRows: ${field.maxRows}`)\n lines.push(parts.join(''))\n }\n lines.push('')\n\n const collRels = relationships.filter((r) => r.fromCollection === slug)\n if (collRels.length > 0) {\n lines.push('### Relationships')\n for (const rel of collRels) {\n const targets = Array.isArray(rel.toCollection)\n ? rel.toCollection.join(', ')\n : rel.toCollection\n lines.push(`- ${rel.fieldName} → ${targets}${rel.hasMany ? ' (hasMany)' : ''}`)\n }\n lines.push('')\n }\n }\n\n return {\n messages: [\n {\n content: { type: 'text' as const, text: lines.join('\\n') },\n role: 'user' as const,\n },\n ],\n }\n },\n }\n}\n\nfunction buildBlockCompositionGuide(catalog: BlockCatalog, nesting: BlockNestingMap) {\n return {\n name: 'blockCompositionGuide',\n title: 'Block Composition Guide',\n description:\n 'Lists every block type, its fields, and which slugs each blocks-typed field accepts. Use this with the relationshipGraph and collectionSchema resources to compose layouts at any depth.',\n handler() {\n const lines: string[] = [\n '# Block Composition Guide',\n '',\n 'Every block has a `blockType` discriminator plus its own fields. A block may include one or more `blocks`-typed fields that nest other blocks. The accepted slugs per field are listed below — recurse into the same map for deeper nesting.',\n '',\n ]\n\n lines.push('## Where blocks can nest')\n lines.push('')\n const collectionEdges = nesting.filter((e) => e.ownerType === 'collection')\n const blockEdges = nesting.filter((e) => e.ownerType === 'block')\n\n if (collectionEdges.length > 0) {\n lines.push('### In collections')\n for (const edge of collectionEdges) {\n const cap = edge.maxRows ? ` (max ${edge.maxRows})` : ''\n lines.push(\n `- \\`${edge.owner}.${edge.fieldPath}\\`${cap} accepts: ${edge.acceptedBlockSlugs.join(', ') || '(none)'}`,\n )\n }\n lines.push('')\n }\n\n if (blockEdges.length > 0) {\n lines.push('### In blocks (nested composition)')\n for (const edge of blockEdges) {\n const cap = edge.maxRows ? ` (max ${edge.maxRows})` : ''\n lines.push(\n `- block \\`${edge.owner}\\` field \\`${edge.fieldPath}\\`${cap} accepts: ${edge.acceptedBlockSlugs.join(', ') || '(none)'}`,\n )\n }\n lines.push('')\n }\n\n lines.push('## Block fields')\n lines.push('')\n for (const block of catalog.blocks) {\n lines.push(`### ${block.slug}`)\n if (block.fields.length === 0) {\n lines.push('(no fields)')\n } else {\n for (const f of block.fields) {\n const parts = [`- ${f.name} (${f.type})`]\n if (f.required) parts.push(' *required*')\n if (f.options?.length) {\n parts.push(` [${f.options.map((o) => o.value).join(', ')}]`)\n }\n if (f.relationTo) {\n const targets = Array.isArray(f.relationTo)\n ? f.relationTo.join(', ')\n : f.relationTo\n parts.push(` → ${targets}`)\n }\n lines.push(parts.join(''))\n }\n }\n lines.push('')\n }\n\n return {\n messages: [\n {\n content: { type: 'text' as const, text: lines.join('\\n') },\n role: 'user' as const,\n },\n ],\n }\n },\n }\n}\n\nfunction buildDraftWorkflowGuide(schemas: Map<string, CollectionSchema>) {\n return {\n name: 'draftWorkflowGuide',\n title: 'Draft Workflow Guide',\n description:\n 'Explains which collections support drafts, how to create drafts, review them, and publish.',\n handler() {\n const draftCollections: string[] = []\n const publishCollections: string[] = []\n\n for (const [slug, schema] of schemas) {\n if (schema.hasDrafts) {\n draftCollections.push(slug)\n } else {\n publishCollections.push(slug)\n }\n }\n\n const lines: string[] = ['# Draft Workflow Guide', '', '## Collections with draft support', '']\n\n if (draftCollections.length === 0) {\n lines.push('No collections have draft support enabled.')\n } else {\n for (const slug of draftCollections) {\n lines.push(`- **${slug}**`)\n }\n lines.push('')\n lines.push('### How drafts work')\n lines.push(\n '1. When you create or update a document in a draft-enabled collection, set `_status: \"draft\"` to keep it unpublished.',\n )\n lines.push(\n '2. Draft documents are only visible via preview URLs or the admin panel — they are not public.',\n )\n lines.push(\n '3. To publish a draft, use the `publishDraft` tool (raw `update` is locked on always-draft collections).',\n )\n lines.push('4. You can review a draft via its preview URL before publishing.')\n }\n\n lines.push('')\n lines.push('## Collections without draft support')\n lines.push('')\n\n if (publishCollections.length === 0) {\n lines.push('All collections support drafts.')\n } else {\n for (const slug of publishCollections) {\n lines.push(`- **${slug}** — changes are published immediately`)\n }\n }\n\n return {\n messages: [\n {\n content: { type: 'text' as const, text: lines.join('\\n') },\n role: 'user' as const,\n },\n ],\n }\n },\n }\n}\n"],"names":["generatePrompts","schemas","catalog","nesting","relationships","domainPrompts","prompts","buildContentModelOverview","buildBlockCompositionGuide","buildDraftWorkflowGuide","length","dp","push","name","title","description","handler","messages","content","type","text","role","lines","slug","schema","hasDrafts","hasLivePreview","field","fields","parts","required","hasMany","relationTo","targets","Array","isArray","join","options","vals","map","o","value","maxRows","collRels","filter","r","fromCollection","rel","toCollection","fieldName","collectionEdges","e","ownerType","blockEdges","edge","cap","owner","fieldPath","acceptedBlockSlugs","block","blocks","f","draftCollections","publishCollections"],"mappings":"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"}
package/dist/registry.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { ZodObject } from 'zod';
2
2
  import { getApiKeyContext } from './auth-strategy';
3
- import { stampMcpContext } from './tools/_helpers';
3
+ import { LIMITED_SCOPE, 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
6
  /**
@@ -72,6 +72,12 @@ function scopeRejectionResult(reason) {
72
72
  return scopeRejectionResult(decision.reason ?? 'denied');
73
73
  }
74
74
  stampMcpContext(req);
75
+ // A key or connection limited to some collections or globals must not see linked entries
76
+ // from the others: tools then read relationships as IDs only (populateDepth).
77
+ req.context = {
78
+ ...req.context,
79
+ [LIMITED_SCOPE]: Boolean(keyCtx?.scopes?.collections || keyCtx?.scopes?.globals)
80
+ };
75
81
  try {
76
82
  const result = await tool.handler(args, req, extra);
77
83
  // A handler may report failure in the result envelope instead of
@@ -1 +1 @@
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":"AAEA,SAASA,SAAS,QAAyB,MAAK;AAChD,SAASC,gBAAgB,QAAQ,kBAAiB;AAElD,SAASC,eAAe,QAA8B,mBAAkB;AACxE,SACEC,iBAAiB,EACjBC,kBAAkB,EAClBC,iBAAiB,QAMZ,iBAAgB;AACvB,SAASC,eAAe,EAAEC,YAAY,EAAEC,WAAW,EAAEC,aAAa,QAAQ,oBAAmB;AAoB7F;;;;;CAKC,GACD,SAASC,WACPC,UAA8E;IAE9E,IAAIA,sBAAsBX,WAAW;QACnC,OAAOW,WAAWC,KAAK;IACzB;IACA,OAAOD;AACT;AAmBA,uEAAuE;AACvE,SACEN,iBAAiB,KAMlB;AAeD,SAASQ,qBAAqBC,MAAc;IAC1C,OAAO;QACLC,SAAS;YAAC;gBAAEC,MAAM;gBAAQC,MAAM,CAAC,iBAAiB,EAAEH,QAAQ;YAAC;SAAE;QAC/DI,SAAS;IACX;AACF;AAEA;;;;;;;;;;;CAWC,GACD,OAAO,SAASC,uBACdC,OAAsC;IAEtC,MAAM,EAAEC,KAAK,EAAEC,UAAU,EAAE,EAAEC,YAAY,EAAE,EAAE,GAAGH;IAChD,MAAMI,SAAwBpB,mBAAmBiB;IAEjD,OAAO,CAACI,MAAwB,CAACC;YAC/B,MAAMC,SAASF,IAAIG,OAAO,EAAED;YAC5B,MAAME,YAAYtB,aAAakB;YAC/B,MAAMK,UAAUtB,YAAYmB;YAE5B,KAAK,MAAMI,QAAQV,MAAO;gBACxB,MAAMW,eAAeR,OAAOS,QAAQ,CAACC,GAAG,CAACH,KAAKI,IAAI,KAAK;gBACvD,MAAMC,UAAU,OACdC,MACAC;oBAEA,MAAMC,QAAQC,KAAKC,GAAG;oBACtB,MAAMC,SAASzC,iBAAiBwB;oBAChC,MAAMkB,aACJ,OAAON,KAAKO,UAAU,KAAK,WACvBP,KAAKO,UAAU,GACf,OAAOP,KAAKQ,IAAI,KAAK,WACnBR,KAAKQ,IAAI,GACTC;oBACR,MAAMC,aAAaf,gBAAgBc;oBACnC,MAAME,WAAW1C,gBAAgB+B;oBAEjC,MAAMY,WAAW9C,kBACfuC,QAAQQ,UAAU,MAClBnB,KAAKI,IAAI,EACTQ,YACAnB;oBAGF,IAAI,CAACyB,SAASE,OAAO,EAAE;wBACrBrB,QACE,QACA;4BACEsB,OAAO;4BACPC,OAAOX,QAAQW;4BACfC,WAAWZ,QAAQY;4BACnBvB,MAAMA,KAAKI,IAAI;4BACfQ;4BACAI;4BACAC;4BACAO,SAAS;4BACTrC,SAAS;4BACTsC,YAAYhB,KAAKC,GAAG,KAAKF;4BACzBV;4BACA4B,YAAY;wBACd,GACA,CAAC,gDAAgD,EAAE1B,KAAKI,IAAI,EAAE;wBAEhE,OAAOtB,qBAAqBoC,SAASnC,MAAM,IAAI;oBACjD;oBAEAZ,gBAAgBuB;oBAEhB,IAAI;wBACF,MAAMiC,SAAS,MAAM3B,KAAK4B,OAAO,CAACtB,MAAMZ,KAAKa;wBAC7C,iEAAiE;wBACjE,uEAAuE;wBACvE,+DAA+D;wBAC/D,cAAc;wBACd,MAAMsB,gBAAgB,AAACF,QAAkCxC,YAAY;wBACrEY,QACE8B,gBAAgB,SAAS,QACzB;4BACER,OAAO;4BACPC,OAAOX,QAAQW;4BACfC,WAAWZ,QAAQY;4BACnBvB,MAAMA,KAAKI,IAAI;4BACfQ;4BACAI;4BACAC;4BACAO,SAAS,CAACK;4BACV1C,SAAS0C;4BACTJ,YAAYhB,KAAKC,GAAG,KAAKF;4BACzBV;4BACA,GAAI+B,gBAAgB;gCAAEH,YAAY;4BAAuB,IAAI,CAAC,CAAC;wBACjE,GACA,CAAC,+BAA+B,EAAEG,gBAAgB,uBAAuB,GAAG,EAAE,EAAE7B,KAAKI,IAAI,EAAE;wBAE7F,OAAOuB;oBACT,EAAE,OAAOG,KAAK;wBACZ,MAAMJ,aAAaI,eAAeC,QAAQD,IAAI1B,IAAI,GAAG;wBACrD,MAAM4B,UAAUF,eAAeC,QAAQD,IAAIE,OAAO,GAAGC,OAAOH;wBAC5D/B,QACE,SACA;4BACEsB,OAAO;4BACPS;4BACAR,OAAOX,QAAQW;4BACfC,WAAWZ,QAAQY;4BACnBvB,MAAMA,KAAKI,IAAI;4BACfQ;4BACAI;4BACAC;4BACAiB,aAAaxD,cAAc4B;4BAC3BkB,SAAS;4BACTrC,SAAS;4BACTsC,YAAYhB,KAAKC,GAAG,KAAKF;4BACzBV;4BACA4B;wBACF,GACA,CAAC,wCAAwC,EAAE1B,KAAKI,IAAI,EAAE;wBAExD,OAAO;4BACLpB,SAAS;gCAAC;oCAAEC,MAAM;oCAAQC,MAAM,CAAC,OAAO,EAAE8C,SAAS;gCAAC;6BAAE;4BACtD7C,SAAS;wBACX;oBACF;gBACF;gBAEAQ,OAAOwC,YAAY,CACjBnC,KAAKI,IAAI,EACT;oBACEgC,aAAapC,KAAKoC,WAAW;oBAC7BC,aAAa1D,WAAWqB,KAAKpB,UAAU;gBACzC,GACAyB;YAEJ;YAEA,KAAK,MAAMiC,UAAU/C,QAAS;gBAC5B,MAAMc,UAAU,OAAOC,MAAeC;oBACpC,IAAI;wBACF,OAAO,MAAM+B,OAAOV,OAAO,CAACtB,MAAMZ,KAAKa;oBACzC,EAAE,OAAOuB,KAAK;wBACZlC,QAAQ2C,QACN;4BAAElB,OAAO;4BAAcS;4BAAKQ,QAAQA,OAAOlC,IAAI;4BAAEN;wBAAU,GAC3D,CAAC,qCAAqC,EAAEwC,OAAOlC,IAAI,EAAE;wBAEvD,MAAM0B;oBACR;gBACF;gBACAnC,OAAO6C,cAAc,CACnBF,OAAOlC,IAAI,EACX;oBACEqC,OAAOH,OAAOG,KAAK;oBACnBL,aAAaE,OAAOF,WAAW;oBAC/BM,YAAYJ,OAAOI,UAAU;gBAC/B,GACArC;YAEJ;YAEA,KAAK,MAAMsC,YAAYnD,UAAW;gBAChC,MAAMa,UAAU,OAAOC,MAAeC;oBACpC,IAAI;wBACF,OAAO,MAAMoC,SAASf,OAAO,CAACtB,MAAMZ,KAAKa;oBAC3C,EAAE,OAAOuB,KAAK;wBACZlC,QAAQ2C,QACN;4BAAElB,OAAO;4BAAgBS;4BAAKa,UAAUA,SAASvC,IAAI;4BAAEN;wBAAU,GACjE,CAAC,4CAA4C,EAAE6C,SAASvC,IAAI,EAAE;wBAEhE,MAAM0B;oBACR;gBACF;gBACAnC,OAAOiD,gBAAgB,CACrBD,SAASvC,IAAI,EACbuC,SAASE,GAAG,EACZ;oBACEJ,OAAOE,SAASF,KAAK;oBACrBL,aAAaO,SAASP,WAAW;oBACjCU,UAAUH,SAASG,QAAQ;gBAC7B,GACAzC;YAEJ;QACF;AACF"}
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 { LIMITED_SCOPE, 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 // A key or connection limited to some collections or globals must not see linked entries\n // from the others: tools then read relationships as IDs only (populateDepth).\n req.context = { ...req.context, [LIMITED_SCOPE]: Boolean(keyCtx?.scopes?.collections || keyCtx?.scopes?.globals) }\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","LIMITED_SCOPE","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","context","Boolean","collections","globals","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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"}
package/dist/resources.js CHANGED
@@ -1,13 +1,13 @@
1
- /**
2
- * Generate MCP resources that expose the introspected schema as static JSON.
3
- *
4
- * Resources:
5
- * - blocks://catalog — flat list of every block and its fields
6
- * - blocks://nesting — per-blocks-field map of which slugs each field accepts
7
- * (includes global-owned edges when globals contain blocks fields)
8
- * - collections://schema — collection field metadata
9
- * - collections://relationships — collection relationship graph
10
- * - globals://schema — global field metadata (when any global is registered)
1
+ /**
2
+ * Generate MCP resources that expose the introspected schema as static JSON.
3
+ *
4
+ * Resources:
5
+ * - blocks://catalog — flat list of every block and its fields
6
+ * - blocks://nesting — per-blocks-field map of which slugs each field accepts
7
+ * (includes global-owned edges when globals contain blocks fields)
8
+ * - collections://schema — collection field metadata
9
+ * - collections://relationships — collection relationship graph
10
+ * - globals://schema — global field metadata (when any global is registered)
11
11
  */ export function generateResources(schemas, catalog, nesting, relationships, globalSchemas = new Map()) {
12
12
  const resources = [
13
13
  buildJsonResource({
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/resources.ts"],"sourcesContent":["import type {\r\n BlockCatalog,\r\n BlockNestingMap,\r\n CollectionSchema,\r\n GlobalSchema,\r\n RelationshipEdge,\r\n} from './types'\r\n\r\n/**\r\n * Generate MCP resources that expose the introspected schema as static JSON.\r\n *\r\n * Resources:\r\n * - blocks://catalog — flat list of every block and its fields\r\n * - blocks://nesting — per-blocks-field map of which slugs each field accepts\r\n * (includes global-owned edges when globals contain blocks fields)\r\n * - collections://schema — collection field metadata\r\n * - collections://relationships — collection relationship graph\r\n * - globals://schema — global field metadata (when any global is registered)\r\n */\r\nexport function generateResources(\r\n schemas: Map<string, CollectionSchema>,\r\n catalog: BlockCatalog,\r\n nesting: BlockNestingMap,\r\n relationships: RelationshipEdge[],\r\n globalSchemas: Map<string, GlobalSchema> = new Map(),\r\n) {\r\n const resources = [\r\n buildJsonResource({\r\n name: 'blockCatalog',\r\n title: 'Block Catalog',\r\n description:\r\n 'Flat list of every block type and its fields. Pair with the blockNesting resource to know where each block can be placed.',\r\n uri: 'blocks://catalog',\r\n payload: catalog,\r\n }),\r\n buildJsonResource({\r\n name: 'blockNesting',\r\n title: 'Block Nesting Map',\r\n description:\r\n 'For every blocks-typed field in the schema (in collections, globals, and inside other blocks), lists the block slugs that field accepts. Use this to compose nested layouts at any depth.',\r\n uri: 'blocks://nesting',\r\n payload: nesting,\r\n }),\r\n buildJsonResource({\r\n name: 'collectionSchema',\r\n title: 'Collection Schema',\r\n description:\r\n 'JSON schema of all collections — fields, select options, and relationship targets.',\r\n uri: 'collections://schema',\r\n payload: Object.fromEntries(schemas),\r\n }),\r\n buildJsonResource({\r\n name: 'relationshipGraph',\r\n title: 'Relationship Graph',\r\n description:\r\n 'JSON representation of the collection relationship graph — which collections link to which.',\r\n uri: 'collections://relationships',\r\n payload: relationships,\r\n }),\r\n ]\r\n\r\n if (globalSchemas.size > 0) {\r\n resources.push(\r\n buildJsonResource({\r\n name: 'globalSchema',\r\n title: 'Global Schema',\r\n description:\r\n 'JSON schema of all globals — fields, select options, draft and live-preview flags. Globals are singletons addressed by slug; pair with the findGlobal / updateGlobal tools.',\r\n uri: 'globals://schema',\r\n payload: Object.fromEntries(globalSchemas),\r\n }),\r\n )\r\n }\r\n\r\n return resources\r\n}\r\n\r\nfunction buildJsonResource(args: {\r\n name: string\r\n title: string\r\n description: string\r\n uri: string\r\n payload: unknown\r\n}) {\r\n const json = JSON.stringify(args.payload, null, 2)\r\n return {\r\n name: args.name,\r\n title: args.title,\r\n description: args.description,\r\n uri: args.uri,\r\n mimeType: 'application/json',\r\n handler(uri: URL) {\r\n return {\r\n contents: [{ uri: uri.href, text: json }],\r\n }\r\n },\r\n }\r\n}\r\n\r\n"],"names":["generateResources","schemas","catalog","nesting","relationships","globalSchemas","Map","resources","buildJsonResource","name","title","description","uri","payload","Object","fromEntries","size","push","args","json","JSON","stringify","mimeType","handler","contents","href","text"],"mappings":"AAQA;;;;;;;;;;CAUC,GACD,OAAO,SAASA,kBACdC,OAAsC,EACtCC,OAAqB,EACrBC,OAAwB,EACxBC,aAAiC,EACjCC,gBAA2C,IAAIC,KAAK;IAEpD,MAAMC,YAAY;QAChBC,kBAAkB;YAChBC,MAAM;YACNC,OAAO;YACPC,aACE;YACFC,KAAK;YACLC,SAASX;QACX;QACAM,kBAAkB;YAChBC,MAAM;YACNC,OAAO;YACPC,aACE;YACFC,KAAK;YACLC,SAASV;QACX;QACAK,kBAAkB;YAChBC,MAAM;YACNC,OAAO;YACPC,aACE;YACFC,KAAK;YACLC,SAASC,OAAOC,WAAW,CAACd;QAC9B;QACAO,kBAAkB;YAChBC,MAAM;YACNC,OAAO;YACPC,aACE;YACFC,KAAK;YACLC,SAAST;QACX;KACD;IAED,IAAIC,cAAcW,IAAI,GAAG,GAAG;QAC1BT,UAAUU,IAAI,CACZT,kBAAkB;YAChBC,MAAM;YACNC,OAAO;YACPC,aACE;YACFC,KAAK;YACLC,SAASC,OAAOC,WAAW,CAACV;QAC9B;IAEJ;IAEA,OAAOE;AACT;AAEA,SAASC,kBAAkBU,IAM1B;IACC,MAAMC,OAAOC,KAAKC,SAAS,CAACH,KAAKL,OAAO,EAAE,MAAM;IAChD,OAAO;QACLJ,MAAMS,KAAKT,IAAI;QACfC,OAAOQ,KAAKR,KAAK;QACjBC,aAAaO,KAAKP,WAAW;QAC7BC,KAAKM,KAAKN,GAAG;QACbU,UAAU;QACVC,SAAQX,GAAQ;YACd,OAAO;gBACLY,UAAU;oBAAC;wBAAEZ,KAAKA,IAAIa,IAAI;wBAAEC,MAAMP;oBAAK;iBAAE;YAC3C;QACF;IACF;AACF"}
1
+ {"version":3,"sources":["../src/resources.ts"],"sourcesContent":["import type {\n BlockCatalog,\n BlockNestingMap,\n CollectionSchema,\n GlobalSchema,\n RelationshipEdge,\n} from './types'\n\n/**\n * Generate MCP resources that expose the introspected schema as static JSON.\n *\n * Resources:\n * - blocks://catalog — flat list of every block and its fields\n * - blocks://nesting — per-blocks-field map of which slugs each field accepts\n * (includes global-owned edges when globals contain blocks fields)\n * - collections://schema — collection field metadata\n * - collections://relationships — collection relationship graph\n * - globals://schema — global field metadata (when any global is registered)\n */\nexport function generateResources(\n schemas: Map<string, CollectionSchema>,\n catalog: BlockCatalog,\n nesting: BlockNestingMap,\n relationships: RelationshipEdge[],\n globalSchemas: Map<string, GlobalSchema> = new Map(),\n) {\n const resources = [\n buildJsonResource({\n name: 'blockCatalog',\n title: 'Block Catalog',\n description:\n 'Flat list of every block type and its fields. Pair with the blockNesting resource to know where each block can be placed.',\n uri: 'blocks://catalog',\n payload: catalog,\n }),\n buildJsonResource({\n name: 'blockNesting',\n title: 'Block Nesting Map',\n description:\n 'For every blocks-typed field in the schema (in collections, globals, and inside other blocks), lists the block slugs that field accepts. Use this to compose nested layouts at any depth.',\n uri: 'blocks://nesting',\n payload: nesting,\n }),\n buildJsonResource({\n name: 'collectionSchema',\n title: 'Collection Schema',\n description:\n 'JSON schema of all collections — fields, select options, and relationship targets.',\n uri: 'collections://schema',\n payload: Object.fromEntries(schemas),\n }),\n buildJsonResource({\n name: 'relationshipGraph',\n title: 'Relationship Graph',\n description:\n 'JSON representation of the collection relationship graph — which collections link to which.',\n uri: 'collections://relationships',\n payload: relationships,\n }),\n ]\n\n if (globalSchemas.size > 0) {\n resources.push(\n buildJsonResource({\n name: 'globalSchema',\n title: 'Global Schema',\n description:\n 'JSON schema of all globals — fields, select options, draft and live-preview flags. Globals are singletons addressed by slug; pair with the findGlobal / updateGlobal tools.',\n uri: 'globals://schema',\n payload: Object.fromEntries(globalSchemas),\n }),\n )\n }\n\n return resources\n}\n\nfunction buildJsonResource(args: {\n name: string\n title: string\n description: string\n uri: string\n payload: unknown\n}) {\n const json = JSON.stringify(args.payload, null, 2)\n return {\n name: args.name,\n title: args.title,\n description: args.description,\n uri: args.uri,\n mimeType: 'application/json',\n handler(uri: URL) {\n return {\n contents: [{ uri: uri.href, text: json }],\n }\n },\n }\n}\n\n"],"names":["generateResources","schemas","catalog","nesting","relationships","globalSchemas","Map","resources","buildJsonResource","name","title","description","uri","payload","Object","fromEntries","size","push","args","json","JSON","stringify","mimeType","handler","contents","href","text"],"mappings":"AAQA;;;;;;;;;;CAUC,GACD,OAAO,SAASA,kBACdC,OAAsC,EACtCC,OAAqB,EACrBC,OAAwB,EACxBC,aAAiC,EACjCC,gBAA2C,IAAIC,KAAK;IAEpD,MAAMC,YAAY;QAChBC,kBAAkB;YAChBC,MAAM;YACNC,OAAO;YACPC,aACE;YACFC,KAAK;YACLC,SAASX;QACX;QACAM,kBAAkB;YAChBC,MAAM;YACNC,OAAO;YACPC,aACE;YACFC,KAAK;YACLC,SAASV;QACX;QACAK,kBAAkB;YAChBC,MAAM;YACNC,OAAO;YACPC,aACE;YACFC,KAAK;YACLC,SAASC,OAAOC,WAAW,CAACd;QAC9B;QACAO,kBAAkB;YAChBC,MAAM;YACNC,OAAO;YACPC,aACE;YACFC,KAAK;YACLC,SAAST;QACX;KACD;IAED,IAAIC,cAAcW,IAAI,GAAG,GAAG;QAC1BT,UAAUU,IAAI,CACZT,kBAAkB;YAChBC,MAAM;YACNC,OAAO;YACPC,aACE;YACFC,KAAK;YACLC,SAASC,OAAOC,WAAW,CAACV;QAC9B;IAEJ;IAEA,OAAOE;AACT;AAEA,SAASC,kBAAkBU,IAM1B;IACC,MAAMC,OAAOC,KAAKC,SAAS,CAACH,KAAKL,OAAO,EAAE,MAAM;IAChD,OAAO;QACLJ,MAAMS,KAAKT,IAAI;QACfC,OAAOQ,KAAKR,KAAK;QACjBC,aAAaO,KAAKP,WAAW;QAC7BC,KAAKM,KAAKN,GAAG;QACbU,UAAU;QACVC,SAAQX,GAAQ;YACd,OAAO;gBACLY,UAAU;oBAAC;wBAAEZ,KAAKA,IAAIa,IAAI;wBAAEC,MAAMP;oBAAK;iBAAE;YAC3C;QACF;IACF;AACF"}
@@ -1,9 +1,9 @@
1
1
  // ─── Audit logging helpers ───────────────────────────────────────────
2
2
  const MAX_LOGGED_STRING = 200;
3
- /**
4
- * Returns the top-level keys of a JSON-string `data` arg, sanitized.
5
- * Per the Codex post-planning finding: logging key names (not values) lets us
6
- * later analyze whether the prose-only input shape is causing AI mistakes.
3
+ /**
4
+ * Returns the top-level keys of a JSON-string `data` arg, sanitized.
5
+ * Per the Codex post-planning finding: logging key names (not values) lets us
6
+ * later analyze whether the prose-only input shape is causing AI mistakes.
7
7
  */ export function extractDataKeys(args) {
8
8
  const data = args.data;
9
9
  if (typeof data !== 'string') return undefined;
@@ -32,11 +32,11 @@ export function getRequestId(req) {
32
32
  const headers = req.headers;
33
33
  return headers?.get?.('x-request-id') ?? undefined;
34
34
  }
35
- /**
36
- * Returns a logger-invoker that swallows transport failures. Audit-log
37
- * writes must never break the tool dispatch path — a throwing logger
38
- * transport (closed stream during HMR, a custom pino dest) would otherwise
39
- * flip a success to isError or mask the real tool error.
35
+ /**
36
+ * Returns a logger-invoker that swallows transport failures. Audit-log
37
+ * writes must never break the tool dispatch path — a throwing logger
38
+ * transport (closed stream during HMR, a custom pino dest) would otherwise
39
+ * flip a success to isError or mask the real tool error.
40
40
  */ export function makeSafeLog(logger) {
41
41
  return (level, payload, message)=>{
42
42
  try {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/scope/audit-log.ts"],"sourcesContent":["import type { PayloadRequest } from 'payload'\r\n\r\n// ─── Audit logging helpers ───────────────────────────────────────────\r\n\r\nconst MAX_LOGGED_STRING = 200\r\n\r\n/**\r\n * Returns the top-level keys of a JSON-string `data` arg, sanitized.\r\n * Per the Codex post-planning finding: logging key names (not values) lets us\r\n * later analyze whether the prose-only input shape is causing AI mistakes.\r\n */\r\nexport function extractDataKeys(args: Record<string, unknown>): string[] | undefined {\r\n const data = args.data\r\n if (typeof data !== 'string') return undefined\r\n try {\r\n const parsed = JSON.parse(data) as unknown\r\n if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {\r\n return Object.keys(parsed as Record<string, unknown>)\r\n }\r\n } catch {\r\n return undefined\r\n }\r\n return undefined\r\n}\r\n\r\nexport function summariseArgs(args: Record<string, unknown>): Record<string, unknown> {\r\n const out: Record<string, unknown> = {}\r\n for (const [k, v] of Object.entries(args)) {\r\n if (typeof v === 'string' && v.length > MAX_LOGGED_STRING) {\r\n out[k] = `<truncated:${v.length}>`\r\n } else {\r\n out[k] = v\r\n }\r\n }\r\n return out\r\n}\r\n\r\nexport function getRequestId(req: PayloadRequest): string | undefined {\r\n const headers = req.headers as Headers | undefined\r\n return headers?.get?.('x-request-id') ?? undefined\r\n}\r\n\r\ntype LoggerLike = Partial<Record<'info' | 'warn' | 'error', (...args: unknown[]) => unknown>>\r\n\r\n/**\r\n * Returns a logger-invoker that swallows transport failures. Audit-log\r\n * writes must never break the tool dispatch path — a throwing logger\r\n * transport (closed stream during HMR, a custom pino dest) would otherwise\r\n * flip a success to isError or mask the real tool error.\r\n */\r\nexport function makeSafeLog(logger: LoggerLike | undefined) {\r\n return (\r\n level: 'info' | 'warn' | 'error',\r\n payload: Record<string, unknown>,\r\n message: string,\r\n ) => {\r\n try {\r\n logger?.[level]?.(payload, message)\r\n } catch {\r\n // Logger transport failure must not break dispatch.\r\n }\r\n }\r\n}\r\n"],"names":["MAX_LOGGED_STRING","extractDataKeys","args","data","undefined","parsed","JSON","parse","Array","isArray","Object","keys","summariseArgs","out","k","v","entries","length","getRequestId","req","headers","get","makeSafeLog","logger","level","payload","message"],"mappings":"AAEA,wEAAwE;AAExE,MAAMA,oBAAoB;AAE1B;;;;CAIC,GACD,OAAO,SAASC,gBAAgBC,IAA6B;IAC3D,MAAMC,OAAOD,KAAKC,IAAI;IACtB,IAAI,OAAOA,SAAS,UAAU,OAAOC;IACrC,IAAI;QACF,MAAMC,SAASC,KAAKC,KAAK,CAACJ;QAC1B,IAAIE,UAAU,OAAOA,WAAW,YAAY,CAACG,MAAMC,OAAO,CAACJ,SAAS;YAClE,OAAOK,OAAOC,IAAI,CAACN;QACrB;IACF,EAAE,OAAM;QACN,OAAOD;IACT;IACA,OAAOA;AACT;AAEA,OAAO,SAASQ,cAAcV,IAA6B;IACzD,MAAMW,MAA+B,CAAC;IACtC,KAAK,MAAM,CAACC,GAAGC,EAAE,IAAIL,OAAOM,OAAO,CAACd,MAAO;QACzC,IAAI,OAAOa,MAAM,YAAYA,EAAEE,MAAM,GAAGjB,mBAAmB;YACzDa,GAAG,CAACC,EAAE,GAAG,CAAC,WAAW,EAAEC,EAAEE,MAAM,CAAC,CAAC,CAAC;QACpC,OAAO;YACLJ,GAAG,CAACC,EAAE,GAAGC;QACX;IACF;IACA,OAAOF;AACT;AAEA,OAAO,SAASK,aAAaC,GAAmB;IAC9C,MAAMC,UAAUD,IAAIC,OAAO;IAC3B,OAAOA,SAASC,MAAM,mBAAmBjB;AAC3C;AAIA;;;;;CAKC,GACD,OAAO,SAASkB,YAAYC,MAA8B;IACxD,OAAO,CACLC,OACAC,SACAC;QAEA,IAAI;YACFH,QAAQ,CAACC,MAAM,GAAGC,SAASC;QAC7B,EAAE,OAAM;QACN,oDAAoD;QACtD;IACF;AACF"}
1
+ {"version":3,"sources":["../../src/scope/audit-log.ts"],"sourcesContent":["import type { PayloadRequest } from 'payload'\n\n// ─── Audit logging helpers ───────────────────────────────────────────\n\nconst MAX_LOGGED_STRING = 200\n\n/**\n * Returns the top-level keys of a JSON-string `data` arg, sanitized.\n * Per the Codex post-planning finding: logging key names (not values) lets us\n * later analyze whether the prose-only input shape is causing AI mistakes.\n */\nexport function extractDataKeys(args: Record<string, unknown>): string[] | undefined {\n const data = args.data\n if (typeof data !== 'string') return undefined\n try {\n const parsed = JSON.parse(data) as unknown\n if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {\n return Object.keys(parsed as Record<string, unknown>)\n }\n } catch {\n return undefined\n }\n return undefined\n}\n\nexport function summariseArgs(args: Record<string, unknown>): Record<string, unknown> {\n const out: Record<string, unknown> = {}\n for (const [k, v] of Object.entries(args)) {\n if (typeof v === 'string' && v.length > MAX_LOGGED_STRING) {\n out[k] = `<truncated:${v.length}>`\n } else {\n out[k] = v\n }\n }\n return out\n}\n\nexport function getRequestId(req: PayloadRequest): string | undefined {\n const headers = req.headers as Headers | undefined\n return headers?.get?.('x-request-id') ?? undefined\n}\n\ntype LoggerLike = Partial<Record<'info' | 'warn' | 'error', (...args: unknown[]) => unknown>>\n\n/**\n * Returns a logger-invoker that swallows transport failures. Audit-log\n * writes must never break the tool dispatch path — a throwing logger\n * transport (closed stream during HMR, a custom pino dest) would otherwise\n * flip a success to isError or mask the real tool error.\n */\nexport function makeSafeLog(logger: LoggerLike | undefined) {\n return (\n level: 'info' | 'warn' | 'error',\n payload: Record<string, unknown>,\n message: string,\n ) => {\n try {\n logger?.[level]?.(payload, message)\n } catch {\n // Logger transport failure must not break dispatch.\n }\n }\n}\n"],"names":["MAX_LOGGED_STRING","extractDataKeys","args","data","undefined","parsed","JSON","parse","Array","isArray","Object","keys","summariseArgs","out","k","v","entries","length","getRequestId","req","headers","get","makeSafeLog","logger","level","payload","message"],"mappings":"AAEA,wEAAwE;AAExE,MAAMA,oBAAoB;AAE1B;;;;CAIC,GACD,OAAO,SAASC,gBAAgBC,IAA6B;IAC3D,MAAMC,OAAOD,KAAKC,IAAI;IACtB,IAAI,OAAOA,SAAS,UAAU,OAAOC;IACrC,IAAI;QACF,MAAMC,SAASC,KAAKC,KAAK,CAACJ;QAC1B,IAAIE,UAAU,OAAOA,WAAW,YAAY,CAACG,MAAMC,OAAO,CAACJ,SAAS;YAClE,OAAOK,OAAOC,IAAI,CAACN;QACrB;IACF,EAAE,OAAM;QACN,OAAOD;IACT;IACA,OAAOA;AACT;AAEA,OAAO,SAASQ,cAAcV,IAA6B;IACzD,MAAMW,MAA+B,CAAC;IACtC,KAAK,MAAM,CAACC,GAAGC,EAAE,IAAIL,OAAOM,OAAO,CAACd,MAAO;QACzC,IAAI,OAAOa,MAAM,YAAYA,EAAEE,MAAM,GAAGjB,mBAAmB;YACzDa,GAAG,CAACC,EAAE,GAAG,CAAC,WAAW,EAAEC,EAAEE,MAAM,CAAC,CAAC,CAAC;QACpC,OAAO;YACLJ,GAAG,CAACC,EAAE,GAAGC;QACX;IACF;IACA,OAAOF;AACT;AAEA,OAAO,SAASK,aAAaC,GAAmB;IAC9C,MAAMC,UAAUD,IAAIC,OAAO;IAC3B,OAAOA,SAASC,MAAM,mBAAmBjB;AAC3C;AAIA;;;;;CAKC,GACD,OAAO,SAASkB,YAAYC,MAA8B;IACxD,OAAO,CACLC,OACAC,SACAC;QAEA,IAAI;YACFH,QAAQ,CAACC,MAAM,GAAGC,SAASC;QAC7B,EAAE,OAAM;QACN,oDAAoD;QACtD;IACF;AACF"}
@@ -101,13 +101,13 @@ export function buildRoutingTables(tools) {
101
101
  if (scopes.tools?.deny?.includes(toolName)) {
102
102
  return {
103
103
  allowed: false,
104
- reason: `Tool "${toolName}" is denied for this API key.`
104
+ reason: `Tool "${toolName}" is denied for this API key or connection.`
105
105
  };
106
106
  }
107
107
  if (scopes.tools?.allow && !scopes.tools.allow.includes(toolName)) {
108
108
  return {
109
109
  allowed: false,
110
- reason: `Tool "${toolName}" is not in the allow-list for this API key.`
110
+ reason: `Tool "${toolName}" is not in the allow-list for this API key or connection.`
111
111
  };
112
112
  }
113
113
  if (scopes.preset && PRESET_TOOL_DENY[scopes.preset]?.includes(toolName)) {
@@ -159,13 +159,13 @@ function checkResource(scopes, toolName, resource, toolAction, policy) {
159
159
  if (!override) {
160
160
  return {
161
161
  allowed: false,
162
- reason: `${policy.Label} "${resource}" is not in this API key's allowed ${policy.scopeAxis}.`
162
+ reason: `${policy.Label} "${resource}" is not in the allowed ${policy.scopeAxis} for this API key or connection.`
163
163
  };
164
164
  }
165
165
  if (!override.includes(action)) {
166
166
  return {
167
167
  allowed: false,
168
- reason: `Action "${action}" on ${policy.label} "${resource}" is not permitted by this API key's scope.`
168
+ reason: `Action "${action}" on ${policy.label} "${resource}" is not permitted for this API key or connection.`
169
169
  };
170
170
  }
171
171
  return {
@@ -178,13 +178,13 @@ function checkResource(scopes, toolName, resource, toolAction, policy) {
178
178
  // intent.
179
179
  return {
180
180
  allowed: false,
181
- reason: `Tool "${toolName}" requires an explicit ${policy.label} scope or preset on this API key.`
181
+ reason: `Tool "${toolName}" requires an explicit ${policy.label} scope or preset on this API key or connection.`
182
182
  };
183
183
  }
184
184
  if (!presetActions.includes(action)) {
185
185
  return {
186
186
  allowed: false,
187
- reason: `Action "${action}" on ${policy.label} "${resource}" is not permitted by this API key's preset.`
187
+ reason: `Action "${action}" on ${policy.label} "${resource}" is not permitted by the preset of this API key or connection.`
188
188
  };
189
189
  }
190
190
  return {
@@ -209,7 +209,7 @@ function checkAccount(scopes, toolName, toolAction) {
209
209
  if (action && !presetActions.includes(action)) {
210
210
  return {
211
211
  allowed: false,
212
- reason: `Action "${action}" is not permitted by this API key's preset.`
212
+ reason: `Action "${action}" is not permitted by the preset of this API key or connection.`
213
213
  };
214
214
  }
215
215
  return {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/scope/policy.ts"],"sourcesContent":["import type { CollectionAction, GlobalAction, KeyScopes, ScopePreset } from '../types'\n\n// ─── Routing primitives ──────────────────────────────────────────────\n\nexport type ResourceKind = 'collection' | 'global' | 'account'\n\n/**\n * Discriminated routing tag attached to every tool factory output.\n *\n * Collocates the scope-routing decision with the tool definition itself —\n * the registry derives the collection/global/account lookups from `tools`\n * at boot. Adding a new tool can no longer drift the routing maps out of\n * sync because TS requires `routing` on every factory return.\n */\nexport type ToolRouting =\n | { kind: 'collection'; action: CollectionAction }\n | { kind: 'global'; action: GlobalAction }\n | { kind: 'account'; action: CollectionAction }\n\n/**\n * Minimal \"routable tool\" interface used by the policy module. The full\n * `ToolFactoryOutput` shape (handler, parameters, description) is irrelevant\n * here; we only need `name` + `routing` to build the lookup tables.\n */\nexport interface RoutableTool {\n name: string\n routing: ToolRouting\n}\n\n// ─── Per-preset action tables ────────────────────────────────────────\n\nconst ALL_ACTIONS: CollectionAction[] = ['read', 'create', 'update', 'delete']\n\nexport const PRESET_ACTIONS: Record<ScopePreset, CollectionAction[]> = {\n 'read-only': ['read'],\n editor: ['read', 'create', 'update'],\n admin: ALL_ACTIONS,\n}\n\n/**\n * Asymmetric per-preset action map for globals. `editor` is intentionally\n * read-only on globals — a single bad write on a singleton broadcasts\n * site-wide with no per-document containment. Operators who want global\n * writes promote the key to `admin` or use a Custom key with explicit\n * `globalScopes`. README and CHANGELOG call out the asymmetry.\n */\nexport const PRESET_GLOBAL_ACTIONS: Record<ScopePreset, GlobalAction[]> = {\n 'read-only': ['read'],\n editor: ['read'],\n admin: ['read', 'update'],\n}\n\nexport const PRESET_TOOL_DENY: Record<ScopePreset, string[]> = {\n 'read-only': [],\n editor: ['safeDelete', 'deleteDocument'],\n admin: [],\n}\n\n// ─── Routing tables built from the tool list ─────────────────────────\n\nexport interface ScopeDecision {\n allowed: boolean\n reason?: string\n}\n\nexport interface RoutingTables {\n collectionToolAction: ReadonlyMap<string, CollectionAction>\n globalToolAction: ReadonlyMap<string, GlobalAction>\n accountToolAction: ReadonlyMap<string, CollectionAction>\n toolKind: ReadonlyMap<string, ResourceKind>\n}\n\nexport function buildRoutingTables(tools: RoutableTool[]): RoutingTables {\n const collectionToolAction = new Map<string, CollectionAction>()\n const globalToolAction = new Map<string, GlobalAction>()\n const accountToolAction = new Map<string, CollectionAction>()\n const toolKind = new Map<string, ResourceKind>()\n for (const t of tools) {\n toolKind.set(t.name, t.routing.kind)\n if (t.routing.kind === 'collection') collectionToolAction.set(t.name, t.routing.action)\n else if (t.routing.kind === 'global') globalToolAction.set(t.name, t.routing.action)\n else accountToolAction.set(t.name, t.routing.action)\n }\n return { collectionToolAction, globalToolAction, accountToolAction, toolKind }\n}\n\n// ─── Scope evaluation ────────────────────────────────────────────────\n\nexport type ScopeChecker = (\n scopes: KeyScopes | null | undefined,\n toolName: string,\n resource: string | undefined,\n) => ScopeDecision\n\n/**\n * Build a scope checker bound to a concrete tool list. The checker is a pure\n * function over (scopes, toolName, resource) — the routing tables are closed\n * over once at construction time.\n *\n * Fail-closed semantics:\n * - Null/undefined scopes grant full access (back-compat).\n * - When `scopes.collections` / `scopes.globals` is set, it is a *whitelist*\n * for that resource kind — unlisted resources are denied.\n * - When a tool resolves to a collection or global kind but the corresponding\n * scope map is undefined and `scopes.preset` is undefined, the call is\n * denied (closes the `tools.allow`-only latent fail-open).\n * - Account-level tools are gated by the preset's action list, if a preset\n * is set. Without a preset, a key scoped to specific collections/globals\n * cannot use account-level tools — they'd broaden the surface.\n */\nexport function buildScopeChecker(tools: RoutableTool[]): ScopeChecker {\n const tables = buildRoutingTables(tools)\n return (scopes, toolName, resource) => assertScopeAllows(scopes, toolName, resource, tables)\n}\n\n/**\n * Internal pure checker. Exposed for the per-request wrapper in the registry\n * so it can re-use the same `RoutingTables` it built once at startup.\n */\nexport function assertScopeAllows(\n scopes: KeyScopes | null | undefined,\n toolName: string,\n resource: string | undefined,\n tables: RoutingTables,\n): ScopeDecision {\n const resourceKind = tables.toolKind.get(toolName) ?? null\n // Unregistered tool — fail-closed at request time. Adding a tool without a\n // routing field is a TS error at the factory return site, so this branch\n // only fires for typo'd tool names sent by the client.\n if (resourceKind === null) {\n return {\n allowed: false,\n reason: `Tool \"${toolName}\" has no registered scope mapping.`,\n }\n }\n\n if (!scopes || (scopes.preset === undefined && !scopes.collections && !scopes.globals && !scopes.tools)) {\n return { allowed: true }\n }\n\n if (scopes.tools?.deny?.includes(toolName)) {\n return { allowed: false, reason: `Tool \"${toolName}\" is denied for this API key.` }\n }\n if (scopes.tools?.allow && !scopes.tools.allow.includes(toolName)) {\n return {\n allowed: false,\n reason: `Tool \"${toolName}\" is not in the allow-list for this API key.`,\n }\n }\n\n if (scopes.preset && PRESET_TOOL_DENY[scopes.preset]?.includes(toolName)) {\n return {\n allowed: false,\n reason: `Tool \"${toolName}\" is not allowed by the \"${scopes.preset}\" preset.`,\n }\n }\n\n if (resourceKind === 'account') {\n return checkAccount(scopes, toolName, tables.accountToolAction)\n }\n const policy = resourceKind === 'collection' ? COLLECTION_POLICY : GLOBAL_POLICY\n const toolAction =\n resourceKind === 'collection' ? tables.collectionToolAction : tables.globalToolAction\n return checkResource(scopes, toolName, resource, toolAction, policy)\n}\n\n/**\n * Per-resource-kind policy. Collapses what used to be two near-identical\n * `checkCollection` / `checkGlobal` helpers — the only differences are\n * the preset-actions table, the label, and which axis of `KeyScopes` to\n * read for explicit overrides.\n */\ninterface ResourcePolicy {\n presetActions: Record<ScopePreset, readonly string[]>\n scopeAxis: 'collections' | 'globals'\n label: 'collection' | 'global'\n Label: 'Collection' | 'Global'\n}\n\nconst COLLECTION_POLICY: ResourcePolicy = {\n presetActions: PRESET_ACTIONS,\n scopeAxis: 'collections',\n label: 'collection',\n Label: 'Collection',\n}\n\nconst GLOBAL_POLICY: ResourcePolicy = {\n presetActions: PRESET_GLOBAL_ACTIONS,\n scopeAxis: 'globals',\n label: 'global',\n Label: 'Global',\n}\n\nfunction checkResource(\n scopes: KeyScopes,\n toolName: string,\n resource: string | undefined,\n toolAction: ReadonlyMap<string, string>,\n policy: ResourcePolicy,\n): ScopeDecision {\n const action = toolAction.get(toolName)\n const presetActions = scopes.preset ? policy.presetActions[scopes.preset] : undefined\n const resourceScope = scopes[policy.scopeAxis]\n\n if (!resource) {\n // Fail-closed. Every built-in collection/global tool takes a required\n // `collection` / `slug` argument, so this only fires for a malformed call\n // or for a host tool that declared resource routing without a resource\n // argument. Allowing it would let such a tool read a hard-coded collection\n // straight past the key's whitelist. A tool that genuinely spans the whole\n // install belongs on `routing.kind: 'account'`.\n return {\n allowed: false,\n reason:\n `Tool \"${toolName}\" is routed to a ${policy.label} but the call carries no ` +\n `${policy.label} argument, so its scope cannot be checked. A tool with a fixed ` +\n `or install-wide target must use routing.kind: 'account'.`,\n }\n }\n if (!action) return { allowed: true }\n\n if (resourceScope) {\n const override = resourceScope[resource]\n if (!override) {\n return {\n allowed: false,\n reason: `${policy.Label} \"${resource}\" is not in this API key's allowed ${policy.scopeAxis}.`,\n }\n }\n if (!override.includes(action as never)) {\n return {\n allowed: false,\n reason: `Action \"${action}\" on ${policy.label} \"${resource}\" is not permitted by this API key's scope.`,\n }\n }\n return { allowed: true }\n }\n\n if (!presetActions) {\n // Fail-closed: `tools.allow` without a resource map or preset would\n // otherwise broadcast the tool across every resource. Require explicit\n // intent.\n return {\n allowed: false,\n reason: `Tool \"${toolName}\" requires an explicit ${policy.label} scope or preset on this API key.`,\n }\n }\n\n if (!presetActions.includes(action)) {\n return {\n allowed: false,\n reason: `Action \"${action}\" on ${policy.label} \"${resource}\" is not permitted by this API key's preset.`,\n }\n }\n return { allowed: true }\n}\n\nfunction checkAccount(\n scopes: KeyScopes,\n toolName: string,\n toolAction: ReadonlyMap<string, CollectionAction>,\n): ScopeDecision {\n const action = toolAction.get(toolName)\n const presetActions = scopes.preset ? PRESET_ACTIONS[scopes.preset] : undefined\n\n // Explicit resource override is the tightest signal: an account-level tool\n // operates across the whole site (searchContent across every collection,\n // uploadMedia into any media coll, etc.) and would broaden the key beyond\n // the resource whitelist regardless of which preset is set. Deny account\n // tools whenever the key carries explicit collection/global scopes.\n if (scopes.collections || scopes.globals) {\n return {\n allowed: false,\n reason: `Tool \"${toolName}\" is denied for keys with explicit collection or global scopes — account-level tools would broaden access beyond the whitelist.`,\n }\n }\n\n if (presetActions) {\n if (action && !presetActions.includes(action)) {\n return {\n allowed: false,\n reason: `Action \"${action}\" is not permitted by this API key's preset.`,\n }\n }\n return { allowed: true }\n }\n\n return { allowed: true }\n}\n"],"names":["ALL_ACTIONS","PRESET_ACTIONS","editor","admin","PRESET_GLOBAL_ACTIONS","PRESET_TOOL_DENY","buildRoutingTables","tools","collectionToolAction","Map","globalToolAction","accountToolAction","toolKind","t","set","name","routing","kind","action","buildScopeChecker","tables","scopes","toolName","resource","assertScopeAllows","resourceKind","get","allowed","reason","preset","undefined","collections","globals","deny","includes","allow","checkAccount","policy","COLLECTION_POLICY","GLOBAL_POLICY","toolAction","checkResource","presetActions","scopeAxis","label","Label","resourceScope","override"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/scope/policy.ts"],"sourcesContent":["import type { CollectionAction, GlobalAction, KeyScopes, ScopePreset } from '../types'\n\n// ─── Routing primitives ──────────────────────────────────────────────\n\nexport type ResourceKind = 'collection' | 'global' | 'account'\n\n/**\n * Discriminated routing tag attached to every tool factory output.\n *\n * Collocates the scope-routing decision with the tool definition itself —\n * the registry derives the collection/global/account lookups from `tools`\n * at boot. Adding a new tool can no longer drift the routing maps out of\n * sync because TS requires `routing` on every factory return.\n */\nexport type ToolRouting =\n | { kind: 'collection'; action: CollectionAction }\n | { kind: 'global'; action: GlobalAction }\n | { kind: 'account'; action: CollectionAction }\n\n/**\n * Minimal \"routable tool\" interface used by the policy module. The full\n * `ToolFactoryOutput` shape (handler, parameters, description) is irrelevant\n * here; we only need `name` + `routing` to build the lookup tables.\n */\nexport interface RoutableTool {\n name: string\n routing: ToolRouting\n}\n\n// ─── Per-preset action tables ────────────────────────────────────────\n\nconst ALL_ACTIONS: CollectionAction[] = ['read', 'create', 'update', 'delete']\n\nexport const PRESET_ACTIONS: Record<ScopePreset, CollectionAction[]> = {\n 'read-only': ['read'],\n editor: ['read', 'create', 'update'],\n admin: ALL_ACTIONS,\n}\n\n/**\n * Asymmetric per-preset action map for globals. `editor` is intentionally\n * read-only on globals — a single bad write on a singleton broadcasts\n * site-wide with no per-document containment. Operators who want global\n * writes promote the key to `admin` or use a Custom key with explicit\n * `globalScopes`. README and CHANGELOG call out the asymmetry.\n */\nexport const PRESET_GLOBAL_ACTIONS: Record<ScopePreset, GlobalAction[]> = {\n 'read-only': ['read'],\n editor: ['read'],\n admin: ['read', 'update'],\n}\n\nexport const PRESET_TOOL_DENY: Record<ScopePreset, string[]> = {\n 'read-only': [],\n editor: ['safeDelete', 'deleteDocument'],\n admin: [],\n}\n\n// ─── Routing tables built from the tool list ─────────────────────────\n\nexport interface ScopeDecision {\n allowed: boolean\n reason?: string\n}\n\nexport interface RoutingTables {\n collectionToolAction: ReadonlyMap<string, CollectionAction>\n globalToolAction: ReadonlyMap<string, GlobalAction>\n accountToolAction: ReadonlyMap<string, CollectionAction>\n toolKind: ReadonlyMap<string, ResourceKind>\n}\n\nexport function buildRoutingTables(tools: RoutableTool[]): RoutingTables {\n const collectionToolAction = new Map<string, CollectionAction>()\n const globalToolAction = new Map<string, GlobalAction>()\n const accountToolAction = new Map<string, CollectionAction>()\n const toolKind = new Map<string, ResourceKind>()\n for (const t of tools) {\n toolKind.set(t.name, t.routing.kind)\n if (t.routing.kind === 'collection') collectionToolAction.set(t.name, t.routing.action)\n else if (t.routing.kind === 'global') globalToolAction.set(t.name, t.routing.action)\n else accountToolAction.set(t.name, t.routing.action)\n }\n return { collectionToolAction, globalToolAction, accountToolAction, toolKind }\n}\n\n// ─── Scope evaluation ────────────────────────────────────────────────\n\nexport type ScopeChecker = (\n scopes: KeyScopes | null | undefined,\n toolName: string,\n resource: string | undefined,\n) => ScopeDecision\n\n/**\n * Build a scope checker bound to a concrete tool list. The checker is a pure\n * function over (scopes, toolName, resource) — the routing tables are closed\n * over once at construction time.\n *\n * Fail-closed semantics:\n * - Null/undefined scopes grant full access (back-compat).\n * - When `scopes.collections` / `scopes.globals` is set, it is a *whitelist*\n * for that resource kind — unlisted resources are denied.\n * - When a tool resolves to a collection or global kind but the corresponding\n * scope map is undefined and `scopes.preset` is undefined, the call is\n * denied (closes the `tools.allow`-only latent fail-open).\n * - Account-level tools are gated by the preset's action list, if a preset\n * is set. Without a preset, a key scoped to specific collections/globals\n * cannot use account-level tools — they'd broaden the surface.\n */\nexport function buildScopeChecker(tools: RoutableTool[]): ScopeChecker {\n const tables = buildRoutingTables(tools)\n return (scopes, toolName, resource) => assertScopeAllows(scopes, toolName, resource, tables)\n}\n\n/**\n * Internal pure checker. Exposed for the per-request wrapper in the registry\n * so it can re-use the same `RoutingTables` it built once at startup.\n */\nexport function assertScopeAllows(\n scopes: KeyScopes | null | undefined,\n toolName: string,\n resource: string | undefined,\n tables: RoutingTables,\n): ScopeDecision {\n const resourceKind = tables.toolKind.get(toolName) ?? null\n // Unregistered tool — fail-closed at request time. Adding a tool without a\n // routing field is a TS error at the factory return site, so this branch\n // only fires for typo'd tool names sent by the client.\n if (resourceKind === null) {\n return {\n allowed: false,\n reason: `Tool \"${toolName}\" has no registered scope mapping.`,\n }\n }\n\n if (!scopes || (scopes.preset === undefined && !scopes.collections && !scopes.globals && !scopes.tools)) {\n return { allowed: true }\n }\n\n if (scopes.tools?.deny?.includes(toolName)) {\n return { allowed: false, reason: `Tool \"${toolName}\" is denied for this API key or connection.` }\n }\n if (scopes.tools?.allow && !scopes.tools.allow.includes(toolName)) {\n return {\n allowed: false,\n reason: `Tool \"${toolName}\" is not in the allow-list for this API key or connection.`,\n }\n }\n\n if (scopes.preset && PRESET_TOOL_DENY[scopes.preset]?.includes(toolName)) {\n return {\n allowed: false,\n reason: `Tool \"${toolName}\" is not allowed by the \"${scopes.preset}\" preset.`,\n }\n }\n\n if (resourceKind === 'account') {\n return checkAccount(scopes, toolName, tables.accountToolAction)\n }\n const policy = resourceKind === 'collection' ? COLLECTION_POLICY : GLOBAL_POLICY\n const toolAction =\n resourceKind === 'collection' ? tables.collectionToolAction : tables.globalToolAction\n return checkResource(scopes, toolName, resource, toolAction, policy)\n}\n\n/**\n * Per-resource-kind policy. Collapses what used to be two near-identical\n * `checkCollection` / `checkGlobal` helpers — the only differences are\n * the preset-actions table, the label, and which axis of `KeyScopes` to\n * read for explicit overrides.\n */\ninterface ResourcePolicy {\n presetActions: Record<ScopePreset, readonly string[]>\n scopeAxis: 'collections' | 'globals'\n label: 'collection' | 'global'\n Label: 'Collection' | 'Global'\n}\n\nconst COLLECTION_POLICY: ResourcePolicy = {\n presetActions: PRESET_ACTIONS,\n scopeAxis: 'collections',\n label: 'collection',\n Label: 'Collection',\n}\n\nconst GLOBAL_POLICY: ResourcePolicy = {\n presetActions: PRESET_GLOBAL_ACTIONS,\n scopeAxis: 'globals',\n label: 'global',\n Label: 'Global',\n}\n\nfunction checkResource(\n scopes: KeyScopes,\n toolName: string,\n resource: string | undefined,\n toolAction: ReadonlyMap<string, string>,\n policy: ResourcePolicy,\n): ScopeDecision {\n const action = toolAction.get(toolName)\n const presetActions = scopes.preset ? policy.presetActions[scopes.preset] : undefined\n const resourceScope = scopes[policy.scopeAxis]\n\n if (!resource) {\n // Fail-closed. Every built-in collection/global tool takes a required\n // `collection` / `slug` argument, so this only fires for a malformed call\n // or for a host tool that declared resource routing without a resource\n // argument. Allowing it would let such a tool read a hard-coded collection\n // straight past the key's whitelist. A tool that genuinely spans the whole\n // install belongs on `routing.kind: 'account'`.\n return {\n allowed: false,\n reason:\n `Tool \"${toolName}\" is routed to a ${policy.label} but the call carries no ` +\n `${policy.label} argument, so its scope cannot be checked. A tool with a fixed ` +\n `or install-wide target must use routing.kind: 'account'.`,\n }\n }\n if (!action) return { allowed: true }\n\n if (resourceScope) {\n const override = resourceScope[resource]\n if (!override) {\n return {\n allowed: false,\n reason: `${policy.Label} \"${resource}\" is not in the allowed ${policy.scopeAxis} for this API key or connection.`,\n }\n }\n if (!override.includes(action as never)) {\n return {\n allowed: false,\n reason: `Action \"${action}\" on ${policy.label} \"${resource}\" is not permitted for this API key or connection.`,\n }\n }\n return { allowed: true }\n }\n\n if (!presetActions) {\n // Fail-closed: `tools.allow` without a resource map or preset would\n // otherwise broadcast the tool across every resource. Require explicit\n // intent.\n return {\n allowed: false,\n reason: `Tool \"${toolName}\" requires an explicit ${policy.label} scope or preset on this API key or connection.`,\n }\n }\n\n if (!presetActions.includes(action)) {\n return {\n allowed: false,\n reason: `Action \"${action}\" on ${policy.label} \"${resource}\" is not permitted by the preset of this API key or connection.`,\n }\n }\n return { allowed: true }\n}\n\nfunction checkAccount(\n scopes: KeyScopes,\n toolName: string,\n toolAction: ReadonlyMap<string, CollectionAction>,\n): ScopeDecision {\n const action = toolAction.get(toolName)\n const presetActions = scopes.preset ? PRESET_ACTIONS[scopes.preset] : undefined\n\n // Explicit resource override is the tightest signal: an account-level tool\n // operates across the whole site (searchContent across every collection,\n // uploadMedia into any media coll, etc.) and would broaden the key beyond\n // the resource whitelist regardless of which preset is set. Deny account\n // tools whenever the key carries explicit collection/global scopes.\n if (scopes.collections || scopes.globals) {\n return {\n allowed: false,\n reason: `Tool \"${toolName}\" is denied for keys with explicit collection or global scopes — account-level tools would broaden access beyond the whitelist.`,\n }\n }\n\n if (presetActions) {\n if (action && !presetActions.includes(action)) {\n return {\n allowed: false,\n reason: `Action \"${action}\" is not permitted by the preset of this API key or connection.`,\n }\n }\n return { allowed: true }\n }\n\n return { allowed: true }\n}\n"],"names":["ALL_ACTIONS","PRESET_ACTIONS","editor","admin","PRESET_GLOBAL_ACTIONS","PRESET_TOOL_DENY","buildRoutingTables","tools","collectionToolAction","Map","globalToolAction","accountToolAction","toolKind","t","set","name","routing","kind","action","buildScopeChecker","tables","scopes","toolName","resource","assertScopeAllows","resourceKind","get","allowed","reason","preset","undefined","collections","globals","deny","includes","allow","checkAccount","policy","COLLECTION_POLICY","GLOBAL_POLICY","toolAction","checkResource","presetActions","scopeAxis","label","Label","resourceScope","override"],"mappings":"AA6BA,wEAAwE;AAExE,MAAMA,cAAkC;IAAC;IAAQ;IAAU;IAAU;CAAS;AAE9E,OAAO,MAAMC,iBAA0D;IACrE,aAAa;QAAC;KAAO;IACrBC,QAAQ;QAAC;QAAQ;QAAU;KAAS;IACpCC,OAAOH;AACT,EAAC;AAED;;;;;;CAMC,GACD,OAAO,MAAMI,wBAA6D;IACxE,aAAa;QAAC;KAAO;IACrBF,QAAQ;QAAC;KAAO;IAChBC,OAAO;QAAC;QAAQ;KAAS;AAC3B,EAAC;AAED,OAAO,MAAME,mBAAkD;IAC7D,aAAa,EAAE;IACfH,QAAQ;QAAC;QAAc;KAAiB;IACxCC,OAAO,EAAE;AACX,EAAC;AAgBD,OAAO,SAASG,mBAAmBC,KAAqB;IACtD,MAAMC,uBAAuB,IAAIC;IACjC,MAAMC,mBAAmB,IAAID;IAC7B,MAAME,oBAAoB,IAAIF;IAC9B,MAAMG,WAAW,IAAIH;IACrB,KAAK,MAAMI,KAAKN,MAAO;QACrBK,SAASE,GAAG,CAACD,EAAEE,IAAI,EAAEF,EAAEG,OAAO,CAACC,IAAI;QACnC,IAAIJ,EAAEG,OAAO,CAACC,IAAI,KAAK,cAAcT,qBAAqBM,GAAG,CAACD,EAAEE,IAAI,EAAEF,EAAEG,OAAO,CAACE,MAAM;aACjF,IAAIL,EAAEG,OAAO,CAACC,IAAI,KAAK,UAAUP,iBAAiBI,GAAG,CAACD,EAAEE,IAAI,EAAEF,EAAEG,OAAO,CAACE,MAAM;aAC9EP,kBAAkBG,GAAG,CAACD,EAAEE,IAAI,EAAEF,EAAEG,OAAO,CAACE,MAAM;IACrD;IACA,OAAO;QAAEV;QAAsBE;QAAkBC;QAAmBC;IAAS;AAC/E;AAUA;;;;;;;;;;;;;;;CAeC,GACD,OAAO,SAASO,kBAAkBZ,KAAqB;IACrD,MAAMa,SAASd,mBAAmBC;IAClC,OAAO,CAACc,QAAQC,UAAUC,WAAaC,kBAAkBH,QAAQC,UAAUC,UAAUH;AACvF;AAEA;;;CAGC,GACD,OAAO,SAASI,kBACdH,MAAoC,EACpCC,QAAgB,EAChBC,QAA4B,EAC5BH,MAAqB;IAErB,MAAMK,eAAeL,OAAOR,QAAQ,CAACc,GAAG,CAACJ,aAAa;IACtD,2EAA2E;IAC3E,yEAAyE;IACzE,uDAAuD;IACvD,IAAIG,iBAAiB,MAAM;QACzB,OAAO;YACLE,SAAS;YACTC,QAAQ,CAAC,MAAM,EAAEN,SAAS,kCAAkC,CAAC;QAC/D;IACF;IAEA,IAAI,CAACD,UAAWA,OAAOQ,MAAM,KAAKC,aAAa,CAACT,OAAOU,WAAW,IAAI,CAACV,OAAOW,OAAO,IAAI,CAACX,OAAOd,KAAK,EAAG;QACvG,OAAO;YAAEoB,SAAS;QAAK;IACzB;IAEA,IAAIN,OAAOd,KAAK,EAAE0B,MAAMC,SAASZ,WAAW;QAC1C,OAAO;YAAEK,SAAS;YAAOC,QAAQ,CAAC,MAAM,EAAEN,SAAS,2CAA2C,CAAC;QAAC;IAClG;IACA,IAAID,OAAOd,KAAK,EAAE4B,SAAS,CAACd,OAAOd,KAAK,CAAC4B,KAAK,CAACD,QAAQ,CAACZ,WAAW;QACjE,OAAO;YACLK,SAAS;YACTC,QAAQ,CAAC,MAAM,EAAEN,SAAS,0DAA0D,CAAC;QACvF;IACF;IAEA,IAAID,OAAOQ,MAAM,IAAIxB,gBAAgB,CAACgB,OAAOQ,MAAM,CAAC,EAAEK,SAASZ,WAAW;QACxE,OAAO;YACLK,SAAS;YACTC,QAAQ,CAAC,MAAM,EAAEN,SAAS,yBAAyB,EAAED,OAAOQ,MAAM,CAAC,SAAS,CAAC;QAC/E;IACF;IAEA,IAAIJ,iBAAiB,WAAW;QAC9B,OAAOW,aAAaf,QAAQC,UAAUF,OAAOT,iBAAiB;IAChE;IACA,MAAM0B,SAASZ,iBAAiB,eAAea,oBAAoBC;IACnE,MAAMC,aACJf,iBAAiB,eAAeL,OAAOZ,oBAAoB,GAAGY,OAAOV,gBAAgB;IACvF,OAAO+B,cAAcpB,QAAQC,UAAUC,UAAUiB,YAAYH;AAC/D;AAeA,MAAMC,oBAAoC;IACxCI,eAAezC;IACf0C,WAAW;IACXC,OAAO;IACPC,OAAO;AACT;AAEA,MAAMN,gBAAgC;IACpCG,eAAetC;IACfuC,WAAW;IACXC,OAAO;IACPC,OAAO;AACT;AAEA,SAASJ,cACPpB,MAAiB,EACjBC,QAAgB,EAChBC,QAA4B,EAC5BiB,UAAuC,EACvCH,MAAsB;IAEtB,MAAMnB,SAASsB,WAAWd,GAAG,CAACJ;IAC9B,MAAMoB,gBAAgBrB,OAAOQ,MAAM,GAAGQ,OAAOK,aAAa,CAACrB,OAAOQ,MAAM,CAAC,GAAGC;IAC5E,MAAMgB,gBAAgBzB,MAAM,CAACgB,OAAOM,SAAS,CAAC;IAE9C,IAAI,CAACpB,UAAU;QACb,sEAAsE;QACtE,0EAA0E;QAC1E,uEAAuE;QACvE,2EAA2E;QAC3E,2EAA2E;QAC3E,gDAAgD;QAChD,OAAO;YACLI,SAAS;YACTC,QACE,CAAC,MAAM,EAAEN,SAAS,iBAAiB,EAAEe,OAAOO,KAAK,CAAC,yBAAyB,CAAC,GAC5E,GAAGP,OAAOO,KAAK,CAAC,+DAA+D,CAAC,GAChF,CAAC,wDAAwD,CAAC;QAC9D;IACF;IACA,IAAI,CAAC1B,QAAQ,OAAO;QAAES,SAAS;IAAK;IAEpC,IAAImB,eAAe;QACjB,MAAMC,WAAWD,aAAa,CAACvB,SAAS;QACxC,IAAI,CAACwB,UAAU;YACb,OAAO;gBACLpB,SAAS;gBACTC,QAAQ,GAAGS,OAAOQ,KAAK,CAAC,EAAE,EAAEtB,SAAS,wBAAwB,EAAEc,OAAOM,SAAS,CAAC,gCAAgC,CAAC;YACnH;QACF;QACA,IAAI,CAACI,SAASb,QAAQ,CAAChB,SAAkB;YACvC,OAAO;gBACLS,SAAS;gBACTC,QAAQ,CAAC,QAAQ,EAAEV,OAAO,KAAK,EAAEmB,OAAOO,KAAK,CAAC,EAAE,EAAErB,SAAS,kDAAkD,CAAC;YAChH;QACF;QACA,OAAO;YAAEI,SAAS;QAAK;IACzB;IAEA,IAAI,CAACe,eAAe;QAClB,oEAAoE;QACpE,uEAAuE;QACvE,UAAU;QACV,OAAO;YACLf,SAAS;YACTC,QAAQ,CAAC,MAAM,EAAEN,SAAS,uBAAuB,EAAEe,OAAOO,KAAK,CAAC,+CAA+C,CAAC;QAClH;IACF;IAEA,IAAI,CAACF,cAAcR,QAAQ,CAAChB,SAAS;QACnC,OAAO;YACLS,SAAS;YACTC,QAAQ,CAAC,QAAQ,EAAEV,OAAO,KAAK,EAAEmB,OAAOO,KAAK,CAAC,EAAE,EAAErB,SAAS,+DAA+D,CAAC;QAC7H;IACF;IACA,OAAO;QAAEI,SAAS;IAAK;AACzB;AAEA,SAASS,aACPf,MAAiB,EACjBC,QAAgB,EAChBkB,UAAiD;IAEjD,MAAMtB,SAASsB,WAAWd,GAAG,CAACJ;IAC9B,MAAMoB,gBAAgBrB,OAAOQ,MAAM,GAAG5B,cAAc,CAACoB,OAAOQ,MAAM,CAAC,GAAGC;IAEtE,2EAA2E;IAC3E,yEAAyE;IACzE,0EAA0E;IAC1E,yEAAyE;IACzE,oEAAoE;IACpE,IAAIT,OAAOU,WAAW,IAAIV,OAAOW,OAAO,EAAE;QACxC,OAAO;YACLL,SAAS;YACTC,QAAQ,CAAC,MAAM,EAAEN,SAAS,+HAA+H,CAAC;QAC5J;IACF;IAEA,IAAIoB,eAAe;QACjB,IAAIxB,UAAU,CAACwB,cAAcR,QAAQ,CAAChB,SAAS;YAC7C,OAAO;gBACLS,SAAS;gBACTC,QAAQ,CAAC,QAAQ,EAAEV,OAAO,+DAA+D,CAAC;YAC5F;QACF;QACA,OAAO;YAAES,SAAS;QAAK;IACzB;IAEA,OAAO;QAAEA,SAAS;IAAK;AACzB"}
@@ -55,6 +55,16 @@ export declare function snapshotPublishMarker(req: PayloadRequest, target: Publi
55
55
  */
56
56
  export declare function verifyPublishSucceededDespiteError(req: PayloadRequest, target: PublishVerifyTarget, preUpdatedAt: string | undefined): Promise<Record<string, unknown> | null>;
57
57
  export declare function stampMcpContext(req: PayloadRequest): void;
58
+ /** Set on req.context by the registry when the caller's scopes list specific collections or globals. */
59
+ export declare const LIMITED_SCOPE = "mcpLimitedScope";
60
+ /**
61
+ * Relationship depth for a Payload call whose result goes back to the client. A key or connection
62
+ * limited to some collections or globals gets depth 0, so linked entries from collections it may
63
+ * not read come back as IDs, not full documents. Otherwise the requested depth (or Payload's default).
64
+ */
65
+ export declare function populateDepth(req: PayloadRequest, requested?: number): number | undefined;
66
+ /** True when a `where` filter uses a dotted path, which can reach into linked entries. */
67
+ export declare function hasDottedPath(where: unknown): boolean;
58
68
  export declare function getDocDisplayName(doc: unknown, fallback: string): string;
59
69
  export declare function requireDraftCollection(collection: string, draftCollections: Set<string>, noun?: string): McpTextResponse | null;
60
70
  /**
@@ -1,12 +1,12 @@
1
1
  import { z } from 'zod';
2
- /**
3
- * Build a `z.enum` over a list of valid resource slugs with a friendly
4
- * error message that names the valid set and clarifies why an unknown
5
- * slug (e.g. one removed via `options.exclude.globals`) is rejected.
6
- *
7
- * Default Zod enum errors are "Invalid enum value. …" — accurate but
8
- * unhelpful when the slug looks plausible to a caller who isn't aware
9
- * the host config excluded it.
2
+ /**
3
+ * Build a `z.enum` over a list of valid resource slugs with a friendly
4
+ * error message that names the valid set and clarifies why an unknown
5
+ * slug (e.g. one removed via `options.exclude.globals`) is rejected.
6
+ *
7
+ * Default Zod enum errors are "Invalid enum value. …" — accurate but
8
+ * unhelpful when the slug looks plausible to a caller who isn't aware
9
+ * the host config excluded it.
10
10
  */ export function slugEnum(slugs, kind) {
11
11
  return z.enum(slugs, {
12
12
  errorMap: ()=>({
@@ -36,12 +36,12 @@ function asIsoString(v) {
36
36
  if (v instanceof Date) return v.toISOString();
37
37
  return undefined;
38
38
  }
39
- /**
40
- * Capture the document's current `updatedAt` BEFORE a publish attempt so
41
- * the recovery branch can tell "this attempt landed despite a post-write
42
- * validator throw" from "an older publish was successful and this attempt
43
- * did nothing". A missing snapshot is non-fatal — the recovery branch
44
- * conservatively falls through to the original error in that case.
39
+ /**
40
+ * Capture the document's current `updatedAt` BEFORE a publish attempt so
41
+ * the recovery branch can tell "this attempt landed despite a post-write
42
+ * validator throw" from "an older publish was successful and this attempt
43
+ * did nothing". A missing snapshot is non-fatal — the recovery branch
44
+ * conservatively falls through to the original error in that case.
45
45
  */ export async function snapshotPublishMarker(req, target) {
46
46
  try {
47
47
  const pre = target.kind === 'collection' ? await req.payload.findByID({
@@ -75,21 +75,21 @@ function asIsoString(v) {
75
75
  return undefined;
76
76
  }
77
77
  }
78
- /**
79
- * After a Payload update throws on a publish call, determine whether the
80
- * publish actually landed despite the error. Returns the live document
81
- * only when (a) the live `_status` is 'published' AND (b) `updatedAt`
82
- * strictly advanced past the pre-update snapshot — i.e. the current
83
- * attempt produced the published row. Without the strictly-newer check,
84
- * a pre-existing published version from an earlier successful publish
85
- * would mask a real failure of the current attempt.
86
- *
87
- * Returns null on:
88
- * - missing pre-snapshot (cannot disambiguate; conservative)
89
- * - verify read failure (do not mask the original error with a
90
- * secondary read error)
91
- * - live `_status` not 'published'
92
- * - live `updatedAt` not strictly newer than the pre-snapshot
78
+ /**
79
+ * After a Payload update throws on a publish call, determine whether the
80
+ * publish actually landed despite the error. Returns the live document
81
+ * only when (a) the live `_status` is 'published' AND (b) `updatedAt`
82
+ * strictly advanced past the pre-update snapshot — i.e. the current
83
+ * attempt produced the published row. Without the strictly-newer check,
84
+ * a pre-existing published version from an earlier successful publish
85
+ * would mask a real failure of the current attempt.
86
+ *
87
+ * Returns null on:
88
+ * - missing pre-snapshot (cannot disambiguate; conservative)
89
+ * - verify read failure (do not mask the original error with a
90
+ * secondary read error)
91
+ * - live `_status` not 'published'
92
+ * - live `updatedAt` not strictly newer than the pre-snapshot
93
93
  */ export async function verifyPublishSucceededDespiteError(req, target, preUpdatedAt) {
94
94
  if (!preUpdatedAt) return null;
95
95
  try {
@@ -135,6 +135,18 @@ export function stampMcpContext(req) {
135
135
  source: 'mcp'
136
136
  };
137
137
  }
138
+ /** Set on req.context by the registry when the caller's scopes list specific collections or globals. */ export const LIMITED_SCOPE = 'mcpLimitedScope';
139
+ /**
140
+ * Relationship depth for a Payload call whose result goes back to the client. A key or connection
141
+ * limited to some collections or globals gets depth 0, so linked entries from collections it may
142
+ * not read come back as IDs, not full documents. Otherwise the requested depth (or Payload's default).
143
+ */ export function populateDepth(req, requested) {
144
+ return req.context?.[LIMITED_SCOPE] ? 0 : requested;
145
+ }
146
+ /** True when a `where` filter uses a dotted path, which can reach into linked entries. */ export function hasDottedPath(where) {
147
+ if (!where || typeof where !== 'object') return false;
148
+ return Object.entries(where).some(([key, value])=>key.includes('.') || Array.isArray(value) && value.some(hasDottedPath));
149
+ }
138
150
  export function getDocDisplayName(doc, fallback) {
139
151
  const d = doc;
140
152
  return typeof d?.name === 'string' && d.name || typeof d?.title === 'string' && d.title || typeof d?.slug === 'string' && d.slug || fallback;
@@ -145,12 +157,12 @@ export function requireDraftCollection(collection, draftCollections, noun = 'dra
145
157
  ...draftCollections
146
158
  ].join(', ') || 'none'}`);
147
159
  }
148
- /**
149
- * Resolves the preview URL for a draft document by delegating to the
150
- * collection's own configured preview function (`admin.livePreview.url`
151
- * preferred, then `admin.preview`). Returns null when no function is
152
- * configured, when it fails, or when it returns a relative path with no
153
- * absolute `siteUrl` to anchor it.
160
+ /**
161
+ * Resolves the preview URL for a draft document by delegating to the
162
+ * collection's own configured preview function (`admin.livePreview.url`
163
+ * preferred, then `admin.preview`). Returns null when no function is
164
+ * configured, when it fails, or when it returns a relative path with no
165
+ * absolute `siteUrl` to anchor it.
154
166
  */ export async function resolvePreviewUrl(collection, doc, req, siteUrl) {
155
167
  const admin = collection.admin ?? {};
156
168
  const locale = req.locale ?? 'en';
@@ -192,12 +204,12 @@ export function requireDraftCollection(collection, draftCollections, noun = 'dra
192
204
  const path = raw.startsWith('/') ? raw : `/${raw}`;
193
205
  return `${base}${path}`;
194
206
  }
195
- /**
196
- * If `doc` is a draft, appends a preview-URL hint to the MCP response so the
197
- * AI can present it to the user. Falls back to a generic admin-panel hint
198
- * when the collection has no preview function configured.
199
- *
200
- * Pure with respect to the response: a fresh content array is returned.
207
+ /**
208
+ * If `doc` is a draft, appends a preview-URL hint to the MCP response so the
209
+ * AI can present it to the user. Falls back to a generic admin-panel hint
210
+ * when the collection has no preview function configured.
211
+ *
212
+ * Pure with respect to the response: a fresh content array is returned.
201
213
  */ export async function decorateDraftResponse(response, doc, collection, req, siteUrl) {
202
214
  if (!doc || doc._status !== 'draft' || !collection) return response;
203
215
  const previewUrl = await resolvePreviewUrl(collection, doc, req, siteUrl);