Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -1001,6 +1001,12 @@ private final class McpToolsCallProxy extends McpHttpProxy
// response completes to resolve tool.summary's ${result.*} references without re-scanning a buffer
private final Map<String, String> capturedResults = new HashMap<>();

// true when this tool's structuredContent is McpHttpResultWrap-wrapped as {"result":<value>} to satisfy
// the MCP wire contract that structuredContent is always a JSON object; a bare ${result} then means the
// real (unwrapped) response value, so it is resolved against the captured "result" key rather than the
// captured root -- keeping the wrapper an implementation detail invisible to tool.summary authors
private boolean resultWrapped;

// the non-2xx response mode: relays the raw upstream body as escaped text with no JsonPipeline at all
// (the body is not guaranteed to be valid JSON), using errorGenerator directly the same way responseStep
// uses responseGenerator — wrap against encodeSlot's live position, drive via consumed()/length()
Expand Down Expand Up @@ -1353,9 +1359,10 @@ void responseBegin(
final String summaryTemplate = tool != null ? tool.summary : null;
final List<String> resultPaths = tool != null ? toolResultReferences(tool) : List.of();

resultWrapped = tool != null && tool.outputMaybeWrapped;
responseGenerator = JsonEx.createGenerator();
JsonStream stream = JsonEx.stream(JsonEx.createParser());
if (tool != null && tool.outputMaybeWrapped)
if (resultWrapped)
{
// nothing proves the upstream body is already an object; route it through the
// transform that decides, from the real body's own first event, whether it actually
Expand Down Expand Up @@ -1453,15 +1460,19 @@ private JsonPipeline.Status errorRelayStep(
return status;
}

// Resolves a result.<path> reference from the values McpHttpResults captured while structuredContent
// streamed past, replacing a re-scan of a fully buffered response copy.
// Resolves a result.<path> reference, or a bare result reference (the response's own root value),
// from the values McpHttpResults captured while structuredContent streamed past, replacing a
// re-scan of a fully buffered response copy. When resultWrapped, the captured root is the
// McpHttpResultWrap envelope rather than the real value, so a bare ${result} is redirected to the
// captured "result" key instead -- see resultWrapped's field doc.
private String resolveCapturedResult(
String expression)
{
String value = "";
if (expression.startsWith("result."))
if ("result".equals(expression) || expression.startsWith("result."))
{
final String captured = capturedResults.get(expression.substring(7));
final String path = "result".equals(expression) ? (resultWrapped ? "result" : "") : expression.substring(7);
final String captured = capturedResults.get(path);
value = captured != null ? captured : "";
}
return value;
Expand Down Expand Up @@ -2398,7 +2409,14 @@ private List<String> toolResultReferences(
private List<String> newToolResultReferences(
McpHttpToolConfig tool)
{
return resultReferences(tool.summary);
final List<String> paths = resultReferences(tool.summary);
if (tool.outputMaybeWrapped)
{
// the streamed root is the McpHttpResultWrap envelope, not the real value -- capture "result"
// (the envelope's own value key) in place of a bare root capture, matching resolveCapturedResult
paths.replaceAll(path -> path.isEmpty() ? "result" : path);
}
return paths;
}

private void appendQuery(
Expand Down Expand Up @@ -2827,25 +2845,39 @@ private static List<String> argReferences(

// Extracts the result.<path> references from a tool.summary template (e.g. "result.number" from
// "Created pull request #${result.number}"), the set McpHttpResults is asked to capture as the response
// streams past.
// streams past. A bare ${result} (no path) — the response body's own root value — is recorded as the
// empty-string path, McpHttpResults' sentinel for a root capture.
private static List<String> resultReferences(
String template)
{
final List<String> result = new ArrayList<>();
if (template != null)
{
int index = 0;
int start = template.indexOf("${result.", index);
int start = template.indexOf("${result", index);
while (start >= 0)
{
final int end = template.indexOf('}', start);
if (end < 0)
final int afterKeyword = start + 8;
if (afterKeyword < template.length() && template.charAt(afterKeyword) == '.')
{
break;
final int end = template.indexOf('}', afterKeyword);
if (end < 0)
{
break;
}
result.add(template.substring(afterKeyword + 1, end));
index = end + 1;
}
result.add(template.substring(start + 9, end));
index = end + 1;
start = template.indexOf("${result.", index);
else if (afterKeyword < template.length() && template.charAt(afterKeyword) == '}')
{
result.add("");
index = afterKeyword + 1;
}
else
{
index = afterKeyword;
}
start = template.indexOf("${result", index);
}
}
return result;
Expand Down
Loading
Loading