[MCP] Server configuration management: definitions, CRUD, tools & access model - #2027
Draft
wwidergoldpimcore wants to merge 15 commits into
Draft
Conversation
First step of #1309: a first-class, config-managed "MCP server" model, backend only (no UI, no endpoint yet). - McpServerDefinition / McpServerAccess value objects, with fromArray()/toArray() as the single (de)serialization boundary so the shipped symfony-config seed and the settings-store JSON map onto one shape. Access mirrors the SavedSearch/Grid sharing model (owner + shareGlobal + sharedUsers[] + sharedRoles[]), with users and roles in separate lists so a shared id is never ambiguous. - McpServerConfigRepository over Pimcore's LocationAwareConfigRepository, mirroring PerspectiveConfigRepository: shipped defaults from the new studio_mcp_servers node, runtime servers from the configured write target (settings-store or symfony-config, deployer-switchable via config_location.studio_mcp_servers). - Config tree + config_location node + prependCustomConfig wiring; the repository is registered and its config/storage args are set in the extension. Inert by default: the node defaults to an empty map and nothing consumes the repository yet. Tool registration, the per-URL endpoint, access enforcement and OAuth discovery follow in later steps. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…istry Second step of #1309: the mechanism by which tools become available to assign to a server. No server/endpoint yet. - McpToolInterface: a tool describes itself via getDefinition() (name, title, description, MCP annotations, JSON schemas) and runs via execute(). Native contract — the bundle does not depend on the MCP SDK; the per-server endpoint (later step) maps these onto the wire types. - Implementing the interface auto-applies the McpToolRegistry::TAG (via registerForAutoconfiguration in the bundle), and McpToolRegistry collects the tagged tools through an #[AutowireIterator], name-keyed, rejecting duplicates. McpToolPass guards against a hand-written tag on a non-tool service. - McpToolDefinition::requiredScope() derives the OAuth scope from the tool's readOnly annotation (read-only -> mcp:read, else mcp:write; unannotated defaults to write, fail-safe) — the basis for the operation-level scope enforcement tracked for a later step. Mirrors the agent bundle's PR #118 ToolAnnotations, authored and enforced server-side. - PingTool: a built-in, dependency-free read-only tool, so a server can be exercised end-to-end without the agent bundle. Inert by default: the registry is populated but nothing consumes it yet. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nd discovery
Makes step-1 testable end to end without a UI: a configured server is reachable
over MCP at /pimcore-mcp/studio/{server} under the shared pimcore_mcp firewall,
so it accepts the OAuth bearer.
- Adds the mcp/sdk dependency (^0.7, matching the agent bundle) and McpServerFactory,
which assembles a tools-only Mcp\Server per definition. Each assigned tool is
resolved from the registry and bridged onto the SDK: the native execute(array)
is wrapped in a handler that reads the call arguments (via RequestContext /
CallToolRequest) and maps the result to CallToolResult, so tools stay SDK-agnostic.
- McpServerController resolves the definition by URL slug, enforces per-server access
(McpServerAccessResolver — admin/global/owner/user/role, mirroring the bundle's
config sharing), and runs the streamable-HTTP transport with an explicit middleware
stack (dropping the SDK's Dns-rebinding middleware, incompatible with a proxy). The
route is namespaced under /studio/ and declared explicitly so it is neither swept
under the Studio API prefix nor colliding with other bundles' /pimcore-mcp/ routes.
- The extension advertises each enabled server as an RFC 9728 protected resource
(derived from the issuer), so the per-server 401 challenge and discovery resolve.
- A dedicated MCP session cache pool keeps sessions isolated from other bundles.
Verified live: unauthenticated -> 401 + WWW-Authenticate with the per-server
resource_metadata + scope; the resource metadata resolves 200; an authenticated
initialize -> tools/list -> tools/call ping returns "pong".
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Expose the location-aware MCP server configuration over the Studio API so a UI can manage servers without touching symfony-config or the settings store: - Servers: GET/POST/PUT/DELETE under /pimcore-studio/api/mcp/servers, one action per controller, guarded by a new `mcp_servers` user permission. - Tools: GET /pimcore-studio/api/mcp/tools returns the registry's tool catalogue (name, title, description, required scope, read-only/destructive hints) for assignment to a server. The service derives a server's advertised OAuth scopes from its tools' required scope, preserves the owner across updates, locks the url slug to the id, and builds the serving URL from the OAuth issuer. Response DTOs flatten the access model (owner/shareGlobal/sharedUsers/sharedRoles) and each carries a pre-response event. Adds the MCP OpenAPI tag and translation keys. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…g installs The mcp_servers permission is created by the installer on a fresh install, but that runs only once — instances updating from an earlier version never get the row. Without it the Studio permission voter cannot resolve the attribute and denies every user (admins included, since the admin bypass lives inside the vote which never runs for an unsupported attribute), so the MCP server management endpoints return 403. The migration inserts the definition idempotently (guarded on existence, so it is a no-op on a fresh install or a forward-merge replay) and drops the cached permission-key list in postUp so the change takes effect without a separate cache clear. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…lists Rework the per-server access model on the agent bundle's run/update pattern, but kept deny-by-default and keyed by id: - Two levels (McpServerPermission Read/Write, write implies read). Read = see the server, view its config, copy the URL, connect a client at runtime; Write = read plus edit, re-share and delete. - Access is a grid: owner (implicit write) + global read flag + user/role share entries, each carrying a level (McpServerAccessEntry). The stored/submitted shapes tolerate the earlier flat id lists, reading them as read grants. - The resolver answers a requested level: admin, then owner, then an authoritative direct-user entry, then a granting role, then global-read, else deny. It backs both the Studio API and the runtime serving endpoint (which asks for Read). Endpoint gating follows the single-permission path: mcp_servers now gates only create and the tool catalogue. List/get are ungated and filtered/asserted by read access, so a user a server is shared with — with no manage permission — still sees it and copies its URL; update/delete assert write. The response carries the caller's resolved permissions (currentUserPermissions) plus the grid, so the UI can mirror the agent-bundle sharing editor. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two docs for the runtime MCP server feature: - Extending/Providing MCP Tools: the McpToolInterface contract, the auto-applied pimcore.studio_backend.mcp_tool tag, annotations-to-scope mapping, and McpToolResult. - Development Details/MCP Server Management: the mcp_servers permission, the settings-store write target, the Studio API surface, the read/write sharing model (deny-by-default, resolution order), and the Studio master/detail management UI including its read-only behavior. Both are cross-linked with the existing MCP infrastructure and OAuth docs and registered in their section indexes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the bundle's parallel MCP tool framework in favour of the mcp/sdk types the agent bundle already uses, so studio tools and agent tools share one contract: - Delete McpToolInterface, McpToolDefinition, McpToolAnnotations, McpToolResult (and the now-dead DuplicateMcpToolException). A tool is now a plain service with an #[McpTool] method returning a CallToolResult. - Tools opt in explicitly with the pimcore.studio_backend.mcp_tool tag (no auto-tag-by-interface); McpToolPass reflects the attribute into the registry and builds a service locator. Tool names must be unique. - McpToolRegistry hands out McpToolReference descriptors (SDK Tool metadata + class/method); McpServerFactory registers them straight onto the SDK builder's addTool([class, method], ...) with a generated + normalized input schema — the former bridge closure and result mapping are gone. - The only Pimcore-specific concern kept is the OAuth scope, now a one-line McpScopes::forReadOnly() helper over the tool's readOnlyHint. - PingTool becomes the reference #[McpTool] example; the tool-authoring doc is rewritten around #[McpTool]/#[Schema]/CallToolResult. Server management, sharing, the mcp_servers permission, the API and routes are unchanged. Full cross-bundle unification of registries/routes is out of scope. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Configurations are meant to be portable across instances where the same user or role carries a different numeric id, so the sharing model now identifies users and roles by their (unique) name — matching how the agent bundle does it: - McpServerAccess.owner and McpServerAccessEntry are name-based; the stored shape and the API (McpServer.owner, McpServerAccessGrant.name) use names. - McpServerAccessResolver matches owner/user entries on the current user's name and resolves the user's role ids to names via RoleResolverInterface (the same id->name resolution the agent bundle performs). - The service stamps the owner from getCurrentUser()->getName(). The feature is experimental and unreleased, so no id->name back-compat is kept; the tolerant deserializer now reads a bare string as a read grant. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the single read/write level with three independent capabilities on an
MCP server, matching the two-checkbox sharing the UI wants:
- A share entry is now { name, canAccess, canEdit }; being listed at all grants
a read-only view. canAccess = connect a client at runtime; canEdit = change
the config. They no longer imply each other.
- The resolver returns { view, access, edit } (union of the user's own and role
entries): view = admin OR public OR listed; access = public OR a granting
entry (admins do NOT get access implicitly); edit = admin OR a granting entry.
- shareGlobal is the "public" flag: any authenticated user may view and use
(not edit) the server.
- The owner (creator) is auto-listed with full capabilities on save, so they
keep view/use/edit of their own server.
- The Studio API gates get on view, put/delete on edit; the runtime serving
endpoint requires access. The server list is filtered to viewable servers.
currentUserPermissions is now { canView, canAccess, canEdit }.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Studio-API write path moved MCP server access to name-based, two-capability
grants, but the symfony-config `studio_mcp_servers` tree still typed identities
as integers — so a YAML-configured server with a string `owner` failed at
container compile with `Expected "int", but got "string"`, and shared_users /
shared_roles could not carry the {name, can_access, can_edit} grid at all.
Align the file-config `access` node with the settings-store shape the repository
feeds into McpServerAccess::fromArray:
- owner is a scalar username (was integerNode)
- shared_users / shared_roles are grants of { name, can_access, can_edit }, and a
bare string is accepted as a view-only grant (was integerPrototype)
Regression test processes the node and the full tree with a string owner and the
capability grid.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Per the refined permission model (#1452), the owner is now symmetric with an admin: implicit Config Read + Config Edit, but MCP Server Access must be granted explicitly — so nobody, not even the owner or an admin, has default access to a server's runtime. - McpServerAccessResolver: the owner resolves to view + edit (not access), like an admin. Access stays public-or-explicit-entry only. - McpServerConfigurationService: stop seeding the owner into the sharing grid with full capabilities; the owner's read/edit is implicit, and they add themselves to the user list to grant access. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The MCP Server Management page still described the original two-level read/write sharing (owner with implicit write). Rewrite it to the current model: three independent capabilities — Config Read, Config Edit, MCP Server Access — where the owner and admins have implicit read+edit but must be granted access explicitly, and a public server grants read+access (not edit). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
View was derived from being listed, so read could not be withheld. Give each
grant its own canRead flag, so a user can hold Access without Read.
- McpServerAccessEntry: { name, canRead, canAccess, canEdit }; canRead normalises
to true when canEdit (edit implies read); fromMixed defaults canRead true so
grants stored before the flag keep "listed = read"; toArray adds can_read.
- McpServerAccessGrant: add canRead (required) + isCanRead().
- McpServerAccessResolver: view = admin || owner || public || a grant with
canRead — drops the "listed = view" shortcut. Access/edit unchanged.
- Hydrator passes canRead through; docs updated (Config Read now per-grant, with
the edit-implies-read invariant + back-compat default).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Admins and the owner always hold Config Read + Edit at resolve time, but the write path stored the grants verbatim — so an admin (or the owner) submitted with Edit unchecked was persisted, and then returned, as non-editable, which the UI faithfully rendered as a locked-down config. Patch user grants on write instead of trusting the client to have disabled the right checkboxes: any user entry belonging to the owner or an admin is forced to Read + Edit (Access is preserved and never made implicit). This keeps the stored config honest even when the frontend has a bug or the owner is somehow entered as read-only. Roles are left untouched — the admin/owner concept applies to users only. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.




Changes in this pull request
Adds user-configurable MCP servers to the Studio backend: define an MCP server, choose the tools it exposes, serve it over HTTP with OAuth-protected per-server access, and share it with other users/roles under a fine-grained capability model.
What it adds
studio_mcp_serversconfig node; runtime servers in the write target).#[McpTool]contract; a tool catalogue is exposed for the UI./pimcore-mcp/studio/{slug}with per-server access checks and RFC 9728 discovery.mcp_serverspermission on existing installs via a migration.The access / sharing model (evolved over the branch)
canRead/canAccess/canEdit), with the invariant that Edit implies Read.Docs
09_MCP_Server_Management.md(the capability model) and15_Providing_MCP_Tools.md(authoring tools), plus module README links. (OAuth-server docs deliberately live on the [Grid] Reuse Resolver in AdvancedColumnResolver #1308 base branch, not here.)Additional info
feature/oauth-1308-basic-integration([Grid] Reuse Resolver in AdvancedColumnResolver #1308); the OAuth authorization server and its docs are owned there.…/pimcore-mcp/studio/{slug}) this branch establishes.🤖 Generated with Claude Code