Skip to content

Commit 3c91ac8

Browse files
committed
Add MCP utilities and integration tests
- Add OpenAIMcpUtils helper functions to parse MCP tool outputs - Functions include: extractMcpCalls, wasToolCalled, getToolOutput, getAllToolOutputs - Add getMcpUsageStats for statistics about MCP tool usage - Add comprehensive integration tests for MCP functionality - Tests cover tool configuration, call parsing, and error handling - Based on Python implementation patterns from aideas-labs
1 parent bbea585 commit 3c91ac8

File tree

5 files changed

+500
-0
lines changed

5 files changed

+500
-0
lines changed

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ set(LLMCPP_SOURCES
7373
src/openai/OpenAISchemaBuilder.cpp
7474
src/openai/OpenAIModels.cpp
7575
src/openai/OpenAITypes.cpp
76+
src/openai/OpenAIMcpUtils.cpp
7677
src/openai/OpenAIUtils.cpp
7778
src/anthropic/AnthropicClient.cpp
7879
src/anthropic/AnthropicHttpClient.cpp

include/openai/OpenAIMcpUtils.h

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
#pragma once
2+
3+
#include <nlohmann/json.hpp>
4+
#include <optional>
5+
#include <string>
6+
#include <vector>
7+
8+
#include "openai/OpenAITypes.h"
9+
10+
using json = nlohmann::json;
11+
12+
namespace OpenAI {
13+
14+
/// MCP tool call information extracted from response
15+
struct McpToolCall {
16+
std::string id;
17+
std::string tool;
18+
json input;
19+
std::optional<json> output;
20+
std::optional<std::string> error;
21+
bool success = false;
22+
23+
std::string toString() const {
24+
std::string result = "McpToolCall { id: " + id + ", tool: " + tool;
25+
result += ", success: " + std::string(success ? "true" : "false");
26+
if (error.has_value()) {
27+
result += ", error: " + error.value();
28+
}
29+
if (output.has_value()) {
30+
std::string preview = output.value().dump();
31+
if (preview.length() > 100) {
32+
preview = preview.substr(0, 100) + "...";
33+
}
34+
result += ", output_preview: " + preview;
35+
}
36+
result += " }";
37+
return result;
38+
}
39+
};
40+
41+
/// Utility functions for parsing MCP tool outputs from OpenAI responses
42+
namespace McpUtils {
43+
44+
/**
45+
* Extract all MCP tool calls from a ResponsesResponse
46+
* @param response The OpenAI Responses API response
47+
* @return Vector of McpToolCall structs with details about each tool invocation
48+
*/
49+
std::vector<McpToolCall> extractMcpCalls(const ResponsesResponse& response);
50+
51+
/**
52+
* Check if a specific MCP tool was called in the response
53+
* @param response The OpenAI Responses API response
54+
* @param toolName The name of the tool to check for
55+
* @return true if the tool was called at least once
56+
*/
57+
bool wasToolCalled(const ResponsesResponse& response, const std::string& toolName);
58+
59+
/**
60+
* Get the output from a specific MCP tool call
61+
* @param response The OpenAI Responses API response
62+
* @param toolName The name of the tool
63+
* @return The output JSON if found, std::nullopt otherwise
64+
*/
65+
std::optional<json> getToolOutput(const ResponsesResponse& response, const std::string& toolName);
66+
67+
/**
68+
* Get all outputs from a specific MCP tool (if called multiple times)
69+
* @param response The OpenAI Responses API response
70+
* @param toolName The name of the tool
71+
* @return Vector of output JSON objects
72+
*/
73+
std::vector<json> getAllToolOutputs(const ResponsesResponse& response, const std::string& toolName);
74+
75+
/**
76+
* Check if any MCP tools were listed in the response
77+
* @param response The OpenAI Responses API response
78+
* @return true if mcp_list_tools output item was found
79+
*/
80+
bool wereMcpToolsListed(const ResponsesResponse& response);
81+
82+
/**
83+
* Get the list of available MCP tools from mcp_list_tools output
84+
* @param response The OpenAI Responses API response
85+
* @return Vector of tool names that are available
86+
*/
87+
std::vector<std::string> getAvailableMcpTools(const ResponsesResponse& response);
88+
89+
/**
90+
* Check if all expected tools were called
91+
* @param response The OpenAI Responses API response
92+
* @param expectedTools Vector of tool names that should have been called
93+
* @return true if all expected tools were called
94+
*/
95+
bool wereAllToolsCalled(const ResponsesResponse& response,
96+
const std::vector<std::string>& expectedTools);
97+
98+
/**
99+
* Get summary statistics about MCP tool usage
100+
* @param response The OpenAI Responses API response
101+
* @return JSON object with statistics (total_calls, successful_calls, failed_calls, tools_used)
102+
*/
103+
json getMcpUsageStats(const ResponsesResponse& response);
104+
105+
} // namespace McpUtils
106+
} // namespace OpenAI

src/openai/OpenAIMcpUtils.cpp

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
#include "openai/OpenAIMcpUtils.h"
2+
3+
namespace OpenAI {
4+
namespace McpUtils {
5+
6+
std::vector<McpToolCall> extractMcpCalls(const ResponsesResponse& response) {
7+
std::vector<McpToolCall> calls;
8+
9+
if (response.output.empty()) {
10+
return calls;
11+
}
12+
13+
for (const auto& item : response.output) {
14+
if (!item.is_object() || !item.contains("type")) {
15+
continue;
16+
}
17+
18+
std::string type = item["type"].get<std::string>();
19+
20+
// Look for mcp_call type items
21+
if (type == "mcp_call") {
22+
McpToolCall call;
23+
24+
if (item.contains("id")) {
25+
call.id = item["id"].get<std::string>();
26+
}
27+
28+
if (item.contains("name")) {
29+
call.tool = item["name"].get<std::string>();
30+
}
31+
32+
if (item.contains("input")) {
33+
call.input = item["input"];
34+
}
35+
36+
// Check for output
37+
if (item.contains("output")) {
38+
call.output = item["output"];
39+
call.success = true;
40+
}
41+
42+
// Check for error
43+
if (item.contains("error")) {
44+
call.error = item["error"].is_string() ? item["error"].get<std::string>()
45+
: item["error"].dump();
46+
call.success = false;
47+
}
48+
49+
calls.push_back(call);
50+
}
51+
}
52+
53+
return calls;
54+
}
55+
56+
bool wasToolCalled(const ResponsesResponse& response, const std::string& toolName) {
57+
auto calls = extractMcpCalls(response);
58+
for (const auto& call : calls) {
59+
if (call.tool == toolName) {
60+
return true;
61+
}
62+
}
63+
return false;
64+
}
65+
66+
std::optional<json> getToolOutput(const ResponsesResponse& response, const std::string& toolName) {
67+
auto calls = extractMcpCalls(response);
68+
for (const auto& call : calls) {
69+
if (call.tool == toolName && call.output.has_value()) {
70+
return call.output.value();
71+
}
72+
}
73+
return std::nullopt;
74+
}
75+
76+
std::vector<json> getAllToolOutputs(const ResponsesResponse& response,
77+
const std::string& toolName) {
78+
std::vector<json> outputs;
79+
auto calls = extractMcpCalls(response);
80+
81+
for (const auto& call : calls) {
82+
if (call.tool == toolName && call.output.has_value()) {
83+
outputs.push_back(call.output.value());
84+
}
85+
}
86+
87+
return outputs;
88+
}
89+
90+
bool wereMcpToolsListed(const ResponsesResponse& response) {
91+
if (response.output.empty()) {
92+
return false;
93+
}
94+
95+
for (const auto& item : response.output) {
96+
if (item.is_object() && item.contains("type")) {
97+
std::string type = item["type"].get<std::string>();
98+
if (type == "mcp_list_tools") {
99+
return true;
100+
}
101+
}
102+
}
103+
104+
return false;
105+
}
106+
107+
std::vector<std::string> getAvailableMcpTools(const ResponsesResponse& response) {
108+
std::vector<std::string> tools;
109+
110+
if (response.output.empty()) {
111+
return tools;
112+
}
113+
114+
for (const auto& item : response.output) {
115+
if (!item.is_object() || !item.contains("type")) {
116+
continue;
117+
}
118+
119+
std::string type = item["type"].get<std::string>();
120+
121+
if (type == "mcp_list_tools" && item.contains("tools") && item["tools"].is_array()) {
122+
for (const auto& tool : item["tools"]) {
123+
if (tool.is_object() && tool.contains("name")) {
124+
tools.push_back(tool["name"].get<std::string>());
125+
} else if (tool.is_string()) {
126+
tools.push_back(tool.get<std::string>());
127+
}
128+
}
129+
}
130+
}
131+
132+
return tools;
133+
}
134+
135+
bool wereAllToolsCalled(const ResponsesResponse& response,
136+
const std::vector<std::string>& expectedTools) {
137+
auto calls = extractMcpCalls(response);
138+
139+
for (const auto& expectedTool : expectedTools) {
140+
bool found = false;
141+
for (const auto& call : calls) {
142+
if (call.tool == expectedTool) {
143+
found = true;
144+
break;
145+
}
146+
}
147+
if (!found) {
148+
return false;
149+
}
150+
}
151+
152+
return true;
153+
}
154+
155+
json getMcpUsageStats(const ResponsesResponse& response) {
156+
auto calls = extractMcpCalls(response);
157+
158+
int totalCalls = static_cast<int>(calls.size());
159+
int successfulCalls = 0;
160+
int failedCalls = 0;
161+
std::vector<std::string> toolsUsed;
162+
163+
for (const auto& call : calls) {
164+
if (call.success) {
165+
successfulCalls++;
166+
} else {
167+
failedCalls++;
168+
}
169+
170+
// Track unique tools used
171+
if (std::find(toolsUsed.begin(), toolsUsed.end(), call.tool) == toolsUsed.end()) {
172+
toolsUsed.push_back(call.tool);
173+
}
174+
}
175+
176+
return json{{"total_calls", totalCalls},
177+
{"successful_calls", successfulCalls},
178+
{"failed_calls", failedCalls},
179+
{"tools_used", toolsUsed}};
180+
}
181+
182+
} // namespace McpUtils
183+
} // namespace OpenAI

tests/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ set(INTEGRATION_TEST_SOURCES
3232
integration/test_simple_integration.cpp
3333
integration/test_openai_integration.cpp # Re-enabling to check errors
3434
integration/test_anthropic_integration.cpp
35+
integration/test_mcp_integration.cpp
3536
)
3637

3738
# Create test executable with all tests

0 commit comments

Comments
 (0)