Wiki source code of Architecture

Last modified by gabrielc on 2026/10/21 11:18

Show last authors
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.

Get Connected