Create a New MCP Tool
Steps
Implement a custom MCP tool as an XWiki component. Architecture explains where the manager discovers it and why the server is cached.
- Add the MCP API and the MCP SDK to your module's POM. Import the BOM and declare mcp-core with the two exclusions. The managed exclusions apply to a direct declaration only, so repeat them on any other direct dependency that brings mcp-core in transitively: the platform rejects the newer reactive-streams the SDK pulls.
<properties> <mcp.version>2.0.1</mcp.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>io.modelcontextprotocol.sdk</groupId> <artifactId>mcp-bom</artifactId> <version>${mcp.version}</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>io.modelcontextprotocol.sdk</groupId> <artifactId>mcp-core</artifactId> <version>${mcp.version}</version> <exclusions> <exclusion> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-annotations</artifactId> </exclusion> <exclusion> <groupId>org.reactivestreams</groupId> <artifactId>reactive-streams</artifactId> </exclusion> </exclusions> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.xwiki.contrib.llm</groupId> <artifactId>application-ai-llm-mcp-api</artifactId> <version>0.11</version> </dependency> <dependency> <groupId>io.modelcontextprotocol.sdk</groupId> <artifactId>mcp-core</artifactId> </dependency> </dependencies> - Create the component class and declare its parameters once with MCPToolSupport. Keep the component hint and TOOL_ID, which is also the advertised tool name, on the same string.
@Component @Named("my_tool") @Singleton public class MyTool implements MCPTool { private static final String TOOL_ID = "my_tool"; private static final MCPToolSupport PARAMS = MCPToolSupport.builder() .requiredString("reference", "The document to read, e.g. \"Sandbox.WebHome\".") .integer("limit", "Maximum number of lines to read (default 20).") .build(); @Inject private MCPDocumentAccess documentAccess; @Override public McpSchema.Tool getToolDefinition() { return McpSchema.Tool.builder(TOOL_ID, PARAMS.inputSchema()) .description("Read a document and return the requested lines.") .build(); } @Override public McpSchema.CallToolResult execute(McpSchema.CallToolRequest request) { // The next steps fill this in: read the parameters, authorize the reference, return a result. return MCPToolSupport.errorResult("Error: 'reference' is required."); } }The manager meta-validates the advertised schema at registration with the SDK's validator. A malformed schema skips this tool with a WARN and leaves the rest of the server up.
- Declare the component in src/main/resources/META-INF/components.txt, which is hand-maintained: a component missing from it does not exist at runtime.
com.example.MyTool - Enforce authorization through MCPDocumentAccess before loading any document, rather than resolving the reference yourself. Catch the checked MCPAccessDeniedException and return its message verbatim.
// Inside execute(): String reference; try { reference = PARAMS.requireString(request.arguments(), "reference"); DocumentReference target = this.documentAccess.resolveAndAuthorize(reference, Right.VIEW); // Load the document at target. } catch (MCPAccessDeniedException e) { return MCPToolSupport.errorResult(e.getMessage()); }When the tool renders exactly one wiki chosen by a wiki parameter, call the overload that takes a WikiReference: it resolves an unqualified reference into that wiki and refuses a reference whose explicit prefix names a different one.
- Return actionable results, using isError results whose message names the corrective action.
private McpSchema.CallToolResult checkLimit(int limit, String reference) { if (limit < 0) { return MCPToolSupport.errorResult("Error: 'limit' must be a non-negative integer."); } return MCPToolSupport.result("Read: " + reference); } - Set the tool metadata that the man catalog is built from.
@Override public String getCategory() { return "Search & Navigation"; } @Override public String getSummary() { return "A one-line summary for the man catalog."; } @Override public String getManPage() { return """ EXAMPLES Read a page: my_tool reference="Sandbox.WebHome" """; } - Install your extension, then save the wiki's MCP configuration page (or restart XWiki) so the cached server is rebuilt. Tick the tool in the tool tree first when it is a write tool or when the wiki stores an explicit tool list. Ask a connected agent for man: the tool now appears in the catalog under its category.
Available tools and reference pages - use `man <name>` for full documentation: Search & Navigation my_tool - A one-line summary for the man catalog.
FAQ
Can my tool be disabled by the administrator?
Yes. Per-wiki visibility goes through the enabledTools configuration list, which your tool id must match to be registered on that wiki, and isEnabled() is a global kill switch you can override.
Does my tool have to live in the server module?
No, but it must be installed at farm scope: the manager discovers tools through the farm-scoped component manager, so a tool contributed by a wiki-scoped extension is invisible to it. The index module's MCPListCollectionsTool and MCPSearchCollectionsTool are complete contributed examples.
Why does my new tool not appear?
The server is cached per wiki and caller variant. A new component is picked up only when that variant is rebuilt: save the wiki's MCP configuration page or restart XWiki, then enable the tool if the wiki stores an explicit list.