Test an MCP Tool
Last modified by Vincent Massol on 2026/09/04 19:20
Steps
Write the unit tests of an MCP tool with the server module's own test base classes, which call the tool the way the server does and pin the text it answers with (see MCP Tool API for the interface under test).
- Extend
AbstractMCPToolTestand carry the component annotations on your own class: the base class is deliberately unannotated, so the concrete test decides between@ComponentTestand@OldcoreTest.@ComponentTest class MyToolTest extends AbstractMCPToolTest { @InjectMockComponents private MyTool tool; @Override protected MCPTool getTool() { return this.tool; } } - Call the tool through
callorcallText, which build theCallToolRequestfrom the tool's own advertised name, so a renamed tool cannot leave its test calling the old one.String text = callText(Map.of("reference", "Sandbox.WebHome")); - Assert the exact text the tool answers with. The message is the contract with the calling agent, so a changed message changes a test, deliberately.
assertEquals("Access denied to Sandbox.WebHome.", callText(Map.of("reference", "Sandbox.WebHome"))); - Extend
AbstractMCPWriteToolTestinstead where the tool writes. It adds the mocked access doors, the stored-document helpers, the version helpers and the failure builders that reproduce a save race, andverifyNothingSavedfor the refusal paths.@OldcoreTest class MyWriteToolTest extends AbstractMCPWriteToolTest { private static final DocumentReference REFERENCE = new DocumentReference("xwiki", "Sandbox", "WebHome"); @Test void refusesAStaleBaseVersion(MockitoOldcore oldcore) throws Exception { storeDocument(oldcore, REFERENCE, "content", null); assertTrue(callText(Map.of("reference", "Sandbox.WebHome", "base_version", "1.0")) .startsWith("Version conflict: ")); verifyNothingSaved(oldcore); } } - Assert the advertised schema, not only the behaviour:
referenceDescription()reads the description the tool advertises for itsreferenceparameter, which is how a schema that stopped matching the parsing is caught. - Run the module's tests and read the coverage gate, which fails the build below the module's floor.
mvn -B -ntp test -pl application-ai-llm-mcp/application-ai-llm-mcp-server -Dtest=MyToolTest
FAQ
Why does the base class carry no annotations?
Because the choice between @ComponentTest and @OldcoreTest belongs to the tool: a tool that only reads components needs the first, one that touches stored documents needs the second. Annotating the base class would impose one on every tool.
Are there functional tests?
No. The module has no *IT.java tests; everything is unit tests, and the tool's answer text is what they assert.
Where do the shared helpers live?
In MCPToolTestUtils, next to the base classes: request builds a call and textOf reads the text of a result's first content block.