Skip to content

Commit d140291

Browse files
MCP catalog Phase A3: ticker resolve endpoint in _meta + doc updates
A3 — tickerResolveEndpoint in _meta: Reads global AxisConfiguration parameter "mcpTickerResolveService" (set in axis2.xml as "<parameter name="mcpTickerResolveService"> ServiceName/operationName</parameter>"). When present, adds _meta.tickerResolveEndpoint = "POST /services/<value>" to the catalog so MCP clients can discover the ticker→assetId resolution service without out-of-band documentation. Field is omitted entirely when the parameter is not configured. 2 new tests. Update json-rpc-mcp-guide.xml: - Section 7 limitations table: mark A1 (descriptions), A2 (annotation tuning), A3 (ticker endpoint) as Implemented with config instructions - Section 6.1 test table: add 13 new tests for A1/A2/A3 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent a02a94a commit d140291

3 files changed

Lines changed: 70 additions & 6 deletions

File tree

modules/openapi/src/main/java/org/apache/axis2/openapi/OpenApiSpecGenerator.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -703,6 +703,23 @@ public String generateMcpCatalogJson(HttpServletRequest request) {
703703
meta.put("authHeader", "Authorization: Bearer <token>");
704704
meta.put("tokenEndpoint", "POST /services/loginService/doLogin");
705705

706+
// Optional: natural key (ticker → assetId) resolution endpoint.
707+
// Set axis2.xml global parameter "mcpTickerResolveService" to the
708+
// "ServiceName/operationName" of the ticker lookup operation, e.g.:
709+
// <parameter name="mcpTickerResolveService">
710+
// TickerLookupService/resolveTicker
711+
// </parameter>
712+
// Omitted from _meta when not configured so deployments without a
713+
// ticker service don't expose a misleading endpoint reference.
714+
org.apache.axis2.description.Parameter tickerParam =
715+
axisConfig.getParameter("mcpTickerResolveService");
716+
if (tickerParam != null && tickerParam.getValue() != null) {
717+
String tickerSvcOp = tickerParam.getValue().toString().trim();
718+
if (!tickerSvcOp.isEmpty()) {
719+
meta.put("tickerResolveEndpoint", "POST /services/" + tickerSvcOp);
720+
}
721+
}
722+
706723
com.fasterxml.jackson.databind.node.ArrayNode toolsArray = root.putArray("tools");
707724

708725
Iterator<AxisService> services = axisConfig.getServices().values().iterator();

modules/openapi/src/test/java/org/apache/axis2/openapi/McpCatalogGeneratorTest.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,42 @@ public void testAllAnnotationHintsAreBooleans() throws Exception {
521521
}
522522
}
523523

524+
// ── A3: tickerResolveEndpoint in _meta ───────────────────────────────────
525+
526+
/**
527+
* When the global axis2 parameter {@code mcpTickerResolveService} is set,
528+
* {@code _meta.tickerResolveEndpoint} must appear with the expected prefix.
529+
* This lets MCP clients discover the ticker→assetId resolution service
530+
* without out-of-band documentation.
531+
*/
532+
public void testTickerResolveEndpointPresentWhenConfigured() throws Exception {
533+
axisConfig.addParameter(new org.apache.axis2.description.Parameter(
534+
"mcpTickerResolveService", "TickerLookupService/resolveTicker"));
535+
536+
JsonNode meta = MAPPER.readTree(generator.generateMcpCatalogJson(mockRequest))
537+
.path("_meta");
538+
assertFalse("tickerResolveEndpoint must appear when mcpTickerResolveService is set",
539+
meta.path("tickerResolveEndpoint").isMissingNode());
540+
assertTrue("tickerResolveEndpoint must start with 'POST /services/'",
541+
meta.path("tickerResolveEndpoint").asText().startsWith("POST /services/"));
542+
assertTrue("tickerResolveEndpoint must contain the configured service/op name",
543+
meta.path("tickerResolveEndpoint").asText()
544+
.contains("TickerLookupService/resolveTicker"));
545+
}
546+
547+
/**
548+
* When {@code mcpTickerResolveService} is not configured, the catalog must
549+
* not include {@code tickerResolveEndpoint} at all — deployments without a
550+
* ticker service should not expose a misleading field.
551+
*/
552+
public void testTickerResolveEndpointAbsentWhenNotConfigured() throws Exception {
553+
// No mcpTickerResolveService parameter added
554+
JsonNode meta = MAPPER.readTree(generator.generateMcpCatalogJson(mockRequest))
555+
.path("_meta");
556+
assertTrue("tickerResolveEndpoint must be absent when mcpTickerResolveService not set",
557+
meta.path("tickerResolveEndpoint").isMissingNode());
558+
}
559+
524560
// ── A1: mcpDescription parameter ─────────────────────────────────────────
525561

526562
/**

src/site/xdoc/docs/json-rpc-mcp-guide.xml

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,17 @@ conformance checklist when extending the catalog or adding new MCP client code.<
399399
<tr><td><code>testAnnotationsHasReadOnlyHint</code></td><td>readOnlyHint boolean present</td></tr>
400400
<tr><td><code>testAllAnnotationHintsAreBooleans</code></td><td>All MCP 2025-03-26 hints are booleans, not strings</td></tr>
401401
<tr><td><code>testMcpToolsMatchOpenApiPaths</code></td><td>Every MCP tool endpoint has a matching OpenAPI path</td></tr>
402+
<tr><td><code>testTickerResolveEndpointPresentWhenConfigured</code></td><td>_meta.tickerResolveEndpoint appears when mcpTickerResolveService global param is set</td></tr>
403+
<tr><td><code>testTickerResolveEndpointAbsentWhenNotConfigured</code></td><td>tickerResolveEndpoint absent from _meta when param not set (no misleading field)</td></tr>
404+
<tr><td><code>testOperationLevelMcpDescriptionOverridesDefault</code></td><td>mcpDescription on AxisOperation replaces auto-generated "ServiceName: opName"</td></tr>
405+
<tr><td><code>testServiceLevelMcpDescriptionUsedWhenNoOperationLevel</code></td><td>Service-level mcpDescription used as fallback when operation has none</td></tr>
406+
<tr><td><code>testOperationLevelMcpDescriptionTakesPrecedenceOverServiceLevel</code></td><td>Operation-level wins over service-level when both set</td></tr>
407+
<tr><td><code>testDescriptionFallsBackToAutoGeneratedWhenNoMcpDescriptionParam</code></td><td>Auto-generated fallback still produced when no mcpDescription param present</td></tr>
408+
<tr><td><code>testServiceLevelMcpReadOnlySetsTrueOnAnnotation</code></td><td>mcpReadOnly=true on service → readOnlyHint: true on all its tools</td></tr>
409+
<tr><td><code>testOperationLevelMcpReadOnlyOverridesServiceLevel</code></td><td>Operation-level mcpReadOnly takes precedence over service-level</td></tr>
410+
<tr><td><code>testMcpIdempotentParamSetsIdempotentHint</code></td><td>mcpIdempotent=true → idempotentHint: true</td></tr>
411+
<tr><td><code>testMcpDestructiveParamSetsDestructiveHint</code></td><td>mcpDestructive=true → destructiveHint: true</td></tr>
412+
<tr><td><code>testAnnotationDefaultsAreConservativeWhenNoParamsSet</code></td><td>All four hints remain false when no MCP params set (no regression)</td></tr>
402413
</table>
403414

404415
<h3>6.2 Catalog HTTP Handler — McpCatalogHandlerTest (17 tests)</h3>
@@ -487,8 +498,8 @@ Each item notes whether the gap is architectural (won't be added to Axis2) or de
487498
</tr>
488499
<tr>
489500
<td>Natural language tool descriptions</td>
490-
<td>Not implemented — architectural gap</td>
491-
<td><code>description</code> is auto-generated as "ServiceName: operationName". Compare rapi-mcp's curated descriptions: "Get all assets in a fund with optional calculation data". If Claude or another LLM uses the catalog for tool selection, it will have less context. Workaround: add descriptions via an OpenAPI customizer.</td>
501+
<td><strong>Implemented</strong> — set <code>mcpDescription</code> parameter on <code>&lt;operation&gt;</code> or <code>&lt;service&gt;</code> in services.xml</td>
502+
<td>Operation-level parameter takes precedence over service-level; falls back to auto-generated "ServiceName: operationName".</td>
492503
</tr>
493504
<tr>
494505
<td>MCP Resources and Prompts</td>
@@ -497,8 +508,8 @@ Each item notes whether the gap is architectural (won't be added to Axis2) or de
497508
</tr>
498509
<tr>
499510
<td>MCP 2025-03-26 annotation tuning</td>
500-
<td>Deferred</td>
501-
<td>All annotations (<code>readOnlyHint</code>, <code>destructiveHint</code>, <code>idempotentHint</code>, <code>openWorldHint</code>) default to <code>false</code>. Read-only services like <code>GetAssetCalculationsService</code> should have <code>readOnlyHint: true</code>. Requires per-service configuration or introspection of service annotations.</td>
511+
<td><strong>Implemented</strong> — set <code>mcpReadOnly</code>, <code>mcpIdempotent</code>, <code>mcpDestructive</code>, <code>mcpOpenWorld</code> on <code>&lt;operation&gt;</code> or <code>&lt;service&gt;</code> in services.xml</td>
512+
<td>Conservative <code>false</code> defaults preserved when parameters absent. Operation-level overrides service-level.</td>
502513
</tr>
503514
<tr>
504515
<td>Streaming / SSE responses</td>
@@ -517,8 +528,8 @@ Each item notes whether the gap is architectural (won't be added to Axis2) or de
517528
</tr>
518529
<tr>
519530
<td>Natural key resolution (ticker → assetId)</td>
520-
<td>Not in catalog — available via service</td>
521-
<td>Ticker resolution exists as an Axis2 service (used by pyRapi's <code>search_assets</code> tool) but the catalog does not expose a dedicated resolution endpoint. Callers must know fund IDs and asset IDs or call the appropriate lookup service separately.</td>
531+
<td><strong>Implemented</strong> — set global parameter <code>mcpTickerResolveService</code> in axis2.xml</td>
532+
<td>When set to <code>ServiceName/operationName</code>, <code>_meta.tickerResolveEndpoint</code> is added to the catalog. Omitted entirely when not configured so deployments without a ticker service are unaffected.</td>
522533
</tr>
523534
<tr>
524535
<td>Cursor-based pagination</td>

0 commit comments

Comments
 (0)