Wiki source code of Test an MCP Tool
Last modified by Vincent Massol on 2026/09/04 19:20
Hide last authors
| author | version | line-number | content |
|---|---|---|---|
![]() |
2.1 | 1 | 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>>doc:documentation.extensions.dev.llm.mcp-server.mcp-tool-api.WebHome]] for the interface under test). |
| 2 | |||
| 3 | 1. Extend {{code}}AbstractMCPToolTest{{/code}} and carry the component annotations on your own class: the base class is deliberately unannotated, so the concrete test decides between {{code}}@ComponentTest{{/code}} and {{code}}@OldcoreTest{{/code}}.((( | ||
| 4 | {{code language="java"}} | ||
| 5 | @ComponentTest | ||
| 6 | class MyToolTest extends AbstractMCPToolTest | ||
| 7 | { | ||
| 8 | @InjectMockComponents | ||
| 9 | private MyTool tool; | ||
| 10 | |||
| 11 | @Override | ||
| 12 | protected MCPTool getTool() | ||
| 13 | { | ||
| 14 | return this.tool; | ||
| 15 | } | ||
| 16 | } | ||
| 17 | {{/code}} | ||
| 18 | ))) | ||
| 19 | 1. Call the tool through {{code}}call{{/code}} or {{code}}callText{{/code}}, which build the {{code}}CallToolRequest{{/code}} from the tool's own advertised name, so a renamed tool cannot leave its test calling the old one.((( | ||
| 20 | {{code language="java"}} | ||
| 21 | String text = callText(Map.of("reference", "Sandbox.WebHome")); | ||
| 22 | {{/code}} | ||
| 23 | ))) | ||
| 24 | 1. 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.((( | ||
| 25 | {{code language="java"}} | ||
| 26 | assertEquals("Access denied to Sandbox.WebHome.", callText(Map.of("reference", "Sandbox.WebHome"))); | ||
| 27 | {{/code}} | ||
| 28 | ))) | ||
| 29 | 1. Extend {{code}}AbstractMCPWriteToolTest{{/code}} instead 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, and {{code}}verifyNothingSaved{{/code}} for the refusal paths.((( | ||
| 30 | {{code language="java"}} | ||
| 31 | @OldcoreTest | ||
| 32 | class MyWriteToolTest extends AbstractMCPWriteToolTest | ||
| 33 | { | ||
| 34 | private static final DocumentReference REFERENCE = | ||
| 35 | new DocumentReference("xwiki", "Sandbox", "WebHome"); | ||
| 36 | |||
| 37 | @Test | ||
| 38 | void refusesAStaleBaseVersion(MockitoOldcore oldcore) throws Exception | ||
| 39 | { | ||
| 40 | storeDocument(oldcore, REFERENCE, "content", null); | ||
| 41 | assertTrue(callText(Map.of("reference", "Sandbox.WebHome", "base_version", "1.0")) | ||
| 42 | .startsWith("Version conflict: ")); | ||
| 43 | verifyNothingSaved(oldcore); | ||
| 44 | } | ||
| 45 | } | ||
| 46 | {{/code}} | ||
| 47 | ))) | ||
| 48 | 1. Assert the advertised schema, not only the behaviour: {{code}}referenceDescription(){{/code}} reads the description the tool advertises for its {{code}}reference{{/code}} parameter, which is how a schema that stopped matching the parsing is caught. | ||
| 49 | 1. Run the module's tests and read the coverage gate, which fails the build below the module's floor.((( | ||
| 50 | {{code}} | ||
| 51 | mvn -B -ntp test -pl application-ai-llm-mcp/application-ai-llm-mcp-server -Dtest=MyToolTest | ||
| 52 | {{/code}} | ||
| 53 | ))) |
