payload-mcp-toolkit 0.8.0 β†’ 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/README.md +100 -237
  2. package/dist/api-keys.js +16 -0
  3. package/dist/api-keys.js.map +1 -1
  4. package/dist/auth-strategy.js +49 -49
  5. package/dist/auth-strategy.js.map +1 -1
  6. package/dist/components/AgentConnectPill.d.ts +3 -0
  7. package/dist/components/AgentConnectPill.js +121 -0
  8. package/dist/components/AgentConnectPill.js.map +1 -0
  9. package/dist/components/CollectionScopesMatrix.js.map +1 -1
  10. package/dist/components/ConsentPermissions.d.ts +10 -0
  11. package/dist/components/ConsentPermissions.js +165 -0
  12. package/dist/components/ConsentPermissions.js.map +1 -0
  13. package/dist/components/GlobalScopesMatrix.js.map +1 -1
  14. package/dist/components/OAuthView.d.ts +4 -0
  15. package/dist/components/OAuthView.js +269 -0
  16. package/dist/components/OAuthView.js.map +1 -0
  17. package/dist/components/ScopesTable.d.ts +28 -0
  18. package/dist/components/ScopesTable.js +165 -130
  19. package/dist/components/ScopesTable.js.map +1 -1
  20. package/dist/components/agentInstructions.d.ts +2 -0
  21. package/dist/components/agentInstructions.js +34 -0
  22. package/dist/components/agentInstructions.js.map +1 -0
  23. package/dist/components/index.d.ts +2 -0
  24. package/dist/components/index.js +2 -0
  25. package/dist/components/index.js.map +1 -1
  26. package/dist/components/oauth.css +111 -0
  27. package/dist/conflict-detection.js +13 -13
  28. package/dist/conflict-detection.js.map +1 -1
  29. package/dist/draft-workflow.js +26 -26
  30. package/dist/draft-workflow.js.map +1 -1
  31. package/dist/endpoint.d.ts +5 -0
  32. package/dist/endpoint.js +21 -13
  33. package/dist/endpoint.js.map +1 -1
  34. package/dist/hash.js +15 -15
  35. package/dist/hash.js.map +1 -1
  36. package/dist/index.d.ts +2 -0
  37. package/dist/index.js +67 -3
  38. package/dist/index.js.map +1 -1
  39. package/dist/introspection.js +47 -47
  40. package/dist/introspection.js.map +1 -1
  41. package/dist/oauth-permissions.d.ts +52 -0
  42. package/dist/oauth-permissions.js +100 -0
  43. package/dist/oauth-permissions.js.map +1 -0
  44. package/dist/oauth-store.d.ts +19 -0
  45. package/dist/oauth-store.js +122 -0
  46. package/dist/oauth-store.js.map +1 -0
  47. package/dist/oauth.d.ts +19 -0
  48. package/dist/oauth.js +523 -0
  49. package/dist/oauth.js.map +1 -0
  50. package/dist/prompts.js +5 -5
  51. package/dist/prompts.js.map +1 -1
  52. package/dist/registry.js +7 -1
  53. package/dist/registry.js.map +1 -1
  54. package/dist/resources.js +10 -10
  55. package/dist/resources.js.map +1 -1
  56. package/dist/scope/audit-log.js +9 -9
  57. package/dist/scope/audit-log.js.map +1 -1
  58. package/dist/scope/policy.js +7 -7
  59. package/dist/scope/policy.js.map +1 -1
  60. package/dist/tools/_helpers.d.ts +10 -0
  61. package/dist/tools/_helpers.js +53 -41
  62. package/dist/tools/_helpers.js.map +1 -1
  63. package/dist/tools/_layout-helpers.js +33 -33
  64. package/dist/tools/_layout-helpers.js.map +1 -1
  65. package/dist/tools/create-document.js +20 -19
  66. package/dist/tools/create-document.js.map +1 -1
  67. package/dist/tools/delete-document.js +10 -9
  68. package/dist/tools/delete-document.js.map +1 -1
  69. package/dist/tools/find-document.js +18 -15
  70. package/dist/tools/find-document.js.map +1 -1
  71. package/dist/tools/find-global.js +13 -13
  72. package/dist/tools/find-global.js.map +1 -1
  73. package/dist/tools/global-versions.js +8 -7
  74. package/dist/tools/global-versions.js.map +1 -1
  75. package/dist/tools/patch-global-layout.js +9 -9
  76. package/dist/tools/patch-global-layout.js.map +1 -1
  77. package/dist/tools/patch-layout.js +15 -14
  78. package/dist/tools/patch-layout.js.map +1 -1
  79. package/dist/tools/publish-draft.js +2 -1
  80. package/dist/tools/publish-draft.js.map +1 -1
  81. package/dist/tools/publish-global-draft.js +4 -4
  82. package/dist/tools/publish-global-draft.js.map +1 -1
  83. package/dist/tools/resolve-reference.js.map +1 -1
  84. package/dist/tools/safe-delete.js +14 -14
  85. package/dist/tools/safe-delete.js.map +1 -1
  86. package/dist/tools/schedule-publish.js +21 -20
  87. package/dist/tools/schedule-publish.js.map +1 -1
  88. package/dist/tools/search-content.js +12 -12
  89. package/dist/tools/search-content.js.map +1 -1
  90. package/dist/tools/update-document.js +6 -5
  91. package/dist/tools/update-document.js.map +1 -1
  92. package/dist/tools/update-global.js +9 -9
  93. package/dist/tools/update-global.js.map +1 -1
  94. package/dist/tools/upload-media.js +2 -1
  95. package/dist/tools/upload-media.js.map +1 -1
  96. package/dist/tools/versions.js +10 -8
  97. package/dist/tools/versions.js.map +1 -1
  98. package/dist/types.d.ts +3 -0
  99. package/dist/types.js.map +1 -1
  100. package/docs/oauth.md +96 -0
  101. package/package.json +7 -2
package/dist/types.js.map CHANGED
@@ -1 +1 @@
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"}
1
+ {"version":3,"sources":["../src/types.ts"],"sourcesContent":["import type { ToolFactoryOutput } from './registry'\nimport type { OAuthOptions } from './oauth'\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 /** Optional website-account authorization for remote MCP connectors. */\n oauth?: OAuthOptions\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":"AAoPA;;;;;;;CAOC,GACD,WAKC"}
package/docs/oauth.md ADDED
@@ -0,0 +1,96 @@
1
+ # Connect Claude with a website account
2
+
3
+ OAuth is optional. Existing API keys continue to work. Users add the connector in Claude, sign into your website, and approve access.
4
+
5
+ ## Host setup
6
+
7
+ ```ts
8
+ mcpToolkitPlugin({
9
+ oauth: {
10
+ // Required: use your own staff-access policy. Do not allow every member.
11
+ canAuthorize: ({ user }) => user?.role === 'super-admin',
12
+ access: 'editor', // default: 'read-only'
13
+ },
14
+ })
15
+ ```
16
+
17
+ Set Payload `serverURL` to your public HTTPS origin. HTTP loopback URLs work for local checks. The API route must remain `/api`.
18
+
19
+ By default, sign-in uses Payload's admin login. For a custom website login:
20
+
21
+ ```ts
22
+ loginURL: (returnTo) => `/login?redirect=${encodeURIComponent(returnTo)}`
23
+ ```
24
+
25
+ Your login page must return to the supplied same-origin path after sign-in. The account must belong to the plugin's configured user collection. The policy runs again when users approve access, refresh tokens, or make MCP requests.
26
+
27
+ Payload custom endpoints cannot serve root discovery URLs. Add these rewrites to your host's `next.config` alongside its existing rewrites:
28
+
29
+ ```js
30
+ async rewrites() {
31
+ return [
32
+ { source: '/.well-known/oauth-authorization-server', destination: '/api/mcp/oauth/metadata' },
33
+ { source: '/.well-known/oauth-protected-resource', destination: '/api/mcp/oauth/resource' },
34
+ { source: '/.well-known/oauth-protected-resource/api/mcp', destination: '/api/mcp/oauth/resource' },
35
+ ]
36
+ }
37
+ ```
38
+
39
+ Enabling OAuth adds the private `payload-mcp-oauth` collection. Generate and review a Payload migration in the host application before production deployment. Keep its unique `key` constraint: it prevents concurrent code redemption and refresh replay. Do not run migrations against a local database configured with `push: true`.
40
+
41
+ The plugin adds a compact "Connect your AI agent" prompt to the admin sidebar (Payload's `afterNavLinks` slot). It shows only to accounts that pass `canAuthorize`. One click copies the agent setup prompt, hovering explains that, and users can dismiss it (stored in their Payload preferences). If your host replaces Payload's `Nav`, render `<AgentConnectPill />` from `payload-mcp-toolkit/client` where you want it, and link to `/admin/mcp-connections` so the setup page stays reachable after a dismissal. Regenerate the host's Payload import map after upgrading. Consent and connection management use custom Payload admin views at `/admin/mcp-authorize` and `/admin/mcp-connections` (respecting your configured admin route). They inherit the admin theme, fonts and CSS variables and use Payload UI buttons and banners. Users need access to the Payload admin, in addition to passing `canAuthorize`.
42
+
43
+ The setup page shows the connector URL and agent instructions, each with a Copy button. Users paste the instructions into their AI app, for example as Claude project instructions. The instructions tell the assistant how to connect, what the site's access level allows, and to ask before each change. Accounts that fail `canAuthorize` see a notice instead of the setup steps. Users can also disconnect their grants there. You can link to this page from your own dashboard too. Pasted instructions do not install a connector: the user still adds the connector and approves access.
44
+
45
+ ChatGPT connects through the same flow. The server returns `iss` in every authorization redirect (RFC 9207) and advertises `authorization_response_iss_parameter_supported`. ChatGPT then uses its fixed callback, `https://chatgpt.com/connector_platform_oauth_redirect`. Without this, ChatGPT uses a different callback for every connector, which an exact allowlist cannot accept. ChatGPT custom connectors work on the web only and need Developer mode. OpenAI's docs differ on which plans allow write actions.
46
+
47
+ Keep the consent view protected against framing. Add these headers to the host's existing Next.js headers configuration, using your configured admin path:
48
+
49
+ ```js
50
+ async headers() {
51
+ return [{ source: '/admin/mcp-:view', headers: [
52
+ { key: 'Content-Security-Policy', value: "frame-ancestors 'none'" },
53
+ { key: 'Referrer-Policy', value: 'same-origin' },
54
+ ] }]
55
+ }
56
+ ```
57
+
58
+ The authenticated data endpoints return JSON only with `Accept: application/json` and never cache consent data. Browser navigation redirects into the admin view. Approval and revocation still use the server's signed consent and origin checks.
59
+
60
+ If your existing policy restricts `form-action`, allow your trusted OAuth callback origins too. Browsers can enforce that directive on the approval redirect back to Claude or ChatGPT.
61
+
62
+ ## Claude and ChatGPT trial
63
+
64
+ 1. Deploy the host with the plugin, migration, and discovery rewrites.
65
+ 2. Open `https://YOUR-SITE/.well-known/oauth-authorization-server`. Confirm it returns JSON with your HTTPS issuer.
66
+ 3. In Claude Desktop, open **Settings β†’ Connectors β†’ Add custom connector**.
67
+ 4. Enter `https://YOUR-SITE/api/mcp`. No API key or client secret is needed.
68
+ 5. Sign into your website. Check the account and permissions shown. Click **Allow access**.
69
+ 6. Ask: β€œList the content I can access. Do not change anything yet.”
70
+ 7. Open `/api/mcp/oauth/connections`, disconnect Claude, and confirm another tool request requires authorization again.
71
+
72
+ For ChatGPT, open chatgpt.com in a browser and turn on **Developer mode** in Settings (in a Business, Enterprise or Edu workspace, an admin must allow it first). Create an app with the URL from step 4 and OAuth authentication, leaving any client ID and secret empty. Then continue from step 5. ChatGPT asks before each write action.
73
+
74
+ Both connectors run through their vendor's cloud. A localhost URL alone is insufficient for this trial. Use a deployed HTTPS test host. The dev app demonstrates the routes and sign-in flow locally.
75
+
76
+ The default trusted callbacks are `https://claude.ai/api/mcp/auth_callback`, `https://claude.com/api/mcp/auth_callback` and `https://chatgpt.com/connector_platform_oauth_redirect`. Dynamic registration accepts one exact trusted callback per public client. Client IDs are stable per callback and installation, with no client secret. To support another client, provide its exact callback in `oauth.redirectURIs`. Wildcards and arbitrary client-metadata URL fetching are not supported.
77
+
78
+ ## Permissions and lifecycle
79
+
80
+ - `mcp:read` uses the existing read-only preset.
81
+ - `mcp:write` is available when `access: 'editor'`. It permits collection reads, creates and updates. Globals remain read-only. Delete remains denied. The MCP `401` challenge then asks for `mcp:read mcp:write`, so clients request write access on first connection.
82
+ - Requested scopes the site does not grant (for example `offline_access`, `openid`, or `mcp:write` on a read-only site) are dropped, never granted. The consent screen and token response show the scope actually granted.
83
+ - The consent screen lets the user narrow the connection, like an API key's Custom preset. **Allow creating and updating entries** switches between read-only and editor access. **Customize access** opens a collection matrix (Read, Create, Update), a global matrix (Read) and a tool list. Everything starts selected, so one click on **Allow access** grants everything the request and the site allow.
84
+ - The choice is stored on the grant and applied on every request through the same scope checker as API keys. It can only narrow access: the server drops collections, globals, actions and tools outside the site's `access` level and the plugin's exposed collections. Search, reference and upload tools work only when every collection and global is selected. The connections page shows each connection's access, for example "Read only Β· 3 of 12 collections Β· all globals Β· 7 of 9 tools". To change it, disconnect and connect again.
85
+ - If the site's `access` level or a token's scope shrinks later, stored choices shrink with it on the next request.
86
+ - Any connection or API key limited to some collections or globals gets linked entries as IDs only (relationship depth 0). `findDocument` also refuses filters with a dotted path, so entries from unchecked collections do not appear through relationships.
87
+ - Existing Payload access controls and plugin exclusions still apply. Custom tools must enforce Payload access with `overrideAccess: false`.
88
+ - Authorization codes expire after five minutes and require PKCE S256. Approval forms expire after ten minutes.
89
+ - Access tokens expire after one hour. Grants and rotating refresh tokens expire after 30 days. Reusing a consumed code or refresh token revokes its grant.
90
+ - OAuth bearer tokens authenticate only the MCP endpoint. They cannot authenticate Payload REST requests or approve another grant.
91
+ - Only token hashes are stored. Website passwords remain in your existing sign-in flow.
92
+ - Removing user eligibility or deleting the user blocks further MCP requests. Disconnecting blocks all tokens for that grant.
93
+ - Run `pruneOAuthRecords(payload)` periodically from your existing scheduler. It deletes expired records. Keep unexpired consumption claims intact.
94
+ - Apply request/body limits and rate limits at your existing reverse proxy, especially for sign-in and OAuth endpoints. Do not log token or consent bodies.
95
+
96
+ Sources: [MCP authorization](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization), [Claude connector setup](https://support.claude.com/en/articles/11175166-get-started-with-custom-connectors-using-remote-mcp), [Claude connector implementation](https://support.anthropic.com/en/articles/11503834-building-custom-connectors-via-remote-mcp-servers).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "payload-mcp-toolkit",
3
- "version": "0.8.0",
3
+ "version": "0.9.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",
@@ -39,12 +39,16 @@
39
39
  "types": "./dist/index.d.ts",
40
40
  "files": [
41
41
  "dist",
42
+ "docs/oauth.md",
42
43
  "!dist/__tests__",
43
44
  "!dist/**/*.test.js",
44
45
  "!dist/**/*.test.js.map",
45
46
  "!dist/**/*.test.d.ts"
46
47
  ],
47
- "sideEffects": false,
48
+ "sideEffects": [
49
+ "**/*.css",
50
+ "**/*.scss"
51
+ ],
48
52
  "scripts": {
49
53
  "build": "pnpm copyfiles && pnpm build:types && pnpm build:swc",
50
54
  "build:swc": "swc ./src -d ./dist --config-file .swcrc --strip-leading-paths",
@@ -66,6 +70,7 @@
66
70
  "mcp-handler": "^1.1.0"
67
71
  },
68
72
  "peerDependencies": {
73
+ "@payloadcms/ui": "^3.0.0",
69
74
  "payload": "^3.0.0",
70
75
  "zod": "^3.25 || ^4"
71
76
  },