payload-mcp-toolkit 0.7.4 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +324 -253
- package/dist/api-keys.js +28 -20
- package/dist/api-keys.js.map +1 -1
- package/dist/conflict-detection.d.ts +12 -0
- package/dist/conflict-detection.js +20 -0
- package/dist/conflict-detection.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +28 -21
- package/dist/index.js.map +1 -1
- package/dist/registry.js +29 -21
- package/dist/registry.js.map +1 -1
- package/dist/scope/policy.js +32 -26
- package/dist/scope/policy.js.map +1 -1
- package/dist/tools/create-document.js +8 -0
- package/dist/tools/create-document.js.map +1 -1
- package/dist/tools/delete-document.d.ts +1 -1
- package/dist/tools/delete-document.js +6 -6
- package/dist/tools/delete-document.js.map +1 -1
- package/dist/tools/find-document.d.ts +3 -3
- package/dist/tools/find-document.js +8 -8
- package/dist/tools/find-document.js.map +1 -1
- package/dist/types.d.ts +38 -0
- package/dist/types.js +7 -12
- package/dist/types.js.map +1 -1
- package/package.json +4 -4
package/dist/scope/policy.js
CHANGED
|
@@ -16,12 +16,12 @@ export const PRESET_ACTIONS = {
|
|
|
16
16
|
],
|
|
17
17
|
admin: ALL_ACTIONS
|
|
18
18
|
};
|
|
19
|
-
/**
|
|
20
|
-
* Asymmetric per-preset action map for globals. `editor` is intentionally
|
|
21
|
-
* read-only on globals — a single bad write on a singleton broadcasts
|
|
22
|
-
* site-wide with no per-document containment. Operators who want global
|
|
23
|
-
* writes promote the key to `admin` or use a Custom key with explicit
|
|
24
|
-
* `globalScopes`. README and CHANGELOG call out the asymmetry.
|
|
19
|
+
/**
|
|
20
|
+
* Asymmetric per-preset action map for globals. `editor` is intentionally
|
|
21
|
+
* read-only on globals — a single bad write on a singleton broadcasts
|
|
22
|
+
* site-wide with no per-document containment. Operators who want global
|
|
23
|
+
* writes promote the key to `admin` or use a Custom key with explicit
|
|
24
|
+
* `globalScopes`. README and CHANGELOG call out the asymmetry.
|
|
25
25
|
*/ export const PRESET_GLOBAL_ACTIONS = {
|
|
26
26
|
'read-only': [
|
|
27
27
|
'read'
|
|
@@ -60,28 +60,28 @@ export function buildRoutingTables(tools) {
|
|
|
60
60
|
toolKind
|
|
61
61
|
};
|
|
62
62
|
}
|
|
63
|
-
/**
|
|
64
|
-
* Build a scope checker bound to a concrete tool list. The checker is a pure
|
|
65
|
-
* function over (scopes, toolName, resource) — the routing tables are closed
|
|
66
|
-
* over once at construction time.
|
|
67
|
-
*
|
|
68
|
-
* Fail-closed semantics:
|
|
69
|
-
* - Null/undefined scopes grant full access (back-compat).
|
|
70
|
-
* - When `scopes.collections` / `scopes.globals` is set, it is a *whitelist*
|
|
71
|
-
* for that resource kind — unlisted resources are denied.
|
|
72
|
-
* - When a tool resolves to a collection or global kind but the corresponding
|
|
73
|
-
* scope map is undefined and `scopes.preset` is undefined, the call is
|
|
74
|
-
* denied (closes the `tools.allow`-only latent fail-open).
|
|
75
|
-
* - Account-level tools are gated by the preset's action list, if a preset
|
|
76
|
-
* is set. Without a preset, a key scoped to specific collections/globals
|
|
77
|
-
* cannot use account-level tools — they'd broaden the surface.
|
|
63
|
+
/**
|
|
64
|
+
* Build a scope checker bound to a concrete tool list. The checker is a pure
|
|
65
|
+
* function over (scopes, toolName, resource) — the routing tables are closed
|
|
66
|
+
* over once at construction time.
|
|
67
|
+
*
|
|
68
|
+
* Fail-closed semantics:
|
|
69
|
+
* - Null/undefined scopes grant full access (back-compat).
|
|
70
|
+
* - When `scopes.collections` / `scopes.globals` is set, it is a *whitelist*
|
|
71
|
+
* for that resource kind — unlisted resources are denied.
|
|
72
|
+
* - When a tool resolves to a collection or global kind but the corresponding
|
|
73
|
+
* scope map is undefined and `scopes.preset` is undefined, the call is
|
|
74
|
+
* denied (closes the `tools.allow`-only latent fail-open).
|
|
75
|
+
* - Account-level tools are gated by the preset's action list, if a preset
|
|
76
|
+
* is set. Without a preset, a key scoped to specific collections/globals
|
|
77
|
+
* cannot use account-level tools — they'd broaden the surface.
|
|
78
78
|
*/ export function buildScopeChecker(tools) {
|
|
79
79
|
const tables = buildRoutingTables(tools);
|
|
80
80
|
return (scopes, toolName, resource)=>assertScopeAllows(scopes, toolName, resource, tables);
|
|
81
81
|
}
|
|
82
|
-
/**
|
|
83
|
-
* Internal pure checker. Exposed for the per-request wrapper in the registry
|
|
84
|
-
* so it can re-use the same `RoutingTables` it built once at startup.
|
|
82
|
+
/**
|
|
83
|
+
* Internal pure checker. Exposed for the per-request wrapper in the registry
|
|
84
|
+
* so it can re-use the same `RoutingTables` it built once at startup.
|
|
85
85
|
*/ export function assertScopeAllows(scopes, toolName, resource, tables) {
|
|
86
86
|
const resourceKind = tables.toolKind.get(toolName) ?? null;
|
|
87
87
|
// Unregistered tool — fail-closed at request time. Adding a tool without a
|
|
@@ -140,9 +140,15 @@ function checkResource(scopes, toolName, resource, toolAction, policy) {
|
|
|
140
140
|
const presetActions = scopes.preset ? policy.presetActions[scopes.preset] : undefined;
|
|
141
141
|
const resourceScope = scopes[policy.scopeAxis];
|
|
142
142
|
if (!resource) {
|
|
143
|
-
//
|
|
143
|
+
// Fail-closed. Every built-in collection/global tool takes a required
|
|
144
|
+
// `collection` / `slug` argument, so this only fires for a malformed call
|
|
145
|
+
// or for a host tool that declared resource routing without a resource
|
|
146
|
+
// argument. Allowing it would let such a tool read a hard-coded collection
|
|
147
|
+
// straight past the key's whitelist. A tool that genuinely spans the whole
|
|
148
|
+
// install belongs on `routing.kind: 'account'`.
|
|
144
149
|
return {
|
|
145
|
-
allowed:
|
|
150
|
+
allowed: false,
|
|
151
|
+
reason: `Tool "${toolName}" is routed to a ${policy.label} but the call carries no ` + `${policy.label} argument, so its scope cannot be checked. A tool with a fixed ` + `or install-wide target must use routing.kind: 'account'.`
|
|
146
152
|
};
|
|
147
153
|
}
|
|
148
154
|
if (!action) 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'\r\n\r\n// ─── Routing primitives ──────────────────────────────────────────────\r\n\r\nexport type ResourceKind = 'collection' | 'global' | 'account'\r\n\r\n/**\r\n * Discriminated routing tag attached to every tool factory output.\r\n *\r\n * Collocates the scope-routing decision with the tool definition itself —\r\n * the registry derives the collection/global/account lookups from `tools`\r\n * at boot. Adding a new tool can no longer drift the routing maps out of\r\n * sync because TS requires `routing` on every factory return.\r\n */\r\nexport type ToolRouting =\r\n | { kind: 'collection'; action: CollectionAction }\r\n | { kind: 'global'; action: GlobalAction }\r\n | { kind: 'account'; action: CollectionAction }\r\n\r\n/**\r\n * Minimal \"routable tool\" interface used by the policy module. The full\r\n * `ToolFactoryOutput` shape (handler, parameters, description) is irrelevant\r\n * here; we only need `name` + `routing` to build the lookup tables.\r\n */\r\nexport interface RoutableTool {\r\n name: string\r\n routing: ToolRouting\r\n}\r\n\r\n// ─── Per-preset action tables ────────────────────────────────────────\r\n\r\nconst ALL_ACTIONS: CollectionAction[] = ['read', 'create', 'update', 'delete']\r\n\r\nexport const PRESET_ACTIONS: Record<ScopePreset, CollectionAction[]> = {\r\n 'read-only': ['read'],\r\n editor: ['read', 'create', 'update'],\r\n admin: ALL_ACTIONS,\r\n}\r\n\r\n/**\r\n * Asymmetric per-preset action map for globals. `editor` is intentionally\r\n * read-only on globals — a single bad write on a singleton broadcasts\r\n * site-wide with no per-document containment. Operators who want global\r\n * writes promote the key to `admin` or use a Custom key with explicit\r\n * `globalScopes`. README and CHANGELOG call out the asymmetry.\r\n */\r\nexport const PRESET_GLOBAL_ACTIONS: Record<ScopePreset, GlobalAction[]> = {\r\n 'read-only': ['read'],\r\n editor: ['read'],\r\n admin: ['read', 'update'],\r\n}\r\n\r\nexport const PRESET_TOOL_DENY: Record<ScopePreset, string[]> = {\r\n 'read-only': [],\r\n editor: ['safeDelete', 'deleteDocument'],\r\n admin: [],\r\n}\r\n\r\n// ─── Routing tables built from the tool list ─────────────────────────\r\n\r\nexport interface ScopeDecision {\r\n allowed: boolean\r\n reason?: string\r\n}\r\n\r\nexport interface RoutingTables {\r\n collectionToolAction: ReadonlyMap<string, CollectionAction>\r\n globalToolAction: ReadonlyMap<string, GlobalAction>\r\n accountToolAction: ReadonlyMap<string, CollectionAction>\r\n toolKind: ReadonlyMap<string, ResourceKind>\r\n}\r\n\r\nexport function buildRoutingTables(tools: RoutableTool[]): RoutingTables {\r\n const collectionToolAction = new Map<string, CollectionAction>()\r\n const globalToolAction = new Map<string, GlobalAction>()\r\n const accountToolAction = new Map<string, CollectionAction>()\r\n const toolKind = new Map<string, ResourceKind>()\r\n for (const t of tools) {\r\n toolKind.set(t.name, t.routing.kind)\r\n if (t.routing.kind === 'collection') collectionToolAction.set(t.name, t.routing.action)\r\n else if (t.routing.kind === 'global') globalToolAction.set(t.name, t.routing.action)\r\n else accountToolAction.set(t.name, t.routing.action)\r\n }\r\n return { collectionToolAction, globalToolAction, accountToolAction, toolKind }\r\n}\r\n\r\n// ─── Scope evaluation ────────────────────────────────────────────────\r\n\r\nexport type ScopeChecker = (\r\n scopes: KeyScopes | null | undefined,\r\n toolName: string,\r\n resource: string | undefined,\r\n) => ScopeDecision\r\n\r\n/**\r\n * Build a scope checker bound to a concrete tool list. The checker is a pure\r\n * function over (scopes, toolName, resource) — the routing tables are closed\r\n * over once at construction time.\r\n *\r\n * Fail-closed semantics:\r\n * - Null/undefined scopes grant full access (back-compat).\r\n * - When `scopes.collections` / `scopes.globals` is set, it is a *whitelist*\r\n * for that resource kind — unlisted resources are denied.\r\n * - When a tool resolves to a collection or global kind but the corresponding\r\n * scope map is undefined and `scopes.preset` is undefined, the call is\r\n * denied (closes the `tools.allow`-only latent fail-open).\r\n * - Account-level tools are gated by the preset's action list, if a preset\r\n * is set. Without a preset, a key scoped to specific collections/globals\r\n * cannot use account-level tools — they'd broaden the surface.\r\n */\r\nexport function buildScopeChecker(tools: RoutableTool[]): ScopeChecker {\r\n const tables = buildRoutingTables(tools)\r\n return (scopes, toolName, resource) => assertScopeAllows(scopes, toolName, resource, tables)\r\n}\r\n\r\n/**\r\n * Internal pure checker. Exposed for the per-request wrapper in the registry\r\n * so it can re-use the same `RoutingTables` it built once at startup.\r\n */\r\nexport function assertScopeAllows(\r\n scopes: KeyScopes | null | undefined,\r\n toolName: string,\r\n resource: string | undefined,\r\n tables: RoutingTables,\r\n): ScopeDecision {\r\n const resourceKind = tables.toolKind.get(toolName) ?? null\r\n // Unregistered tool — fail-closed at request time. Adding a tool without a\r\n // routing field is a TS error at the factory return site, so this branch\r\n // only fires for typo'd tool names sent by the client.\r\n if (resourceKind === null) {\r\n return {\r\n allowed: false,\r\n reason: `Tool \"${toolName}\" has no registered scope mapping.`,\r\n }\r\n }\r\n\r\n if (!scopes || (scopes.preset === undefined && !scopes.collections && !scopes.globals && !scopes.tools)) {\r\n return { allowed: true }\r\n }\r\n\r\n if (scopes.tools?.deny?.includes(toolName)) {\r\n return { allowed: false, reason: `Tool \"${toolName}\" is denied for this API key.` }\r\n }\r\n if (scopes.tools?.allow && !scopes.tools.allow.includes(toolName)) {\r\n return {\r\n allowed: false,\r\n reason: `Tool \"${toolName}\" is not in the allow-list for this API key.`,\r\n }\r\n }\r\n\r\n if (scopes.preset && PRESET_TOOL_DENY[scopes.preset]?.includes(toolName)) {\r\n return {\r\n allowed: false,\r\n reason: `Tool \"${toolName}\" is not allowed by the \"${scopes.preset}\" preset.`,\r\n }\r\n }\r\n\r\n if (resourceKind === 'account') {\r\n return checkAccount(scopes, toolName, tables.accountToolAction)\r\n }\r\n const policy = resourceKind === 'collection' ? COLLECTION_POLICY : GLOBAL_POLICY\r\n const toolAction =\r\n resourceKind === 'collection' ? tables.collectionToolAction : tables.globalToolAction\r\n return checkResource(scopes, toolName, resource, toolAction, policy)\r\n}\r\n\r\n/**\r\n * Per-resource-kind policy. Collapses what used to be two near-identical\r\n * `checkCollection` / `checkGlobal` helpers — the only differences are\r\n * the preset-actions table, the label, and which axis of `KeyScopes` to\r\n * read for explicit overrides.\r\n */\r\ninterface ResourcePolicy {\r\n presetActions: Record<ScopePreset, readonly string[]>\r\n scopeAxis: 'collections' | 'globals'\r\n label: 'collection' | 'global'\r\n Label: 'Collection' | 'Global'\r\n}\r\n\r\nconst COLLECTION_POLICY: ResourcePolicy = {\r\n presetActions: PRESET_ACTIONS,\r\n scopeAxis: 'collections',\r\n label: 'collection',\r\n Label: 'Collection',\r\n}\r\n\r\nconst GLOBAL_POLICY: ResourcePolicy = {\r\n presetActions: PRESET_GLOBAL_ACTIONS,\r\n scopeAxis: 'globals',\r\n label: 'global',\r\n Label: 'Global',\r\n}\r\n\r\nfunction checkResource(\r\n scopes: KeyScopes,\r\n toolName: string,\r\n resource: string | undefined,\r\n toolAction: ReadonlyMap<string, string>,\r\n policy: ResourcePolicy,\r\n): ScopeDecision {\r\n const action = toolAction.get(toolName)\r\n const presetActions = scopes.preset ? policy.presetActions[scopes.preset] : undefined\r\n const resourceScope = scopes[policy.scopeAxis]\r\n\r\n if (!resource) {\r\n // Resource-keyed tool called without a slug; defer to schema validation.\r\n return { allowed: true }\r\n }\r\n if (!action) return { allowed: true }\r\n\r\n if (resourceScope) {\r\n const override = resourceScope[resource]\r\n if (!override) {\r\n return {\r\n allowed: false,\r\n reason: `${policy.Label} \"${resource}\" is not in this API key's allowed ${policy.scopeAxis}.`,\r\n }\r\n }\r\n if (!override.includes(action as never)) {\r\n return {\r\n allowed: false,\r\n reason: `Action \"${action}\" on ${policy.label} \"${resource}\" is not permitted by this API key's scope.`,\r\n }\r\n }\r\n return { allowed: true }\r\n }\r\n\r\n if (!presetActions) {\r\n // Fail-closed: `tools.allow` without a resource map or preset would\r\n // otherwise broadcast the tool across every resource. Require explicit\r\n // intent.\r\n return {\r\n allowed: false,\r\n reason: `Tool \"${toolName}\" requires an explicit ${policy.label} scope or preset on this API key.`,\r\n }\r\n }\r\n\r\n if (!presetActions.includes(action)) {\r\n return {\r\n allowed: false,\r\n reason: `Action \"${action}\" on ${policy.label} \"${resource}\" is not permitted by this API key's preset.`,\r\n }\r\n }\r\n return { allowed: true }\r\n}\r\n\r\nfunction checkAccount(\r\n scopes: KeyScopes,\r\n toolName: string,\r\n toolAction: ReadonlyMap<string, CollectionAction>,\r\n): ScopeDecision {\r\n const action = toolAction.get(toolName)\r\n const presetActions = scopes.preset ? PRESET_ACTIONS[scopes.preset] : undefined\r\n\r\n // Explicit resource override is the tightest signal: an account-level tool\r\n // operates across the whole site (searchContent across every collection,\r\n // uploadMedia into any media coll, etc.) and would broaden the key beyond\r\n // the resource whitelist regardless of which preset is set. Deny account\r\n // tools whenever the key carries explicit collection/global scopes.\r\n if (scopes.collections || scopes.globals) {\r\n return {\r\n allowed: false,\r\n reason: `Tool \"${toolName}\" is denied for keys with explicit collection or global scopes — account-level tools would broaden access beyond the whitelist.`,\r\n }\r\n }\r\n\r\n if (presetActions) {\r\n if (action && !presetActions.includes(action)) {\r\n return {\r\n allowed: false,\r\n reason: `Action \"${action}\" is not permitted by this API key's preset.`,\r\n }\r\n }\r\n return { allowed: true }\r\n }\r\n\r\n return { allowed: true }\r\n}\r\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.` }\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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"}
|
|
@@ -60,6 +60,14 @@ const MEDIA_SLUG = 'media';
|
|
|
60
60
|
stampMcpContext(req);
|
|
61
61
|
const isDraftCollection = draftCollections.has(collection);
|
|
62
62
|
const asDraft = draft ?? isDraftCollection;
|
|
63
|
+
// Payload's `draft: false` writes a published version but leaves the main
|
|
64
|
+
// row's `_status` at its field default ('draft'), so the admin list shows
|
|
65
|
+
// "draft" even though routes serve the published doc. Set `_status`
|
|
66
|
+
// explicitly to match intent (mirrors publishDraft); respect a caller
|
|
67
|
+
// value if one was passed in `data`.
|
|
68
|
+
if (isDraftCollection && data._status === undefined) {
|
|
69
|
+
data._status = asDraft ? 'draft' : 'published';
|
|
70
|
+
}
|
|
63
71
|
try {
|
|
64
72
|
const doc = await req.payload.create({
|
|
65
73
|
collection: collection,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/tools/create-document.ts"],"sourcesContent":["import { z } from 'zod'\r\nimport type { PayloadRequest } from 'payload'\r\nimport type { CollectionSchema } from '../types'\r\nimport {\r\n DRAFT_NOTE,\r\n errorMessage,\r\n getDocDisplayName,\r\n stampMcpContext,\r\n textResponse,\r\n} from './_helpers'\r\n\r\nconst MEDIA_SLUG = 'media'\r\n\r\n/**\r\n * Custom replacement for the official plugin's `create<Resource>` tools.\r\n *\r\n * The official plugin builds its create-tool input schema by spreading\r\n * `convertCollectionSchemaToZod(schema).shape`, then validates the request\r\n * with `additionalProperties: false`. For any collection whose JSON schema\r\n * trips `json-schema-to-zod` (richText, upload, blocks, relationship arrays\r\n * — i.e. virtually every real-world content collection), the converter\r\n * falls back to `z.record(z.any())`, `.shape` is undefined, and the spread\r\n * silently produces a metadata-only schema. The MCP SDK then strips every\r\n * content field before it reaches `payload.create()`, so creates end up\r\n * with empty `data` and fail required-field validation.\r\n *\r\n * `createDocument` sidesteps the whole pipeline: take a JSON `data` string,\r\n * call `payload.create()` directly via the local API.\r\n *\r\n * Defaults to `draft: true` for draft-enabled collections so newly created\r\n * documents land in the same draft-first workflow used by `updateDocument`.\r\n */\r\nexport function createCreateDocumentTool(\r\n collectionSchemas: Map<string, CollectionSchema>,\r\n draftCollections: Set<string>,\r\n) {\r\n const creatableSlugs: string[] = []\r\n const descriptionLines: string[] = []\r\n for (const [slug, schema] of collectionSchemas) {\r\n if (slug === MEDIA_SLUG) continue\r\n creatableSlugs.push(slug)\r\n descriptionLines.push(` - \"${slug}\": ${schema.fields.map((f) => f.name).join(', ')}`)\r\n }\r\n const collectionDescriptions = descriptionLines.join('\\n')\r\n\r\n return {\r\n name: 'createDocument',\r\n routing: { kind: 'collection', action: 'create' } as const,\r\n description:\r\n 'Create a new document in any collection. Pass the field values as a JSON string in `data`. ' +\r\n 'For draft-enabled collections, the document is created as a draft by default — use publishDraft to make it live, ' +\r\n 'or pass `draft: false` to publish immediately. ' +\r\n 'For relationship fields, pass the related document ID (use resolveReference to find IDs). ' +\r\n 'For upload fields, pass the media document ID (use uploadMedia to create one first).\\n\\n' +\r\n 'Available collections and their fields:\\n' +\r\n collectionDescriptions,\r\n parameters: {\r\n collection: z\r\n .string()\r\n .describe(`The collection slug. One of: ${creatableSlugs.join(', ')}`),\r\n data: z\r\n .string()\r\n .describe(\r\n 'JSON string of field names to values for the new document. ' +\r\n 'Examples: \\'{\"name\": \"Aria\", \"slug\": \"aria\"}\\', ' +\r\n '\\'{\"title\": \"First-Time Clients\", \"heroTitle\": \"Welcome\", \"slug\": \"first-time-clients\"}\\'',\r\n ),\r\n draft: z\r\n .boolean()\r\n .optional()\r\n .describe(\r\n 'Override draft status. Defaults to `true` for draft-enabled collections, `false` otherwise. ' +\r\n 'Set explicitly to `false` on a draft-enabled collection to publish immediately.',\r\n ),\r\n },\r\n handler: async (\r\n args: Record<string, unknown>,\r\n req: PayloadRequest,\r\n _extra: unknown,\r\n ) => {\r\n const { collection, data: rawData, draft } = args as {\r\n collection: string\r\n data: string\r\n draft?: boolean\r\n }\r\n\r\n let data: Record<string, unknown>\r\n try {\r\n data = JSON.parse(rawData)\r\n } catch {\r\n return textResponse(\r\n 'Error: \"data\" must be a valid JSON string. Example: \\'{\"name\": \"Aria\", \"slug\": \"aria\"}\\'',\r\n )\r\n }\r\n\r\n if (!collectionSchemas.has(collection)) {\r\n return textResponse(\r\n `Error: Unknown collection \"${collection}\". Available: ${creatableSlugs.join(', ')}`,\r\n )\r\n }\r\n\r\n if (collection === MEDIA_SLUG) {\r\n return textResponse('Error: Use the uploadMedia tool to create media files.')\r\n }\r\n\r\n if (!data || Object.keys(data).length === 0) {\r\n return textResponse(\r\n 'Error: No fields provided in \"data\". Pass an object with field names and values for the new document.',\r\n )\r\n }\r\n\r\n stampMcpContext(req)\r\n\r\n const isDraftCollection = draftCollections.has(collection)\r\n const asDraft = draft ?? isDraftCollection\r\n\r\n try {\r\n const doc = await req.payload.create({\r\n collection: collection as any,\r\n data: data as any,\r\n draft: asDraft,\r\n req,\r\n overrideAccess: false,\r\n user: req.user,\r\n })\r\n\r\n const displayName = getDocDisplayName(doc, String((doc as { id?: unknown }).id ?? ''))\r\n const newId = String((doc as { id?: unknown }).id ?? '')\r\n const draftNote = isDraftCollection && asDraft ? DRAFT_NOTE : ''\r\n\r\n return textResponse(\r\n `Created \"${displayName}\" in ${collection} (ID: ${newId}).${draftNote}`,\r\n )\r\n } catch (error) {\r\n return textResponse(\r\n `Error creating document in ${collection}: ${errorMessage(error)}`,\r\n )\r\n }\r\n },\r\n }\r\n}\r\n"],"names":["z","DRAFT_NOTE","errorMessage","getDocDisplayName","stampMcpContext","textResponse","MEDIA_SLUG","createCreateDocumentTool","collectionSchemas","draftCollections","creatableSlugs","descriptionLines","slug","schema","push","fields","map","f","name","join","collectionDescriptions","routing","kind","action","description","parameters","collection","string","describe","data","draft","boolean","optional","handler","args","req","_extra","rawData","JSON","parse","has","Object","keys","length","isDraftCollection","asDraft","doc","payload","create","overrideAccess","user","displayName","String","id","newId","draftNote","error"],"mappings":"AAAA,SAASA,CAAC,QAAQ,MAAK;AAGvB,SACEC,UAAU,EACVC,YAAY,EACZC,iBAAiB,EACjBC,eAAe,EACfC,YAAY,QACP,aAAY;AAEnB,MAAMC,aAAa;AAEnB;;;;;;;;;;;;;;;;;;CAkBC,GACD,OAAO,SAASC,yBACdC,iBAAgD,EAChDC,gBAA6B;IAE7B,MAAMC,iBAA2B,EAAE;IACnC,MAAMC,mBAA6B,EAAE;IACrC,KAAK,MAAM,CAACC,MAAMC,OAAO,IAAIL,kBAAmB;QAC9C,IAAII,SAASN,YAAY;QACzBI,eAAeI,IAAI,CAACF;QACpBD,iBAAiBG,IAAI,CAAC,CAAC,KAAK,EAAEF,KAAK,GAAG,EAAEC,OAAOE,MAAM,CAACC,GAAG,CAAC,CAACC,IAAMA,EAAEC,IAAI,EAAEC,IAAI,CAAC,OAAO;IACvF;IACA,MAAMC,yBAAyBT,iBAAiBQ,IAAI,CAAC;IAErD,OAAO;QACLD,MAAM;QACNG,SAAS;YAAEC,MAAM;YAAcC,QAAQ;QAAS;QAChDC,aACE,gGACA,sHACA,oDACA,+FACA,6FACA,8CACAJ;QACFK,YAAY;YACVC,YAAY1B,EACT2B,MAAM,GACNC,QAAQ,CAAC,CAAC,6BAA6B,EAAElB,eAAeS,IAAI,CAAC,OAAO;YACvEU,MAAM7B,EACH2B,MAAM,GACNC,QAAQ,CACP,gEACE,qDACA;YAENE,OAAO9B,EACJ+B,OAAO,GACPC,QAAQ,GACRJ,QAAQ,CACP,iGACE;QAER;QACAK,SAAS,OACPC,MACAC,KACAC;YAEA,MAAM,EAAEV,UAAU,EAAEG,MAAMQ,OAAO,EAAEP,KAAK,EAAE,GAAGI;YAM7C,IAAIL;YACJ,IAAI;gBACFA,OAAOS,KAAKC,KAAK,CAACF;YACpB,EAAE,OAAM;gBACN,OAAOhC,aACL;YAEJ;YAEA,IAAI,CAACG,kBAAkBgC,GAAG,CAACd,aAAa;gBACtC,OAAOrB,aACL,CAAC,2BAA2B,EAAEqB,WAAW,cAAc,EAAEhB,eAAeS,IAAI,CAAC,OAAO;YAExF;YAEA,IAAIO,eAAepB,YAAY;gBAC7B,OAAOD,aAAa;YACtB;YAEA,IAAI,CAACwB,QAAQY,OAAOC,IAAI,CAACb,MAAMc,MAAM,KAAK,GAAG;gBAC3C,OAAOtC,aACL;YAEJ;YAEAD,gBAAgB+B;YAEhB,MAAMS,oBAAoBnC,iBAAiB+B,GAAG,CAACd;YAC/C,MAAMmB,UAAUf,SAASc;YAEzB,IAAI;gBACF,
|
|
1
|
+
{"version":3,"sources":["../../src/tools/create-document.ts"],"sourcesContent":["import { z } from 'zod'\r\nimport type { PayloadRequest } from 'payload'\r\nimport type { CollectionSchema } from '../types'\r\nimport {\r\n DRAFT_NOTE,\r\n errorMessage,\r\n getDocDisplayName,\r\n stampMcpContext,\r\n textResponse,\r\n} from './_helpers'\r\n\r\nconst MEDIA_SLUG = 'media'\r\n\r\n/**\r\n * Custom replacement for the official plugin's `create<Resource>` tools.\r\n *\r\n * The official plugin builds its create-tool input schema by spreading\r\n * `convertCollectionSchemaToZod(schema).shape`, then validates the request\r\n * with `additionalProperties: false`. For any collection whose JSON schema\r\n * trips `json-schema-to-zod` (richText, upload, blocks, relationship arrays\r\n * — i.e. virtually every real-world content collection), the converter\r\n * falls back to `z.record(z.any())`, `.shape` is undefined, and the spread\r\n * silently produces a metadata-only schema. The MCP SDK then strips every\r\n * content field before it reaches `payload.create()`, so creates end up\r\n * with empty `data` and fail required-field validation.\r\n *\r\n * `createDocument` sidesteps the whole pipeline: take a JSON `data` string,\r\n * call `payload.create()` directly via the local API.\r\n *\r\n * Defaults to `draft: true` for draft-enabled collections so newly created\r\n * documents land in the same draft-first workflow used by `updateDocument`.\r\n */\r\nexport function createCreateDocumentTool(\r\n collectionSchemas: Map<string, CollectionSchema>,\r\n draftCollections: Set<string>,\r\n) {\r\n const creatableSlugs: string[] = []\r\n const descriptionLines: string[] = []\r\n for (const [slug, schema] of collectionSchemas) {\r\n if (slug === MEDIA_SLUG) continue\r\n creatableSlugs.push(slug)\r\n descriptionLines.push(` - \"${slug}\": ${schema.fields.map((f) => f.name).join(', ')}`)\r\n }\r\n const collectionDescriptions = descriptionLines.join('\\n')\r\n\r\n return {\r\n name: 'createDocument',\r\n routing: { kind: 'collection', action: 'create' } as const,\r\n description:\r\n 'Create a new document in any collection. Pass the field values as a JSON string in `data`. ' +\r\n 'For draft-enabled collections, the document is created as a draft by default — use publishDraft to make it live, ' +\r\n 'or pass `draft: false` to publish immediately. ' +\r\n 'For relationship fields, pass the related document ID (use resolveReference to find IDs). ' +\r\n 'For upload fields, pass the media document ID (use uploadMedia to create one first).\\n\\n' +\r\n 'Available collections and their fields:\\n' +\r\n collectionDescriptions,\r\n parameters: {\r\n collection: z\r\n .string()\r\n .describe(`The collection slug. One of: ${creatableSlugs.join(', ')}`),\r\n data: z\r\n .string()\r\n .describe(\r\n 'JSON string of field names to values for the new document. ' +\r\n 'Examples: \\'{\"name\": \"Aria\", \"slug\": \"aria\"}\\', ' +\r\n '\\'{\"title\": \"First-Time Clients\", \"heroTitle\": \"Welcome\", \"slug\": \"first-time-clients\"}\\'',\r\n ),\r\n draft: z\r\n .boolean()\r\n .optional()\r\n .describe(\r\n 'Override draft status. Defaults to `true` for draft-enabled collections, `false` otherwise. ' +\r\n 'Set explicitly to `false` on a draft-enabled collection to publish immediately.',\r\n ),\r\n },\r\n handler: async (\r\n args: Record<string, unknown>,\r\n req: PayloadRequest,\r\n _extra: unknown,\r\n ) => {\r\n const { collection, data: rawData, draft } = args as {\r\n collection: string\r\n data: string\r\n draft?: boolean\r\n }\r\n\r\n let data: Record<string, unknown>\r\n try {\r\n data = JSON.parse(rawData)\r\n } catch {\r\n return textResponse(\r\n 'Error: \"data\" must be a valid JSON string. Example: \\'{\"name\": \"Aria\", \"slug\": \"aria\"}\\'',\r\n )\r\n }\r\n\r\n if (!collectionSchemas.has(collection)) {\r\n return textResponse(\r\n `Error: Unknown collection \"${collection}\". Available: ${creatableSlugs.join(', ')}`,\r\n )\r\n }\r\n\r\n if (collection === MEDIA_SLUG) {\r\n return textResponse('Error: Use the uploadMedia tool to create media files.')\r\n }\r\n\r\n if (!data || Object.keys(data).length === 0) {\r\n return textResponse(\r\n 'Error: No fields provided in \"data\". Pass an object with field names and values for the new document.',\r\n )\r\n }\r\n\r\n stampMcpContext(req)\r\n\r\n const isDraftCollection = draftCollections.has(collection)\r\n const asDraft = draft ?? isDraftCollection\r\n\r\n // Payload's `draft: false` writes a published version but leaves the main\r\n // row's `_status` at its field default ('draft'), so the admin list shows\r\n // \"draft\" even though routes serve the published doc. Set `_status`\r\n // explicitly to match intent (mirrors publishDraft); respect a caller\r\n // value if one was passed in `data`.\r\n if (isDraftCollection && data._status === undefined) {\r\n data._status = asDraft ? 'draft' : 'published'\r\n }\r\n\r\n try {\r\n const doc = await req.payload.create({\r\n collection: collection as any,\r\n data: data as any,\r\n draft: asDraft,\r\n req,\r\n overrideAccess: false,\r\n user: req.user,\r\n })\r\n\r\n const displayName = getDocDisplayName(doc, String((doc as { id?: unknown }).id ?? ''))\r\n const newId = String((doc as { id?: unknown }).id ?? '')\r\n const draftNote = isDraftCollection && asDraft ? DRAFT_NOTE : ''\r\n\r\n return textResponse(\r\n `Created \"${displayName}\" in ${collection} (ID: ${newId}).${draftNote}`,\r\n )\r\n } catch (error) {\r\n return textResponse(\r\n `Error creating document in ${collection}: ${errorMessage(error)}`,\r\n )\r\n }\r\n },\r\n }\r\n}\r\n"],"names":["z","DRAFT_NOTE","errorMessage","getDocDisplayName","stampMcpContext","textResponse","MEDIA_SLUG","createCreateDocumentTool","collectionSchemas","draftCollections","creatableSlugs","descriptionLines","slug","schema","push","fields","map","f","name","join","collectionDescriptions","routing","kind","action","description","parameters","collection","string","describe","data","draft","boolean","optional","handler","args","req","_extra","rawData","JSON","parse","has","Object","keys","length","isDraftCollection","asDraft","_status","undefined","doc","payload","create","overrideAccess","user","displayName","String","id","newId","draftNote","error"],"mappings":"AAAA,SAASA,CAAC,QAAQ,MAAK;AAGvB,SACEC,UAAU,EACVC,YAAY,EACZC,iBAAiB,EACjBC,eAAe,EACfC,YAAY,QACP,aAAY;AAEnB,MAAMC,aAAa;AAEnB;;;;;;;;;;;;;;;;;;CAkBC,GACD,OAAO,SAASC,yBACdC,iBAAgD,EAChDC,gBAA6B;IAE7B,MAAMC,iBAA2B,EAAE;IACnC,MAAMC,mBAA6B,EAAE;IACrC,KAAK,MAAM,CAACC,MAAMC,OAAO,IAAIL,kBAAmB;QAC9C,IAAII,SAASN,YAAY;QACzBI,eAAeI,IAAI,CAACF;QACpBD,iBAAiBG,IAAI,CAAC,CAAC,KAAK,EAAEF,KAAK,GAAG,EAAEC,OAAOE,MAAM,CAACC,GAAG,CAAC,CAACC,IAAMA,EAAEC,IAAI,EAAEC,IAAI,CAAC,OAAO;IACvF;IACA,MAAMC,yBAAyBT,iBAAiBQ,IAAI,CAAC;IAErD,OAAO;QACLD,MAAM;QACNG,SAAS;YAAEC,MAAM;YAAcC,QAAQ;QAAS;QAChDC,aACE,gGACA,sHACA,oDACA,+FACA,6FACA,8CACAJ;QACFK,YAAY;YACVC,YAAY1B,EACT2B,MAAM,GACNC,QAAQ,CAAC,CAAC,6BAA6B,EAAElB,eAAeS,IAAI,CAAC,OAAO;YACvEU,MAAM7B,EACH2B,MAAM,GACNC,QAAQ,CACP,gEACE,qDACA;YAENE,OAAO9B,EACJ+B,OAAO,GACPC,QAAQ,GACRJ,QAAQ,CACP,iGACE;QAER;QACAK,SAAS,OACPC,MACAC,KACAC;YAEA,MAAM,EAAEV,UAAU,EAAEG,MAAMQ,OAAO,EAAEP,KAAK,EAAE,GAAGI;YAM7C,IAAIL;YACJ,IAAI;gBACFA,OAAOS,KAAKC,KAAK,CAACF;YACpB,EAAE,OAAM;gBACN,OAAOhC,aACL;YAEJ;YAEA,IAAI,CAACG,kBAAkBgC,GAAG,CAACd,aAAa;gBACtC,OAAOrB,aACL,CAAC,2BAA2B,EAAEqB,WAAW,cAAc,EAAEhB,eAAeS,IAAI,CAAC,OAAO;YAExF;YAEA,IAAIO,eAAepB,YAAY;gBAC7B,OAAOD,aAAa;YACtB;YAEA,IAAI,CAACwB,QAAQY,OAAOC,IAAI,CAACb,MAAMc,MAAM,KAAK,GAAG;gBAC3C,OAAOtC,aACL;YAEJ;YAEAD,gBAAgB+B;YAEhB,MAAMS,oBAAoBnC,iBAAiB+B,GAAG,CAACd;YAC/C,MAAMmB,UAAUf,SAASc;YAEzB,0EAA0E;YAC1E,0EAA0E;YAC1E,oEAAoE;YACpE,sEAAsE;YACtE,qCAAqC;YACrC,IAAIA,qBAAqBf,KAAKiB,OAAO,KAAKC,WAAW;gBACnDlB,KAAKiB,OAAO,GAAGD,UAAU,UAAU;YACrC;YAEA,IAAI;gBACF,MAAMG,MAAM,MAAMb,IAAIc,OAAO,CAACC,MAAM,CAAC;oBACnCxB,YAAYA;oBACZG,MAAMA;oBACNC,OAAOe;oBACPV;oBACAgB,gBAAgB;oBAChBC,MAAMjB,IAAIiB,IAAI;gBAChB;gBAEA,MAAMC,cAAclD,kBAAkB6C,KAAKM,OAAO,AAACN,IAAyBO,EAAE,IAAI;gBAClF,MAAMC,QAAQF,OAAO,AAACN,IAAyBO,EAAE,IAAI;gBACrD,MAAME,YAAYb,qBAAqBC,UAAU5C,aAAa;gBAE9D,OAAOI,aACL,CAAC,SAAS,EAAEgD,YAAY,KAAK,EAAE3B,WAAW,MAAM,EAAE8B,MAAM,EAAE,EAAEC,WAAW;YAE3E,EAAE,OAAOC,OAAO;gBACd,OAAOrD,aACL,CAAC,2BAA2B,EAAEqB,WAAW,EAAE,EAAExB,aAAawD,QAAQ;YAEtE;QACF;IACF;AACF"}
|
|
@@ -19,7 +19,7 @@ export declare function createDeleteDocumentTool(collectionSchemas: Map<string,
|
|
|
19
19
|
description: string;
|
|
20
20
|
parameters: {
|
|
21
21
|
collection: z.ZodString;
|
|
22
|
-
|
|
22
|
+
documentId: z.ZodString;
|
|
23
23
|
};
|
|
24
24
|
handler: (args: Record<string, unknown>, req: PayloadRequest, _extra: unknown) => Promise<import("./_helpers").McpTextResponse>;
|
|
25
25
|
};
|
|
@@ -21,10 +21,10 @@ import { errorMessage, getDocDisplayName, stampMcpContext, textResponse } from '
|
|
|
21
21
|
description: 'Delete a document by ID. Skips the inbound-relationship safety check that `safeDelete` performs — use only when you know the document has no inbound references, or when broken relationships are acceptable. Prefer `safeDelete` for general use.\n\n' + `Collections: ${deletableSlugs.join(', ')}`,
|
|
22
22
|
parameters: {
|
|
23
23
|
collection: z.string().describe(`Collection slug. One of: ${deletableSlugs.join(', ')}`),
|
|
24
|
-
|
|
24
|
+
documentId: z.string().describe('Document ID to delete.')
|
|
25
25
|
},
|
|
26
26
|
handler: async (args, req, _extra)=>{
|
|
27
|
-
const { collection,
|
|
27
|
+
const { collection, documentId } = args;
|
|
28
28
|
if (!collectionSchemas.has(collection)) {
|
|
29
29
|
return textResponse(`Error: Unknown collection "${collection}". Valid: ${deletableSlugs.join(', ')}`);
|
|
30
30
|
}
|
|
@@ -32,15 +32,15 @@ import { errorMessage, getDocDisplayName, stampMcpContext, textResponse } from '
|
|
|
32
32
|
try {
|
|
33
33
|
const doc = await req.payload.delete({
|
|
34
34
|
collection: collection,
|
|
35
|
-
id,
|
|
35
|
+
id: documentId,
|
|
36
36
|
req,
|
|
37
37
|
overrideAccess: false,
|
|
38
38
|
user: req.user
|
|
39
39
|
});
|
|
40
|
-
const displayName = getDocDisplayName(doc,
|
|
41
|
-
return textResponse(`Deleted "${displayName}" from ${collection} (ID: ${
|
|
40
|
+
const displayName = getDocDisplayName(doc, documentId);
|
|
41
|
+
return textResponse(`Deleted "${displayName}" from ${collection} (ID: ${documentId}).`);
|
|
42
42
|
} catch (err) {
|
|
43
|
-
return textResponse(`Error deleting ${
|
|
43
|
+
return textResponse(`Error deleting ${documentId} from ${collection}: ${errorMessage(err)}`);
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/tools/delete-document.ts"],"sourcesContent":["import { z } from 'zod'\r\nimport type { PayloadRequest } from 'payload'\r\nimport type { CollectionSchema } from '../types'\r\nimport {\r\n errorMessage,\r\n getDocDisplayName,\r\n stampMcpContext,\r\n textResponse,\r\n} from './_helpers'\r\n\r\ninterface DeleteDocumentArgs {\r\n collection: string\r\n
|
|
1
|
+
{"version":3,"sources":["../../src/tools/delete-document.ts"],"sourcesContent":["import { z } from 'zod'\r\nimport type { PayloadRequest } from 'payload'\r\nimport type { CollectionSchema } from '../types'\r\nimport {\r\n errorMessage,\r\n getDocDisplayName,\r\n stampMcpContext,\r\n textResponse,\r\n} from './_helpers'\r\n\r\ninterface DeleteDocumentArgs {\r\n collection: string\r\n documentId: string\r\n}\r\n\r\n/**\r\n * Polymorphic, fast unsafe-delete tool. Mirrors `safeDelete`'s args but skips\r\n * the relationship-walk — use this when the caller knows the doc has no\r\n * inbound references, or when relationship breakage is acceptable.\r\n *\r\n * `safeDelete` remains the recommended default; this exists for surgical\r\n * deletes inside scripts and AI workflows where a relationship walk would\r\n * be wasteful.\r\n */\r\nexport function createDeleteDocumentTool(collectionSchemas: Map<string, CollectionSchema>) {\r\n const deletableSlugs = [...collectionSchemas.keys()]\r\n\r\n return {\r\n name: 'deleteDocument',\r\n routing: { kind: 'collection', action: 'delete' } as const,\r\n description:\r\n 'Delete a document by ID. Skips the inbound-relationship safety check that `safeDelete` performs — use only when you know the document has no inbound references, or when broken relationships are acceptable. Prefer `safeDelete` for general use.\\n\\n' +\r\n `Collections: ${deletableSlugs.join(', ')}`,\r\n parameters: {\r\n collection: z.string().describe(`Collection slug. One of: ${deletableSlugs.join(', ')}`),\r\n documentId: z.string().describe('Document ID to delete.'),\r\n },\r\n handler: async (args: Record<string, unknown>, req: PayloadRequest, _extra: unknown) => {\r\n const { collection, documentId } = args as unknown as DeleteDocumentArgs\r\n\r\n if (!collectionSchemas.has(collection)) {\r\n return textResponse(\r\n `Error: Unknown collection \"${collection}\". Valid: ${deletableSlugs.join(', ')}`,\r\n )\r\n }\r\n\r\n stampMcpContext(req)\r\n\r\n try {\r\n const doc = await req.payload.delete({\r\n collection: collection as never,\r\n id: documentId,\r\n req,\r\n overrideAccess: false,\r\n user: req.user,\r\n })\r\n\r\n const displayName = getDocDisplayName(doc, documentId)\r\n return textResponse(\r\n `Deleted \"${displayName}\" from ${collection} (ID: ${documentId}).`,\r\n )\r\n } catch (err) {\r\n return textResponse(\r\n `Error deleting ${documentId} from ${collection}: ${errorMessage(err)}`,\r\n )\r\n }\r\n },\r\n }\r\n}\r\n"],"names":["z","errorMessage","getDocDisplayName","stampMcpContext","textResponse","createDeleteDocumentTool","collectionSchemas","deletableSlugs","keys","name","routing","kind","action","description","join","parameters","collection","string","describe","documentId","handler","args","req","_extra","has","doc","payload","delete","id","overrideAccess","user","displayName","err"],"mappings":"AAAA,SAASA,CAAC,QAAQ,MAAK;AAGvB,SACEC,YAAY,EACZC,iBAAiB,EACjBC,eAAe,EACfC,YAAY,QACP,aAAY;AAOnB;;;;;;;;CAQC,GACD,OAAO,SAASC,yBAAyBC,iBAAgD;IACvF,MAAMC,iBAAiB;WAAID,kBAAkBE,IAAI;KAAG;IAEpD,OAAO;QACLC,MAAM;QACNC,SAAS;YAAEC,MAAM;YAAcC,QAAQ;QAAS;QAChDC,aACE,2PACA,CAAC,aAAa,EAAEN,eAAeO,IAAI,CAAC,OAAO;QAC7CC,YAAY;YACVC,YAAYhB,EAAEiB,MAAM,GAAGC,QAAQ,CAAC,CAAC,yBAAyB,EAAEX,eAAeO,IAAI,CAAC,OAAO;YACvFK,YAAYnB,EAAEiB,MAAM,GAAGC,QAAQ,CAAC;QAClC;QACAE,SAAS,OAAOC,MAA+BC,KAAqBC;YAClE,MAAM,EAAEP,UAAU,EAAEG,UAAU,EAAE,GAAGE;YAEnC,IAAI,CAACf,kBAAkBkB,GAAG,CAACR,aAAa;gBACtC,OAAOZ,aACL,CAAC,2BAA2B,EAAEY,WAAW,UAAU,EAAET,eAAeO,IAAI,CAAC,OAAO;YAEpF;YAEAX,gBAAgBmB;YAEhB,IAAI;gBACF,MAAMG,MAAM,MAAMH,IAAII,OAAO,CAACC,MAAM,CAAC;oBACnCX,YAAYA;oBACZY,IAAIT;oBACJG;oBACAO,gBAAgB;oBAChBC,MAAMR,IAAIQ,IAAI;gBAChB;gBAEA,MAAMC,cAAc7B,kBAAkBuB,KAAKN;gBAC3C,OAAOf,aACL,CAAC,SAAS,EAAE2B,YAAY,OAAO,EAAEf,WAAW,MAAM,EAAEG,WAAW,EAAE,CAAC;YAEtE,EAAE,OAAOa,KAAK;gBACZ,OAAO5B,aACL,CAAC,eAAe,EAAEe,WAAW,MAAM,EAAEH,WAAW,EAAE,EAAEf,aAAa+B,MAAM;YAE3E;QACF;IACF;AACF"}
|
|
@@ -8,8 +8,8 @@ import type { CollectionSchema } from '../types';
|
|
|
8
8
|
* `createDocument` / `updateDocument`.
|
|
9
9
|
*
|
|
10
10
|
* Two modes:
|
|
11
|
-
* - `
|
|
12
|
-
* - `
|
|
11
|
+
* - `documentId` set: `payload.findByID` (single doc)
|
|
12
|
+
* - `documentId` unset: `payload.find` with optional JSON-string `where`
|
|
13
13
|
*
|
|
14
14
|
* Draft-enabled collections get preview URLs appended to draft documents
|
|
15
15
|
* via `decorateDraftResponse`.
|
|
@@ -23,7 +23,7 @@ export declare function createFindDocumentTool(collectionSchemas: Map<string, Co
|
|
|
23
23
|
description: string;
|
|
24
24
|
parameters: {
|
|
25
25
|
collection: z.ZodString;
|
|
26
|
-
|
|
26
|
+
documentId: z.ZodOptional<z.ZodString>;
|
|
27
27
|
where: z.ZodOptional<z.ZodString>;
|
|
28
28
|
limit: z.ZodOptional<z.ZodNumber>;
|
|
29
29
|
depth: z.ZodOptional<z.ZodNumber>;
|
|
@@ -7,8 +7,8 @@ import { decorateDraftResponse, errorMessage, jsonResponse, stampMcpContext, tex
|
|
|
7
7
|
* `createDocument` / `updateDocument`.
|
|
8
8
|
*
|
|
9
9
|
* Two modes:
|
|
10
|
-
* - `
|
|
11
|
-
* - `
|
|
10
|
+
* - `documentId` set: `payload.findByID` (single doc)
|
|
11
|
+
* - `documentId` unset: `payload.find` with optional JSON-string `where`
|
|
12
12
|
*
|
|
13
13
|
* Draft-enabled collections get preview URLs appended to draft documents
|
|
14
14
|
* via `decorateDraftResponse`.
|
|
@@ -23,28 +23,28 @@ import { decorateDraftResponse, errorMessage, jsonResponse, stampMcpContext, tex
|
|
|
23
23
|
kind: 'collection',
|
|
24
24
|
action: 'read'
|
|
25
25
|
},
|
|
26
|
-
description: 'Read documents from any collection. Pass `
|
|
26
|
+
description: 'Read documents from any collection. Pass `documentId` for a single document, or omit `documentId` and pass a Payload `where` filter as a JSON string for a list. ' + 'Draft-enabled collections include a preview URL on draft documents when available.\n\n' + 'Available collections:\n' + descriptionLines.join('\n'),
|
|
27
27
|
parameters: {
|
|
28
28
|
collection: z.string().describe(`The collection slug. One of: ${findableSlugs.join(', ')}`),
|
|
29
|
-
|
|
30
|
-
where: z.string().optional().describe('JSON-encoded Payload `where` clause. Examples: \'{"status":{"equals":"published"}}\', ' + '\'{"slug":{"equals":"hello-world"}}\'. Ignored if `
|
|
29
|
+
documentId: z.string().optional().describe('Document ID. When set, returns a single document.'),
|
|
30
|
+
where: z.string().optional().describe('JSON-encoded Payload `where` clause. Examples: \'{"status":{"equals":"published"}}\', ' + '\'{"slug":{"equals":"hello-world"}}\'. Ignored if `documentId` is set.'),
|
|
31
31
|
limit: z.number().int().min(1).max(100).optional().describe('Max results when listing. Default 25.'),
|
|
32
32
|
depth: z.number().int().min(0).max(3).optional().describe('Relationship population depth. Default 1.'),
|
|
33
33
|
draft: z.boolean().optional().describe('When true, returns draft versions of draft-enabled collections. Default false (published only).')
|
|
34
34
|
},
|
|
35
35
|
handler: async (rawArgs, req, _extra)=>{
|
|
36
36
|
const args = rawArgs;
|
|
37
|
-
const { collection,
|
|
37
|
+
const { collection, documentId, where, limit, depth, draft } = args;
|
|
38
38
|
if (!collectionSchemas.has(collection)) {
|
|
39
39
|
return textResponse(`Error: Unknown collection "${collection}". Valid: ${findableSlugs.join(', ')}`);
|
|
40
40
|
}
|
|
41
41
|
stampMcpContext(req);
|
|
42
42
|
const collectionConfig = collectionsBySlug.get(collection);
|
|
43
43
|
try {
|
|
44
|
-
if (
|
|
44
|
+
if (documentId) {
|
|
45
45
|
const doc = await req.payload.findByID({
|
|
46
46
|
collection: collection,
|
|
47
|
-
id,
|
|
47
|
+
id: documentId,
|
|
48
48
|
depth: depth ?? 1,
|
|
49
49
|
draft: draft ?? false,
|
|
50
50
|
req,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/tools/find-document.ts"],"sourcesContent":["import { z } from 'zod'\r\nimport type { CollectionConfig, PayloadRequest } from 'payload'\r\nimport type { CollectionSchema } from '../types'\r\nimport {\r\n decorateDraftResponse,\r\n errorMessage,\r\n jsonResponse,\r\n stampMcpContext,\r\n textResponse,\r\n} from './_helpers'\r\n\r\ninterface FindDocumentArgs {\r\n collection: string\r\n
|
|
1
|
+
{"version":3,"sources":["../../src/tools/find-document.ts"],"sourcesContent":["import { z } from 'zod'\r\nimport type { CollectionConfig, PayloadRequest } from 'payload'\r\nimport type { CollectionSchema } from '../types'\r\nimport {\r\n decorateDraftResponse,\r\n errorMessage,\r\n jsonResponse,\r\n stampMcpContext,\r\n textResponse,\r\n} from './_helpers'\r\n\r\ninterface FindDocumentArgs {\r\n collection: string\r\n documentId?: string\r\n where?: string\r\n limit?: number\r\n depth?: number\r\n draft?: boolean\r\n}\r\n\r\n/**\r\n * Polymorphic replacement for the upstream plugin's per-collection\r\n * `find<Resource>` tools. Takes the collection as an arg rather than\r\n * generating one tool per collection — same authoring shape as\r\n * `createDocument` / `updateDocument`.\r\n *\r\n * Two modes:\r\n * - `documentId` set: `payload.findByID` (single doc)\r\n * - `documentId` unset: `payload.find` with optional JSON-string `where`\r\n *\r\n * Draft-enabled collections get preview URLs appended to draft documents\r\n * via `decorateDraftResponse`.\r\n */\r\nexport function createFindDocumentTool(\r\n collectionSchemas: Map<string, CollectionSchema>,\r\n draftCollections: Set<string>,\r\n collectionsBySlug: Map<string, CollectionConfig>,\r\n previewSiteUrl: string | undefined,\r\n previewDisabled = false,\r\n) {\r\n const findableSlugs = [...collectionSchemas.keys()]\r\n const descriptionLines = findableSlugs.map(\r\n (slug) =>\r\n ` - \"${slug}\"${draftCollections.has(slug) ? ' (draft-enabled)' : ''}`,\r\n )\r\n\r\n return {\r\n name: 'findDocument',\r\n routing: { kind: 'collection', action: 'read' } as const,\r\n description:\r\n 'Read documents from any collection. Pass `documentId` for a single document, or omit `documentId` and pass a Payload `where` filter as a JSON string for a list. ' +\r\n 'Draft-enabled collections include a preview URL on draft documents when available.\\n\\n' +\r\n 'Available collections:\\n' +\r\n descriptionLines.join('\\n'),\r\n parameters: {\r\n collection: z\r\n .string()\r\n .describe(`The collection slug. One of: ${findableSlugs.join(', ')}`),\r\n documentId: z\r\n .string()\r\n .optional()\r\n .describe('Document ID. When set, returns a single document.'),\r\n where: z\r\n .string()\r\n .optional()\r\n .describe(\r\n 'JSON-encoded Payload `where` clause. Examples: \\'{\"status\":{\"equals\":\"published\"}}\\', ' +\r\n '\\'{\"slug\":{\"equals\":\"hello-world\"}}\\'. Ignored if `documentId` is set.',\r\n ),\r\n limit: z\r\n .number()\r\n .int()\r\n .min(1)\r\n .max(100)\r\n .optional()\r\n .describe('Max results when listing. Default 25.'),\r\n depth: z\r\n .number()\r\n .int()\r\n .min(0)\r\n .max(3)\r\n .optional()\r\n .describe('Relationship population depth. Default 1.'),\r\n draft: z\r\n .boolean()\r\n .optional()\r\n .describe(\r\n 'When true, returns draft versions of draft-enabled collections. Default false (published only).',\r\n ),\r\n },\r\n handler: async (\r\n rawArgs: Record<string, unknown>,\r\n req: PayloadRequest,\r\n _extra: unknown,\r\n ) => {\r\n const args = rawArgs as unknown as FindDocumentArgs\r\n const { collection, documentId, where, limit, depth, draft } = args\r\n\r\n if (!collectionSchemas.has(collection)) {\r\n return textResponse(\r\n `Error: Unknown collection \"${collection}\". Valid: ${findableSlugs.join(', ')}`,\r\n )\r\n }\r\n\r\n stampMcpContext(req)\r\n const collectionConfig = collectionsBySlug.get(collection)\r\n\r\n try {\r\n if (documentId) {\r\n const doc = await req.payload.findByID({\r\n collection: collection as never,\r\n id: documentId,\r\n depth: depth ?? 1,\r\n draft: draft ?? false,\r\n req,\r\n overrideAccess: false,\r\n user: req.user,\r\n })\r\n const base = jsonResponse(doc)\r\n if (previewDisabled) return base\r\n return await decorateDraftResponse(\r\n base,\r\n doc as Record<string, unknown>,\r\n collectionConfig,\r\n req,\r\n previewSiteUrl,\r\n )\r\n }\r\n\r\n let parsedWhere: unknown\r\n if (where && where.trim().length > 0) {\r\n try {\r\n parsedWhere = JSON.parse(where)\r\n } catch (err) {\r\n return textResponse(\r\n `Error: \\`where\\` must be a valid JSON string. ${errorMessage(err)}`,\r\n )\r\n }\r\n }\r\n\r\n const result = await req.payload.find({\r\n collection: collection as never,\r\n where: parsedWhere as never,\r\n depth: depth ?? 1,\r\n limit: limit ?? 25,\r\n draft: draft ?? false,\r\n req,\r\n overrideAccess: false,\r\n user: req.user,\r\n pagination: false,\r\n })\r\n\r\n const base = jsonResponse({\r\n totalDocs: (result as { totalDocs?: number }).totalDocs ?? result.docs.length,\r\n docs: result.docs,\r\n })\r\n\r\n if (previewDisabled || !collectionConfig || !draftCollections.has(collection)) return base\r\n\r\n // Decorate any draft docs in the page with preview URLs.\r\n let decorated = base\r\n for (const doc of result.docs as Array<Record<string, unknown>>) {\r\n if (doc._status === 'draft') {\r\n decorated = await decorateDraftResponse(\r\n decorated,\r\n doc,\r\n collectionConfig,\r\n req,\r\n previewSiteUrl,\r\n )\r\n }\r\n }\r\n return decorated\r\n } catch (err) {\r\n return textResponse(\r\n `Error reading from ${collection}: ${errorMessage(err)}`,\r\n )\r\n }\r\n },\r\n }\r\n}\r\n"],"names":["z","decorateDraftResponse","errorMessage","jsonResponse","stampMcpContext","textResponse","createFindDocumentTool","collectionSchemas","draftCollections","collectionsBySlug","previewSiteUrl","previewDisabled","findableSlugs","keys","descriptionLines","map","slug","has","name","routing","kind","action","description","join","parameters","collection","string","describe","documentId","optional","where","limit","number","int","min","max","depth","draft","boolean","handler","rawArgs","req","_extra","args","collectionConfig","get","doc","payload","findByID","id","overrideAccess","user","base","parsedWhere","trim","length","JSON","parse","err","result","find","pagination","totalDocs","docs","decorated","_status"],"mappings":"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"}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ToolFactoryOutput } from './registry';
|
|
1
2
|
/**
|
|
2
3
|
* payload-mcp-toolkit configuration.
|
|
3
4
|
*
|
|
@@ -62,6 +63,43 @@ export interface ContentToolkitOptions {
|
|
|
62
63
|
/** Media collection slug (default: 'media') */
|
|
63
64
|
collectionSlug?: string;
|
|
64
65
|
};
|
|
66
|
+
/**
|
|
67
|
+
* Extra tools to register alongside the built-in ones.
|
|
68
|
+
*
|
|
69
|
+
* Each entry is a plain `ToolFactoryOutput`: a name, a description, a Zod
|
|
70
|
+
* shape (or `z.object({...})`), a handler, and a `routing` tag. Custom tools
|
|
71
|
+
* go through the same wrapper as the built-ins — scope checks, `req.context
|
|
72
|
+
* .source = 'mcp'` stamping, and the audit log all apply — and their names
|
|
73
|
+
* appear in the API-key scope dropdowns.
|
|
74
|
+
*
|
|
75
|
+
* The handler receives the live `PayloadRequest`, so a tool that needs the
|
|
76
|
+
* authenticated user or the Payload instance reads them off `req` per call
|
|
77
|
+
* rather than closing over them at boot.
|
|
78
|
+
*
|
|
79
|
+
* `routing` decides which scope axis gates the tool. Use
|
|
80
|
+
* `{kind: 'collection', action: 'read'}` for a tool whose args carry a
|
|
81
|
+
* `collection` (or `slug`) key — the registry reads that key to find the
|
|
82
|
+
* target for the scope check.
|
|
83
|
+
*
|
|
84
|
+
* A custom tool may not reuse a built-in tool's name; the plugin throws at
|
|
85
|
+
* boot if one does.
|
|
86
|
+
*
|
|
87
|
+
* ```ts
|
|
88
|
+
* mcpToolkitPlugin({
|
|
89
|
+
* customTools: [{
|
|
90
|
+
* name: 'countActiveMembers',
|
|
91
|
+
* description: 'Number of members with an active membership.',
|
|
92
|
+
* parameters: { since: z.string().optional() },
|
|
93
|
+
* routing: { kind: 'collection', action: 'read' },
|
|
94
|
+
* handler: async (args, req) => {
|
|
95
|
+
* const { totalDocs } = await req.payload.count({ collection: 'memberships' })
|
|
96
|
+
* return { content: [{ type: 'text', text: String(totalDocs) }] }
|
|
97
|
+
* },
|
|
98
|
+
* }],
|
|
99
|
+
* })
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
customTools?: ToolFactoryOutput[];
|
|
65
103
|
/**
|
|
66
104
|
* MCP transport / auth configuration. Mostly safe to leave unset;
|
|
67
105
|
* defaults to no-CORS server-to-server use only.
|
package/dist/types.js
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* - `collections` / `globals` are whitelists when present: a resource not
|
|
10
|
-
* listed there is denied for this key.
|
|
11
|
-
* - `tools.allow` / `tools.deny` are per-tool overrides that take precedence
|
|
12
|
-
* over the preset / resource maps.
|
|
1
|
+
/**
|
|
2
|
+
* Runtime scope shape consumed by `registry.assertScopeAllows`.
|
|
3
|
+
*
|
|
4
|
+
* - `collections` / `globals` are whitelists when present: a resource not
|
|
5
|
+
* listed there is denied for this key.
|
|
6
|
+
* - `tools.allow` / `tools.deny` are per-tool overrides that take precedence
|
|
7
|
+
* over the preset / resource maps.
|
|
13
8
|
*/ export { };
|
|
14
9
|
|
|
15
10
|
//# sourceMappingURL=types.js.map
|
package/dist/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/types.ts"],"sourcesContent":["
|
|
1
|
+
{"version":3,"sources":["../src/types.ts"],"sourcesContent":["import type { ToolFactoryOutput } from './registry'\n\n/**\n * payload-mcp-toolkit configuration.\n *\n * The plugin works with zero options — every field below is an escape hatch\n * for the cases where Payload's own config doesn't carry enough signal.\n */\nexport interface ContentToolkitOptions {\n /**\n * Preview URL behavior. The toolkit reads `collection.admin.livePreview.url`\n * (or `collection.admin.preview` as a fallback) when generating preview links\n * for draft documents. Provide this object only to override what Payload\n * already knows.\n */\n preview?: {\n /**\n * Absolute base URL prepended to relative preview paths. Defaults to\n * `incomingConfig.serverURL`, then `process.env.NEXT_PUBLIC_SERVER_URL`,\n * then `process.env.SITE_URL`. If none of those resolve and your preview\n * URL function returns a relative path, no preview URL is appended.\n */\n siteUrl?: string\n\n /**\n * Disable preview URL injection entirely.\n */\n disabled?: boolean\n }\n\n /**\n * Per-collection draft behavior overrides. The default behavior is inferred\n * from each collection's `versions.drafts` setting:\n * - drafts enabled → `'always-draft'` (raw `update` is locked; clients go\n * through `publishDraft` / `patchLayout` / `updateDocument` which preserve\n * draft semantics)\n * - drafts disabled → `'always-publish'`\n *\n * Override per slug only if you need to allow raw publish on a draftable\n * collection.\n */\n draftBehavior?: Record<string, 'always-draft' | 'always-publish'>\n\n /**\n * Override the auth collection used for API key linkage. By default the\n * toolkit scans `incomingConfig.collections` for the first collection with\n * `auth: true`, preferring one named `'users'`.\n */\n userCollection?: string\n\n /**\n * Hide collections or globals from the MCP surface. Useful for internal\n * bookkeeping collections that should not be exposed to AI clients.\n */\n exclude?: {\n collections?: string[]\n globals?: string[]\n }\n\n /**\n * Site-specific domain prompts that teach the AI business vocabulary.\n * Merged with the auto-generated prompts.\n */\n domainPrompts?: DomainPrompt[]\n\n /** Media upload configuration */\n mediaUpload?: {\n /** Maximum file size in bytes (default: 10MB) */\n maxFileSize?: number\n /** Media collection slug (default: 'media') */\n collectionSlug?: string\n }\n\n /**\n * Extra tools to register alongside the built-in ones.\n *\n * Each entry is a plain `ToolFactoryOutput`: a name, a description, a Zod\n * shape (or `z.object({...})`), a handler, and a `routing` tag. Custom tools\n * go through the same wrapper as the built-ins — scope checks, `req.context\n * .source = 'mcp'` stamping, and the audit log all apply — and their names\n * appear in the API-key scope dropdowns.\n *\n * The handler receives the live `PayloadRequest`, so a tool that needs the\n * authenticated user or the Payload instance reads them off `req` per call\n * rather than closing over them at boot.\n *\n * `routing` decides which scope axis gates the tool. Use\n * `{kind: 'collection', action: 'read'}` for a tool whose args carry a\n * `collection` (or `slug`) key — the registry reads that key to find the\n * target for the scope check.\n *\n * A custom tool may not reuse a built-in tool's name; the plugin throws at\n * boot if one does.\n *\n * ```ts\n * mcpToolkitPlugin({\n * customTools: [{\n * name: 'countActiveMembers',\n * description: 'Number of members with an active membership.',\n * parameters: { since: z.string().optional() },\n * routing: { kind: 'collection', action: 'read' },\n * handler: async (args, req) => {\n * const { totalDocs } = await req.payload.count({ collection: 'memberships' })\n * return { content: [{ type: 'text', text: String(totalDocs) }] }\n * },\n * }],\n * })\n * ```\n */\n customTools?: ToolFactoryOutput[]\n\n /**\n * MCP transport / auth configuration. Mostly safe to leave unset;\n * defaults to no-CORS server-to-server use only.\n */\n auth?: {\n /**\n * Origins permitted on the `Origin` header. Empty / unset means\n * server-to-server callers only (no browser-based MCP clients).\n * `*` is intentionally not honoured.\n */\n allowedOrigins?: string[]\n }\n\n /**\n * Override API-key collection settings. Slug defaults to\n * `payload-mcp-api-keys` for zero-touch upgrade compatibility with\n * `@payloadcms/plugin-mcp` v0.3.x rows.\n */\n apiKeyCollection?: {\n slug?: string\n /**\n * Override the user collection that API keys link to. By default\n * the toolkit reuses the same `userCollection` resolution as elsewhere\n * (`options.userCollection`, then `incomingConfig.admin.user`).\n */\n userCollection?: string\n }\n}\n\n/** A domain prompt that teaches the AI site-specific vocabulary */\nexport interface DomainPrompt {\n /** Unique name for the prompt */\n name: string\n /** Display title */\n title: string\n /** Description of what this prompt teaches */\n description: string\n /** The prompt content */\n content: string\n}\n\n/** Introspected field metadata */\nexport interface FieldSchema {\n name: string\n type: string\n required?: boolean\n hasMany?: boolean\n relationTo?: string | string[]\n options?: Array<{ label: string; value: string }>\n fields?: FieldSchema[]\n maxRows?: number\n}\n\n/** Introspected collection metadata */\nexport interface CollectionSchema {\n slug: string\n fields: FieldSchema[]\n hasDrafts: boolean\n hasLivePreview: boolean\n relationships: Array<{ fieldName: string; relationTo: string | string[]; hasMany: boolean }>\n searchableFields: string[]\n}\n\n/** Introspected global metadata. Globals are singletons — no relationships or searchable-fields graph. */\nexport interface GlobalSchema {\n slug: string\n fields: FieldSchema[]\n hasDrafts: boolean\n hasLivePreview: boolean\n}\n\n/**\n * One block in the catalog. Flat — no section/leaf distinction. Whether a\n * block can nest other blocks is encoded in the `BlockNestingMap` keyed by\n * the path to its `blocks` field.\n */\nexport interface BlockSchema {\n slug: string\n fields: FieldSchema[]\n}\n\n/**\n * Flat catalog of every block referenced by the schema.\n */\nexport interface BlockCatalog {\n blocks: BlockSchema[]\n}\n\n/**\n * One entry per `blocks`-typed field anywhere in the schema.\n *\n * `path` is `<owner>.<dottedFieldPath>` where owner is the collection or\n * block slug that contains the field. Values list the slugs that field\n * accepts. The AI uses this to compose blocks at any nesting depth without\n * us pre-classifying anything as a \"section\" or \"leaf\".\n */\nexport interface BlockNestingEdge {\n /** Owner of the blocks field — a collection slug, a block slug, or a global slug. */\n owner: string\n /** Whether the owner is a collection, a block, or a global */\n ownerType: 'collection' | 'block' | 'global'\n /** Dotted path to the blocks field within the owner (e.g. `layout`, `hero.content`) */\n fieldPath: string\n /** Block slugs that this field accepts */\n acceptedBlockSlugs: string[]\n /** Optional row cap from the field config */\n maxRows?: number\n}\n\n/** Map of every blocks-field in the schema to the slugs it accepts */\nexport type BlockNestingMap = BlockNestingEdge[]\n\n/** Relationship edge in the collection graph */\nexport interface RelationshipEdge {\n fromCollection: string\n fieldName: string\n toCollection: string | string[]\n hasMany: boolean\n}\n\n// ─── Scope shapes ─────────────────────────────────────────────────────\n//\n// Canonical scope types live here so the auth strategy, registry, and admin\n// API-keys collection all import from the same surface. Globals support\n// only `read` / `update` — they don't have `create` / `delete` semantics.\n\nexport type CollectionAction = 'read' | 'create' | 'update' | 'delete'\nexport type GlobalAction = 'read' | 'update'\nexport type ScopePreset = 'read-only' | 'editor' | 'admin'\n\n/**\n * Runtime scope shape consumed by `registry.assertScopeAllows`.\n *\n * - `collections` / `globals` are whitelists when present: a resource not\n * listed there is denied for this key.\n * - `tools.allow` / `tools.deny` are per-tool overrides that take precedence\n * over the preset / resource maps.\n */\nexport interface KeyScopes {\n preset?: ScopePreset\n collections?: Record<string, CollectionAction[]>\n globals?: Record<string, GlobalAction[]>\n tools?: { allow?: string[]; deny?: string[] }\n}\n"],"names":[],"mappings":"AAiPA;;;;;;;CAOC,GACD,WAKC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payload-mcp-toolkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Standalone schema-aware MCP plugin for Payload CMS v3 — owns the /api/mcp endpoint, scoped API keys, draft workflow, and AI-friendly tools so non-technical editors can manage content via AI chat.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "jon8800",
|
|
@@ -62,12 +62,12 @@
|
|
|
62
62
|
"test:watch": "vitest"
|
|
63
63
|
},
|
|
64
64
|
"dependencies": {
|
|
65
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
65
|
+
"@modelcontextprotocol/sdk": "^1.23.0",
|
|
66
66
|
"mcp-handler": "^1.1.0"
|
|
67
67
|
},
|
|
68
68
|
"peerDependencies": {
|
|
69
69
|
"payload": "^3.0.0",
|
|
70
|
-
"zod": "^3.
|
|
70
|
+
"zod": "^3.25 || ^4"
|
|
71
71
|
},
|
|
72
72
|
"devDependencies": {
|
|
73
73
|
"@payloadcms/db-sqlite": "3.82.1",
|
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
},
|
|
95
95
|
"engines": {
|
|
96
96
|
"node": "^18.20.2 || >=20.9.0",
|
|
97
|
-
"pnpm": "
|
|
97
|
+
"pnpm": ">=9"
|
|
98
98
|
},
|
|
99
99
|
"pnpm": {
|
|
100
100
|
"onlyBuiltDependencies": [
|