Skip to content

Commit d76ae63

Browse files
authored
Merge pull request #35 from 0xeb/fix/issue-33-interop-stdio
fix(client+stdio): normalize JSON-RPC envelopes and notification handling
2 parents 4837e8e + 0116c58 commit d76ae63

4 files changed

Lines changed: 310 additions & 79 deletions

File tree

include/fastmcpp/client/client.hpp

Lines changed: 119 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -285,11 +285,13 @@ class Client
285285
response = invoke_request();
286286
}
287287

288+
const auto& response_body = unwrap_rpc_result(response);
289+
288290
// Optional server-side progress events
289-
if (options.progress_handler && response.contains("progress") &&
290-
response["progress"].is_array())
291+
if (options.progress_handler && response_body.contains("progress") &&
292+
response_body["progress"].is_array())
291293
{
292-
for (const auto& p : response["progress"])
294+
for (const auto& p : response_body["progress"])
293295
{
294296
float value = p.value("progress", 0.0f);
295297
std::optional<float> total = std::nullopt;
@@ -301,9 +303,9 @@ class Client
301303
}
302304

303305
// Notification forwarding (sampling/elicitation/roots) if provided by server
304-
if (response.contains("notifications") && response["notifications"].is_array())
306+
if (response_body.contains("notifications") && response_body["notifications"].is_array())
305307
{
306-
for (const auto& n : response["notifications"])
308+
for (const auto& n : response_body["notifications"])
307309
{
308310
if (!n.contains("method"))
309311
continue;
@@ -385,16 +387,18 @@ class Client
385387
TaskStatus get_task_status(const std::string& task_id)
386388
{
387389
fastmcpp::Json response = call("tasks/get", {{"taskId", task_id}});
390+
const auto& body = unwrap_rpc_result(response);
388391
TaskStatus status;
389-
from_json(response, status);
392+
from_json(body, status);
390393
return status;
391394
}
392395

393396
/// Retrieve raw task result via MCP 'tasks/result' (tool/prompt/resource specific).
394397
/// Callers are responsible for parsing into appropriate result type.
395398
fastmcpp::Json get_task_result_raw(const std::string& task_id)
396399
{
397-
return call("tasks/result", {{"taskId", task_id}});
400+
fastmcpp::Json response = call("tasks/result", {{"taskId", task_id}});
401+
return unwrap_rpc_result(response);
398402
}
399403

400404
/// List tasks via MCP 'tasks/list'. Returns raw JSON as provided by server.
@@ -406,16 +410,18 @@ class Client
406410
params["cursor"] = *cursor;
407411
if (limit > 0)
408412
params["limit"] = limit;
409-
return call("tasks/list", params);
413+
fastmcpp::Json response = call("tasks/list", params);
414+
return unwrap_rpc_result(response);
410415
}
411416

412417
/// Cancel a background task via MCP 'tasks/cancel'. Returns final task status.
413418
/// @throws fastmcpp::Error if task does not exist or server returns error
414419
TaskStatus cancel_task(const std::string& task_id)
415420
{
416421
fastmcpp::Json response = call("tasks/cancel", {{"taskId", task_id}});
422+
const auto& body = unwrap_rpc_result(response);
417423
TaskStatus status;
418-
from_json(response, status);
424+
from_json(body, status);
419425
return status;
420426
}
421427

@@ -705,9 +711,10 @@ class Client
705711
void poll_notifications()
706712
{
707713
auto response = call("notifications/poll", fastmcpp::Json::object());
708-
if (!response.contains("notifications") || !response["notifications"].is_array())
714+
const auto& body = unwrap_rpc_result(response);
715+
if (!body.contains("notifications") || !body["notifications"].is_array())
709716
return;
710-
for (const auto& n : response["notifications"])
717+
for (const auto& n : body["notifications"])
711718
{
712719
if (!n.contains("method"))
713720
continue;
@@ -916,36 +923,66 @@ class Client
916923
return value;
917924
}
918925

926+
const fastmcpp::Json& unwrap_rpc_result(const fastmcpp::Json& response)
927+
{
928+
if (!response.is_object())
929+
return response;
930+
931+
if (response.contains("error"))
932+
{
933+
if (response["error"].is_object())
934+
{
935+
const auto& error = response["error"];
936+
std::string message = error.value("message", "Unknown JSON-RPC error");
937+
if (error.contains("code") && error["code"].is_number_integer())
938+
{
939+
throw fastmcpp::Error("JSON-RPC error (" +
940+
std::to_string(error["code"].get<int>()) + "): " +
941+
message);
942+
}
943+
throw fastmcpp::Error("JSON-RPC error: " + message);
944+
}
945+
throw fastmcpp::Error("JSON-RPC error: " + response["error"].dump());
946+
}
947+
948+
if (response.contains("result"))
949+
return response["result"];
950+
951+
return response;
952+
}
953+
919954
ListToolsResult parse_list_tools_result(const fastmcpp::Json& response)
920955
{
956+
const auto& body = unwrap_rpc_result(response);
921957
ListToolsResult result;
922-
if (response.contains("tools"))
923-
for (const auto& t : response["tools"])
958+
if (body.contains("tools"))
959+
for (const auto& t : body["tools"])
924960
result.tools.push_back(t.get<ToolInfo>());
925-
if (response.contains("nextCursor"))
926-
result.nextCursor = response["nextCursor"].get<std::string>();
927-
if (response.contains("_meta"))
928-
result._meta = response["_meta"];
961+
if (body.contains("nextCursor"))
962+
result.nextCursor = body["nextCursor"].get<std::string>();
963+
if (body.contains("_meta"))
964+
result._meta = body["_meta"];
929965
return result;
930966
}
931967

932968
CallToolResult parse_call_tool_result(const fastmcpp::Json& response,
933969
const std::string& tool_name)
934970
{
971+
const auto& body = unwrap_rpc_result(response);
935972

936973
CallToolResult result;
937-
result.isError = response.value("isError", false);
974+
result.isError = body.value("isError", false);
938975

939-
if (!response.contains("content"))
976+
if (!body.contains("content"))
940977
throw fastmcpp::ValidationError("tools/call response missing content");
941978

942-
if (response.contains("content"))
943-
for (const auto& c : response["content"])
979+
if (body.contains("content"))
980+
for (const auto& c : body["content"])
944981
result.content.push_back(parse_content_block(c));
945982

946-
if (response.contains("structuredContent"))
983+
if (body.contains("structuredContent"))
947984
{
948-
result.structuredContent = response["structuredContent"];
985+
result.structuredContent = body["structuredContent"];
949986
// Try to provide a convenient data view similar to Python
950987
auto structured = *result.structuredContent;
951988
auto it = tool_output_schemas_.find(tool_name);
@@ -1011,31 +1048,33 @@ class Client
10111048
}
10121049
}
10131050

1014-
if (response.contains("_meta"))
1015-
result.meta = response["_meta"];
1051+
if (body.contains("_meta"))
1052+
result.meta = body["_meta"];
10161053

10171054
return result;
10181055
}
10191056

10201057
ListResourcesResult parse_list_resources_result(const fastmcpp::Json& response)
10211058
{
1059+
const auto& body = unwrap_rpc_result(response);
10221060
ListResourcesResult result;
1023-
if (response.contains("resources"))
1024-
for (const auto& r : response["resources"])
1061+
if (body.contains("resources"))
1062+
for (const auto& r : body["resources"])
10251063
result.resources.push_back(r.get<ResourceInfo>());
1026-
if (response.contains("nextCursor"))
1027-
result.nextCursor = response["nextCursor"].get<std::string>();
1028-
if (response.contains("_meta"))
1029-
result._meta = response["_meta"];
1064+
if (body.contains("nextCursor"))
1065+
result.nextCursor = body["nextCursor"].get<std::string>();
1066+
if (body.contains("_meta"))
1067+
result._meta = body["_meta"];
10301068
return result;
10311069
}
10321070

10331071
ListResourceTemplatesResult parse_list_resource_templates_result(const fastmcpp::Json& response)
10341072
{
1073+
const auto& body = unwrap_rpc_result(response);
10351074
ListResourceTemplatesResult result;
1036-
if (response.contains("resourceTemplates"))
1075+
if (body.contains("resourceTemplates"))
10371076
{
1038-
for (const auto& r : response["resourceTemplates"])
1077+
for (const auto& r : body["resourceTemplates"])
10391078
{
10401079
ResourceTemplate rt;
10411080
rt.uriTemplate = r.at("uriTemplate").get<std::string>();
@@ -1071,45 +1110,48 @@ class Client
10711110
result.resourceTemplates.push_back(rt);
10721111
}
10731112
}
1074-
if (response.contains("nextCursor"))
1075-
result.nextCursor = response["nextCursor"].get<std::string>();
1076-
if (response.contains("_meta"))
1077-
result._meta = response["_meta"];
1113+
if (body.contains("nextCursor"))
1114+
result.nextCursor = body["nextCursor"].get<std::string>();
1115+
if (body.contains("_meta"))
1116+
result._meta = body["_meta"];
10781117
return result;
10791118
}
10801119

10811120
ReadResourceResult parse_read_resource_result(const fastmcpp::Json& response)
10821121
{
1122+
const auto& body = unwrap_rpc_result(response);
10831123
ReadResourceResult result;
1084-
if (response.contains("contents"))
1085-
for (const auto& c : response["contents"])
1124+
if (body.contains("contents"))
1125+
for (const auto& c : body["contents"])
10861126
result.contents.push_back(parse_resource_content(c));
1087-
if (response.contains("_meta"))
1088-
result._meta = response["_meta"];
1127+
if (body.contains("_meta"))
1128+
result._meta = body["_meta"];
10891129
return result;
10901130
}
10911131

10921132
ListPromptsResult parse_list_prompts_result(const fastmcpp::Json& response)
10931133
{
1134+
const auto& body = unwrap_rpc_result(response);
10941135
ListPromptsResult result;
1095-
if (response.contains("prompts"))
1096-
for (const auto& p : response["prompts"])
1136+
if (body.contains("prompts"))
1137+
for (const auto& p : body["prompts"])
10971138
result.prompts.push_back(p.get<PromptInfo>());
1098-
if (response.contains("nextCursor"))
1099-
result.nextCursor = response["nextCursor"].get<std::string>();
1100-
if (response.contains("_meta"))
1101-
result._meta = response["_meta"];
1139+
if (body.contains("nextCursor"))
1140+
result.nextCursor = body["nextCursor"].get<std::string>();
1141+
if (body.contains("_meta"))
1142+
result._meta = body["_meta"];
11021143
return result;
11031144
}
11041145

11051146
GetPromptResult parse_get_prompt_result(const fastmcpp::Json& response)
11061147
{
1148+
const auto& body = unwrap_rpc_result(response);
11071149
GetPromptResult result;
1108-
if (response.contains("description"))
1109-
result.description = response["description"].get<std::string>();
1110-
if (response.contains("messages"))
1150+
if (body.contains("description"))
1151+
result.description = body["description"].get<std::string>();
1152+
if (body.contains("messages"))
11111153
{
1112-
for (const auto& m : response["messages"])
1154+
for (const auto& m : body["messages"])
11131155
{
11141156
PromptMessage msg;
11151157
std::string role = m.at("role").get<std::string>();
@@ -1136,37 +1178,39 @@ class Client
11361178
result.messages.push_back(msg);
11371179
}
11381180
}
1139-
if (response.contains("_meta"))
1140-
result._meta = response["_meta"];
1181+
if (body.contains("_meta"))
1182+
result._meta = body["_meta"];
11411183
return result;
11421184
}
11431185

11441186
CompleteResult parse_complete_result(const fastmcpp::Json& response)
11451187
{
1188+
const auto& body = unwrap_rpc_result(response);
11461189
CompleteResult result;
1147-
if (response.contains("completion"))
1190+
if (body.contains("completion"))
11481191
{
1149-
const auto& c = response["completion"];
1192+
const auto& c = body["completion"];
11501193
if (c.contains("values"))
11511194
for (const auto& v : c["values"])
11521195
result.completion.values.push_back(v.get<std::string>());
11531196
if (c.contains("total"))
11541197
result.completion.total = c["total"].get<int>();
11551198
result.completion.hasMore = c.value("hasMore", false);
11561199
}
1157-
if (response.contains("_meta"))
1158-
result._meta = response["_meta"];
1200+
if (body.contains("_meta"))
1201+
result._meta = body["_meta"];
11591202
return result;
11601203
}
11611204

11621205
InitializeResult parse_initialize_result(const fastmcpp::Json& response)
11631206
{
1207+
const auto& body = unwrap_rpc_result(response);
11641208
InitializeResult result;
1165-
result.protocolVersion = response.value("protocolVersion", "2024-11-05");
1209+
result.protocolVersion = body.value("protocolVersion", "2024-11-05");
11661210

1167-
if (response.contains("capabilities"))
1211+
if (body.contains("capabilities"))
11681212
{
1169-
const auto& caps = response["capabilities"];
1213+
const auto& caps = body["capabilities"];
11701214
if (caps.contains("experimental"))
11711215
result.capabilities.experimental = caps["experimental"];
11721216
if (caps.contains("logging"))
@@ -1185,17 +1229,17 @@ class Client
11851229
result.capabilities.extensions = caps["extensions"];
11861230
}
11871231

1188-
if (response.contains("serverInfo"))
1232+
if (body.contains("serverInfo"))
11891233
{
1190-
result.serverInfo.name = response["serverInfo"].value("name", "unknown");
1191-
result.serverInfo.version = response["serverInfo"].value("version", "unknown");
1234+
result.serverInfo.name = body["serverInfo"].value("name", "unknown");
1235+
result.serverInfo.version = body["serverInfo"].value("version", "unknown");
11921236
}
11931237

1194-
if (response.contains("instructions"))
1195-
result.instructions = response["instructions"].get<std::string>();
1238+
if (body.contains("instructions"))
1239+
result.instructions = body["instructions"].get<std::string>();
11961240

1197-
if (response.contains("_meta"))
1198-
result._meta = response["_meta"];
1241+
if (body.contains("_meta"))
1242+
result._meta = body["_meta"];
11991243

12001244
return result;
12011245
}
@@ -1588,18 +1632,19 @@ inline std::shared_ptr<ResourceTask> Client::read_resource_task(const std::strin
15881632
payload["_meta"] = *propagated_meta;
15891633

15901634
auto response = call("resources/read", payload);
1635+
const auto& body = unwrap_rpc_result(response);
15911636

1592-
if (response.contains("_meta") && response["_meta"].contains("modelcontextprotocol.io/task"))
1637+
if (body.contains("_meta") && body["_meta"].contains("modelcontextprotocol.io/task"))
15931638
{
1594-
const auto& task_obj = response["_meta"]["modelcontextprotocol.io/task"];
1639+
const auto& task_obj = body["_meta"]["modelcontextprotocol.io/task"];
15951640
if (task_obj.contains("taskId"))
15961641
{
15971642
std::string task_id = task_obj["taskId"].get<std::string>();
15981643
return std::make_shared<ResourceTask>(this, std::move(task_id), uri, std::nullopt);
15991644
}
16001645
}
16011646

1602-
ReadResourceResult result = parse_read_resource_result(response);
1647+
ReadResourceResult result = parse_read_resource_result(body);
16031648
return std::make_shared<ResourceTask>(this, std::string{}, uri, std::move(result.contents));
16041649
}
16051650

@@ -1625,18 +1670,19 @@ Client::get_prompt_task(const std::string& name, const fastmcpp::Json& arguments
16251670
payload["_meta"] = *propagated_meta;
16261671

16271672
auto response = call("prompts/get", payload);
1673+
const auto& body = unwrap_rpc_result(response);
16281674

1629-
if (response.contains("_meta") && response["_meta"].contains("modelcontextprotocol.io/task"))
1675+
if (body.contains("_meta") && body["_meta"].contains("modelcontextprotocol.io/task"))
16301676
{
1631-
const auto& task_obj = response["_meta"]["modelcontextprotocol.io/task"];
1677+
const auto& task_obj = body["_meta"]["modelcontextprotocol.io/task"];
16321678
if (task_obj.contains("taskId"))
16331679
{
16341680
std::string task_id = task_obj["taskId"].get<std::string>();
16351681
return std::make_shared<PromptTask>(this, std::move(task_id), name, std::nullopt);
16361682
}
16371683
}
16381684

1639-
GetPromptResult result = parse_get_prompt_result(response);
1685+
GetPromptResult result = parse_get_prompt_result(body);
16401686
return std::make_shared<PromptTask>(this, std::string{}, name, std::move(result));
16411687
}
16421688

0 commit comments

Comments
 (0)