morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
agent.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2#pragma once
3
4#include <atomic>
5#include <chrono>
6#include <cstdint>
7#include <exception>
8#include <expected>
9#include <functional>
10#include <istream>
11#include <map>
12#include <memory>
13#include <mutex>
14#include <optional>
15#include <ostream>
16#include <set>
17#include <stdexcept>
18#include <string>
19#include <string_view>
20#include <utility>
21#include <vector>
22
23#include "../core/completion.hpp"
24#include "../core/executor.hpp"
25#include "../core/remote.hpp"
26#include "../core/wire.hpp"
27#include "../forms/forms.hpp"
28#include "../session/session.hpp"
29#include "bundle.hpp"
30#include "value.hpp"
31
32/// @file
33/// The MCP adapter (spec 8 §11): an agent's tool calls run through the server as ordinary envelopes on a
34/// connection of their own, so every check a GUI gets applies to them, and every GUI watching what they change is
35/// told. The adapter speaks MCP's JSON-RPC 2.0 and knows no transport; `serveStdio` drives it over MCP's stdio
36/// transport.
37
38namespace morph::ui {
39
40/// @brief What an `McpAdapter` is built with.
41struct McpOptions {
42 /// @brief The session every envelope carries: the agent's identity, which the server authenticates on each one,
43 /// as it does a GUI's.
45 /// @brief The name `initialize` answers in `serverInfo`.
46 std::string serverName = "morph";
47 /// @brief The version `initialize` answers in `serverInfo`.
48 std::string serverVersion = "1";
49 /// @brief The owner `handle`'s completions are delivered on: a serial executor that outlives the adapter and
50 /// every call still in flight, such as the `exec::MainThreadExecutor` `serveStdio` pumps. Borrowed;
51 /// required.
53};
54
55/// @brief The JSON-RPC error code of a method the adapter does not answer.
56inline constexpr std::int64_t kMcpMethodNotFound = -32601;
57/// @brief The JSON-RPC error code of a message that is not JSON.
58inline constexpr std::int64_t kMcpParseError = -32700;
59/// @brief The JSON-RPC error code of a message that is not a request.
60inline constexpr std::int64_t kMcpInvalidRequest = -32600;
61/// @brief The JSON-RPC error code of a call whose arguments the adapter cannot use.
62inline constexpr std::int64_t kMcpInvalidParams = -32602;
63
64/// @brief Serves MCP's tools over one server connection (spec 8 §11).
65///
66/// It answers `initialize`, `tools/list`, `tools/call` and `ping`, and refuses every other request with JSON-RPC error
67/// `-32601`; a notification, which carries no `id`, gets no reply.
68///
69/// - **Tools.** One per action the catalog lists as permitted for the session, named by the action id, whose
70/// `inputSchema` is the action's schema from `schemas`. A keyed model's tool also takes the instance key as the
71/// argument `instance`, and runs on the shared instance with that key. Any other model's tool runs on one private
72/// instance per model type, registered on the adapter's connection at its first call. A tool `evaluate`, taking
73/// `{action, body}`, is a dry run through `ui-evaluate`.
74/// - **A call's result** is the action's result JSON as text content, or the server's `err` message with `isError`.
75/// - **The connection** is opened when the adapter is made and closed when it is destroyed, which releases its
76/// private instances. The server treats it like any other connection, so its changes reach every watching GUI.
77///
78/// `handle` is called on the owner, `McpOptions::callbacks`, and its completion is delivered there.
80public:
81 /// @brief An adapter on a new connection of @p server.
82 /// @param server The server; the adapter keeps it alive.
83 /// @param options Its options.
84 /// @throws std::invalid_argument without a server or a callbacks executor.
85 McpAdapter(std::shared_ptr<backend::RemoteServer> server, McpOptions options)
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"};
89 }
90 _shared->cid = _shared->server->openConnection();
91 }
92
93 /// @brief Closes the adapter's connection, releasing its private instances.
95 // A destructor must not throw, so a close that fails is dropped.
96 try {
97 _shared->server->closeConnection(_shared->cid);
98 } catch (...) { // NOLINT(bugprone-empty-catch)
99 }
100 }
101
102 McpAdapter(McpAdapter const&) = delete;
103 McpAdapter& operator=(McpAdapter const&) = delete;
104 McpAdapter(McpAdapter&&) = delete;
105 McpAdapter& operator=(McpAdapter&&) = delete;
106
107 /// @brief Handles one JSON-RPC message.
108 /// @param message The message's JSON text.
109 /// @return Completes with the reply's JSON text, or with nothing for a notification. It never fails: a fault is
110 /// answered as a JSON-RPC error.
111 [[nodiscard]] async::Completion<std::optional<std::string>> handle(std::string const& message) {
112 auto [completion, promise] =
113 async::Completion<std::optional<std::string>>::makeSettleable(_shared->options.callbacks);
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);
118 }
119
120private:
121 using Reply = std::function<void(std::optional<std::string>)>;
122 using EnvelopeReply = std::function<void(wire::Envelope)>;
123
124 // Everything a reply still in flight needs, kept alive by it after the adapter is gone.
125 struct Shared {
126 Shared(std::shared_ptr<backend::RemoteServer> made, McpOptions given)
127 : server{std::move(made)}, options{std::move(given)} {}
128
129 std::shared_ptr<backend::RemoteServer> server;
130 McpOptions options;
132 std::atomic<std::uint64_t> nextCall{0};
133 std::mutex mutex;
134 std::optional<CatalogReply> catalog;
135 // Model ids: a private instance by model type, a shared one by model type and key.
136 std::map<std::string, std::uint64_t, std::less<>> privateIds;
137 std::map<std::pair<std::string, std::string>, std::uint64_t> sharedIds;
138
139 // The schemas of every model a permitted action belongs to, gathered for one tool list.
140 struct Gather {
141 std::mutex mutex;
142 std::size_t waiting = 0;
143 std::map<std::string, Json, std::less<>> schemas;
144 };
145
146 // Sends one envelope on the adapter's connection, carrying its session.
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(
151 wire::encode(env),
152 [done = std::move(done)](std::string const& reply) {
153 try {
154 done(wire::decode(reply));
155 } catch (std::exception const& error) {
156 done(wire::makeErr(error.what()));
157 }
158 },
159 self->cid);
160 }
161
162 static std::string write(Json const& value) { return glz::write_json(value).value_or(std::string{"null"}); }
163
164 // A member of an object, or nullptr when the value is not an object or has no such member.
165 [[nodiscard]] static Json const* member(Json const& value, std::string_view key) {
166 return forms::detail::findMember(value, key);
167 }
168
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);
172 }
173
174 // The object member @p key of @p object, made an object when it is missing or is not one.
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{};
179 }
180 return std::get<Json::object_t>(slot.data);
181 }
182
183 // A JSON-RPC response: @p requestId's answer, under the member @p outcome (`result` or `error`).
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)}});
186 }
187
188 static std::string result(Json const& requestId, Json value) {
189 return answer(requestId, "result", std::move(value));
190 }
191
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}});
194 }
195
196 // A tool call's result: the text, flagged as an error or not.
197 static Json toolResult(std::string const& text, bool isError) {
198 Json::array_t content;
199 content.emplace_back(Json::object_t{{"type", "text"}, {"text", text}});
200 return Json::object_t{{"content", std::move(content)}, {"isError", isError}};
201 }
202
203 static void dispatch(std::shared_ptr<Shared> const& self, std::string const& message, Reply const& done) {
204 auto parsed = parseJson(message);
205 if (!parsed) {
206 done(error(Json{}, kMcpParseError, "not JSON: " + parsed.error()));
207 return;
208 }
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) {
215 if (notification) {
216 done(std::nullopt);
217 } else {
218 done(error(requestId, kMcpInvalidRequest, "no method"));
219 }
220 return;
221 }
222 if (notification) {
223 done(std::nullopt);
224 return;
225 }
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()));
238 });
239 } else {
240 done(error(requestId, kMcpMethodNotFound, "method not found: " + *method));
241 }
242 }
243
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}}}}},
249 {"serverInfo",
250 Json::object_t{{"name", self.options.serverName}, {"version", self.options.serverVersion}}},
251 };
252 }
253
254 // The catalog for the session, fetched once; an unreadable or refused one is empty.
255 static void withCatalog(std::shared_ptr<Shared> const& self, std::function<void(CatalogReply const&)> then) {
256 // Copied under the lock and used after it: the continuation takes the lock again.
257 std::optional<CatalogReply> cached;
258 {
259 std::scoped_lock const lock{self->mutex};
260 cached = self->catalog;
261 }
262 if (cached) {
263 then(*cached);
264 return;
265 }
266 wire::Envelope request;
267 request.kind = std::string{kCatalogKind};
268 send(self, std::move(request), [self, then = std::move(then)](wire::Envelope const& reply) {
269 CatalogReply catalog;
270 if (reply.kind == "ok") {
271 if (auto read = parseCatalogReply(reply.body)) {
272 catalog = std::move(*read);
273 }
274 }
275 {
276 std::scoped_lock const lock{self->mutex};
277 self->catalog = catalog;
278 }
279 then(catalog);
280 });
281 }
282
283 [[nodiscard]] static bool keyed(CatalogReply const& catalog, std::string const& modelType) {
284 if (!catalog.models) {
285 return false;
286 }
287 auto const found = catalog.models->find(modelType);
288 return found != catalog.models->end() && found->second.keyed;
289 }
290
291 [[nodiscard]] static std::string modelOf(CatalogReply const& catalog, std::string_view actionId) {
292 if (!catalog.actions) {
293 return {};
294 }
295 auto const found = catalog.actions->find(actionId);
296 return found == catalog.actions->end() ? std::string{} : found->second.modelType;
297 }
298
299 // One tool: the action's schema, with `instance` added for a keyed model.
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{};
303 }
304 auto& object = std::get<Json::object_t>(schema.data);
305 object.insert_or_assign("type", "object");
306 if (keyedModel) {
307 objectMember(object, "properties")
308 .insert_or_assign(
309 "instance",
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{};
314 }
315 std::get<Json::array_t>(required.data).emplace_back("instance");
316 }
317 return Json::object_t{{"name", actionId},
318 {"description", "Runs " + actionId + " on the server."},
319 {"inputSchema", std::move(schema)}};
320 }
321
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"},
328 {"description",
329 "A dry run: the action's body reconciled, its field errors, and whether it would run."},
330 {"inputSchema",
331 Json::object_t{
332 {"type", "object"}, {"properties", std::move(properties)}, {"required", std::move(required)}}},
333 };
334 }
335
336 // The tool list: one tool per permitted action of a known model, then `evaluate`.
337 static Json toolList(CatalogReply const& catalog, std::map<std::string, Json, std::less<>> const& schemas) {
338 Json::array_t tools;
339 for (std::string const& action : catalog.permitted) {
340 std::string const type = modelOf(catalog, action);
341 if (type.empty()) {
342 continue;
343 }
344 auto const found = schemas.find(action);
345 tools.push_back(tool(action, found == schemas.end() ? Json{} : found->second, keyed(catalog, type)));
346 }
347 tools.push_back(evaluateTool());
348 return Json::object_t{{"tools", std::move(tools)}};
349 }
350
351 // Folds one model's `schemas` reply into @p gather; true when it was the last one awaited.
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{});
355 if (parsed) {
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));
359 }
360 }
361 }
362 return --gather.waiting == 0;
363 }
364
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));
371 }
372 }
373 if (types.empty()) {
374 done(toolList(catalog, {}));
375 return;
376 }
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));
383 }
384 });
385 }
386 });
387 }
388
389 // The model id a tool call runs on, registering the instance at its first use.
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;
394 {
395 std::scoped_lock const lock{self->mutex};
396 if (key) {
397 if (auto const found = self->sharedIds.find(std::pair{type, *key});
398 found != self->sharedIds.end()) {
399 cached = found->second;
400 }
401 } else if (auto const found = self->privateIds.find(type); found != self->privateIds.end()) {
402 cached = found->second;
403 }
404 }
405 if (cached) {
406 then(*cached);
407 return;
408 }
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"}
413 : reply.message));
414 return;
415 }
416 {
417 std::scoped_lock const lock{self->mutex};
418 if (key) {
419 self->sharedIds.insert_or_assign(std::pair{type, *key}, reply.modelId);
420 } else {
421 self->privateIds.insert_or_assign(type, reply.modelId);
422 }
423 }
424 then(reply.modelId);
425 });
426 }
427
428 [[nodiscard]] static std::optional<std::string> instanceKey(Json const* value) {
429 if (value == nullptr || std::holds_alternative<std::nullptr_t>(value->data)) {
430 return std::nullopt;
431 }
432 if (auto const* const text = std::get_if<std::string>(&value->data)) {
433 return *text;
434 }
435 return write(*value);
436 }
437
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"}));
443 return;
444 }
445 Json const* const given = member(params, "arguments");
446 // Parentheses: braces would pick the initializer-list constructor and make `[{}]`.
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"}));
450 return;
451 }
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));
456 return;
457 }
458 std::string const type = modelOf(catalog, tool);
459 if (type.empty()) {
460 done(std::unexpected("no tool named " + tool));
461 return;
462 }
463 std::optional<std::string> key;
464 if (keyed(catalog, type)) {
465 key = instanceKey(member(arguments, "instance"));
466 if (!key) {
467 done(std::unexpected(tool + " runs on a keyed model: give the instance key as `instance`"));
468 return;
469 }
470 std::get<Json::object_t>(arguments.data).erase("instance");
471 }
472 std::string body = write(arguments);
473 withInstance(
474 self, type, key,
475 [self, type, tool, body = std::move(body),
476 done = std::move(done)](std::expected<std::uint64_t, std::string> const& modelId) mutable {
477 if (!modelId) {
478 done(toolResult(modelId.error(), true));
479 return;
480 }
481 wire::Envelope request;
482 request.kind = "execute";
483 request.modelId = *modelId;
484 request.modelType = type;
485 request.actionType = tool;
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));
489 });
490 });
491 });
492 }
493
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"}));
499 return;
500 }
501 Json const* const body = member(arguments, "body");
502 wire::Envelope request;
503 request.kind = std::string{kEvaluateKind};
504 request.modelType = modelOf(catalog, *action);
505 request.actionType = *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));
509 });
510 }
511 };
512
513 std::shared_ptr<Shared> _shared;
514};
515
516/// @brief Serves an adapter over MCP's stdio transport: one JSON-RPC message per line of @p input, each reply on a
517/// line of @p output, in order, until @p input ends. An empty line is skipped.
518///
519/// The calling thread is the adapter's owner: it pumps @p owner while it waits for each reply.
520/// @param adapter The adapter; its `McpOptions::callbacks` is @p owner.
521/// @param owner The adapter's owner, run on the calling thread.
522/// @param input Where messages arrive.
523/// @param output Where replies go; flushed after each.
524inline void serveStdio(McpAdapter& adapter, exec::MainThreadExecutor& owner, std::istream& input,
525 std::ostream& output) {
526 std::string line;
527 while (std::getline(input, line)) {
528 if (line.empty()) {
529 continue;
530 }
531 std::optional<std::optional<std::string>> replied;
532 adapter.handle(line).then([&replied](std::optional<std::string> const& answer) { replied = answer; });
533 while (!replied) {
534 owner.runFor(std::chrono::milliseconds{10});
535 }
536 if (*replied) {
537 output << **replied << '\n' << std::flush;
538 }
539 }
540}
541
542} // namespace morph::ui
constexpr std::int64_t kMcpMethodNotFound
The JSON-RPC error code of a method the adapter does not answer.
Definition agent.hpp:56
constexpr std::int64_t kMcpParseError
The JSON-RPC error code of a message that is not JSON.
Definition agent.hpp:58
constexpr std::int64_t kMcpInvalidRequest
The JSON-RPC error code of a message that is not a request.
Definition agent.hpp:60
void serveStdio(McpAdapter &adapter, exec::MainThreadExecutor &owner, std::istream &input, std::ostream &output)
Serves an adapter over MCP's stdio transport: one JSON-RPC message per line of input,...
Definition agent.hpp:524
constexpr std::int64_t kMcpInvalidParams
The JSON-RPC error code of a call whose arguments the adapter cannot use.
Definition agent.hpp:62
Node text(TextOptions const &options)
A text node.
Definition author.hpp:3126
AnyHandle env(std::string const &name)
A client environment value, {"env": name}: widthClass, locale, platform, online, backend.
Definition author.hpp:1209
AnyHandle content()
The content sizing, "content": as much as the content needs.
Definition author.hpp:1104
constexpr Self self
The dialog or drawer a command list runs in.
Definition author.hpp:1295
What the UI service's client and server share (spec 5 §12): content hashes, the bundle's manifest,...
constexpr std::string_view kCatalogKind
The envelope kind of the catalog.
Definition bundle.hpp:41
std::expected< CatalogReply, std::string > parseCatalogReply(std::string_view text)
Reads a ui-catalog reply body.
Definition bundle.hpp:683
constexpr std::string_view kEvaluateKind
The envelope kind that evaluates an action body without running it (spec 5 §7).
Definition bundle.hpp:45
Move-only handle representing the eventual result of an asynchronous operation.
Single-thread executor intended for use on the calling (main) thread.
Definition executor.hpp:253
void runFor(std::chrono::milliseconds timeout)
Pumps the task queue for up to timeout.
Definition executor.hpp:280
Serves MCP's tools over one server connection (spec 8 §11).
Definition agent.hpp:79
async::Completion< std::optional< std::string > > handle(std::string const &message)
Handles one JSON-RPC message.
Definition agent.hpp:111
~McpAdapter()
Closes the adapter's connection, releasing its private instances.
Definition agent.hpp:94
McpAdapter(std::shared_ptr< backend::RemoteServer > server, McpOptions options)
An adapter on a new connection of server.
Definition agent.hpp:85
std::uint64_t ConnectionId
Opaque id a transport uses to scope model registrations to one connection so RemoteServer::closeConne...
Value evaluate(Expr const &expr, Environment const &env)
Evaluates an expression.
JSON-Forms-style schema generation for morph actions.
void request(Store< ViewState, Msg > &store, bridge::BridgeHandler< Model, Sharing > &handler, async::CallbackScope const &scope, Action action, ToMsg toMsg, ToFailMsg toFailMsg)
Executes action and sends its result, or its failure, to store as one message.
Definition store.hpp:202
Abstract executor interface.
Definition executor.hpp:70
Per-call session context carried through the bridge to the model.
Definition session.hpp:26
What an McpAdapter is built with.
Definition agent.hpp:41
exec::IExecutor * callbacks
The owner handle's completions are delivered on: a serial executor that outlives the adapter and ever...
Definition agent.hpp:52
std::string serverVersion
The version initialize answers in serverInfo.
Definition agent.hpp:48
std::string serverName
The name initialize answers in serverInfo.
Definition agent.hpp:46
session::Context session
The session every envelope carries: the agent's identity, which the server authenticates on each one,...
Definition agent.hpp:44
JSON wire envelope used between any client and RemoteServer.
Definition wire.hpp:91
The UI document's typed values (ui::Value), its types (ui::Type), their JSON form,...
SlotBinding< std::decay_t< F > > slot(SlotId slotId, F &&read)
A slot with an id.
Definition view.hpp:63