Skip to content

Commit 83500ec

Browse files
B2/B3 mcpAuthScope + mcpStreaming hint parameters
B2 — mcpAuthScope (operation > service level): When set, the tool node gains an "x-authScope" field that MCP clients supporting fine-grained OAuth2 / custom scopes can use to request only the required scope instead of a broad full-access token. Absent param → no x-authScope field (compact catalog). 4 tests: operation-level, absent, service-level fallback, op overrides svc. B3 — mcpStreaming (operation > service level): When true, the tool node gains "x-streaming": true to signal that the operation returns a stream (chunked JSON, SSE, long-poll) rather than a single response body. MCP clients that support progressive rendering can use this to display partial results as they arrive. false / absent → no x-streaming field (absent = non-streaming default). 4 tests: true, absent, explicit-false-suppressed, service-level fallback. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 33fc4e9 commit 83500ec

2 files changed

Lines changed: 127 additions & 0 deletions

File tree

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -855,6 +855,25 @@ public String generateMcpCatalogJson(HttpServletRequest request) {
855855
// Whether the caller must supply a Bearer token (from doLogin).
856856
toolNode.put("x-requiresAuth", requiresAuth);
857857

858+
// B2 — mcpAuthScope: optional OAuth2 / custom scope string.
859+
// When present, MCP clients that support fine-grained auth can
860+
// request just this scope rather than a full-access token.
861+
// Declared at operation OR service level (operation wins).
862+
// Example services.xml: <parameter name="mcpAuthScope">read:portfolio</parameter>
863+
String authScope = getMcpStringParam(operation, service, "mcpAuthScope", null);
864+
if (authScope != null) {
865+
toolNode.put("x-authScope", authScope);
866+
}
867+
868+
// B3 — mcpStreaming: signals that this operation returns a stream
869+
// (chunked JSON, SSE, or long-poll) rather than a single response.
870+
// MCP clients that support progressive rendering can use this hint
871+
// to display partial results as they arrive.
872+
// Example services.xml: <parameter name="mcpStreaming">true</parameter>
873+
if (getMcpBoolParam(operation, service, "mcpStreaming", false)) {
874+
toolNode.put("x-streaming", true);
875+
}
876+
858877
// MCP 2025-03-26 tool annotations.
859878
// Tunable via services.xml parameters at operation or service level:
860879
// mcpReadOnly → readOnlyHint (true for GET-equivalent operations)

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

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -958,6 +958,114 @@ public void testMcpOpenWorldParamSetsOpenWorldHint() throws Exception {
958958
annotations.path("openWorldHint").asBoolean());
959959
}
960960

961+
// ── B2: mcpAuthScope ─────────────────────────────────────────────────────
962+
963+
public void testMcpAuthScopeParamAppearsAsXAuthScope() throws Exception {
964+
AxisService svc = new AxisService("PortfolioService");
965+
AxisOperation op = new InOutAxisOperation();
966+
op.setName(QName.valueOf("readPositions"));
967+
op.addParameter(new org.apache.axis2.description.Parameter(
968+
"mcpAuthScope", "read:portfolio"));
969+
svc.addOperation(op);
970+
axisConfig.addService(svc);
971+
972+
JsonNode tool = getCatalogTools().get(0);
973+
assertEquals("x-authScope must reflect mcpAuthScope param",
974+
"read:portfolio", tool.path("x-authScope").asText());
975+
}
976+
977+
public void testAbsentMcpAuthScopeProducesNoXAuthScopeField() throws Exception {
978+
addService("PortfolioService", "readPositions");
979+
980+
JsonNode tool = getCatalogTools().get(0);
981+
assertTrue("x-authScope must be absent when mcpAuthScope not set",
982+
tool.path("x-authScope").isMissingNode());
983+
}
984+
985+
public void testServiceLevelMcpAuthScopeAppliedWhenNoOperationLevel() throws Exception {
986+
AxisService svc = new AxisService("PortfolioService");
987+
svc.addParameter(new org.apache.axis2.description.Parameter(
988+
"mcpAuthScope", "read:global"));
989+
AxisOperation op = new InOutAxisOperation();
990+
op.setName(QName.valueOf("readPositions"));
991+
svc.addOperation(op);
992+
axisConfig.addService(svc);
993+
994+
JsonNode tool = getCatalogTools().get(0);
995+
assertEquals("service-level mcpAuthScope must apply when op level absent",
996+
"read:global", tool.path("x-authScope").asText());
997+
}
998+
999+
public void testOperationLevelMcpAuthScopeOverridesServiceLevel() throws Exception {
1000+
AxisService svc = new AxisService("PortfolioService");
1001+
svc.addParameter(new org.apache.axis2.description.Parameter(
1002+
"mcpAuthScope", "read:global"));
1003+
AxisOperation op = new InOutAxisOperation();
1004+
op.setName(QName.valueOf("writePositions"));
1005+
op.addParameter(new org.apache.axis2.description.Parameter(
1006+
"mcpAuthScope", "write:portfolio"));
1007+
svc.addOperation(op);
1008+
axisConfig.addService(svc);
1009+
1010+
JsonNode tool = getCatalogTools().get(0);
1011+
assertEquals("operation-level mcpAuthScope must override service level",
1012+
"write:portfolio", tool.path("x-authScope").asText());
1013+
}
1014+
1015+
// ── B3: mcpStreaming ──────────────────────────────────────────────────────
1016+
1017+
public void testMcpStreamingParamSetsXStreamingTrue() throws Exception {
1018+
AxisService svc = new AxisService("FeedService");
1019+
AxisOperation op = new InOutAxisOperation();
1020+
op.setName(QName.valueOf("streamPrices"));
1021+
op.addParameter(new org.apache.axis2.description.Parameter(
1022+
"mcpStreaming", "true"));
1023+
svc.addOperation(op);
1024+
axisConfig.addService(svc);
1025+
1026+
JsonNode tool = getCatalogTools().get(0);
1027+
assertTrue("x-streaming must be true when mcpStreaming=true",
1028+
tool.path("x-streaming").asBoolean());
1029+
}
1030+
1031+
public void testAbsentMcpStreamingProducesNoXStreamingField() throws Exception {
1032+
addService("FeedService", "getSnapshot");
1033+
1034+
JsonNode tool = getCatalogTools().get(0);
1035+
assertTrue("x-streaming field must be absent when mcpStreaming not set",
1036+
tool.path("x-streaming").isMissingNode());
1037+
}
1038+
1039+
public void testMcpStreamingFalseProducesNoXStreamingField() throws Exception {
1040+
// mcpStreaming=false is the default; field must be suppressed (not emitted as false)
1041+
// to keep the catalog compact — clients treat absence as non-streaming.
1042+
AxisService svc = new AxisService("FeedService");
1043+
AxisOperation op = new InOutAxisOperation();
1044+
op.setName(QName.valueOf("getSnapshot"));
1045+
op.addParameter(new org.apache.axis2.description.Parameter(
1046+
"mcpStreaming", "false"));
1047+
svc.addOperation(op);
1048+
axisConfig.addService(svc);
1049+
1050+
JsonNode tool = getCatalogTools().get(0);
1051+
assertTrue("x-streaming must be absent when mcpStreaming=false",
1052+
tool.path("x-streaming").isMissingNode());
1053+
}
1054+
1055+
public void testServiceLevelMcpStreamingApplied() throws Exception {
1056+
AxisService svc = new AxisService("FeedService");
1057+
svc.addParameter(new org.apache.axis2.description.Parameter(
1058+
"mcpStreaming", "true"));
1059+
AxisOperation op = new InOutAxisOperation();
1060+
op.setName(QName.valueOf("streamPrices"));
1061+
svc.addOperation(op);
1062+
axisConfig.addService(svc);
1063+
1064+
JsonNode tool = getCatalogTools().get(0);
1065+
assertTrue("service-level mcpStreaming must apply when op level absent",
1066+
tool.path("x-streaming").asBoolean());
1067+
}
1068+
9611069
// ── helpers ─────────────────────────────────────────────────────────────
9621070

9631071
private void addService(String serviceName, String operationName) throws Exception {

0 commit comments

Comments
 (0)