-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathmcpHTTPClient.m
More file actions
195 lines (175 loc) · 7.13 KB
/
mcpHTTPClient.m
File metadata and controls
195 lines (175 loc) · 7.13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
classdef mcpHTTPClient < handle
% An MCP client (streamable HTTP only)
% Copyright 2025 The MathWorks, Inc.
properties (SetAccess=private)
Endpoint
ServerTools (1,:)
end
properties (Access=private)
counter = 0;
end
properties (Hidden)
% test seam
Send = @send
end
methods
function obj = mcpHTTPClient(endpoint, varargin)
obj.Endpoint = endpoint;
% undocumented test seam
if nargin > 2 && isequal(varargin{1}, "Send")
obj.Send = varargin{2};
end
obj.ServerTools = obj.retrieveTools;
end
function result = callTool(obj, tool, varargin)
if isstruct(tool)
toolName = string(tool.name);
if isfield(tool,"arguments") && ~isstruct(tool.arguments)
tool.arguments = mcp.util.verbatimJSON(tool.arguments);
end
else
toolName = string(tool);
tool = struct(...
"name",tool,"arguments",struct(varargin{:}));
end
for k=1:numel(obj)
toolNames = string(cellfun(@(s) s.name,obj(k).ServerTools,'UniformOutput',false));
if ~any(toolName==toolNames)
continue;
end
sessionIDField = obj(k).initialize();
request = obj(k).makeRequest("tools/call", ...
tool, sessionIDField);
response = obj(k).Send(request,obj(k).Endpoint);
if response.StatusCode == matlab.net.http.StatusCode.Unauthorized
throwError("mcp:needsAuthorization");
elseif response.StatusCode ~= matlab.net.http.StatusCode.OK
throwError("mcp:serverError",getServerErrorText(response));
end
data = response.Body.Data;
if ~isstruct(data)
data = erase(data,"data: ping"+optionalPattern(char(13))+newline);
data = extractBetween(data,"data: ",optionalPattern(char(13))+newline);
data = jsondecode(data);
end
if isfield(data,"error")
error(data.error);
end
if isempty([data.result.content])
result = "";
else
result = string(join([data.result.content.text],""));
end
if isfield(data.result,"isError") && isequal(data.result.isError,true)
error(result);
end
return
end
% if we fall off the end of this for loop, nothing in the input
% vector knows about that tool.
throwError("mcp:unknownTool")
end
end
methods (Access=private)
function sessionIDField = initialize(obj)
request = obj.makeRequest("initialize",struct(...
"protocolVersion", "2025-06-18",...
"capabilities",struct(),...
"clientInfo",struct(...
"name","MATLAB MCP HTTP Client",...
"version","0.0.1"...
)...
),[]);
try
response = obj.Send(request,obj.Endpoint);
catch exc
if isa(exc, "matlab.net.http.HTTPException")
if exc.identifier == "MATLAB:webservices:UnknownHost"
throwError("mcp:noSuchHost",obj.Endpoint);
end
end
throw(exc);
end
values = [response.Header.Value];
sessionID = values([response.Header.Name] == "mcp-session-id");
sessionIDField = matlab.net.http.field.GenericParameterizedField("mcp-session-id",sessionID);
initDone = obj.makeRequest("notifications/initialized",[],sessionIDField);
obj.Send(initDone,obj.Endpoint);
end
function body = makeBody(obj,method,params)
payload = struct(...
"jsonrpc","2.0",...
"method",method);
if ~strcmp(method,"notifications/initialized")
obj.counter = obj.counter + 1;
payload.id = obj.counter;
end
if ~isequal(params,[])
payload.params = params;
end
body = matlab.net.http.MessageBody(payload);
end
function request = makeRequest(obj,method,params,sessionIDField)
contentTypeField = matlab.net.http.field.ContentTypeField('application/json');
type1 = matlab.net.http.MediaType("application/json");
type2 = matlab.net.http.MediaType("text/event-stream");
acceptField = matlab.net.http.field.AcceptField([type1 type2]);
expectNothing = matlab.net.http.HeaderField('Expect', '');
header = [acceptField contentTypeField sessionIDField expectNothing];
body = obj.makeBody(method,params);
method = matlab.net.http.RequestMethod.POST;
request = matlab.net.http.RequestMessage(method,header,body);
end
function tools = retrieveTools(obj)
arguments
obj (1,1) mcpHTTPClient
end
sessionIDField = obj.initialize();
request = obj.makeRequest("tools/list", struct(), sessionIDField);
response = obj.Send(request,obj.Endpoint);
if response.StatusCode == matlab.net.http.StatusCode.Unauthorized
throwError("mcp:needsAuthorization");
elseif response.StatusCode ~= matlab.net.http.StatusCode.OK
throwError("mcp:serverError",getServerErrorText(response));
end
tools = response.Body.Data;
if ~isstruct(tools)
tools = jsondecode("{"+extractBetween(tools,"data: {","}"+optionalPattern(char(13))+newline)+"}");
end
if isfield(tools,"result") && isfield(tools.result,"tools")
tools = tools.result.tools;
end
if ~iscell(tools)
tools = mat2cell(tools,ones(numel(tools),1));
end
end
end
end
function errortxt = getServerErrorText(response)
errortxt = response.Body.Data;
if isstruct(errortxt)
if isfield(errortxt,"error_description")
errortxt = errortxt.error_description;
else
errortxt = jsonencode(errortxt);
end
end
errortxt = string(errortxt);
if response.Body.ContentType.Subtype == "html"
errortxt = extractHTMLText(errortxt);
end
if strlength(errortxt) < 1
errortxt = string(response.StatusLine);
end
end
function throwError(id,varargin)
persistent catalog
if isempty(catalog)
catalog = dictionary("string", "string");
catalog("mcp:needsAuthorization") = "Servers that require authorization are not supported.";
catalog("mcp:unknownTool") = "Unrecognized tool";
catalog("mcp:serverError") = "Server returned error: ""%s""";
catalog("mcp:noSuchHost") = "Unknown host ""%s""";
end
throwAsCaller(MException(id,catalog(id),varargin{:}));
end