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.
- package/README.md +100 -237
- package/dist/api-keys.js +16 -0
- package/dist/api-keys.js.map +1 -1
- package/dist/auth-strategy.js +49 -49
- package/dist/auth-strategy.js.map +1 -1
- package/dist/components/AgentConnectPill.d.ts +3 -0
- package/dist/components/AgentConnectPill.js +121 -0
- package/dist/components/AgentConnectPill.js.map +1 -0
- package/dist/components/CollectionScopesMatrix.js.map +1 -1
- package/dist/components/ConsentPermissions.d.ts +10 -0
- package/dist/components/ConsentPermissions.js +165 -0
- package/dist/components/ConsentPermissions.js.map +1 -0
- package/dist/components/GlobalScopesMatrix.js.map +1 -1
- package/dist/components/OAuthView.d.ts +4 -0
- package/dist/components/OAuthView.js +269 -0
- package/dist/components/OAuthView.js.map +1 -0
- package/dist/components/ScopesTable.d.ts +28 -0
- package/dist/components/ScopesTable.js +165 -130
- package/dist/components/ScopesTable.js.map +1 -1
- package/dist/components/agentInstructions.d.ts +2 -0
- package/dist/components/agentInstructions.js +34 -0
- package/dist/components/agentInstructions.js.map +1 -0
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.js +2 -0
- package/dist/components/index.js.map +1 -1
- package/dist/components/oauth.css +111 -0
- package/dist/conflict-detection.js +13 -13
- package/dist/conflict-detection.js.map +1 -1
- package/dist/draft-workflow.js +26 -26
- package/dist/draft-workflow.js.map +1 -1
- package/dist/endpoint.d.ts +5 -0
- package/dist/endpoint.js +21 -13
- package/dist/endpoint.js.map +1 -1
- package/dist/hash.js +15 -15
- package/dist/hash.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +67 -3
- package/dist/index.js.map +1 -1
- package/dist/introspection.js +47 -47
- package/dist/introspection.js.map +1 -1
- package/dist/oauth-permissions.d.ts +52 -0
- package/dist/oauth-permissions.js +100 -0
- package/dist/oauth-permissions.js.map +1 -0
- package/dist/oauth-store.d.ts +19 -0
- package/dist/oauth-store.js +122 -0
- package/dist/oauth-store.js.map +1 -0
- package/dist/oauth.d.ts +19 -0
- package/dist/oauth.js +523 -0
- package/dist/oauth.js.map +1 -0
- package/dist/prompts.js +5 -5
- package/dist/prompts.js.map +1 -1
- package/dist/registry.js +7 -1
- package/dist/registry.js.map +1 -1
- package/dist/resources.js +10 -10
- package/dist/resources.js.map +1 -1
- package/dist/scope/audit-log.js +9 -9
- package/dist/scope/audit-log.js.map +1 -1
- package/dist/scope/policy.js +7 -7
- package/dist/scope/policy.js.map +1 -1
- package/dist/tools/_helpers.d.ts +10 -0
- package/dist/tools/_helpers.js +53 -41
- package/dist/tools/_helpers.js.map +1 -1
- package/dist/tools/_layout-helpers.js +33 -33
- package/dist/tools/_layout-helpers.js.map +1 -1
- package/dist/tools/create-document.js +20 -19
- package/dist/tools/create-document.js.map +1 -1
- package/dist/tools/delete-document.js +10 -9
- package/dist/tools/delete-document.js.map +1 -1
- package/dist/tools/find-document.js +18 -15
- package/dist/tools/find-document.js.map +1 -1
- package/dist/tools/find-global.js +13 -13
- package/dist/tools/find-global.js.map +1 -1
- package/dist/tools/global-versions.js +8 -7
- package/dist/tools/global-versions.js.map +1 -1
- package/dist/tools/patch-global-layout.js +9 -9
- package/dist/tools/patch-global-layout.js.map +1 -1
- package/dist/tools/patch-layout.js +15 -14
- package/dist/tools/patch-layout.js.map +1 -1
- package/dist/tools/publish-draft.js +2 -1
- package/dist/tools/publish-draft.js.map +1 -1
- package/dist/tools/publish-global-draft.js +4 -4
- package/dist/tools/publish-global-draft.js.map +1 -1
- package/dist/tools/resolve-reference.js.map +1 -1
- package/dist/tools/safe-delete.js +14 -14
- package/dist/tools/safe-delete.js.map +1 -1
- package/dist/tools/schedule-publish.js +21 -20
- package/dist/tools/schedule-publish.js.map +1 -1
- package/dist/tools/search-content.js +12 -12
- package/dist/tools/search-content.js.map +1 -1
- package/dist/tools/update-document.js +6 -5
- package/dist/tools/update-document.js.map +1 -1
- package/dist/tools/update-global.js +9 -9
- package/dist/tools/update-global.js.map +1 -1
- package/dist/tools/upload-media.js +2 -1
- package/dist/tools/upload-media.js.map +1 -1
- package/dist/tools/versions.js +10 -8
- package/dist/tools/versions.js.map +1 -1
- package/dist/types.d.ts +3 -0
- package/dist/types.js.map +1 -1
- package/docs/oauth.md +96 -0
- package/package.json +7 -2
package/dist/prompts.js.map
CHANGED
|
@@ -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
|
package/dist/registry.js.map
CHANGED
|
@@ -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":"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"}
|
|
1
|
+
{"version":3,"sources":["../src/registry.ts"],"sourcesContent":["import type { PayloadRequest } from 'payload'\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport { ZodObject, type ZodTypeAny } from 'zod'\nimport { getApiKeyContext } from './auth-strategy'\nimport type { InitializeServerForRequest } from './endpoint'\nimport { 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":"AAEA,SAASA,SAAS,QAAyB,MAAK;AAChD,SAASC,gBAAgB,QAAQ,kBAAiB;AAElD,SAASC,aAAa,EAAEC,eAAe,QAA8B,mBAAkB;AACvF,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,sBAAsBZ,WAAW;QACnC,OAAOY,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,SAAS1C,iBAAiByB;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;oBAChB,yFAAyF;oBACzF,8EAA8E;oBAC9EA,IAAIiC,OAAO,GAAG;wBAAE,GAAGjC,IAAIiC,OAAO;wBAAE,CAACzD,cAAc,EAAE0D,QAAQjB,QAAQQ,QAAQU,eAAelB,QAAQQ,QAAQW;oBAAS;oBAEjH,IAAI;wBACF,MAAMC,SAAS,MAAM/B,KAAKgC,OAAO,CAAC1B,MAAMZ,KAAKa;wBAC7C,iEAAiE;wBACjE,uEAAuE;wBACvE,+DAA+D;wBAC/D,cAAc;wBACd,MAAM0B,gBAAgB,AAACF,QAAkC5C,YAAY;wBACrEY,QACEkC,gBAAgB,SAAS,QACzB;4BACEZ,OAAO;4BACPC,OAAOX,QAAQW;4BACfC,WAAWZ,QAAQY;4BACnBvB,MAAMA,KAAKI,IAAI;4BACfQ;4BACAI;4BACAC;4BACAO,SAAS,CAACS;4BACV9C,SAAS8C;4BACTR,YAAYhB,KAAKC,GAAG,KAAKF;4BACzBV;4BACA,GAAImC,gBAAgB;gCAAEP,YAAY;4BAAuB,IAAI,CAAC,CAAC;wBACjE,GACA,CAAC,+BAA+B,EAAEO,gBAAgB,uBAAuB,GAAG,EAAE,EAAEjC,KAAKI,IAAI,EAAE;wBAE7F,OAAO2B;oBACT,EAAE,OAAOG,KAAK;wBACZ,MAAMR,aAAaQ,eAAeC,QAAQD,IAAI9B,IAAI,GAAG;wBACrD,MAAMgC,UAAUF,eAAeC,QAAQD,IAAIE,OAAO,GAAGC,OAAOH;wBAC5DnC,QACE,SACA;4BACEsB,OAAO;4BACPa;4BACAZ,OAAOX,QAAQW;4BACfC,WAAWZ,QAAQY;4BACnBvB,MAAMA,KAAKI,IAAI;4BACfQ;4BACAI;4BACAC;4BACAqB,aAAa5D,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,EAAEkD,SAAS;gCAAC;6BAAE;4BACtDjD,SAAS;wBACX;oBACF;gBACF;gBAEAQ,OAAO4C,YAAY,CACjBvC,KAAKI,IAAI,EACT;oBACEoC,aAAaxC,KAAKwC,WAAW;oBAC7BC,aAAa9D,WAAWqB,KAAKpB,UAAU;gBACzC,GACAyB;YAEJ;YAEA,KAAK,MAAMqC,UAAUnD,QAAS;gBAC5B,MAAMc,UAAU,OAAOC,MAAeC;oBACpC,IAAI;wBACF,OAAO,MAAMmC,OAAOV,OAAO,CAAC1B,MAAMZ,KAAKa;oBACzC,EAAE,OAAO2B,KAAK;wBACZtC,QAAQ+C,QACN;4BAAEtB,OAAO;4BAAca;4BAAKQ,QAAQA,OAAOtC,IAAI;4BAAEN;wBAAU,GAC3D,CAAC,qCAAqC,EAAE4C,OAAOtC,IAAI,EAAE;wBAEvD,MAAM8B;oBACR;gBACF;gBACAvC,OAAOiD,cAAc,CACnBF,OAAOtC,IAAI,EACX;oBACEyC,OAAOH,OAAOG,KAAK;oBACnBL,aAAaE,OAAOF,WAAW;oBAC/BM,YAAYJ,OAAOI,UAAU;gBAC/B,GACAzC;YAEJ;YAEA,KAAK,MAAM0C,YAAYvD,UAAW;gBAChC,MAAMa,UAAU,OAAOC,MAAeC;oBACpC,IAAI;wBACF,OAAO,MAAMwC,SAASf,OAAO,CAAC1B,MAAMZ,KAAKa;oBAC3C,EAAE,OAAO2B,KAAK;wBACZtC,QAAQ+C,QACN;4BAAEtB,OAAO;4BAAgBa;4BAAKa,UAAUA,SAAS3C,IAAI;4BAAEN;wBAAU,GACjE,CAAC,4CAA4C,EAAEiD,SAAS3C,IAAI,EAAE;wBAEhE,MAAM8B;oBACR;gBACF;gBACAvC,OAAOqD,gBAAgB,CACrBD,SAAS3C,IAAI,EACb2C,SAASE,GAAG,EACZ;oBACEJ,OAAOE,SAASF,KAAK;oBACrBL,aAAaO,SAASP,WAAW;oBACjCU,UAAUH,SAASG,QAAQ;gBAC7B,GACA7C;YAEJ;QACF;AACF"}
|
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({
|
package/dist/resources.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/resources.ts"],"sourcesContent":["import type {\
|
|
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"}
|
package/dist/scope/audit-log.js
CHANGED
|
@@ -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'\
|
|
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"}
|
package/dist/scope/policy.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
212
|
+
reason: `Action "${action}" is not permitted by the preset of this API key or connection.`
|
|
213
213
|
};
|
|
214
214
|
}
|
|
215
215
|
return {
|
package/dist/scope/policy.js.map
CHANGED
|
@@ -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":"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"}
|
package/dist/tools/_helpers.d.ts
CHANGED
|
@@ -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
|
/**
|
package/dist/tools/_helpers.js
CHANGED
|
@@ -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);
|