ascii-chat
Binary v0.15.83-53-gb711d8720
Library lib/v0.3.2-701-gb711d8720
Video chat in your terminal
Loading...
Searching...
No Matches
protocol.c File Reference

Go to the source code of this file.

Functions

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)

Function Documentation

◆ mcp_dispatch()

yyjson_mut_doc * mcp_dispatch ( mcp_server_t * s,
yyjson_doc * message,
void * connection,
const char * session_id,
bool * pending )

Definition at line 222 of file mcp-server/protocol.c.

223 {
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 HAS_ERRNO(context)
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:423
asciichat_error_t
Error and exit codes - unified status values (0-255).
Definition error_codes.h:44
@ ASCIICHAT_OK
Definition error_codes.h:46
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Platform-safe snprintf wrapper.
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 stats_counter_add(stats_scope_t *scope, stats_counter_id_t id, uint64_t amount)
yyjson_mut_doc * mcp_error(yyjson_val *id, int code, const char *message)
void mcp_jobs_reap(mcp_server_t *s, bool all)
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_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
gotel_context_t ascii_otel_context(void)
Definition otel.c:725
#define ASCII_OTEL_SCOPE(id)
Definition otel.h:72
#define ASCII_OTEL_ATTR_STRING(key, value)
Definition otel.h:66
stats_scope_t * stats_runtime_scope(void)
Definition runtime.c:80
uint8_t session_id[SESSION_ID_SIZE]
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
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

References ASCII_OTEL_ATTR_STRING, ascii_otel_context(), ASCII_OTEL_SCOPE, ASCIICHAT_OK, mcp_job::connection, asciichat_error_context_t::context_message, http_renderer_process_result_t::error, HAS_ERRNO, mcp_policy_t::hosted, http_renderer_job_cancel(), http_renderer_job_start(), mcp_server::initialized, mcp_server::jobs, mcp_policy_t::max_jobs, mcp_error(), mcp_jobs_reap(), mcp_request_destroy(), mcp_request_parse(), mcp_tools_schema(), MCP_WEBCAM, mcp_job::message, mcp_server::policy, mcp_job::renderer_job, mcp_job::request, mcp_policy_t::restricted, safe_snprintf(), SAFE_STRNCPY, mcp_job::server, mcp_job::session_id, session_id, http_renderer_job::started, stats_counter_add(), STATS_COUNTER_MCP_REJECTED, STATS_COUNTER_MCP_REQUESTS, stats_runtime_scope(), mcp_job::telemetry_parent, and mcp_job::tool.

Referenced by mcp_stdio_run().

◆ mcp_error()

yyjson_mut_doc * mcp_error ( yyjson_val * id,
int code,
const char * message )

Definition at line 17 of file mcp-server/protocol.c.

17 {
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}
void ascii_otel_error(const char *message)
Definition otel.c:804
#define ASCII_OTEL_ATTR_INT(key, value)
Definition otel.h:67

References ASCII_OTEL_ATTR_INT, ascii_otel_error(), ASCII_OTEL_SCOPE, stats_counter_add(), STATS_COUNTER_MCP_REJECTED, and stats_runtime_scope().

Referenced by mcp_dispatch(), and mcp_stdio_run().

◆ mcp_jobs_reap()

void mcp_jobs_reap ( mcp_server_t * s,
bool all )

Definition at line 203 of file mcp-server/protocol.c.

203 {
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}
void http_renderer_job_join(http_renderer_job_t *job)
bool http_renderer_job_is_done(const http_renderer_job_t *job)
yyjson_mut_doc * response
Definition mcp.h:63

References mcp_job::connection, mcp_policy_t::hosted, http_renderer_job_is_done(), http_renderer_job_join(), mcp_server::jobs, mcp_policy_t::max_jobs, mcp_job::message, mcp_server::policy, mcp_job::renderer_job, mcp_job::response, and http_renderer_job::started.

Referenced by mcp_dispatch(), mcp_http_run(), mcp_server_main(), and mcp_stdio_run().