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 <edition> 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 <michael@photoprism.app>
This commit is contained in:
Michael Mayer 2026-04-13 08:46:07 +02:00
parent 16fc758f54
commit 4f14ed90c0
8 changed files with 30 additions and 29 deletions

View file

@ -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

View file

@ -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)

View file

@ -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")

View file

@ -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:

View file

@ -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),

View file

@ -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.

View file

@ -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),
}
}

View file

@ -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.