Scope: Expose Horde application APIs to AI agents via the Model Context
Protocol (MCP), starting with Wicked (wiki) as the pilot application.
This RFC proposes wiring up the existing Horde\Rpc\Mcp server
implementation into a usable endpoint, plus the per-app provider work needed
to make it useful. Feedback from maintainers is requested on the work
packages and the open questions at the end; most of the changes live in
other repositories (Core, base, wicked), but the MCP layer itself is
here, so this seems the right place for the design discussion.
Related: horde/Core#199 (descriptor schema preservation in ApiRegistry,
found while preparing this plan).
1. Goal
MCP-capable AI clients should be able to discover and invoke Horde
application functionality — read and edit wiki pages, later query calendars,
contacts, tasks, and mail — through a single authenticated MCP endpoint on a
Horde installation.
Target user story for phase 1:
"Update the FooBar wiki page with the new deployment steps" — an AI agent
calls wiki_get_page, edits the markup, calls wiki_edit_page with a
changelog, and the change appears in Wicked's normal page history under
the bot's (or user's) identity, subject to normal Horde permissions.
2. Starting point: the MCP protocol layer already exists
The FRAMEWORK_6_0 branch of horde/rpc already contains a complete, tested
MCP server implementation in src/Mcp/:
| Component |
Purpose |
Horde\Rpc\Mcp\McpServer |
Facade wiring all components |
Horde\Rpc\Mcp\McpRouter |
MCP lifecycle: initialize, ping, tools/list, tools/call, resources/list, resources/read |
Horde\Rpc\Mcp\Transport\HttpHandler |
PSR-15 handler/middleware, MCP Streamable HTTP (POST + JSON), protocol versions 2025-11-25 / 2025-03-26 |
Horde\Rpc\Mcp\AuthContext |
Permissions from PSR-7 request attributes (auth_permissions, authenticated) |
Horde\Rpc\Mcp\Protocol\ToolDescriptor |
Maps MethodDescriptor → MCP tool JSON (auto-generates inputSchema from parameter metadata) |
Horde\Rpc\Mcp\ResourceProviderInterface |
Read-only MCP resources |
Unit tests exist under test/Unit/Mcp/, and doc/MCP-USAGE.md documents the
design. The MCP layer deliberately shares one dispatch abstraction with the
modern JSON-RPC 2.0 stack (see horde/core/doc/INTERAPP_RPC.md):
JSON-RPC client MCP client (LLM)
| |
JsonRpcHandler McpServer
+----------+----------+
|
Shared dispatch layer (horde/rpc)
ApiProvider / MethodInvoker / MethodDescriptor
|
ApiRegistry (horde/core) — aggregates per-app providers
|
Horde\{App}\Api providers (per application)
A provider written once serves JSON-RPC, MCP, and SOAP. Horde also already
ships the auth building blocks MCP prescribes for remote HTTP servers: a
full OAuth2/OIDC server (/oauth2/*, /.well-known/* routes in
horde/horde/config/routes.php) and JwtAuthMiddleware /
AuthHttpBasic middleware in horde/core.
The implementation is homegrown, not the official mcp/sdk. That is a
deliberate trade-off already made here: no external pre-1.0 dependency,
protocol surface limited to what Horde needs, one dispatch layer for all
transports. This plan builds on that decision (see Open Questions §10.1).
3. Gap analysis — what is actually missing
3.1 No HTTP endpoint
Nothing in horde/horde (or any app) instantiates McpServer. The MCP
layer is dormant library code. A route + controller/factory wiring is needed.
3.2 No per-app tool providers (except Content)
ApiRegistryFactory discovers providers by convention (Horde\{App}\Api
implementing ApiInterfaceListProvider). Today only horde/content ships
modern providers (TaggerProvider, TaggerAdminProvider). Wicked, Kronolith,
Turba, Nag, Mnemo, IMP only have legacy lib/Api.php classes, which carry no
structured metadata (no schemas, no permission declarations).
3.3 User identity does not reach the tools
McpRouter::handleToolsCall() invokes with:
$result = $this->invoker->invoke($name, (array) $arguments);
— no ApiCallContext. The provider cannot know who is calling. Wicked
permissions are per-user and per-page; a wiki edit must be attributed to the
authenticated user. The router must build an ApiCallContext (user id,
permissions, auth state) from the transport's AuthContext and pass it to
both invoke() and listMethods() (the latter so tools/list can hide
tools the caller may not use — the visibility-filtering pattern
TaggerAdminProvider already implements).
3.4 Tool naming
ApiRegistry produces dot-notation names (wiki.editPage). Several MCP
clients restrict tool names to [a-zA-Z0-9_-]. The MCP layer needs a
reversible name mapping (proposal: dots → underscores at the
ToolDescriptor boundary, reverse-mapped in tools/call).
3.5 Parameter style
Legacy registry methods take positional parameters. MCP sends named
arguments (JSON object). Modern providers must accept named parameters —
already the stated convention for the AJAX transport, so this aligns.
3.6 Configuration and operational controls
No switch exists to enable/disable the MCP endpoint or select which
interfaces are exposed. Deployments must be able to run without any MCP
surface (default off).
4. Architecture
4.1 Request flow (target state)
MCP client
| POST /horde/api/v1/mcp (Bearer JWT or Basic auth)
v
Route (horde/horde config/routes.php)
middleware: ErrorFilter
→ JwtAuthMiddleware (Bearer token → authenticated user)
→ AuthHttpBasic (fallback for simple deployments)
→ DemandAuthenticatedUser (reject anonymous)
→ McpPermissionAttributes (NEW: map Horde perms of the
authenticated user into the
`auth_permissions` request attribute,
set `authenticated`, `user_id`)
v
McpHandler (thin PSR-15 controller, horde/horde or horde/core)
builds McpServer{ServerInfo, ApiRegistry, ApiRegistry, ResponseFactory,
StreamFactory, WikiResourceProvider?}
v
HttpHandler (horde/rpc, existing) → McpRouter (existing, + ApiCallContext fix)
v
ApiRegistry (horde/core, existing)
v
Horde\Wicked\Api\WikiProvider (NEW) → legacy Wicked driver / Wicked_Page
4.2 Authentication strategy
Phased, reusing what exists:
- Phase 1 (pilot):
JwtAuthMiddleware (Bearer token obtained via the
existing /api/v1/auth/login JWT flow) and HTTP Basic as a
fallback for self-hosted setups. Both terminate in
DemandAuthenticatedUser. This matches how rpc.php handles external
clients today, upgraded to token auth.
- Phase 2: wire the existing OAuth2 authorization-code flow +
/.well-known/oauth-protected-resource metadata so spec-compliant MCP
clients can do the standard OAuth dance against Horde's own OAuth server.
Horde already ships authorize/token/introspection endpoints and a consent
middleware — this is configuration and glue, not new infrastructure.
Anonymous MCP access is not offered. tools/list is filtered by the
caller's context; tools/call re-checks per-method permissions (already
implemented in McpRouter) and the application enforces its own
object-level permissions (e.g. Wicked_Page::allows()), so authorization is
defense-in-depth: transport → descriptor → application.
4.3 Tool design principles
- Curated, not blanket. Apps declare an explicit MCP-suitable tool set;
we do not auto-expose every registry method. Tool descriptions are written
for LLM consumption (imperative, self-contained, parameter docs).
- Named parameters with JSON schemas, authored explicitly per tool
(not relying on auto-generation) including property descriptions.
- Mutating tools demand a changelog parameter; Wicked already supports
require_change_log, giving every AI edit an auditable trail.
- Results are compact JSON (the MCP layer wraps them as text content).
Avoid returning rendered HTML unless requested; wiki source + metadata is
what agents work with.
4.4 Wicked pilot tool set
Interface name wiki (matches its legacy provides), exposed as MCP tools:
| Tool (wire name) |
Maps to |
Notes |
wiki_list_pages |
driver getPages() |
names only; special flag |
wiki_get_page |
Wicked_Page::getText() + metadata |
source, version, author, changelog |
wiki_render_page |
processor transform(..., 'Plain'|'Xhtml') |
for read-only context |
wiki_edit_page |
update/create semantics of legacy edit() |
required changelog; optional expected_version for optimistic locking |
wiki_page_history |
driver getHistory() |
|
wiki_recent_changes |
driver getRecentChanges($days) |
|
wiki_search_pages |
driver getMatchingPages() / searchTitles() |
important for agents; legacy API lacks it |
Optionally (phase 1.5): a ResourceProviderInterface exposing pages as MCP
resources (wiki://PageName), so clients can attach wiki pages as context
without a tool call. The MCP layer supports this already.
The provider wraps the same driver calls the legacy Wicked_Api uses, but
does not call the legacy API class (which reads $GLOBALS). Dependencies
(driver, registry, logger) are constructor-injected.
5. Work packages by repository
WP1 — horde/rpc: context propagation + naming (small)
McpRouter: accept/construct an ApiCallContext from AuthContext
(user id, permissions, authenticated) and pass it to
hasMethod, getMethodDescriptor, listMethods, invoke.
HttpHandler::extractAuthContext(): also read a user_id attribute.
- Tool-name mapping dots↔underscores at the
ToolDescriptor /
tools/call boundary.
- Tests for all three.
WP2 — horde/core: endpoint plumbing (small)
- New middleware
McpPermissionAttributes (working title): translate the
authenticated Horde user's permissions into auth_permissions /
user_id request attributes consumed by the MCP transport.
McpServerFactory (injector binding): builds McpServer with
ServerInfo from Horde version/config, ApiRegistry as
provider+invoker, HTTP factories from DI.
WP3 — horde/horde: endpoint + configuration (small)
- Route
POST /api/v1/mcp in config/routes.php with the middleware stack
from §4.1, dispatching to a thin McpController.
conf.xml switches: $conf['api']['mcp']['enabled'] (default off),
optional interface allowlist.
- Admin visibility: the existing
/admin/apis/ screen already lists modern
providers; add an "exposed via MCP" indicator (nice-to-have).
WP4 — horde/wicked: modern API provider (medium — the pilot)
src/Api.php implementing ApiInterfaceListProvider
(['wiki' => WikiProvider::class], naming convention
Horde\Wicked\Api).
src/Api/WikiProvider.php implementing ApiProvider + MethodInvoker:
descriptors with hand-written JSON schemas, named params, object-level
permission enforcement, changelog required on edit.
- PHPUnit tests with a stubbed driver (
test/.../Stub/).
- Legacy
lib/Api.php remains untouched — both systems coexist by design.
WP5 — Documentation
- Extend
horde/rpc/doc/MCP-USAGE.md (context propagation, naming).
- New
horde/horde doc: "Connecting AI clients to Horde via MCP" —
endpoint enablement, token issuance, client configuration examples,
security guidance.
- Update
horde/core/doc/INTERAPP_RPC.md (MCP transport section).
Later phases (out of scope for the pilot, listed for the roadmap)
- Kronolith (
calendar_*), Turba (contacts_*), Nag (tasks_*),
Mnemo (notes_*) providers — each an independent, parallelizable WP
following the Wicked template.
- OAuth resource-server metadata for spec-compliant client onboarding (§4.2
phase 2).
- MCP prompts capability (e.g. reusable "summarize my week" prompt) — the
current implementation supports tools + resources only.
6. Testing strategy
- Unit: new/changed classes in rpc, core, wicked.
- Integration: end-to-end MCP handshake against a deployed endpoint
(initialize → tools/list → tools/call) with a scripted client; assert a
wiki edit lands in page history with correct author + changelog.
- Client smoke test: register the endpoint in an MCP-capable AI client
against a test installation; exercise read + edit on a scratch wiki page.
- Negative tests: anonymous request rejected; user without page EDIT
permission gets isError result; missing changelog rejected when
require_change_log is on.
7. Security considerations
- Endpoint off by default; explicit admin opt-in.
- No anonymous tool surface; three-layer authorization (see §4.2).
- Mutations are attributable: Horde user identity + mandatory changelog.
- The MCP layer never exposes credentials; token issuance uses existing
Horde auth flows. Recommend dedicated bot users with scoped permissions
for autonomous agents; interactive use runs under the person's own account.
- Rate limiting is left to the webserver/reverse proxy for the pilot
(documented); revisit if abuse surfaces.
- Prompt-injection realism: wiki content read by an agent may contain
hostile instructions. Mitigation is client-side (agent policy), but the
docs must state it, and destructive operations beyond page edits are
deliberately not exposed (no page deletion/rename in phase 1).
8. Rollout plan
| Phase |
Content |
Exit criterion |
| 0 |
Discussion of this RFC |
Maintainer buy-in on WP1/WP2 changes |
| 1 |
WP1 + WP2 + WP3 + WP4 (Wicked pilot), PRs per repository |
AI client edits a wiki page end-to-end on a test installation |
| 1.5 |
Wiki resources; /admin/apis/ MCP indicator |
|
| 2 |
OAuth client onboarding; second app (proposal: Kronolith read-only first) |
External MCP client connects without manual token handling |
9. Effort estimate (coding, excl. review cycles)
| WP |
Estimate |
| WP1 rpc |
1–2 days incl. tests |
| WP2 core |
0.5–1 day incl. tests |
| WP3 horde |
0.5–1 day |
| WP4 wicked |
2–3 days incl. tests |
| WP5 docs |
1 day |
10. Open questions
- Homegrown MCP vs official
mcp/sdk: the in-tree implementation
avoids a pre-1.0 dependency and reuses the shared dispatch layer, but it
must track the MCP spec ourselves (currently 2025-11-25). Do we commit
to maintaining it, or plan a later migration path to mcp/sdk behind the
same dispatch interfaces? (Recommendation: keep homegrown; the surface is
small and the dispatch abstraction isolates a future swap.)
- Endpoint path and ownership:
/api/v1/mcp under the horde app
(proposed) vs a per-app endpoint. Single endpoint + interface prefixes is
simpler for clients and matches ApiRegistry's design.
- Identity model for
ApiCallContext: minimal (user_id,
permissions, authenticated) attribute-bag entries, or a typed
identity object? Affects WP1 API.
- GET/SSE support: the current transport is POST-only (valid for
Streamable HTTP without server-initiated streams). Some clients probe
GET; do we return 405 (current behavior) or add an SSE stub later?
- Interface allowlist granularity: global config list (proposed) vs
per-user/per-permission exposure of whole interfaces.
- Tool naming convention:
wiki_edit_page style (snake, proposed) vs
wiki_editPage (mixed). Cosmetic but should be uniform across apps from
day one.
Scope: Expose Horde application APIs to AI agents via the Model Context
Protocol (MCP), starting with Wicked (wiki) as the pilot application.
This RFC proposes wiring up the existing
Horde\Rpc\Mcpserverimplementation into a usable endpoint, plus the per-app provider work needed
to make it useful. Feedback from maintainers is requested on the work
packages and the open questions at the end; most of the changes live in
other repositories (
Core,base,wicked), but the MCP layer itself ishere, so this seems the right place for the design discussion.
Related: horde/Core#199 (descriptor schema preservation in
ApiRegistry,found while preparing this plan).
1. Goal
MCP-capable AI clients should be able to discover and invoke Horde
application functionality — read and edit wiki pages, later query calendars,
contacts, tasks, and mail — through a single authenticated MCP endpoint on a
Horde installation.
Target user story for phase 1:
2. Starting point: the MCP protocol layer already exists
The FRAMEWORK_6_0 branch of
horde/rpcalready contains a complete, testedMCP server implementation in
src/Mcp/:Horde\Rpc\Mcp\McpServerHorde\Rpc\Mcp\McpRouterinitialize,ping,tools/list,tools/call,resources/list,resources/readHorde\Rpc\Mcp\Transport\HttpHandler2025-11-25/2025-03-26Horde\Rpc\Mcp\AuthContextauth_permissions,authenticated)Horde\Rpc\Mcp\Protocol\ToolDescriptorMethodDescriptor→ MCP tool JSON (auto-generatesinputSchemafrom parameter metadata)Horde\Rpc\Mcp\ResourceProviderInterfaceUnit tests exist under
test/Unit/Mcp/, anddoc/MCP-USAGE.mddocuments thedesign. The MCP layer deliberately shares one dispatch abstraction with the
modern JSON-RPC 2.0 stack (see
horde/core/doc/INTERAPP_RPC.md):A provider written once serves JSON-RPC, MCP, and SOAP. Horde also already
ships the auth building blocks MCP prescribes for remote HTTP servers: a
full OAuth2/OIDC server (
/oauth2/*,/.well-known/*routes inhorde/horde/config/routes.php) andJwtAuthMiddleware/AuthHttpBasicmiddleware inhorde/core.The implementation is homegrown, not the official
mcp/sdk. That is adeliberate trade-off already made here: no external pre-1.0 dependency,
protocol surface limited to what Horde needs, one dispatch layer for all
transports. This plan builds on that decision (see Open Questions §10.1).
3. Gap analysis — what is actually missing
3.1 No HTTP endpoint
Nothing in
horde/horde(or any app) instantiatesMcpServer. The MCPlayer is dormant library code. A route + controller/factory wiring is needed.
3.2 No per-app tool providers (except Content)
ApiRegistryFactorydiscovers providers by convention (Horde\{App}\Apiimplementing
ApiInterfaceListProvider). Today onlyhorde/contentshipsmodern providers (
TaggerProvider,TaggerAdminProvider). Wicked, Kronolith,Turba, Nag, Mnemo, IMP only have legacy
lib/Api.phpclasses, which carry nostructured metadata (no schemas, no permission declarations).
3.3 User identity does not reach the tools
McpRouter::handleToolsCall()invokes with:— no
ApiCallContext. The provider cannot know who is calling. Wickedpermissions are per-user and per-page; a wiki edit must be attributed to the
authenticated user. The router must build an
ApiCallContext(user id,permissions, auth state) from the transport's
AuthContextand pass it toboth
invoke()andlistMethods()(the latter sotools/listcan hidetools the caller may not use — the visibility-filtering pattern
TaggerAdminProvideralready implements).3.4 Tool naming
ApiRegistryproduces dot-notation names (wiki.editPage). Several MCPclients restrict tool names to
[a-zA-Z0-9_-]. The MCP layer needs areversible name mapping (proposal: dots → underscores at the
ToolDescriptorboundary, reverse-mapped intools/call).3.5 Parameter style
Legacy registry methods take positional parameters. MCP sends named
arguments (JSON object). Modern providers must accept named parameters —
already the stated convention for the AJAX transport, so this aligns.
3.6 Configuration and operational controls
No switch exists to enable/disable the MCP endpoint or select which
interfaces are exposed. Deployments must be able to run without any MCP
surface (default off).
4. Architecture
4.1 Request flow (target state)
4.2 Authentication strategy
Phased, reusing what exists:
JwtAuthMiddleware(Bearer token obtained via theexisting
/api/v1/auth/loginJWT flow) and HTTP Basic as afallback for self-hosted setups. Both terminate in
DemandAuthenticatedUser. This matches howrpc.phphandles externalclients today, upgraded to token auth.
/.well-known/oauth-protected-resourcemetadata so spec-compliant MCPclients can do the standard OAuth dance against Horde's own OAuth server.
Horde already ships authorize/token/introspection endpoints and a consent
middleware — this is configuration and glue, not new infrastructure.
Anonymous MCP access is not offered.
tools/listis filtered by thecaller's context;
tools/callre-checks per-method permissions (alreadyimplemented in
McpRouter) and the application enforces its ownobject-level permissions (e.g.
Wicked_Page::allows()), so authorization isdefense-in-depth: transport → descriptor → application.
4.3 Tool design principles
we do not auto-expose every registry method. Tool descriptions are written
for LLM consumption (imperative, self-contained, parameter docs).
(not relying on auto-generation) including property descriptions.
require_change_log, giving every AI edit an auditable trail.Avoid returning rendered HTML unless requested; wiki source + metadata is
what agents work with.
4.4 Wicked pilot tool set
Interface name
wiki(matches its legacyprovides), exposed as MCP tools:wiki_list_pagesgetPages()specialflagwiki_get_pageWicked_Page::getText()+ metadatawiki_render_pagetransform(..., 'Plain'|'Xhtml')wiki_edit_pageedit()changelog; optionalexpected_versionfor optimistic lockingwiki_page_historygetHistory()wiki_recent_changesgetRecentChanges($days)wiki_search_pagesgetMatchingPages()/searchTitles()Optionally (phase 1.5): a
ResourceProviderInterfaceexposing pages as MCPresources (
wiki://PageName), so clients can attach wiki pages as contextwithout a tool call. The MCP layer supports this already.
The provider wraps the same driver calls the legacy
Wicked_Apiuses, butdoes not call the legacy API class (which reads
$GLOBALS). Dependencies(driver, registry, logger) are constructor-injected.
5. Work packages by repository
WP1 —
horde/rpc: context propagation + naming (small)McpRouter: accept/construct anApiCallContextfromAuthContext(user id, permissions,
authenticated) and pass it tohasMethod,getMethodDescriptor,listMethods,invoke.HttpHandler::extractAuthContext(): also read auser_idattribute.ToolDescriptor/tools/callboundary.WP2 —
horde/core: endpoint plumbing (small)McpPermissionAttributes(working title): translate theauthenticated Horde user's permissions into
auth_permissions/user_idrequest attributes consumed by the MCP transport.McpServerFactory(injector binding): buildsMcpServerwithServerInfofrom Horde version/config,ApiRegistryasprovider+invoker, HTTP factories from DI.
WP3 —
horde/horde: endpoint + configuration (small)POST /api/v1/mcpinconfig/routes.phpwith the middleware stackfrom §4.1, dispatching to a thin
McpController.conf.xmlswitches:$conf['api']['mcp']['enabled'](default off),optional interface allowlist.
/admin/apis/screen already lists modernproviders; add an "exposed via MCP" indicator (nice-to-have).
WP4 —
horde/wicked: modern API provider (medium — the pilot)src/Api.phpimplementingApiInterfaceListProvider(
['wiki' => WikiProvider::class], naming conventionHorde\Wicked\Api).src/Api/WikiProvider.phpimplementingApiProvider+MethodInvoker:descriptors with hand-written JSON schemas, named params, object-level
permission enforcement, changelog required on edit.
test/.../Stub/).lib/Api.phpremains untouched — both systems coexist by design.WP5 — Documentation
horde/rpc/doc/MCP-USAGE.md(context propagation, naming).horde/hordedoc: "Connecting AI clients to Horde via MCP" —endpoint enablement, token issuance, client configuration examples,
security guidance.
horde/core/doc/INTERAPP_RPC.md(MCP transport section).Later phases (out of scope for the pilot, listed for the roadmap)
calendar_*), Turba (contacts_*), Nag (tasks_*),Mnemo (
notes_*) providers — each an independent, parallelizable WPfollowing the Wicked template.
phase 2).
current implementation supports tools + resources only.
6. Testing strategy
(initialize → tools/list → tools/call) with a scripted client; assert a
wiki edit lands in page history with correct author + changelog.
against a test installation; exercise read + edit on a scratch wiki page.
permission gets
isErrorresult; missing changelog rejected whenrequire_change_logis on.7. Security considerations
Horde auth flows. Recommend dedicated bot users with scoped permissions
for autonomous agents; interactive use runs under the person's own account.
(documented); revisit if abuse surfaces.
hostile instructions. Mitigation is client-side (agent policy), but the
docs must state it, and destructive operations beyond page edits are
deliberately not exposed (no page deletion/rename in phase 1).
8. Rollout plan
/admin/apis/MCP indicator9. Effort estimate (coding, excl. review cycles)
10. Open questions
mcp/sdk: the in-tree implementationavoids a pre-1.0 dependency and reuses the shared dispatch layer, but it
must track the MCP spec ourselves (currently
2025-11-25). Do we committo maintaining it, or plan a later migration path to
mcp/sdkbehind thesame dispatch interfaces? (Recommendation: keep homegrown; the surface is
small and the dispatch abstraction isolates a future swap.)
/api/v1/mcpunder the horde app(proposed) vs a per-app endpoint. Single endpoint + interface prefixes is
simpler for clients and matches
ApiRegistry's design.ApiCallContext: minimal (user_id,permissions,authenticated) attribute-bag entries, or a typedidentity object? Affects WP1 API.
Streamable HTTP without server-initiated streams). Some clients probe
GET; do we return 405 (current behavior) or add an SSE stub later?
per-user/per-permission exposure of whole interfaces.
wiki_edit_pagestyle (snake, proposed) vswiki_editPage(mixed). Cosmetic but should be uniform across apps fromday one.