86 : _shared{std::make_shared<Shared>(std::move(server), std::move(options))} {
87 if (_shared->server ==
nullptr || _shared->options.callbacks ==
nullptr) {
88 throw std::invalid_argument{
"McpAdapter: a server and a callbacks executor are required"};
90 _shared->cid = _shared->server->openConnection();
97 _shared->server->closeConnection(_shared->cid);
112 auto [completion, promise] =
114 auto const settle = std::make_shared<decltype(promise)>(std::move(promise));
115 Shared::dispatch(_shared, message,
116 [settle](std::optional<std::string> reply) { settle->resolve(std::move(reply)); });
117 return std::move(completion);
121 using Reply = std::function<void(std::optional<std::string>)>;
126 Shared(std::shared_ptr<backend::RemoteServer> made,
McpOptions given)
127 : server{std::move(made)}, options{std::move(given)} {}
129 std::shared_ptr<backend::RemoteServer> server;
132 std::atomic<std::uint64_t> nextCall{0};
134 std::optional<CatalogReply> catalog;
136 std::map<std::string, std::uint64_t, std::less<>> privateIds;
137 std::map<std::pair<std::string, std::string>, std::uint64_t> sharedIds;
142 std::size_t waiting = 0;
143 std::map<std::string, Json, std::less<>> schemas;
147 static void send(std::shared_ptr<Shared>
const& self, wire::Envelope env, EnvelopeReply done) {
148 env.callId =
self->nextCall.fetch_add(1) + 1;
149 env.session =
self->options.session;
150 self->server->handle(
152 [done = std::move(done)](std::string
const& reply) {
154 done(wire::decode(reply));
155 }
catch (std::exception
const& error) {
156 done(wire::makeErr(error.what()));
162 static std::string write(Json
const& value) {
return glz::write_json(value).value_or(std::string{
"null"}); }
165 [[nodiscard]]
static Json
const* member(Json
const& value, std::string_view key) {
166 return forms::detail::findMember(value, key);
169 [[nodiscard]]
static std::string
const* textMember(Json
const& value, std::string_view key) {
170 Json
const*
const found = member(value, key);
171 return found ==
nullptr ? nullptr : std::get_if<std::string>(&found->data);
175 static Json::object_t& objectMember(Json::object_t&
object, std::string
const& key) {
176 Json&
slot =
object.try_emplace(key, Json::object_t{}).first->second;
177 if (!std::holds_alternative<Json::object_t>(
slot.data)) {
178 slot = Json::object_t{};
180 return std::get<Json::object_t>(
slot.data);
184 static std::string answer(Json
const& requestId, std::string
const& outcome, Json content) {
185 return write(Json::object_t{{
"jsonrpc",
"2.0"}, {
"id", requestId}, {outcome, std::move(content)}});
188 static std::string result(Json
const& requestId, Json value) {
189 return answer(requestId,
"result", std::move(value));
192 static std::string error(Json
const& requestId, std::int64_t code, std::string
const& message) {
193 return answer(requestId,
"error", Json::object_t{{
"code", code}, {
"message", message}});
197 static Json toolResult(std::string
const& text,
bool isError) {
199 content.emplace_back(Json::object_t{{
"type",
"text"}, {
"text",
text}});
200 return Json::object_t{{
"content", std::move(content)}, {
"isError", isError}};
203 static void dispatch(std::shared_ptr<Shared>
const& self, std::string
const& message, Reply
const& done) {
204 auto parsed = parseJson(message);
206 done(error(Json{},
kMcpParseError,
"not JSON: " + parsed.error()));
209 bool const isObject = std::holds_alternative<Json::object_t>(parsed->data);
210 std::string
const*
const method = textMember(*parsed,
"method");
211 Json
const*
const givenId = member(*parsed,
"id");
212 Json
const requestId = givenId ==
nullptr ? Json{} : *givenId;
213 bool const notification = isObject && givenId ==
nullptr;
214 if (method ==
nullptr) {
218 done(error(requestId, kMcpInvalidRequest,
"no method"));
226 Json
const*
const givenParams = member(*parsed,
"params");
227 Json
const params = givenParams ==
nullptr ? Json{} : *givenParams;
228 if (*method ==
"initialize") {
229 done(result(requestId, initialize(*self, params)));
230 }
else if (*method ==
"ping") {
231 done(result(requestId, Json::object_t{}));
232 }
else if (*method ==
"tools/list") {
233 listTools(self, [requestId, done](Json tools) { done(result(requestId, std::move(tools))); });
234 }
else if (*method ==
"tools/call") {
235 callTool(self, params, [requestId, done](std::expected<Json, std::string> called) {
236 done(called ? result(requestId, std::move(*called))
237 : error(requestId, kMcpInvalidParams, called.error()));
240 done(error(requestId, kMcpMethodNotFound,
"method not found: " + *method));
244 static Json initialize(Shared
const& self, Json
const& params) {
245 std::string
const*
const asked = textMember(params,
"protocolVersion");
246 return Json::object_t{
247 {
"protocolVersion", asked !=
nullptr ? *asked : std::string{
"2025-06-18"}},
248 {
"capabilities", Json::object_t{{
"tools", Json::object_t{{
"listChanged",
false}}}}},
250 Json::object_t{{
"name",
self.options.serverName}, {
"version",
self.options.serverVersion}}},
255 static void withCatalog(std::shared_ptr<Shared>
const& self, std::function<
void(CatalogReply
const&)> then) {
257 std::optional<CatalogReply> cached;
259 std::scoped_lock
const lock{
self->mutex};
260 cached =
self->catalog;
268 send(self, std::move(request), [self, then = std::move(then)](wire::Envelope
const& reply) {
269 CatalogReply catalog;
270 if (reply.kind ==
"ok") {
272 catalog = std::move(*read);
276 std::scoped_lock
const lock{
self->mutex};
277 self->catalog = catalog;
283 [[nodiscard]]
static bool keyed(CatalogReply
const& catalog, std::string
const& modelType) {
284 if (!catalog.models) {
287 auto const found = catalog.models->find(modelType);
288 return found != catalog.models->end() && found->second.keyed;
291 [[nodiscard]]
static std::string modelOf(CatalogReply
const& catalog, std::string_view actionId) {
292 if (!catalog.actions) {
295 auto const found = catalog.actions->find(actionId);
296 return found == catalog.actions->end() ? std::string{} : found->second.modelType;
300 static Json tool(std::string
const& actionId, Json schema,
bool keyedModel) {
301 if (!std::holds_alternative<Json::object_t>(schema.data)) {
302 schema = Json::object_t{};
304 auto&
object = std::get<Json::object_t>(schema.data);
305 object.insert_or_assign(
"type",
"object");
307 objectMember(
object,
"properties")
310 Json::object_t{{
"type",
"string"}, {
"description",
"the key of the instance to run on"}});
311 Json& required =
object.try_emplace(
"required", Json::array_t{}).first->second;
312 if (!std::holds_alternative<Json::array_t>(required.data)) {
313 required = Json::array_t{};
315 std::get<Json::array_t>(required.data).emplace_back(
"instance");
317 return Json::object_t{{
"name", actionId},
318 {
"description",
"Runs " + actionId +
" on the server."},
319 {
"inputSchema", std::move(schema)}};
322 static Json evaluateTool() {
323 Json::object_t properties{{
"action", Json::object_t{{
"type",
"string"}}},
324 {
"body", Json::object_t{{
"type",
"object"}}}};
325 Json::array_t required{std::string{
"action"}, std::string{
"body"}};
326 return Json::object_t{
327 {
"name",
"evaluate"},
329 "A dry run: the action's body reconciled, its field errors, and whether it would run."},
332 {
"type",
"object"}, {
"properties", std::move(properties)}, {
"required", std::move(required)}}},
337 static Json toolList(CatalogReply
const& catalog, std::map<std::string, Json, std::less<>>
const& schemas) {
339 for (std::string
const& action : catalog.permitted) {
340 std::string
const type = modelOf(catalog, action);
344 auto const found = schemas.find(action);
345 tools.push_back(tool(action, found == schemas.end() ? Json{} : found->second, keyed(catalog, type)));
347 tools.push_back(evaluateTool());
348 return Json::object_t{{
"tools", std::move(tools)}};
352 [[nodiscard]]
static bool gathered(Gather& gather, wire::Envelope
const& reply) {
353 std::scoped_lock
const lock{gather.mutex};
354 auto parsed = reply.kind ==
"ok" ? parseJson(reply.body) : std::unexpected(std::string{});
356 if (
auto*
const actions = std::get_if<Json::object_t>(&parsed->data)) {
357 for (
auto& [action, schema] : *actions) {
358 gather.schemas.insert_or_assign(action, std::move(schema));
362 return --gather.waiting == 0;
365 static void listTools(std::shared_ptr<Shared>
const& self, std::function<
void(Json)> done) {
366 withCatalog(self, [self, done = std::move(done)](CatalogReply
const& catalog) {
367 std::set<std::string, std::less<>> types;
368 for (std::string
const& action : catalog.permitted) {
369 if (std::string type = modelOf(catalog, action); !type.empty()) {
370 types.insert(std::move(type));
374 done(toolList(catalog, {}));
377 auto const gather = std::make_shared<Gather>();
378 gather->waiting = types.size();
379 for (std::string
const& type : types) {
380 send(self, wire::makeSchemas(type), [catalog, done, gather](wire::Envelope
const& reply) {
381 if (gathered(*gather, reply)) {
382 done(toolList(catalog, gather->schemas));
390 static void withInstance(std::shared_ptr<Shared>
const& self, std::string
const& type,
391 std::optional<std::string>
const& key,
392 std::function<
void(std::expected<std::uint64_t, std::string>)> then) {
393 std::optional<std::uint64_t> cached;
395 std::scoped_lock
const lock{
self->mutex};
397 if (
auto const found =
self->sharedIds.find(std::pair{type, *key});
398 found !=
self->sharedIds.end()) {
399 cached = found->second;
401 }
else if (
auto const found =
self->privateIds.find(type); found !=
self->privateIds.end()) {
402 cached = found->second;
409 wire::Envelope
request = key ? wire::makeRegisterShared(type, *key) : wire::makeRegister(type);
410 send(self, std::move(request), [self, type, key, then = std::move(then)](wire::Envelope
const& reply) {
411 if (reply.kind !=
"ok" || reply.modelId == 0) {
412 then(std::unexpected(reply.message.empty() ? std::string{
"the instance was not registered"}
417 std::scoped_lock
const lock{
self->mutex};
419 self->sharedIds.insert_or_assign(std::pair{type, *key}, reply.modelId);
421 self->privateIds.insert_or_assign(type, reply.modelId);
428 [[nodiscard]]
static std::optional<std::string> instanceKey(Json
const* value) {
429 if (value ==
nullptr || std::holds_alternative<std::nullptr_t>(value->data)) {
432 if (
auto const*
const text = std::get_if<std::string>(&value->data)) {
435 return write(*value);
438 static void callTool(std::shared_ptr<Shared>
const& self, Json
const& params,
439 std::function<
void(std::expected<Json, std::string>)> done) {
440 std::string
const*
const name = textMember(params,
"name");
441 if (name ==
nullptr) {
442 done(std::unexpected(std::string{
"tools/call names no tool"}));
445 Json
const*
const given = member(params,
"arguments");
447 Json arguments = given ==
nullptr ? Json(Json::object_t{}) : *given;
448 if (!std::holds_alternative<Json::object_t>(arguments.data)) {
449 done(std::unexpected(std::string{
"the arguments are not an object"}));
452 withCatalog(self, [self, tool = *name, arguments = std::move(arguments),
453 done = std::move(done)](CatalogReply
const& catalog)
mutable {
454 if (tool ==
"evaluate") {
455 evaluate(self, catalog, arguments, std::move(done));
458 std::string
const type = modelOf(catalog, tool);
460 done(std::unexpected(
"no tool named " + tool));
463 std::optional<std::string> key;
464 if (keyed(catalog, type)) {
465 key = instanceKey(member(arguments,
"instance"));
467 done(std::unexpected(tool +
" runs on a keyed model: give the instance key as `instance`"));
470 std::get<Json::object_t>(arguments.data).erase(
"instance");
472 std::string body = write(arguments);
475 [self, type, tool, body = std::move(body),
476 done = std::move(done)](std::expected<std::uint64_t, std::string>
const& modelId)
mutable {
478 done(toolResult(modelId.error(),
true));
486 request.body = std::move(body);
487 send(self, std::move(request), [done = std::move(done)](wire::Envelope
const& reply) {
488 done(reply.kind ==
"ok" ? toolResult(reply.body,
false) : toolResult(reply.message,
true));
494 static void evaluate(std::shared_ptr<Shared>
const& self, CatalogReply
const& catalog, Json
const& arguments,
495 std::function<
void(std::expected<Json, std::string>)> done) {
496 std::string
const*
const action = textMember(arguments,
"action");
497 if (action ==
nullptr || modelOf(catalog, *action).empty()) {
498 done(std::unexpected(std::string{
"evaluate names no action the catalog knows"}));
501 Json
const*
const body = member(arguments,
"body");
504 request.modelType = modelOf(catalog, *action);
506 request.body = body ==
nullptr ? std::string{
"{}"} : write(*body);
507 send(self, std::move(request), [done = std::move(done)](wire::Envelope
const& reply) {
508 done(reply.kind ==
"ok" ? toolResult(reply.body,
false) : toolResult(reply.message,
true));
513 std::shared_ptr<Shared> _shared;