Skip to content

Commit 95de3e4

Browse files
Add unit tests for MCP auto-schema reflection (6 tests)
McpAutoSchemaTest covers: - Auto-schema from ServiceClass: int->integer, double->number, String->string, boolean->boolean, long->integer - Array type mapping: double[]->array of numbers, double[][]->array of arrays of numbers - Primitive parameter (String) skipped, falls back to empty schema - Explicit mcpInputSchema overrides auto-generation - SpringBeanName without HttpServletRequest falls back to empty - Boolean is-getter detection (isActive -> active: boolean) Total openapi tests: 244 -> 250, 0 failures. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 5530655 commit 95de3e4

1 file changed

Lines changed: 278 additions & 0 deletions

File tree

Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.axis2.openapi;
20+
21+
import com.fasterxml.jackson.databind.JsonNode;
22+
import com.fasterxml.jackson.databind.ObjectMapper;
23+
import org.apache.axis2.context.ConfigurationContext;
24+
import org.apache.axis2.context.ConfigurationContextFactory;
25+
import org.apache.axis2.description.AxisOperation;
26+
import org.apache.axis2.description.AxisService;
27+
import org.apache.axis2.description.InOutAxisOperation;
28+
import org.apache.axis2.description.Parameter;
29+
import org.apache.axis2.engine.AxisConfiguration;
30+
import org.junit.Before;
31+
import org.junit.Test;
32+
33+
import javax.xml.namespace.QName;
34+
35+
import static org.junit.Assert.*;
36+
37+
/**
38+
* Tests for MCP auto-schema generation from Java method parameter types.
39+
* Covers the reflection-based fallback when mcpInputSchema is not set
40+
* in services.xml.
41+
*/
42+
public class McpAutoSchemaTest {
43+
44+
private ConfigurationContext configurationContext;
45+
private OpenApiSpecGenerator generator;
46+
private ObjectMapper jackson;
47+
48+
// ── Test service class with typed POJO parameter ──
49+
public static class SampleRequest {
50+
private int count;
51+
private double price;
52+
private String name;
53+
private boolean active;
54+
private long timestamp;
55+
private double[] values;
56+
private double[][] matrix;
57+
private String requestId;
58+
59+
public int getCount() { return count; }
60+
public void setCount(int count) { this.count = count; }
61+
public double getPrice() { return price; }
62+
public void setPrice(double price) { this.price = price; }
63+
public String getName() { return name; }
64+
public void setName(String name) { this.name = name; }
65+
public boolean isActive() { return active; }
66+
public void setActive(boolean active) { this.active = active; }
67+
public long getTimestamp() { return timestamp; }
68+
public void setTimestamp(long timestamp) { this.timestamp = timestamp; }
69+
public double[] getValues() { return values; }
70+
public void setValues(double[] values) { this.values = values; }
71+
public double[][] getMatrix() { return matrix; }
72+
public void setMatrix(double[][] matrix) { this.matrix = matrix; }
73+
public String getRequestId() { return requestId; }
74+
public void setRequestId(String requestId) { this.requestId = requestId; }
75+
}
76+
77+
public static class SampleResponse {
78+
private String result;
79+
public String getResult() { return result; }
80+
public void setResult(String result) { this.result = result; }
81+
}
82+
83+
public static class SampleService {
84+
public SampleResponse calculate(SampleRequest request) {
85+
return new SampleResponse();
86+
}
87+
public String echo(String message) {
88+
return message;
89+
}
90+
}
91+
92+
@Before
93+
public void setUp() throws Exception {
94+
configurationContext = ConfigurationContextFactory
95+
.createEmptyConfigurationContext();
96+
generator = new OpenApiSpecGenerator(configurationContext);
97+
jackson = io.swagger.v3.core.util.Json.mapper();
98+
}
99+
100+
private AxisService createServiceWithClass(String serviceName, String className)
101+
throws Exception {
102+
AxisConfiguration axisConfig = configurationContext.getAxisConfiguration();
103+
AxisService service = new AxisService(serviceName);
104+
service.addParameter(new Parameter("ServiceClass", className));
105+
axisConfig.addService(service);
106+
return service;
107+
}
108+
109+
private void addOperation(AxisService service, String opName) throws Exception {
110+
AxisOperation op = new InOutAxisOperation(new QName(opName));
111+
service.addOperation(op);
112+
}
113+
114+
@Test
115+
public void testAutoSchemaFromServiceClass() throws Exception {
116+
AxisService service = createServiceWithClass("SampleService",
117+
McpAutoSchemaTest.SampleService.class.getName());
118+
addOperation(service, "calculate");
119+
120+
String catalog = generator.generateMcpCatalogJson(null);
121+
assertNotNull(catalog);
122+
123+
JsonNode root = jackson.readTree(catalog);
124+
JsonNode tools = root.get("tools");
125+
assertNotNull(tools);
126+
127+
JsonNode calcTool = null;
128+
for (JsonNode tool : tools) {
129+
if ("calculate".equals(tool.get("name").asText())) {
130+
calcTool = tool;
131+
break;
132+
}
133+
}
134+
assertNotNull("calculate tool should be in catalog", calcTool);
135+
136+
JsonNode schema = calcTool.get("inputSchema");
137+
assertNotNull("inputSchema should be present", schema);
138+
assertEquals("object", schema.get("type").asText());
139+
140+
JsonNode props = schema.get("properties");
141+
assertNotNull("properties should be present", props);
142+
143+
// Verify type mappings
144+
assertTrue("should have count property", props.has("count"));
145+
assertEquals("integer", props.get("count").get("type").asText());
146+
147+
assertTrue("should have price property", props.has("price"));
148+
assertEquals("number", props.get("price").get("type").asText());
149+
150+
assertTrue("should have name property", props.has("name"));
151+
assertEquals("string", props.get("name").get("type").asText());
152+
153+
assertTrue("should have active property", props.has("active"));
154+
assertEquals("boolean", props.get("active").get("type").asText());
155+
156+
assertTrue("should have timestamp property", props.has("timestamp"));
157+
assertEquals("integer", props.get("timestamp").get("type").asText());
158+
}
159+
160+
@Test
161+
public void testAutoSchemaArrayTypes() throws Exception {
162+
AxisService service = createServiceWithClass("SampleService",
163+
McpAutoSchemaTest.SampleService.class.getName());
164+
addOperation(service, "calculate");
165+
166+
String catalog = generator.generateMcpCatalogJson(null);
167+
JsonNode root = jackson.readTree(catalog);
168+
JsonNode calcTool = null;
169+
for (JsonNode tool : root.get("tools")) {
170+
if ("calculate".equals(tool.get("name").asText())) {
171+
calcTool = tool;
172+
break;
173+
}
174+
}
175+
JsonNode props = calcTool.get("inputSchema").get("properties");
176+
177+
// double[] -> array of numbers
178+
assertTrue("should have values property", props.has("values"));
179+
assertEquals("array", props.get("values").get("type").asText());
180+
assertEquals("number", props.get("values").get("items").get("type").asText());
181+
182+
// double[][] -> array of arrays of numbers
183+
assertTrue("should have matrix property", props.has("matrix"));
184+
assertEquals("array", props.get("matrix").get("type").asText());
185+
assertEquals("array", props.get("matrix").get("items").get("type").asText());
186+
assertEquals("number", props.get("matrix").get("items").get("items").get("type").asText());
187+
}
188+
189+
@Test
190+
public void testAutoSchemaSkipsPrimitiveParameter() throws Exception {
191+
// echo(String) has a primitive parameter — should fall back to empty schema
192+
AxisService service = createServiceWithClass("SampleService",
193+
McpAutoSchemaTest.SampleService.class.getName());
194+
addOperation(service, "echo");
195+
196+
String catalog = generator.generateMcpCatalogJson(null);
197+
JsonNode root = jackson.readTree(catalog);
198+
JsonNode echoTool = null;
199+
for (JsonNode tool : root.get("tools")) {
200+
if ("echo".equals(tool.get("name").asText())) {
201+
echoTool = tool;
202+
break;
203+
}
204+
}
205+
assertNotNull("echo tool should be in catalog", echoTool);
206+
JsonNode props = echoTool.get("inputSchema").get("properties");
207+
assertEquals("String param should produce empty properties",
208+
0, props.size());
209+
}
210+
211+
@Test
212+
public void testExplicitSchemaOverridesAutoGeneration() throws Exception {
213+
AxisService service = createServiceWithClass("SampleService",
214+
McpAutoSchemaTest.SampleService.class.getName());
215+
AxisOperation op = new InOutAxisOperation(new QName("calculate"));
216+
op.addParameter(new Parameter("mcpInputSchema",
217+
"{\"type\":\"object\",\"properties\":{\"custom\":{\"type\":\"string\"}}}"));
218+
service.addOperation(op);
219+
220+
String catalog = generator.generateMcpCatalogJson(null);
221+
JsonNode root = jackson.readTree(catalog);
222+
JsonNode calcTool = null;
223+
for (JsonNode tool : root.get("tools")) {
224+
if ("calculate".equals(tool.get("name").asText())) {
225+
calcTool = tool;
226+
break;
227+
}
228+
}
229+
JsonNode props = calcTool.get("inputSchema").get("properties");
230+
assertTrue("explicit schema should have custom property", props.has("custom"));
231+
assertFalse("explicit schema should NOT have auto-generated count property",
232+
props.has("count"));
233+
}
234+
235+
@Test
236+
public void testNoServiceClassProducesEmptySchema() throws Exception {
237+
// Service with SpringBeanName only, no ServiceClass, no HttpServletRequest
238+
AxisConfiguration axisConfig = configurationContext.getAxisConfiguration();
239+
AxisService service = new AxisService("SpringOnlyService");
240+
service.addParameter(new Parameter("SpringBeanName", "myBean"));
241+
axisConfig.addService(service);
242+
addOperation(service, "doSomething");
243+
244+
String catalog = generator.generateMcpCatalogJson(null);
245+
JsonNode root = jackson.readTree(catalog);
246+
JsonNode tool = null;
247+
for (JsonNode t : root.get("tools")) {
248+
if ("doSomething".equals(t.get("name").asText())) {
249+
tool = t;
250+
break;
251+
}
252+
}
253+
assertNotNull(tool);
254+
// Without HttpServletRequest, Spring bean can't be resolved — empty schema
255+
JsonNode props = tool.get("inputSchema").get("properties");
256+
assertEquals("Should fall back to empty schema without request", 0, props.size());
257+
}
258+
259+
@Test
260+
public void testBooleanIsGetterDetected() throws Exception {
261+
AxisService service = createServiceWithClass("SampleService",
262+
McpAutoSchemaTest.SampleService.class.getName());
263+
addOperation(service, "calculate");
264+
265+
String catalog = generator.generateMcpCatalogJson(null);
266+
JsonNode root = jackson.readTree(catalog);
267+
JsonNode calcTool = null;
268+
for (JsonNode tool : root.get("tools")) {
269+
if ("calculate".equals(tool.get("name").asText())) {
270+
calcTool = tool;
271+
break;
272+
}
273+
}
274+
JsonNode props = calcTool.get("inputSchema").get("properties");
275+
assertTrue("isActive() should produce active property", props.has("active"));
276+
assertEquals("boolean", props.get("active").get("type").asText());
277+
}
278+
}

0 commit comments

Comments
 (0)