ascii-chat
Binary v0.15.83-53-gb711d8720
Library lib/v0.3.2-701-gb711d8720
Video chat in your terminal
Loading...
Searching...
No Matches
mcp-server/protocol.c
Go to the documentation of this file.
1#include "mcp.h"
4#include <ascii-chat/version.h>
7#include <string.h>
8
9static yyjson_mut_doc *envelope(yyjson_val *id) {
10 yyjson_mut_doc *d = yyjson_mut_doc_new(NULL);
11 yyjson_mut_val *root = yyjson_mut_obj(d);
12 yyjson_mut_doc_set_root(d, root);
13 yyjson_mut_obj_add_str(d, root, "jsonrpc", "2.0");
14 yyjson_mut_obj_add_val(d, root, "id", id ? yyjson_val_mut_copy(d, id) : yyjson_mut_null(d));
15 return d;
16}
17yyjson_mut_doc *mcp_error(yyjson_val *id, int code, const char *message) {
18 ASCII_OTEL_SCOPE(MCP_PROTOCOL_ERROR);
19 ASCII_OTEL_ATTR_INT("rpc.jsonrpc.error_code", code);
21 ascii_otel_error("MCP protocol request rejected");
22 yyjson_mut_doc *d = envelope(id);
23 yyjson_mut_val *e = yyjson_mut_obj(d);
24 yyjson_mut_obj_add_int(d, e, "code", code);
25 yyjson_mut_obj_add_strcpy(d, e, "message", message);
26 yyjson_mut_obj_add_val(d, yyjson_mut_doc_get_root(d), "error", e);
27 return d;
28}
29static yyjson_mut_doc *tool_result(yyjson_val *id, const http_renderer_process_result_t *r) {
30 yyjson_mut_doc *d = envelope(id);
31 yyjson_mut_val *root = yyjson_mut_doc_get_root(d), *result = yyjson_mut_obj(d), *content = yyjson_mut_arr(d),
32 *item = yyjson_mut_obj(d);
33 yyjson_mut_obj_add_str(d, item, "type", "text");
34 const char *text = r->error[0] ? r->error : r->text && r->text[0] ? r->text : r->output_path;
35 yyjson_mut_obj_add_strcpy(d, item, "text", text ? text : "");
36 yyjson_mut_arr_add_val(content, item);
37 yyjson_mut_obj_add_val(d, result, "content", content);
38 yyjson_mut_obj_add_bool(d, result, "isError", r->error[0] != 0);
39 if (!r->error[0]) {
40 yyjson_mut_val *data = yyjson_mut_obj(d), *files = yyjson_mut_arr(d), *frames = yyjson_mut_arr(d);
41 yyjson_mut_obj_add_strcpy(d, data, "ascii", r->text ? r->text : "");
42 yyjson_mut_obj_add_uint(d, data, "elapsed_ms", r->elapsed_ms);
43 for (size_t i = 0; i < r->frame_count; ++i) {
44 const http_render_frame_t *source = &r->frames[i];
45 yyjson_mut_val *frame = yyjson_mut_obj(d);
46 if (source->text) {
47 yyjson_mut_obj_add_str(d, frame, "type", "text");
48 yyjson_mut_obj_add_strcpy(d, frame, "text", source->text);
49 } else {
50 yyjson_mut_obj_add_str(d, frame, "type", "file");
51 yyjson_mut_obj_add_strcpy(d, frame, "path", source->path);
52 }
53 yyjson_mut_obj_add_uint(d, frame, "timestamp_ms", source->timestamp_ms);
54 yyjson_mut_arr_add_val(frames, frame);
55 }
56 if (r->output_path[0]) {
57 yyjson_mut_arr_add_strcpy(d, files, r->output_path);
58 yyjson_mut_val *frame = yyjson_mut_obj(d);
59 yyjson_mut_obj_add_str(d, frame, "type", "file");
60 yyjson_mut_obj_add_strcpy(d, frame, "path", r->output_path);
61 yyjson_mut_arr_add_val(frames, frame);
62 }
63 yyjson_mut_obj_add_val(d, data, "saved_files", files);
64 yyjson_mut_obj_add_val(d, data, "frames", frames);
65 yyjson_mut_obj_add_val(d, result, "structuredContent", data);
66 }
67 yyjson_mut_obj_add_val(d, root, "result", result);
68 return d;
69}
70
71// Enumerate through the same platform APIs as the CLI, without opening capture streams.
72static yyjson_mut_doc *device_list_result(yyjson_val *id, yyjson_val *arguments) {
73 if (arguments && (!yyjson_is_obj(arguments) || yyjson_obj_size(arguments)))
74 return mcp_error(id, -32602, "device_list accepts no arguments");
75 yyjson_mut_doc *d = envelope(id);
76 yyjson_mut_val *result = yyjson_mut_obj(d), *data = yyjson_mut_obj(d), *video = yyjson_mut_arr(d),
77 *audio = yyjson_mut_arr(d), *errors = yyjson_mut_arr(d);
78 webcam_device_info_t *cameras = NULL;
79 audio_device_info_t *microphones = NULL;
80 unsigned camera_count = 0, microphone_count = 0;
81 int default_video = -1, default_audio = -1;
82 asciichat_error_t err = webcam_list_devices(&cameras, &camera_count);
83 if (err != ASCIICHAT_OK) {
85 yyjson_mut_arr_add_strcpy(d, errors, HAS_ERRNO(&context) ? context.context_message : "Cannot enumerate webcams");
86 } else {
87 for (unsigned i = 0; i < camera_count; ++i) {
88 yyjson_mut_val *device = yyjson_mut_obj(d);
89 bool is_default = cameras[i].index == 0;
90 if (is_default)
91 default_video = (int)cameras[i].index;
92 yyjson_mut_obj_add_uint(d, device, "index", cameras[i].index);
93 yyjson_mut_obj_add_strcpy(d, device, "name", cameras[i].name);
94 yyjson_mut_obj_add_bool(d, device, "is_default", is_default);
95 yyjson_mut_arr_add_val(video, device);
96 }
97 }
99 err = audio_list_input_devices(&microphones, &microphone_count);
100 if (err != ASCIICHAT_OK) {
102 yyjson_mut_arr_add_strcpy(d, errors,
103 HAS_ERRNO(&context) ? context.context_message : "Cannot enumerate microphones");
104 } else {
105 for (unsigned i = 0; i < microphone_count; ++i) {
106 yyjson_mut_val *device = yyjson_mut_obj(d);
107 if (microphones[i].is_default_input)
108 default_audio = microphones[i].index;
109 yyjson_mut_obj_add_int(d, device, "index", microphones[i].index);
110 yyjson_mut_obj_add_strcpy(d, device, "name", microphones[i].name);
111 yyjson_mut_obj_add_bool(d, device, "is_default", microphones[i].is_default_input);
112 yyjson_mut_arr_add_val(audio, device);
113 }
114 }
115 audio_free_device_list(microphones);
116 yyjson_mut_obj_add_val(d, data, "video_devices", video);
117 yyjson_mut_obj_add_val(d, data, "audio_devices", audio);
118 yyjson_mut_obj_add_val(d, data, "default_video_device",
119 default_video < 0 ? yyjson_mut_null(d) : yyjson_mut_int(d, default_video));
120 yyjson_mut_obj_add_val(d, data, "default_audio_device",
121 default_audio < 0 ? yyjson_mut_null(d) : yyjson_mut_int(d, default_audio));
122 yyjson_mut_obj_add_val(d, data, "errors", errors);
123 yyjson_mut_val *content = yyjson_mut_arr(d), *item = yyjson_mut_obj(d);
124 char *text = yyjson_mut_val_write(data, 0, NULL);
125 yyjson_mut_obj_add_str(d, item, "type", "text");
126 yyjson_mut_obj_add_strcpy(d, item, "text", text ? text : "Cannot serialize device list");
127 free(text);
128 yyjson_mut_arr_add_val(content, item);
129 yyjson_mut_obj_add_val(d, result, "content", content);
130 yyjson_mut_obj_add_bool(d, result, "isError", yyjson_mut_arr_size(errors) != 0);
131 yyjson_mut_obj_add_val(d, result, "structuredContent", data);
132 yyjson_mut_obj_add_val(d, yyjson_mut_doc_get_root(d), "result", result);
133 return d;
134}
135static void run_job(void *arg, atomic_t *cancelled) {
136 mcp_job_t *job = arg;
137 ASCII_OTEL_SCOPE_PARENT(MCP_RENDER, &job->telemetry_parent);
138 static const char *const tools[] = {"render_file", "render_url", "render_webcam", "render_visualization"};
139 ASCII_OTEL_ATTR_STRING("mcp.tool", tools[job->tool]);
140 ASCII_OTEL_ATTR_STRING("mcp.transport", job->server->policy.hosted ? "http" : "stdio");
143 uint64_t render_started = time_get_ns();
144 log_info("MCP render started");
146 (job->request.source_path[0] || job->request.source_url[0])) {
147 http_render_request_t shared_request;
148 http_render_request_init(&shared_request);
150 if (job->request.source_url[0])
151 SAFE_STRNCPY(shared_request.source.url, job->request.source_url, sizeof(shared_request.source.url));
152 else
153 SAFE_STRNCPY(shared_request.source.path, job->request.source_path, sizeof(shared_request.source.path));
154 shared_request.options = job->request.options;
155 http_render_result_t shared_result = {0};
156 const char *shared_source = job->request.source_url[0] ? job->request.source_url : job->request.source_path;
157 asciichat_error_t shared_error =
158 http_renderer_render_file(shared_source, &shared_request, &shared_result, cancelled);
159 memset(&result, 0, sizeof(result));
160 result.elapsed_ms = shared_result.elapsed_ms;
161 if (shared_error == ASCIICHAT_OK) {
162 result.text = shared_result.ascii;
163 result.length = shared_result.ascii_length;
164 shared_result.ascii = NULL;
165 } else {
166 SAFE_STRNCPY(result.error, asciichat_error_string(shared_error), sizeof(result.error));
167 }
168 http_render_result_destroy(&shared_result);
169 } else {
170 http_renderer_process(&job->request, cancelled, &result);
171 }
172 stats_duration_record(stats_runtime_scope(), STATS_DURATION_MCP_RENDER, time_get_ns() - render_started);
174 ASCII_OTEL_ATTR_INT("mcp.output.bytes", (int64_t)result.length);
175 if (result.error[0]) {
177 const char *failure = result.timed_out ? "timeout" : atomic_load_bool(cancelled) ? "cancelled" : "render";
178 ASCII_OTEL_ATTR_STRING("error.type", failure);
179 if (result.timed_out)
181 else if (atomic_load_bool(cancelled))
183 SET_ERRNO(ERROR_FILE_OPERATION, "MCP render failed: %s", result.error);
184 log_error("MCP render failed: %s", result.error);
185 } else
186 log_info("MCP render completed in %llu ms", (unsigned long long)result.elapsed_ms);
187 if (!result.error[0] && result.output_path[0])
189 job->response = tool_result(mcp_get(yyjson_doc_get_root(job->message), "id"), &result);
192 if (!job->server->policy.hosted) {
193 mcp_stdio_send(job->server, job->response);
194 yyjson_mut_doc_free(job->response);
195 job->response = NULL;
196 }
197 if (job->server->policy.hosted) {
198 // The HTTP wakeup runs on the server loop and must observe completion.
199 atomic_store_bool(&job->renderer_job.done, true);
200 mcp_http_wake(job->server);
201 }
202}
203void mcp_jobs_reap(mcp_server_t *s, bool all) {
204 for (unsigned i = 0; i < s->policy.max_jobs; ++i) {
205 mcp_job_t *j = &s->jobs[i];
206 if (!j->renderer_job.started ||
207 (!all && (!http_renderer_job_is_done(&j->renderer_job) || (s->policy.hosted && j->connection))))
208 continue;
210 yyjson_doc_free(j->message);
211 if (j->response)
212 yyjson_mut_doc_free(j->response);
213 memset(j, 0, sizeof(*j));
214 }
215}
216static bool same_id(yyjson_val *a, yyjson_val *b) {
217 return a && b &&
218 ((yyjson_is_str(a) && yyjson_is_str(b) && yyjson_get_len(a) == yyjson_get_len(b) &&
219 !strcmp(yyjson_get_str(a), yyjson_get_str(b))) ||
220 (yyjson_is_int(a) && yyjson_is_int(b) && yyjson_get_sint(a) == yyjson_get_sint(b)));
221}
222yyjson_mut_doc *mcp_dispatch(mcp_server_t *s, yyjson_doc *message, void *connection, const char *session_id,
223 bool *pending) {
224 ASCII_OTEL_SCOPE(MCP_REQUEST);
225 ASCII_OTEL_ATTR_STRING("rpc.system", "jsonrpc");
226 ASCII_OTEL_ATTR_STRING("mcp.transport", s->policy.hosted ? "http" : "stdio");
228 *pending = false;
229 yyjson_val *root = yyjson_doc_get_root(message), *id = mcp_get(root, "id"), *method = mcp_get(root, "method"),
230 *version = mcp_get(root, "jsonrpc");
231 if (!yyjson_is_obj(root) || !yyjson_is_str(version) || strcmp(yyjson_get_str(version), "2.0") ||
232 !yyjson_is_str(method) || strlen(yyjson_get_str(method)) != yyjson_get_len(method) ||
233 (id && !yyjson_is_str(id) && !yyjson_is_int(id)))
234 return mcp_error(id, -32600, "Invalid JSON-RPC request");
235 const char *name = yyjson_get_str(method);
236 static const char *const methods[] = {
237 "initialize", "ping", "tools/list", "tools/call", "notifications/initialized", "notifications/cancelled"};
238 const char *method_name = "unknown";
239 for (size_t i = 0; i < sizeof(methods) / sizeof(methods[0]); i++)
240 if (!strcmp(name, methods[i]))
241 method_name = methods[i];
242 ASCII_OTEL_ATTR_STRING("rpc.method", method_name);
243 yyjson_val *params = mcp_get(root, "params");
244 if (params && !yyjson_is_obj(params))
245 return id ? mcp_error(id, -32602, "params must be an object") : NULL;
246 if (!id) {
247 if (!strcmp(name, "notifications/initialized"))
248 s->initialized = true;
249 if (!strcmp(name, "notifications/cancelled")) {
250 yyjson_val *cancel = mcp_get(params, "requestId");
251 for (unsigned i = 0; i < s->policy.max_jobs; ++i)
252 if (s->jobs[i].renderer_job.started &&
253 (!s->policy.hosted || (session_id && *session_id && !strcmp(s->jobs[i].session_id, session_id)) ||
254 ((!session_id || !*session_id) && s->jobs[i].connection == connection)) &&
255 same_id(cancel, mcp_get(yyjson_doc_get_root(s->jobs[i].message), "id")))
257 }
258 return NULL;
259 }
260 yyjson_mut_doc *d = envelope(id);
261 yyjson_mut_val *result = yyjson_mut_obj(d);
262 yyjson_mut_obj_add_val(d, yyjson_mut_doc_get_root(d), "result", result);
263 if (!strcmp(name, "initialize")) {
264 yyjson_val *requested = mcp_get(params, "protocolVersion");
265 const char *v = yyjson_get_str(requested);
266 if (!v || (strcmp(v, "2025-11-25") && strcmp(v, "2025-06-18") && strcmp(v, "2025-03-26")))
267 v = "2025-11-25";
268 yyjson_mut_obj_add_strcpy(d, result, "protocolVersion", v);
269 yyjson_mut_val *caps = yyjson_mut_obj(d), *info = yyjson_mut_obj(d);
270 yyjson_mut_obj_add_val(d, caps, "tools", yyjson_mut_obj(d));
271 yyjson_mut_obj_add_val(d, result, "capabilities", caps);
272 yyjson_mut_obj_add_str(d, info, "name", "ascii-chat");
273 yyjson_mut_obj_add_str(d, info, "version", ASCII_CHAT_VERSION_FULL);
274 yyjson_mut_obj_add_val(d, result, "serverInfo", info);
275 const char *instructions =
277 ? "Render uploaded bytes or HTTP(S) media URLs as ASCII art. The hosted server cannot access your webcam "
278 "or "
279 "local filesystem. Rendered files are returned as temporary download links with UTC expiration "
280 "timestamps; "
281 "links expire after 30 minutes. Use output_format to select the file format, snapshot-delay to set clip "
282 "duration, and options.audio=true to include source audio."
283 : "Render media as ASCII art. Uploaded bytes must be base64 encoded by the client. snapshot-delay measures "
284 "capture duration after the first rendered frame; use seek for media timestamps. output_path saves ASCII "
285 "art "
286 "as an image/video and never replaces an existing file.";
287 yyjson_mut_obj_add_str(d, result, "instructions", instructions);
288 return d;
289 }
290 if (!strcmp(name, "ping"))
291 return d;
292 if (!strcmp(name, "tools/list")) {
293 yyjson_mut_obj_add_val(d, result, "tools", mcp_tools_schema(d, &s->policy));
294 return d;
295 }
296 yyjson_mut_doc_free(d);
297 if (strcmp(name, "tools/call"))
298 return mcp_error(id, -32601, "Method not found");
299 const char *tool = yyjson_get_str(mcp_get(params, "name"));
300 if (tool && yyjson_get_len(mcp_get(params, "name")) == sizeof("device_list") - 1 && !strcmp(tool, "device_list")) {
301 if (s->policy.restricted)
302 return mcp_error(id, -32602, "Unknown tool");
303 ASCII_OTEL_ATTR_STRING("mcp.tool", "device_list");
304 return device_list_result(id, mcp_get(params, "arguments"));
305 }
306 const char *names[] = {"render_file", "render_url", "render_webcam", "render_visualization"};
307 int kind = -1;
308 for (int i = 0; i < 4; ++i)
309 if (tool && strlen(tool) == yyjson_get_len(mcp_get(params, "name")) && !strcmp(tool, names[i]))
310 kind = i;
311 if (kind < 0 || (s->policy.restricted && kind == MCP_WEBCAM))
312 return mcp_error(id, -32602, "Unknown tool");
313 mcp_jobs_reap(s, false);
314 mcp_job_t *j = NULL;
315 for (unsigned i = 0; i < s->policy.max_jobs; ++i)
316 if (!s->jobs[i].renderer_job.started) {
317 j = &s->jobs[i];
318 break;
319 }
320 if (!j) {
321 char message[128];
322 safe_snprintf(message, sizeof(message), "Server busy; at most %u render%s may run concurrently", s->policy.max_jobs,
323 s->policy.max_jobs == 1 ? "" : "s");
324 return mcp_error(id, -32000, message);
325 }
326 yyjson_val *arguments = mcp_get(params, "arguments");
327 yyjson_doc *empty = NULL;
328 if (!arguments) {
329 empty = yyjson_read("{}", 2, 0);
330 arguments = yyjson_doc_get_root(empty);
331 }
332 asciichat_error_t err = mcp_request_parse(&s->policy, (mcp_tool_t)kind, arguments, &j->request);
333 if (empty)
334 yyjson_doc_free(empty);
335 if (err != ASCIICHAT_OK) {
337 ASCII_OTEL_ATTR_STRING("error.type", "invalid_arguments");
340 SAFE_STRNCPY(fail.error, HAS_ERRNO(&ctx) ? ctx.context_message : "Invalid render request", sizeof(fail.error));
342 return tool_result(id, &fail);
343 }
344 // Copy before returning to the transport, which owns and frees the input document.
345 size_t len;
346 char *copy = yyjson_write(message, 0, &len);
347 j->message = yyjson_read(copy, len, 0);
348 free(copy);
349 j->tool = (mcp_tool_t)kind;
351 j->server = s;
352 j->connection = connection;
353 if (session_id)
355 if (http_renderer_job_start(&j->renderer_job, "mcp-render", run_job, j) != ASCIICHAT_OK) {
357 yyjson_doc_free(j->message);
358 memset(j, 0, sizeof(*j));
359 return mcp_error(id, -32603, "Cannot create render worker");
360 }
361 *pending = true;
362 return NULL;
363}
#define SET_ERRNO(code,...)
#define HAS_ERRNO(context)
audio_visualization_source_t source
Definition fft.c:24
void audio_free_device_list(audio_device_info_t *devices)
Free device list allocated by audio_list_input_devices/audio_list_output_devices.
asciichat_error_t audio_list_input_devices(audio_device_info_t **out_devices, unsigned int *out_count)
List available audio input devices (microphones).
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:423
unsigned long long uint64_t
Definition common.h:63
asciichat_error_t
Error and exit codes - unified status values (0-255).
Definition error_codes.h:44
@ ASCIICHAT_OK
Definition error_codes.h:46
@ ERROR_FILE_OPERATION
#define log_error(...)
Log an ERROR message.
Definition log/log.h:634
#define log_info(...)
Log an INFO message.
Definition log/log.h:608
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Platform-safe snprintf wrapper.
asciichat_error_t webcam_list_devices(webcam_device_info_t **out_devices, unsigned int *out_count)
Enumerate available webcam devices.
void webcam_free_device_list(webcam_device_info_t *devices)
Free device list returned by webcam_list_devices().
void mcp_http_wake(mcp_server_t *s)
Definition http.c:30
asciichat_error_t http_renderer_render_file(const char *path, const http_render_request_t *request, http_render_result_t *result, atomic_t *cancelled)
void http_render_request_init(http_render_request_t *request)
void http_renderer_job_join(http_renderer_job_t *job)
void http_render_result_destroy(http_render_result_t *result)
bool http_renderer_job_is_done(const http_renderer_job_t *job)
asciichat_error_t http_renderer_job_start(http_renderer_job_t *job, const char *name, http_renderer_job_fn execute, void *context)
void http_renderer_job_cancel(http_renderer_job_t *job)
void http_renderer_process(http_renderer_process_request_t *request, atomic_t *cancelled, http_renderer_process_result_t *result)
@ HTTP_RENDER_SOURCE_FILE
@ HTTP_RENDER_SOURCE_URL
@ HTTP_RENDER_OUTPUT_TEXT
void http_renderer_process_result_destroy(http_renderer_process_result_t *result)
🔊 Audio Capture and Playback Interface for ascii-chat
@ STATS_COUNTER_MCP_RENDER_FAILURES
void stats_counter_add(stats_scope_t *scope, stats_counter_id_t id, uint64_t amount)
void stats_duration_record(stats_scope_t *scope, stats_duration_id_t id, uint64_t elapsed_ns)
yyjson_mut_doc * mcp_error(yyjson_val *id, int code, const char *message)
void mcp_jobs_reap(mcp_server_t *s, bool all)
yyjson_mut_doc * mcp_dispatch(mcp_server_t *s, yyjson_doc *message, void *connection, const char *session_id, bool *pending)
void mcp_request_destroy(http_renderer_process_request_t *request)
Definition request.c:678
yyjson_mut_val * mcp_tools_schema(yyjson_mut_doc *doc, const mcp_policy_t *policy)
Definition request.c:158
mcp_tool_t
Definition mcp.h:34
@ MCP_WEBCAM
Definition mcp.h:34
struct mcp_server mcp_server_t
The useful part of mcp job is in server, message, response, request: together they describe what is p...
struct mcp_job mcp_job_t
When code needs to work with mcp policy, it keeps hosted, restricted, address, address6 here instead ...
asciichat_error_t mcp_request_parse(const mcp_policy_t *policy, mcp_tool_t tool, yyjson_val *arguments, http_renderer_process_request_t *out)
Definition request.c:416
void mcp_stdio_send(mcp_server_t *server, yyjson_mut_doc *response)
Definition stdio.c:19
gotel_context_t ascii_otel_context(void)
Definition otel.c:725
void ascii_otel_error(const char *message)
Definition otel.c:804
#define ASCII_OTEL_SCOPE(id)
Definition otel.h:72
#define ASCII_OTEL_SCOPE_PARENT(id, parent)
Definition otel.h:69
#define ASCII_OTEL_ATTR_INT(key, value)
Definition otel.h:67
#define ASCII_OTEL_ATTR_STRING(key, value)
Definition otel.h:66
static bool size_t index
Definition queries.h:8
stats_scope_t * stats_runtime_scope(void)
Definition runtime.c:80
uint8_t session_id[SESSION_ID_SIZE]
🔤 String Manipulation and Shell Escaping Utilities
Audio device information structure.
int index
PortAudio device index.
http_render_source_t source
http_render_options_t options
char path[PLATFORM_MAX_PATH_LENGTH]
http_render_source_type_t type
char url[OPTIONS_BUFF_SIZE]
http_render_output_type_t output_type
http_render_options_t options
char source_path[PLATFORM_MAX_PATH_LENGTH]
char source_url[OPTIONS_BUFF_SIZE]
char output_path[PLATFORM_MAX_PATH_LENGTH]
http_render_frame_t * frames
yyjson_doc * message
Definition mcp.h:62
mcp_tool_t tool
Definition mcp.h:65
void * connection
Definition mcp.h:68
gotel_context_t telemetry_parent
Definition mcp.h:66
yyjson_mut_doc * response
Definition mcp.h:63
http_renderer_job_t renderer_job
Definition mcp.h:67
char session_id[32+1]
Definition mcp.h:69
struct mcp_server * server
Definition mcp.h:61
http_renderer_process_request_t request
Definition mcp.h:64
unsigned max_jobs
Definition mcp.h:41
bool restricted
Definition mcp.h:37
bool hosted
Definition mcp.h:36
bool initialized
Definition mcp.h:86
mcp_job_t * jobs
Definition mcp.h:82
mcp_policy_t policy
Definition mcp.h:81
Webcam device information structure.
Definition webcam.h:88
unsigned int index
Device index (use with webcam_init_context).
Definition webcam.h:89
⏱️ High-precision timing utilities using sokol_time.h and uthash