Skip to content

Commit 2f99e86

Browse files
committed
More mcp stuff
1 parent d47d352 commit 2f99e86

7 files changed

Lines changed: 571 additions & 1 deletion

File tree

modules/yup_ai/mcp/yup_MCPServer.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,8 @@ void MCPServer::unregisterTool (const String& name)
293293
void MCPServer::registerResource (MCPResourceDefinition resource, std::function<String()> reader)
294294
{
295295
const ScopedLock lock (pimpl->mutex);
296-
pimpl->resourcesByUri[resource.uri] = Pimpl::ResourceEntry { std::move (resource), std::move (reader) };
296+
const auto uri = resource.uri;
297+
pimpl->resourcesByUri[uri] = Pimpl::ResourceEntry { std::move (resource), std::move (reader) };
297298
pimpl->options.capabilities.supportsResources = true;
298299
}
299300

modules/yup_python/bindings/yup_YupAi_bindings.cpp

Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,16 +49,144 @@ class PyLLMClient : public LLMClient
4949
}
5050
};
5151

52+
class PyMCPTransport : public MCPTransport
53+
{
54+
public:
55+
Result sendMessage (const var& message) override
56+
{
57+
py::gil_scoped_acquire gil;
58+
auto override = py::get_override (this, "sendMessage");
59+
60+
if (! override)
61+
py::pybind11_fail ("Tried to call pure virtual function \"MCPTransport.sendMessage\"");
62+
63+
auto result = override (message);
64+
65+
if (py::isinstance<Result> (result))
66+
return result.cast<Result>();
67+
68+
if (result.is_none() || result.cast<bool>())
69+
return Result::ok();
70+
71+
return Result::fail ("Python MCPTransport.sendMessage returned false");
72+
}
73+
74+
ResultValue<var> receiveMessage (int timeoutMs = -1) override
75+
{
76+
py::gil_scoped_acquire gil;
77+
auto override = py::get_override (this, "receiveMessage");
78+
79+
if (! override)
80+
py::pybind11_fail ("Tried to call pure virtual function \"MCPTransport.receiveMessage\"");
81+
82+
auto result = override (timeoutMs);
83+
if (result.is_none())
84+
return makeResultValueFail ("Python MCPTransport.receiveMessage returned None");
85+
86+
return makeResultValueOk (result.cast<var>());
87+
}
88+
89+
void setMessageHandler (MessageHandler handler) override
90+
{
91+
py::gil_scoped_acquire gil;
92+
auto override = py::get_override (this, "setMessageHandler");
93+
94+
if (! override)
95+
py::pybind11_fail ("Tried to call pure virtual function \"MCPTransport.setMessageHandler\"");
96+
97+
override (std::move (handler));
98+
}
99+
100+
Result start() override
101+
{
102+
py::gil_scoped_acquire gil;
103+
auto override = py::get_override (this, "start");
104+
105+
if (! override)
106+
py::pybind11_fail ("Tried to call pure virtual function \"MCPTransport.start\"");
107+
108+
auto result = override();
109+
110+
if (py::isinstance<Result> (result))
111+
return result.cast<Result>();
112+
113+
if (result.is_none() || result.cast<bool>())
114+
return Result::ok();
115+
116+
return Result::fail ("Python MCPTransport.start returned false");
117+
}
118+
119+
void stop() override
120+
{
121+
py::gil_scoped_acquire gil;
122+
auto override = py::get_override (this, "stop");
123+
124+
if (! override)
125+
py::pybind11_fail ("Tried to call pure virtual function \"MCPTransport.stop\"");
126+
127+
override();
128+
}
129+
130+
bool isConnected() const noexcept override
131+
{
132+
py::gil_scoped_acquire gil;
133+
auto override = py::get_override (this, "isConnected");
134+
135+
if (! override)
136+
return false;
137+
138+
try
139+
{
140+
return override().cast<bool>();
141+
}
142+
catch (...)
143+
{
144+
return false;
145+
}
146+
}
147+
};
148+
52149
String messageRepr (const LLMMessage& message)
53150
{
54151
return "LLMMessage(role='" + LLMMessage::roleToString (message.role) + "', content='" + message.content + "')";
55152
}
153+
154+
py::object optionalJsonRpcRequestToPython (const std::optional<JsonRpcRequest>& value)
155+
{
156+
return value.has_value() ? py::cast (*value) : py::none();
157+
}
158+
159+
py::object optionalJsonRpcResponseToPython (const std::optional<JsonRpcResponse>& value)
160+
{
161+
return value.has_value() ? py::cast (*value) : py::none();
162+
}
163+
164+
py::object optionalJsonRpcErrorToPython (const std::optional<JsonRpcError>& value)
165+
{
166+
return value.has_value() ? py::cast (*value) : py::none();
167+
}
168+
169+
py::object optionalMCPToolDefinitionToPython (const std::optional<MCPToolDefinition>& value)
170+
{
171+
return value.has_value() ? py::cast (*value) : py::none();
172+
}
173+
174+
py::object optionalMCPResourceDefinitionToPython (const std::optional<MCPResourceDefinition>& value)
175+
{
176+
return value.has_value() ? py::cast (*value) : py::none();
177+
}
56178
} // namespace
57179

58180
void registerYupAiBindings (py::module_& m)
59181
{
60182
auto ai = m.def_submodule ("ai");
61183

184+
ai.attr ("MCP_PARSE_ERROR") = MCPErrorCodes::parseError;
185+
ai.attr ("MCP_INVALID_REQUEST") = MCPErrorCodes::invalidRequest;
186+
ai.attr ("MCP_METHOD_NOT_FOUND") = MCPErrorCodes::methodNotFound;
187+
ai.attr ("MCP_INVALID_PARAMS") = MCPErrorCodes::invalidParams;
188+
ai.attr ("MCP_INTERNAL_ERROR") = MCPErrorCodes::internalError;
189+
62190
py::enum_<LLMMessage::Role> (ai, "LLMMessageRole")
63191
.value ("system", LLMMessage::Role::system)
64192
.value ("user", LLMMessage::Role::user)
@@ -226,6 +354,157 @@ void registerYupAiBindings (py::module_& m)
226354
.def ("embed", &EmbeddingModel::embed)
227355
.def ("embedBatch", &EmbeddingModel::embedBatch)
228356
.def_static ("cosineSimilarity", &EmbeddingModel::cosineSimilarity);
357+
358+
py::class_<JsonRpcError> (ai, "JsonRpcError")
359+
.def (py::init<>())
360+
.def_readwrite ("code", &JsonRpcError::code)
361+
.def_readwrite ("message", &JsonRpcError::message)
362+
.def_readwrite ("data", &JsonRpcError::data)
363+
.def ("toVar", &JsonRpcError::toVar)
364+
.def_static ("fromVar", [] (const var& value)
365+
{
366+
return optionalJsonRpcErrorToPython (JsonRpcError::fromVar (value));
367+
});
368+
369+
py::class_<JsonRpcRequest> (ai, "JsonRpcRequest")
370+
.def (py::init<>())
371+
.def_readwrite ("jsonrpc", &JsonRpcRequest::jsonrpc)
372+
.def_readwrite ("id", &JsonRpcRequest::id)
373+
.def_readwrite ("method", &JsonRpcRequest::method)
374+
.def_readwrite ("params", &JsonRpcRequest::params)
375+
.def ("isNotification", &JsonRpcRequest::isNotification)
376+
.def ("toVar", &JsonRpcRequest::toVar)
377+
.def_static ("fromVar", [] (const var& value)
378+
{
379+
return optionalJsonRpcRequestToPython (JsonRpcRequest::fromVar (value));
380+
});
381+
382+
py::class_<JsonRpcResponse> (ai, "JsonRpcResponse")
383+
.def (py::init<>())
384+
.def_readwrite ("jsonrpc", &JsonRpcResponse::jsonrpc)
385+
.def_readwrite ("id", &JsonRpcResponse::id)
386+
.def_readwrite ("result", &JsonRpcResponse::result)
387+
.def_readwrite ("error", &JsonRpcResponse::error)
388+
.def ("isError", &JsonRpcResponse::isError)
389+
.def ("toVar", &JsonRpcResponse::toVar)
390+
.def_static ("fromVar", [] (const var& value)
391+
{
392+
return optionalJsonRpcResponseToPython (JsonRpcResponse::fromVar (value));
393+
});
394+
395+
py::class_<MCPCapabilities> (ai, "MCPCapabilities")
396+
.def (py::init<>())
397+
.def_readwrite ("supportsTools", &MCPCapabilities::supportsTools)
398+
.def_readwrite ("supportsResources", &MCPCapabilities::supportsResources)
399+
.def_readwrite ("supportsPrompts", &MCPCapabilities::supportsPrompts)
400+
.def_readwrite ("supportsLogging", &MCPCapabilities::supportsLogging)
401+
.def ("toVar", &MCPCapabilities::toVar)
402+
.def_static ("fromVar", &MCPCapabilities::fromVar);
403+
404+
py::class_<MCPToolDefinition> (ai, "MCPToolDefinition")
405+
.def (py::init<>())
406+
.def_readwrite ("name", &MCPToolDefinition::name)
407+
.def_readwrite ("description", &MCPToolDefinition::description)
408+
.def_readwrite ("inputSchema", &MCPToolDefinition::inputSchema)
409+
.def ("toVar", &MCPToolDefinition::toVar)
410+
.def_static ("fromVar", [] (const var& value)
411+
{
412+
return optionalMCPToolDefinitionToPython (MCPToolDefinition::fromVar (value));
413+
});
414+
415+
py::class_<MCPResourceDefinition> (ai, "MCPResourceDefinition")
416+
.def (py::init<>())
417+
.def_readwrite ("uri", &MCPResourceDefinition::uri)
418+
.def_readwrite ("name", &MCPResourceDefinition::name)
419+
.def_readwrite ("description", &MCPResourceDefinition::description)
420+
.def_readwrite ("mimeType", &MCPResourceDefinition::mimeType)
421+
.def ("toVar", &MCPResourceDefinition::toVar)
422+
.def_static ("fromVar", [] (const var& value)
423+
{
424+
return optionalMCPResourceDefinitionToPython (MCPResourceDefinition::fromVar (value));
425+
});
426+
427+
py::class_<MCPTransport, PyMCPTransport> (ai, "MCPTransport")
428+
.def (py::init<>())
429+
.def ("sendMessage", &MCPTransport::sendMessage)
430+
.def ("receiveMessage", [] (MCPTransport& self, int timeoutMs)
431+
{
432+
auto result = self.receiveMessage (timeoutMs);
433+
if (result.failed())
434+
py::pybind11_fail (result.getErrorMessage().toRawUTF8());
435+
436+
return result.getValue();
437+
},
438+
"timeoutMs"_a = -1)
439+
.def ("setMessageHandler", &MCPTransport::setMessageHandler)
440+
.def ("start", &MCPTransport::start)
441+
.def ("stop", &MCPTransport::stop)
442+
.def ("isConnected", &MCPTransport::isConnected);
443+
444+
py::class_<MCPClient> (ai, "MCPClient")
445+
.def (py::init<std::unique_ptr<MCPTransport>>(), "transport"_a)
446+
.def ("initialize", &MCPClient::initialize, "clientCapabilities"_a = MCPCapabilities {})
447+
.def ("listTools", &MCPClient::listTools)
448+
.def ("callTool", [] (MCPClient& self, const String& toolName, const var& arguments)
449+
{
450+
auto result = self.callTool (toolName, arguments);
451+
if (result.failed())
452+
py::pybind11_fail (result.getErrorMessage().toRawUTF8());
453+
454+
return result.getValue();
455+
},
456+
"toolName"_a,
457+
"arguments"_a)
458+
.def ("listResources", &MCPClient::listResources)
459+
.def ("readResource", [] (MCPClient& self, const String& uri)
460+
{
461+
auto result = self.readResource (uri);
462+
if (result.failed())
463+
py::pybind11_fail (result.getErrorMessage().toRawUTF8());
464+
465+
return result.getValue();
466+
},
467+
"uri"_a)
468+
.def ("registerToolsWith", &MCPClient::registerToolsWith)
469+
.def ("getTransport", &MCPClient::getTransport, py::return_value_policy::reference_internal);
470+
471+
py::class_<MCPServer::Options> (ai, "MCPServerOptions")
472+
.def (py::init<>())
473+
.def_readwrite ("serverName", &MCPServer::Options::serverName)
474+
.def_readwrite ("serverVersion", &MCPServer::Options::serverVersion)
475+
.def_readwrite ("capabilities", &MCPServer::Options::capabilities);
476+
477+
py::class_<MCPServer> (ai, "MCPServer")
478+
.def (py::init<>())
479+
.def (py::init<MCPServer::Options>())
480+
.def ("registerTool", [] (MCPServer& self, MCPToolDefinition tool, py::function function)
481+
{
482+
self.registerTool (std::move (tool), [function = std::move (function)] (const var& arguments) -> var
483+
{
484+
py::gil_scoped_acquire gil;
485+
return function (arguments).cast<var>();
486+
});
487+
},
488+
"tool"_a,
489+
"function"_a)
490+
.def ("registerLLMTool", static_cast<void (MCPServer::*) (LLMTool)> (&MCPServer::registerTool), "tool"_a)
491+
.def ("unregisterTool", &MCPServer::unregisterTool)
492+
.def ("registerResource", [] (MCPServer& self, MCPResourceDefinition resource, py::function function)
493+
{
494+
self.registerResource (std::move (resource), [function = std::move (function)]() -> String
495+
{
496+
py::gil_scoped_acquire gil;
497+
return py::str (function()).cast<String>();
498+
});
499+
},
500+
"resource"_a,
501+
"function"_a)
502+
.def ("unregisterResource", &MCPServer::unregisterResource)
503+
.def ("start", &MCPServer::start, "transport"_a)
504+
.def ("stop", &MCPServer::stop)
505+
.def ("isRunning", &MCPServer::isRunning)
506+
.def ("startStdio", &MCPServer::startStdio)
507+
.def ("startHttp", &MCPServer::startHttp, "port"_a);
229508
}
230509

231510
} // namespace yup::Bindings
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import yup
2+
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import yup
2+
3+
#==================================================================================================
4+
5+
def test_server_options_are_exposed():
6+
options = yup.ai.MCPServerOptions()
7+
options.serverName = "Python MCP Server"
8+
options.serverVersion = "2.1"
9+
options.capabilities.supportsTools = True
10+
11+
server = yup.ai.MCPServer(options)
12+
13+
assert not server.isRunning()
14+
15+
#==================================================================================================
16+
17+
def test_server_accepts_python_tool_and_resource_callbacks():
18+
server = yup.ai.MCPServer()
19+
20+
tool = yup.ai.MCPToolDefinition()
21+
tool.name = "echo"
22+
tool.description = "Echoes text."
23+
tool.inputSchema = {
24+
"type": "object",
25+
"properties": {
26+
"value": { "type": "string" },
27+
},
28+
"required": [ "value" ],
29+
}
30+
31+
server.registerTool(tool, lambda arguments: { "echoed": arguments["value"] })
32+
33+
resource = yup.ai.MCPResourceDefinition()
34+
resource.uri = "yup://python/status"
35+
resource.name = "Status"
36+
resource.description = "Python status."
37+
38+
server.registerResource(resource, lambda: '{"ok":true}')
39+
40+
assert not server.isRunning()
41+

0 commit comments

Comments
 (0)