Access Doors

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

Reference

The access doors centralize the MCP server's authorization and scoping decisions so that no tool re-implements them.

Three are public component roles of the application-ai-llm-mcp-api module (package org.xwiki.contrib.llm.mcp, each annotated @Role and @Unstable), so a tool contributed by any module can inject them: MCPDocumentAccess, MCPWikiReach and MCPDocumentSearch. Two are internal to the server module (package org.xwiki.contrib.llm.mcp.internal.access): MCPSpaceFilter, which the public doors apply on the caller's behalf, and MCPRowQuery.

A tool must never resolve a DocumentReference itself: doing so escapes the space policy and can reach another wiki unchecked.

MCPDocumentAccess

Single sanctioned way to resolve a reference and enforce authorization.

@Role
public interface MCPDocumentAccess {
    DocumentReference resolveAndAuthorize(String reference, Right right)
        throws MCPAccessDeniedException;

    DocumentReference resolveAndAuthorize(String reference, Right right, WikiReference wikiContext)
        throws MCPAccessDeniedException;
}

Applies in order: reach gate, then ContextualAuthorizationManager.hasAccess, then space filter. Throws MCPAccessDeniedException with an agent-facing refusal message. The three-argument overload resolves an unqualified reference into the given wiki; a reference carrying an explicit wiki prefix that differs from wikiContext is refused, so a tool's wiki parameter and a prefixed reference cannot contradict each other.

MCPSpaceFilter

Applies the MCP space whitelist or blacklist. Internal to the server module: the public doors above already apply it, so a tool in another module does not call it directly.

@Role
public interface MCPSpaceFilter {
    boolean isAllowed(DocumentReference reference);
    List<String> filterQueries();
    void invalidate(String wikiId);
    void invalidateAll();
}
Method Returns What it does
isAllowed(reference) boolean Whether that one document passes the configured filter mode and entries
filterQueries() List<String> The Solr filter query clauses that scope search results to the same set
invalidate(wikiId) void Drops the parsed filter cached for one source wiki
invalidateAll() void Drops every wiki's parsed filter

A space entry covers the space and its whole subtree; a document entry matches exactly one document. An entry is resolved with the current resolver: Sandbox lands in the source wiki, while a wiki-qualified second:Sandbox lands in the wiki it names and therefore matches nothing in the source wiki. The parsed filter is cached per source wiki, and the configuration listener drops that cache when the wiki's configuration page is saved or deleted. Both checks fail closed on read errors: isAllowed denies, and filterQueries returns a match-nothing -*:* clause.

MCPWikiReach

Decides how far an endpoint reaches beyond its own wiki.

@Role
public interface MCPWikiReach {
    boolean isReachEnabled();
    boolean canReachWiki(String wikiId);
    List<String> resolveSearchWikis(String wikiParam) throws MCPAccessDeniedException;
    String resolveSingleWiki(String wikiParam) throws MCPAccessDeniedException;
}
Method Returns What it does
isReachEnabled() boolean Whether this endpoint may cross wikis at all
canReachWiki(wikiId) boolean Whether that one target wiki is reachable: always true for the current wiki, otherwise whether cross-wiki reach is enabled
resolveSearchWikis(wikiParam) List<String> The wikis a search covers: blank or the current wiki gives the current wiki, "all" gives null meaning the whole farm, an id gives that single wiki
resolveSingleWiki(wikiParam) String The one wiki a single-wiki call operates on; there is no "all" value here

Backed by the main-wiki-only reachEnabledWikis list. A reach-enabled endpoint reaches every wiki in the farm regardless of the target's own enable gate, tool toggles, or space filter.

Default posture

Until the main wiki's reach dashboard is saved for the first time, only the main wiki has reach and every other wiki fails closed. That first save materializes the list, after which it is authoritative and may exclude the main wiki too. If the main wiki's configuration page is untrusted, reach is off for every wiki, the main wiki included.

MCPDocumentSearch

Builds a Solr search query scoped to wikis, space filter, and rights.

@Role
public interface MCPDocumentSearch {
    Query createQuery(String statement, List<String> additionalFilterQueries,
                      List<String> targetWikiIds) throws QueryException;
}

Uses SecureQuery with checkCurrentUser(true), wiki-scope clauses, and the space filter. It is the single query path for query_documents. A null wiki list means the whole farm; an empty list is a defensive no-match.

MCPAccessDeniedException

Checked exception (extends Exception) whose message is the agent-facing refusal. Thrown by MCPDocumentAccess.resolveAndAuthorize() and caught by the tool to produce an isError result.

MCPRowQuery

Internal door to the authorized HQL row pipeline of the navigation tools: bounded row fetching against one target wiki, plus per-row resolution and authorization of the fetched rows.

@Role
public interface MCPRowQuery {
    int MAX_FETCH_PER_QUERY = 2000;

    List<Object[]> rows(String statement, String wiki, String bindName, Object bindValue, int limit)
        throws QueryException;

    List<Object[]> rows(String statement, String wiki, Map<String, Object> bindValues, int limit)
        throws QueryException;

    List<Object[]> hierarchyRows(String baseStatement, boolean showHidden, boolean hasSpaceAlias,
        String wiki, String bindName, Object bindValue, int limit) throws QueryException;

    DocumentReference resolveInto(String fullName, WikiReference wiki);
    boolean isAuthorized(DocumentReference reference);
    DocumentReference authorizedDocument(String fullName, WikiReference wiki);

    record Contains(String text) { }
}
Member Returns What it does
rows(statement, wiki, bindName, bindValue, limit) List<Object[]> Runs one HQL statement against one wiki with an optional named bind
rows(statement, wiki, bindValues, limit) List<Object[]> The same with any number of named binds; a Contains value is escaped by the platform
hierarchyRows(base, showHidden, hasSpaceAlias, wiki, bindName, bindValue, limit) List<Object[]> Composes the hidden-page clauses and a deterministic order by doc.fullName
resolveInto(fullName, wiki) DocumentReference Resolves a row explicitly into the wiki it was fetched from
isAuthorized(reference) boolean Applies the space filter, then the view right, in that order
authorizedDocument(fullName, wiki) DocumentReference The resolved reference when authorized, otherwise null
Contains(text) bind value Marks a raw substring bind, escaped at execution time

Every fetch limit is clamped into 1..MAX_FETCH_PER_QUERY, so a broad statement cannot materialize an unbounded row set. Hidden-page exclusion is explicit HQL rather than a query filter, so it holds regardless of the calling user's own profile preference. A single-column statement yields the scalar itself in each row element at runtime, despite the declared List<Object[]> type.

FAQ

Why is the space filter internal?

Because the public doors already apply it. A tool that called it directly would apply the policy twice, and it lives in an internal package that is not part of the tool API.

Related

Get Connected