Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions common/arg.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3784,6 +3784,22 @@ common_params_context common_params_parser_init(common_params & params, llama_ex
}
).set_examples({LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_CLI}));

add_opt(common_arg(
{"--mcp-config"}, "FILE",
"path to MCP configuration file",
[](common_params & params, const std::string & value) {
params.mcp_config = value;
}
).set_examples({LLAMA_EXAMPLE_CLI}));

add_opt(common_arg(
{"--mcp-yolo"},
"auto-approve all MCP tool calls (no user confirmation)",
[](common_params & params) {
params.mcp_yolo = true;
}
).set_examples({LLAMA_EXAMPLE_CLI}));

return ctx_arg;
}

Expand Down
5 changes: 5 additions & 0 deletions common/common.h
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,11 @@ struct common_params {

std::map<std::string, std::string> default_template_kwargs;

// MCP params
std::string mcp_config = "";
std::string mcp_servers = "";
bool mcp_yolo = false;

// webui configs
bool webui = true;
std::string webui_config_json;
Expand Down
13 changes: 13 additions & 0 deletions mcp_config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"mcpServers": {
"serena": {
"command": "uvx",
"args": [
"--from",
"git+https://github.com/oraios/serena",
"serena",
"start-mcp-server"
]
}
}
}
83 changes: 83 additions & 0 deletions mcp_dummy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import sys
import json
import logging

logging.basicConfig(filename='mcp_dummy.log', level=logging.DEBUG)


def main():
logging.info("Starting MCP Dummy Server")
while True:
try:
line = sys.stdin.readline()
if not line:
break
logging.info(f"Received: {line.strip()}")
try:
req = json.loads(line)
except json.JSONDecodeError:
continue

if "method" in req:
method = req["method"]
req_id = req.get("id")

resp = {"jsonrpc": "2.0", "id": req_id}

if method == "initialize":
resp["result"] = {
"protocolVersion": "2024-11-05",
"capabilities": {},
"serverInfo": {"name": "dummy", "version": "1.0"}
}
elif method == "tools/list":
resp["result"] = {
"tools": [
{
"name": "get_weather",
"description": "Get weather for a location",
"inputSchema": {
"type": "object",
"properties": {
"location": {"type": "string"},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
},
"required": ["location"]
}
}
]
}
elif method == "tools/call":
params = req.get("params", {})
name = params.get("name")
args = params.get("arguments", {})

logging.info(f"Tool call: {name} with {args}")

content = [{"type": "text", "text": f"Weather in {args.get('location')} is 25C"}]
# For simplicity, return raw content or follow MCP spec?
# MCP spec: result: { content: [ {type: "text", text: "..."} ] }
# My mcp.hpp returns res["result"].
# My cli.cpp dumps res.dump().
# So passing full result object is fine.
resp["result"] = {
"content": content
}
else:
# Ignore notifications or other methods
if req_id is not None:
resp["error"] = {"code": -32601, "message": "Method not found"}
else:
continue

logging.info(f"Sending: {json.dumps(resp)}")
if req_id is not None:
sys.stdout.write(json.dumps(resp) + "\n\n")
sys.stdout.flush()
except Exception as e:
logging.error(f"Error: {e}")
break


if __name__ == "__main__":
main()
4 changes: 4 additions & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
llama_add_compile_flags()

add_executable(test-mcp-integration test-mcp-integration.cpp)
target_link_libraries(test-mcp-integration PRIVATE common)
target_include_directories(test-mcp-integration PRIVATE ../tools/cli)

function(llama_build source)
set(TEST_SOURCES ${source} ${ARGN})

Expand Down
8 changes: 4 additions & 4 deletions tests/test-chat.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ static common_chat_msg normalize(const common_chat_msg & msg) {
for (auto & tool_call : normalized.tool_calls) {
try {
tool_call.arguments = json::parse(tool_call.arguments).dump();
} catch (const std::exception &) {
// Do nothing
} catch (const std::exception &e) {
LOG_DBG("Normalize failed on tool call: %s\n", e.what());
}
}
return normalized;
Expand Down Expand Up @@ -183,7 +183,7 @@ static void assert_msg_equals(const common_chat_msg & expected, const common_cha
}
}

common_chat_tool special_function_tool {
static common_chat_tool special_function_tool {
/* .name = */ "special_function",
/* .description = */ "I'm special",
/* .parameters = */ R"({
Expand All @@ -197,7 +197,7 @@ common_chat_tool special_function_tool {
"required": ["arg1"]
})",
};
common_chat_tool special_function_tool_with_optional_param {
static common_chat_tool special_function_tool_with_optional_param {
/* .name = */ "special_function_with_opt",
/* .description = */ "I'm special but have optional stuff",
/* .parameters = */ R"({
Expand Down
71 changes: 71 additions & 0 deletions tests/test-mcp-integration.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#include "../tools/cli/mcp.hpp"
#include "common.h"
#include "log.h"

#include <chrono>
#include <iostream>
#include <string>
#include <thread>
#include <vector>

int main(int argc, char ** argv) {
if (argc < 2) {
fprintf(stderr, "Usage: %s <mcp_config_file>\n", argv[0]);
return 1;
}

std::string config_file = argv[1];

printf("Testing MCP integration with config: %s\n", config_file.c_str());

mcp_context mcp;
std::string tool_to_run;
json tool_args = json::object();
std::string servers_arg = "";

if (argc >= 3) {
tool_to_run = argv[2];
}
if (argc >= 4) {
try {
tool_args = json::parse(argv[3]);
} catch (const std::exception & e) {
fprintf(stderr, "Error parsing tool arguments JSON: %s\n", e.what());
return 1;
}
}

if (mcp.load_config(config_file, servers_arg)) {
printf("MCP config loaded successfully.\n");

auto tools = mcp.get_tools();
printf("Found %zu tools.\n", tools.size());

for (const auto & tool : tools) {
printf("Tool: %s\n", tool.name.c_str());
printf(" Description: %s\n", tool.description.c_str());
}

if (!tool_to_run.empty()) {
printf("Calling tool %s...\n", tool_to_run.c_str());
mcp.set_yolo(true);
json res = mcp.call_tool(tool_to_run, tool_args);
printf("Result: %s\n", res.dump().c_str());
} else if (!tools.empty()) {
printf("No tool specified. Calling first tool '%s' with empty args as smoke test...\n", tools[0].name.c_str());
json args = json::object();
mcp.set_yolo(true);
json res = mcp.call_tool(tools[0].name, args);
printf("Result: %s\n", res.dump().c_str());
}

} else {
printf("Failed to load MCP config.\n");
return 1;
}

// Allow some time for threads to shutdown if any
std::this_thread::sleep_for(std::chrono::seconds(1));

return 0;
}
Loading
Loading