Skip to content

Commit f4c2a3e

Browse files
committed
content response refers to structuredContent
1 parent f688d9f commit f4c2a3e

File tree

6 files changed

+90
-11
lines changed

6 files changed

+90
-11
lines changed

+prodserver/+mcp/+internal/defineForMCP.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
end
5050

5151
% MCP tool JSON expects tools field to have an array value,
52-
% event if there is only one tool. The only way to ensure
52+
% even if there is only one tool. The only way to ensure
5353
% that is to place the tool description in a cell array.
5454
% Which is a good idea anyway because the structures won't
5555
% be identical even at the highest level, so might not

+prodserver/+mcp/+internal/mcpHandler.m

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -208,9 +208,12 @@
208208
r.structuredContent.(out{n}) = outArgs{n};
209209
end
210210

211-
% Should not be required but some clients apparently don't
212-
% implement the protocol correctly.
213-
r.content = {};
211+
% Should not be required but some clients require non-empty
212+
% content, even when structuredContent has a value (looking at you,
213+
% Claude).
214+
indirection.type = "text";
215+
indirection.text = MCPConstants.IndirectionMsg;
216+
r.content = {indirection};
214217
end
215218

216219
if ~isempty(result)

+prodserver/+mcp/MCPConstants.m

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121

2222
DefaultArgType = "double";
2323

24+
IndirectionMsg = "Refer to structuredContent for tool results.";
25+
2426
% Call and response
2527
Ping = "ping";
2628
Pong = "pong";

Test/unit/handlers/HandlerBase.m

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,13 @@ function initTest(test)
1818

1919
% Put the earthquake and signal example folders on the path.
2020
import matlab.unittest.fixtures.PathFixture
21-
test.applyFixture(PathFixture(fullfile(test.exampleFolder,...
22-
"Earthquake")));
23-
test.applyFixture(PathFixture(fullfile(test.exampleFolder,...
24-
"Primes")));
21+
22+
earthquakeFolder = fullfile(test.exampleFolder,...
23+
"Earthquake");
24+
test.applyFixture(PathFixture(earthquakeFolder));
25+
26+
primeFolder = fullfile(test.exampleFolder,"Primes");
27+
test.applyFixture(PathFixture(primeFolder));
2528

2629
% Make a temporary folder for output and put it on the path
2730
import matlab.unittest.fixtures.TemporaryFolderFixture
@@ -34,8 +37,28 @@ function initTest(test)
3437
% work.
3538
test.fcnNames = ["plotTrajectoriesMCP","primeSequence"];
3639
test.toolNames = ["plotTrajectories","primeSequence"];
37-
definition = prodserver.mcp.internal.defineForMCP(...
38-
test.toolNames, test.fcnNames);
40+
41+
% Definition generation support in 26a and later.
42+
if isMATLABReleaseOlderThan("R2026a")
43+
dFiles = [ ...
44+
fullfile(earthquakeFolder,"plotTrajectories.json"), ...
45+
fullfile(primeFolder,"primeSequence.json"), ...
46+
];
47+
dJSON = arrayfun(@(f)jsondecode(fileread(f)),dFiles, ...
48+
UniformOutput=false);
49+
definition.tools = cell(1,numel(dJSON));
50+
for n = 1:numel(dJSON)
51+
td = dJSON{n};
52+
definition.tools{n} = td.tools;
53+
for f = string(fieldnames(td.signatures))'
54+
definition.signatures.(f) = td.signatures.(f);
55+
end
56+
end
57+
else
58+
definition = prodserver.mcp.internal.defineForMCP(...
59+
test.toolNames, test.fcnNames);
60+
end
61+
3962
test.definitionFile = fullfile(test.tempFolder.Folder,...
4063
MCPConstants.DefinitionFile);
4164
def.(MCPConstants.DefinitionVariable) = definition;

Test/unit/handlers/tSignature.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ function cathode(test)
5656
function anode(test)
5757
import prodserver.mcp.MCPConstants
5858

59-
req =test.request;
59+
req = test.request;
6060
req.Headers = [req.Headers; {MCPConstants.ContentType, 'application/json'}];
6161

6262
body = { 17, { struct.empty }, struct('x',21), 867.5309 };

Test/unit/handlers/tmcpHandler.m

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
classdef tmcpHandler < HandlerBase
22

33
methods(Test)
4+
45
function toolsList(test)
56
import prodserver.mcp.internal.mcpHandler
67
import prodserver.mcp.internal.hasField
@@ -54,6 +55,56 @@ function notStreamable(test)
5455
response = prodserver.mcp.internal.mcpHandler(reqT);
5556
test.verifyEqual(response.HttpCode,405);
5657
end
58+
59+
function content(test)
60+
import prodserver.mcp.internal.mcpHandler
61+
import prodserver.mcp.MCPConstants
62+
import prodserver.mcp.internal.hasField
63+
64+
% Get base request structure
65+
req = test.request;
66+
req.Headers = [req.Headers; {MCPConstants.ContentType, ...
67+
'application/json'}];
68+
69+
% Make up a session ID
70+
id = matlab.lang.internal.uuid;
71+
req.Headers = [req.Headers; { MCPConstants.SessionId id} ];
72+
73+
% Call primeSequence requesting 13 Eisenstein primes.
74+
body = [...
75+
'{' ...
76+
' "jsonrpc": "2.0",' ...
77+
' "id": 1,'...
78+
' "method": "tools/call",'...
79+
' "params": {'...
80+
' "name": "primeSequence",'...
81+
' "arguments": {'...
82+
' "n": 13,'...
83+
' "type": "Eisenstein"'...
84+
' }'...
85+
' }'...
86+
'}'
87+
];
88+
reqT = req;
89+
reqT.Method = "POST"; % Because there's a body
90+
reqT.Path = "http://localhost:9910/prime/mcp";
91+
reqT.Headers = [reqT.Headers; {MCPConstants.ContentLength, numel(body)}];
92+
reqT.Body = unicode2native(body,"UTF-8");
93+
response = prodserver.mcp.internal.mcpHandler(reqT);
94+
response = prodserver.mcp.internal.decodeBody(response);
95+
96+
% Expecting content and structuredContent
97+
test.verifyEqual(response.id, 1);
98+
test.verifyTrue(hasField(response, 'result.content'));
99+
test.verifyTrue(hasField(response, 'result.structuredContent'));
100+
101+
% Result is a column vector.
102+
expected.seq = primeSequence(13,"Eisenstein")';
103+
test.verifyEqual(response.result.structuredContent,expected);
104+
% Since HTTP interface sends all strings as char
105+
test.verifyEqual(response.result.content.text, ...
106+
char(MCPConstants.IndirectionMsg));
107+
end
57108
end
58109

59110
end

0 commit comments

Comments
 (0)