From 4f14ed90c0705517288f2e04a69b84ce0c63e892 Mon Sep 17 00:00:00 2001 From: Michael Mayer Date: Mon, 13 Apr 2026 08:46:07 +0200 Subject: [PATCH] MCP: Remove "prototype" wording for production-readiness #5024 #5519 - Rewrite package doc, struct comments, log line, and route-registration comment so they describe the MCP server as a first-class feature rather than a pre-release prototype. The "Usage" strings on MCPCommands / MCPServeCommand were updated separately and stay unchanged. - Drop "in this prototype" from the edition-advisory warning emitted by list_config_keys; the message now reads "edition filtering is advisory; results come from the current build metadata". - Rewrite internal/mcp/README.md: new "Scope" section with in-scope/out-of-scope invariants, corrected authorization summary (admin + API client roles + manager in Pro/Portal, anonymous in public mode for the registered read-only tools), and a clearer "Current Capabilities" block. Signed-off-by: Michael Mayer --- internal/api/mcp.go | 2 +- internal/api/mcp_test.go | 2 +- internal/commands/mcp.go | 10 +++++----- internal/mcp/README.md | 33 ++++++++++++++++---------------- internal/mcp/data.go | 4 ++-- internal/mcp/mcp.go | 4 +++- internal/mcp/tool_config_keys.go | 2 +- internal/server/routes.go | 2 +- 8 files changed, 30 insertions(+), 29 deletions(-) diff --git a/internal/api/mcp.go b/internal/api/mcp.go index 8eecff237..d3d59e010 100644 --- a/internal/api/mcp.go +++ b/internal/api/mcp.go @@ -53,7 +53,7 @@ func ServeMCP(router *gin.RouterGroup) { // 401/403/429 response if the session is invalid. In public mode, // Session() returns the default public session so the currently // registered read-only tools are reachable without a token — this - // is intentional so the prototype can be showcased on + // is intentional so the MCP server can be showcased on // demo.photoprism.app. Any future tool that touches per-user state, // the database, or mutates anything MUST NOT be registered on this // server without an additional per-tool check (see internal/mcp diff --git a/internal/api/mcp_test.go b/internal/api/mcp_test.go index a4ecbac59..2f996e805 100644 --- a/internal/api/mcp_test.go +++ b/internal/api/mcp_test.go @@ -59,7 +59,7 @@ func TestServeMCP(t *testing.T) { // the currently registered MCP tools only surface static reference // data. Anonymous callers must therefore be able to initialize an // MCP session and call both tools without a token — this is what - // lets the prototype run on demo.photoprism.app. Guard the policy + // lets the MCP server run on demo.photoprism.app. Guard the policy // here so it regresses loudly if a future change tightens it. app, router, _ := NewApiTest() conf := prepareMCPTest(t) diff --git a/internal/commands/mcp.go b/internal/commands/mcp.go index 17da89228..59c73207f 100644 --- a/internal/commands/mcp.go +++ b/internal/commands/mcp.go @@ -11,19 +11,19 @@ import ( internalmcp "github.com/photoprism/photoprism/internal/mcp" ) -// MCPCommands configures the MCP prototype command group. +// MCPCommands configures the Model Context Protocol (MCP) command group. var MCPCommands = &cli.Command{ Name: "mcp", - Usage: "Runs the internal read-only MCP prototype", + Usage: "Shows the Model Context Protocol (MCP) server subcommands", Subcommands: []*cli.Command{ MCPServeCommand, }, } -// MCPServeCommand starts the MCP prototype over stdio. +// MCPServeCommand starts the MCP server over the stdio transport. var MCPServeCommand = &cli.Command{ Name: "serve", - Usage: "Starts the internal read-only MCP prototype over stdio", + Usage: "Starts the internal MCP server via stdio for development and testing", Action: mcpServeAction, } @@ -35,7 +35,7 @@ func mcpServeAction(ctx *cli.Context) error { } logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelInfo})) - logger.Info("starting mcp prototype", "transport", "stdio", "tools", 2, "resources", 2) + logger.Info("starting mcp server", "transport", "stdio", "tools", 2, "resources", 2) edition := mcpAppMetadata(ctx, "Edition", "unknown") diff --git a/internal/mcp/README.md b/internal/mcp/README.md index dda477a84..4811f44be 100644 --- a/internal/mcp/README.md +++ b/internal/mcp/README.md @@ -1,16 +1,16 @@ -## PhotoPrism MCP Prototype +## PhotoPrism MCP Server **Last Updated:** April 13, 2026 > See `specs/platform/mcp.md` for the canonical specification, including the rationale for the user-access policy and the role/grant matrix per edition. -### Current capabilities +### Current Capabilities - **Transports:** - - CLI: `photoprism mcp serve` (stdio, no auth) + - CLI: `photoprism mcp serve` (stdio, no auth; development and testing) - HTTP: `POST/GET/DELETE /api/v1/mcp` (Streamable HTTP, authenticated) -- **Authentication:** HTTP endpoint requires admin role via `ResourceMCP` ACL -- **Feature gate:** HTTP endpoint requires `--experimental` flag +- **Authorization:** HTTP endpoint enforces the `ResourceMCP` ACL (admin plus the API client roles in every edition, manager in Pro/Portal); anonymous access is permitted in public mode for the currently registered read-only tools. +- **Feature gate:** HTTP endpoint requires the `--experimental` flag; absent that flag `/api/v1/mcp` returns 404. - Read-only resources: - `photoprism://config-options` - `photoprism://search-filters` @@ -27,21 +27,20 @@ | `internal/commands/mcp.go` | CLI command (`photoprism mcp serve`) using stdio transport | | `internal/auth/acl/` | `ResourceMCP` constant and ACL grant rules (`GrantFullAccess` for admin; `GrantSearchAll` for manager in Pro/Portal and for the API client roles: client, instance, service, portal) | -### Goals and non-goals +### Scope -Goals: +In scope for the current server: -- Prove the MCP model works end-to-end inside the PhotoPrism codebase -- Reuse internal reference data instead of maintaining a separate copy -- Keep outputs concise enough for LLM use -- Provide authenticated remote access via Streamable HTTP transport +- Reuse existing internal reference data (`config.Flags`, `config.OptionsReportSections`, `form.Report(&form.SearchPhotos{})`) instead of maintaining a parallel dataset. +- Keep outputs compact enough for LLM consumption. +- Authenticated remote access via the Streamable HTTP transport, plus a stdio transport for local development and testing. -Non-Goals: +Out of scope for the current server (must not regress without additional per-tool gates): -- No write-capable tools -- No direct database access -- No live PhotoPrism instance or API queries -- No non-admin access +- Write-capable tools. +- Direct database access. +- Live PhotoPrism instance or API queries. +- Per-user state (albums, photos, sessions, settings). ### Internal data sources @@ -221,7 +220,7 @@ Whichever path you pick, **add a test in `internal/mcp/server_test.go` that fail ### How Users Get Access -Regular user accounts (`RoleUser`, `RoleViewer`, etc.) are intentionally **not** in the `ResourceMCP` ACL. Regular users typically don't have shell access to the server, so they can't run the CLI commands themselves — and the prototype's tools only return static reference data, so there's no per-user information to authorize against. Access is therefore granted through admin-issued client tokens. +Regular user accounts (`RoleUser`, `RoleViewer`, etc.) are intentionally **not** in the `ResourceMCP` ACL. Regular users typically don't have shell access to the server, so they can't run the CLI commands themselves — and the currently registered tools only return static reference data, so there's no per-user information to authorize against. Access is therefore granted through admin-issued client tokens. To onboard a user (or a CI job, IDE, etc.), an administrator runs the following on the PhotoPrism server: diff --git a/internal/mcp/data.go b/internal/mcp/data.go index 49920a2eb..f939eed3a 100644 --- a/internal/mcp/data.go +++ b/internal/mcp/data.go @@ -13,7 +13,7 @@ const ( searchFiltersURI = "photoprism://search-filters" ) -// Dataset caches the static MCP prototype data returned by resources and tools. +// Dataset caches the static MCP data returned by resources and tools. type Dataset struct { CurrentEdition string ConfigOptions []ConfigOption @@ -50,7 +50,7 @@ type SearchFiltersResource struct { Items []SearchFilter `json:"items"` } -// NewDataset builds the static MCP prototype dataset for the current build. +// NewDataset builds the static MCP dataset for the current build. func NewDataset(currentEdition string) *Dataset { return &Dataset{ CurrentEdition: normalizeEdition(currentEdition), diff --git a/internal/mcp/mcp.go b/internal/mcp/mcp.go index 2ad26b21a..e460ab9f0 100644 --- a/internal/mcp/mcp.go +++ b/internal/mcp/mcp.go @@ -1,5 +1,7 @@ /* -Package mcp provides a small read-only MCP prototype for internal evaluation. +Package mcp implements the PhotoPrism Model Context Protocol (MCP) server, +which exposes read-only reference data (config options and search filters) +to MCP-aware clients over both stdio and authenticated Streamable HTTP. Copyright (c) 2018 - 2026 PhotoPrism UG. All rights reserved. diff --git a/internal/mcp/tool_config_keys.go b/internal/mcp/tool_config_keys.go index 30c3f208c..f4631d70f 100644 --- a/internal/mcp/tool_config_keys.go +++ b/internal/mcp/tool_config_keys.go @@ -126,7 +126,7 @@ func listConfigKeys(_ context.Context, _ *sdkmcp.CallToolRequest, input ListConf if edition != "all" && edition != data.CurrentEdition { result.Warnings = []string{ - fmt.Sprintf("edition filtering is advisory in this prototype; results come from the current %s build metadata", data.CurrentEdition), + fmt.Sprintf("edition filtering is advisory; results come from the current %s build metadata", data.CurrentEdition), } } diff --git a/internal/server/routes.go b/internal/server/routes.go index dcf28bf52..413467d4d 100644 --- a/internal/server/routes.go +++ b/internal/server/routes.go @@ -205,7 +205,7 @@ func registerRoutes(router *gin.Engine, conf *config.Config) { api.ClusterMetrics(APIv1) api.ClusterHealth(APIv1) - // MCP Prototype. + // Model Context Protocol (MCP). api.ServeMCP(APIv1) // Technical Endpoints.