Wiki source code of Architecture
Last modified by gabrielc on 2026/10/21 11:18
Show last authors
| author | version | line-number | content |
|---|---|---|---|
| 1 | The MCP server of the [[LLM Application>>doc:extensions:Extension.LLM.WebHome]] is a thin HTTP endpoint over four XWiki facilities: the component manager, the rights model, the Solr index and the observation bus. It has no compile-time knowledge of its tools: each tool is an ##MCPTool## component, discovered when the server for a wiki is built. | ||
| 2 | |||
| 3 | == Module layout == | ||
| 4 | |||
| 5 | {{code language="none"}} | ||
| 6 | application-ai-llm-mcp/ | ||
| 7 | ├── application-ai-llm-mcp-api/ MCPTool @Role (the extension point), | ||
| 8 | │ MCPToolSupport, the public access doors | ||
| 9 | ├── application-ai-llm-mcp-server/ endpoint, manager, config reader, | ||
| 10 | │ access-door implementations, space | ||
| 11 | │ filter, 18 bundled tools, the shared | ||
| 12 | │ tool seams (internal) | ||
| 13 | └── application-ai-llm-mcp-ui/ admin UI + config pages (XAR) | ||
| 14 | {{/code}} | ||
| 15 | |||
| 16 | The server module has no dependency on the index/RAG stack. It is installable standalone. Bundled tools query XWiki's built-in Solr index. | ||
| 17 | |||
| 18 | == Four-layer architecture == | ||
| 19 | |||
| 20 | A tool call crosses four layers, each with one job. Every one of them is parameterized by the same per-wiki configuration document, and a save of that document is what makes a running server pick the change up. | ||
| 21 | |||
| 22 | {{plantuml}} | ||
| 23 | @startuml | ||
| 24 | !theme bluegray | ||
| 25 | skinparam componentStyle rectangle | ||
| 26 | |||
| 27 | actor "AI agent" as AGENT | ||
| 28 | component "**DefaultMCPResource**\n/rest/wikis/{wiki}/aiLLM/mcp\ncanonical id, enable gate, guest gate" as RES | ||
| 29 | component "**XWikiMCPServerManager**\none server per wiki and caller variant\nbuilt lazily, cached, invalidated on save" as MGR | ||
| 30 | component "**MCPTool** components\nhint = advertised name\nguest variant drops isWrite" as TOOLS | ||
| 31 | component "**Access doors**\nMCPDocumentAccess, MCPWikiReach\nMCPSpaceFilter, MCPDocumentSearch" as DOORS | ||
| 32 | database "XWiki documents\nand the built-in Solr index" as XWIKI | ||
| 33 | component "**MCPServerConfiguration**\nAI.MCP.Code.MCPServerConfig per wiki\ntrusted only when its author is a wiki admin" as CONF | ||
| 34 | component "**MCPConfigChangeEventListener**" as EVL | ||
| 35 | |||
| 36 | AGENT --> RES : JSON-RPC over HTTP | ||
| 37 | RES --> MGR : target wiki and caller variant | ||
| 38 | MGR --> TOOLS : executeWrapped, on the request thread | ||
| 39 | TOOLS --> DOORS : every reference, every query | ||
| 40 | DOORS --> XWIKI : as the calling user | ||
| 41 | CONF ..> RES : enable gate and guest gate | ||
| 42 | CONF ..> MGR : enabled tool set | ||
| 43 | CONF ..> DOORS : space filter, cross-wiki reach | ||
| 44 | CONF ..> EVL : save or delete | ||
| 45 | EVL ..> MGR : drops both variants of the wiki | ||
| 46 | @enduml | ||
| 47 | {{/plantuml}} | ||
| 48 | |||
| 49 | What each layer is responsible for: | ||
| 50 | |||
| 51 | * **Identity and routing** : ##DefaultMCPResource## checks that the path wiki id is the canonical id of an existing wiki, applies the per-wiki enable gate, applies the guest gate, advertises the OAuth/OIDC discovery metadata (RFC 9728) when an OIDC Provider is installed, and sets the target wiki on the ##XWikiContext##. | ||
| 52 | * **Lifecycle and middleware** : ##XWikiMCPServerManager## keeps one SDK server per wiki and caller variant, builds each lazily, registers only that wiki's enabled ##MCPTool## components on it, and routes every call through ##executeWrapped()##, which enforces the guest write rule, audits the call and normalizes the exceptions it throws. | ||
| 53 | * **The capabilities** : the tools themselves, 18 bundled ones in four categories (Help, Search and Navigation, Structured Data, Authoring), of which ##list_wikis## is reach-gated, plus ##list_collections## and ##search_collections## contributed by the index module when it is installed. The full list is on [[MCP Tools>>doc:documentation.extensions.user.llm.mcp-server.tools.WebHome]]. | ||
| 54 | * **Shared authorization** : the access doors, which every tool goes through instead of resolving a reference itself: the reach gate, then the rights check, then the space filter, in that order, as the calling user and through XWiki's own rights model. | ||
| 55 | |||
| 56 | == Who may call == | ||
| 57 | |||
| 58 | The MCP transport is stateless: every JSON-RPC request is routed on its own, with no MCP session to resume. Two variants of each wiki's server are cached, one for authenticated callers and one for guests. | ||
| 59 | |||
| 60 | {{plantuml}} | ||
| 61 | @startuml | ||
| 62 | !theme bluegray | ||
| 63 | start | ||
| 64 | :POST /rest/wikis/{wiki}/aiLLM/mcp; | ||
| 65 | if (canonical wiki id and endpoint enabled?) then (no) | ||
| 66 | :404; | ||
| 67 | stop | ||
| 68 | endif | ||
| 69 | if (authenticated user?) then (yes) | ||
| 70 | :authenticated server variant; | ||
| 71 | else (no) | ||
| 72 | if (Authorization header present?) then (yes) | ||
| 73 | :401, failed credentials; | ||
| 74 | stop | ||
| 75 | endif | ||
| 76 | if (allowGuest?) then (no) | ||
| 77 | :401; | ||
| 78 | stop | ||
| 79 | else (yes) | ||
| 80 | :guest server variant, read tools only; | ||
| 81 | endif | ||
| 82 | endif | ||
| 83 | :dispatch the call; | ||
| 84 | stop | ||
| 85 | @enduml | ||
| 86 | {{/plantuml}} | ||
| 87 | |||
| 88 | ##allowGuest## is the single gate for unauthenticated callers, and it is off by default. With it off, every caller that is still guest after the authentication filter gets 401. With it on, the request is forwarded to the guest variant, which registers only tools whose ##isWrite()## is false and runs them as the guest user, so the space filter and XWiki's view rights bound what it can read. | ||
| 89 | |||
| 90 | Two rules do not depend on the flag. A request carrying an ##Authorization## header whose credentials failed is always refused with 401, because serving it as guest would silently downgrade the caller and a client only refreshes its token on a 401. And the presence of the XWiki OIDC Provider only decides the shape of the 401: with it, the response carries the RFC 9728 challenge and the endpoint serves protected-resource metadata; without it, the 401 is bare, and credentials can still be presented through XWiki's standard authenticator (HTTP Basic, for example). | ||
| 91 | |||
| 92 | == Configuration trust == | ||
| 93 | |||
| 94 | A wiki's settings are applied only when its configuration page was last saved by a user with ##ADMIN## right on that wiki. ##MCPServerConfiguration## evaluates this on every read: a missing page is trusted and the defaults apply; a page with no recorded author, or one saved by a guest or a non-administrator, is untrusted. | ||
| 95 | |||
| 96 | An untrusted page is not a degraded mode. Its stored values are never read, ##isEnabled## returns ##false## so the endpoint answers 404, the main wiki grants no cross-wiki reach to any wiki, and the administration section offers a Reset. Resetting removes every stored setting, stamps the acting administrator as author and creator, and returns the wiki to the defaults. ##MCPConfigPageAuthorship## owns the author lookup and the stamping; the change listener logs a WARN when a save leaves a page untrusted, so the state does not pass unnoticed. | ||
| 97 | |||
| 98 | == Per-wiki server variants == | ||
| 99 | |||
| 100 | A server is built per ##ServerKey(wikiId, guest)## and cached in a ##ConcurrentHashMap##. The first request for a variant builds it lazily with ##computeIfAbsent##; the guest variant filters out every ##MCPTool## whose ##isWrite()## is true before registration. Invalidation removes both variants and closes them. Closing only marks the stateless transport as closing, so in-flight requests finish, and the next request rebuilds from the current configuration. | ||
| 101 | |||
| 102 | == Request lifecycle == | ||
| 103 | |||
| 104 | 1. An HTTP request arrives at ##DefaultMCPResource## (##/rest/wikis/{wiki}/aiLLM/mcp##). | ||
| 105 | 1. The canonical-wiki-id check runs first, so no configuration is ever read through a case-variant id; then the enable gate. Either failure returns 404. | ||
| 106 | 1. Authentication has already run in XWiki's filter chain. A caller still guest is refused with 401 unless ##allowGuest## is on and the request presents no ##Authorization## header. | ||
| 107 | 1. The resource sets the target wiki on the ##XWikiContext## and calls ##XWikiMCPServerManager.handleRequest()##. | ||
| 108 | 1. The manager resolves the cached server for ##ServerKey(wiki, guest)##, building that variant on its first request. | ||
| 109 | 1. The stateless transport parses the JSON-RPC message and dispatches ##tools/call## to the registered handler. | ||
| 110 | 1. The handler is ##executeWrapped##: it refuses an authoring tool to a guest without running it, otherwise invokes the tool's ##execute()## inline on the request thread, then writes the audit line. | ||
| 111 | 1. The result is serialized back as the HTTP response. | ||
| 112 | |||
| 113 | == Threading == | ||
| 114 | |||
| 115 | Tool handlers run inline on the HTTP request thread, where XWiki's thread-locals (current user, wiki) natively live. The SDK builder sets ##immediateExecution(true)## to prevent offloading to Reactor worker threads. | ||
| 116 | |||
| 117 | == Server lifecycle == | ||
| 118 | |||
| 119 | * ##XWikiMCPServerManager## owns a ##ConcurrentHashMap## of ##ServerHolder## values keyed by ##ServerKey(wikiId, guest)##. | ||
| 120 | * ##buildServer(key)## reads the wiki's configuration, enumerates the ##MCPTool## components through the farm-scoped component manager, filters by ##isEnabled()##, by the guest variant's ##isWrite()## rule and by the wiki's effective tool ids, meta-validates each schema, and registers the survivors on a fresh stateless transport. | ||
| 121 | * Registration is fault-isolated per tool: a throwing tool, a malformed schema or a duplicate name is skipped with a WARN, and the rest of the server still builds. | ||
| 122 | * Invalidation is event-driven: ##MCPConfigChangeEventListener## watches created, updated and deleted events of ##AI.MCP.Code.MCPServerConfig##. A main-wiki change invalidates every cached server and every cached space filter, because the farm-level reach list may have changed; any other wiki's change invalidates only that wiki. Translations are ignored, since they carry no configuration. | ||
| 123 | * ##dispose()## closes every cached server and clears the map. | ||
| 124 | |||
| 125 | == Server identity and instructions == | ||
| 126 | |||
| 127 | The server name comes from the wiki's configuration, with ##XWiki## as the shipped default, and the version comes from the implementation version in the module's manifest, falling back to ##0.0.0##. The initialization instructions are the configured description followed by the ##man## hint whenever ##man## actually registered: {{code language="none"}}Call the 'man' tool with no arguments first for a catalog of the available tools and reference pages.{{/code}} The hint is omitted when ##man## is not in the registered set. | ||
| 128 | |||
| 129 | == Audit line == | ||
| 130 | |||
| 131 | Every call goes through ##executeWrapped##, which emits one INFO line: {{code language="none"}}MCP tool call: tool=[query_documents] user=[xwiki:XWiki.Admin] outcome=[ok] duration=[42ms]{{/code}}. The outcome is ##ok##, ##error## when the tool returned an ##isError## result, ##refused## when an authoring tool was invoked by a guest, or ##failed## for an unexpected exception or a null result. Arguments are never logged, because they can carry document content. | ||
| 132 | |||
| 133 | == Shared tool seams == | ||
| 134 | |||
| 135 | Mechanics that several tools must implement identically live in exactly one place each, so the tools cannot drift apart on them: | ||
| 136 | |||
| 137 | * Text and rendering: ##MCPSourceText## in the api module (line-ending normalization, line numbering, the output budget), and in the server module's internal tool package ##MCPContentWindow## (the range-read window grammar), ##MCPRenderedHtml## and ##MCPRenderedHtmlResponses## (rendered HTML parsing and response composition), ##MCPSchemaText## (the schema field grammar) and ##MCPTextGuards## (neutralizing and clamping echoed wiki-authored text). | ||
| 138 | * Translation, links, history and attachments: ##MCPTranslationSupport##, ##MCPLinksSupport##, ##MCPHistorySupport##, ##MCPAttachmentSupport## and ##MCPAttachmentWriteSupport##. | ||
| 139 | * Writes: ##MCPWriteSupport## (write rights, the ##[AI]## version comment, the ##base_version## conflict messages), ##MCPObjectWriteSupport## and ##MCPSchemaWriteSupport##. | ||
| 140 | * Queries: ##MCPObjectQuerySupport## on the object side, and ##MCPRowQuery## in the access package for the navigation tools' bounded HQL rows. | ||
| 141 | |||
| 142 | == Farm administration == | ||
| 143 | |||
| 144 | The main wiki's MCP admin section is scripted through ##MCPFarmScriptService## (##$services.mcp##). It reads and reconciles the per-wiki enabled, guest-access and cross-wiki reach flags across the farm. The bulk applies skip a wiki whose configuration page is untrusted instead of applying stored settings nobody with admin right saved, and reach changes write nothing while the main wiki's page is untrusted. | ||
| 145 | |||
| 146 | == MCP SDK == | ||
| 147 | |||
| 148 | The server uses ##io.modelcontextprotocol.sdk:mcp-core##, whose version is owned by the ##mcp-bom## import in the root POM. Two SDK defaults are overridden: | ||
| 149 | |||
| 150 | * ##immediateExecution(true)## : tools run on the request thread, not Reactor workers. | ||
| 151 | * ##validateToolInputs(false)## : the MCPToolSupport declarative parameter layer is the designed validation gate. | ||
| 152 | |||
| 153 | The transport is ##HttpServletStatelessServerTransport## with ##McpStatelessSyncServer##: each request stands alone, and the transport keeps no session between calls. |