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.h File Reference

Go to the source code of this file.

Data Structures

struct  mcp_policy_t
struct  mcp_job
 When code needs to work with mcp policy, it keeps hosted, restricted, address, address6 here instead of scattering it across globals. Initialization establishes the handles and sizes; teardown must release any allocation, lock, thread, or callback context recorded in those fields. More...
struct  mcp_server
 The useful part of mcp job is in server, message, response, request: together they describe what is pending, who owns it, and how much data is valid. Keep the object with its parent operation and use the parent cleanup path. More...

Macros

#define MCP_SERVER_MCP_ERROR_BUFFER_SIZE   2048
#define MCP_MAX_INPUT   (24u * 1024u * 1024u)
#define MCP_MAX_OUTPUT   (8u * 1024u * 1024u)
#define MCP_MAX_DOWNLOAD   (64u * 1024u * 1024u)
#define MCP_SESSION_ID_LENGTH   32

Typedefs

typedef struct mcp_job mcp_job_t
 When code needs to work with mcp policy, it keeps hosted, restricted, address, address6 here instead of scattering it across globals. Initialization establishes the handles and sizes; teardown must release any allocation, lock, thread, or callback context recorded in those fields.
typedef struct mcp_server mcp_server_t
 The useful part of mcp job is in server, message, response, request: together they describe what is pending, who owns it, and how much data is valid. Keep the object with its parent operation and use the parent cleanup path.

Enumerations

enum  mcp_tool_t { MCP_FILE , MCP_URL , MCP_WEBCAM , MCP_VISUALIZATION }

Functions

int mcp_server_main (void)
asciichat_error_t mcp_stdio_reserve (void)
void mcp_stdio_release (void)
yyjson_mut_val * mcp_tools_schema (yyjson_mut_doc *doc, const mcp_policy_t *policy)
asciichat_error_t mcp_request_parse (const mcp_policy_t *policy, mcp_tool_t tool, yyjson_val *arguments, http_renderer_process_request_t *out)
void mcp_request_destroy (http_renderer_process_request_t *request)
yyjson_mut_doc * mcp_dispatch (mcp_server_t *server, yyjson_doc *message, void *connection, const char *session_id, bool *pending)
yyjson_mut_doc * mcp_error (yyjson_val *id, int code, const char *message)
void mcp_jobs_reap (mcp_server_t *server, bool all)
void mcp_stdio_send (mcp_server_t *server, yyjson_mut_doc *response)
asciichat_error_t mcp_stdio_run (mcp_server_t *server)
asciichat_error_t mcp_http_run (mcp_server_t *server)
void mcp_http_wake (mcp_server_t *server)
const char * mcp_output_mime (const char *extension)
bool mcp_download_name (const char *name, int64_t *expires)
void mcp_download_prune (const char *directory, int64_t now)
asciichat_error_t mcp_download_finalize (const mcp_policy_t *policy, yyjson_mut_doc *doc, int64_t now)

Variables

const char *const mcp_output_formats []

Macro Definition Documentation

◆ MCP_MAX_DOWNLOAD

#define MCP_MAX_DOWNLOAD   (64u * 1024u * 1024u)

Definition at line 31 of file mcp.h.

Referenced by mcp_http_run().

◆ MCP_MAX_INPUT

#define MCP_MAX_INPUT   (24u * 1024u * 1024u)

Definition at line 29 of file mcp.h.

Referenced by mcp_http_run(), and mcp_stdio_run().

◆ MCP_MAX_OUTPUT

#define MCP_MAX_OUTPUT   (8u * 1024u * 1024u)

Definition at line 30 of file mcp.h.

◆ MCP_SERVER_MCP_ERROR_BUFFER_SIZE

#define MCP_SERVER_MCP_ERROR_BUFFER_SIZE   2048

Definition at line 4 of file mcp.h.

◆ MCP_SESSION_ID_LENGTH

#define MCP_SESSION_ID_LENGTH   32

Definition at line 32 of file mcp.h.

Typedef Documentation

◆ mcp_job_t

typedef struct mcp_job mcp_job_t

When code needs to work with mcp policy, it keeps hosted, restricted, address, address6 here instead of scattering it across globals. Initialization establishes the handles and sizes; teardown must release any allocation, lock, thread, or callback context recorded in those fields.

For mcp render result, this is the live record that ties text, length, elapsed_ms, timed_out together. The owning code creates it, updates it while work is active, and releases its handles and pointed-to storage before freeing the record.

◆ mcp_server_t

typedef struct mcp_server mcp_server_t

The useful part of mcp job is in server, message, response, request: together they describe what is pending, who owns it, and how much data is valid. Keep the object with its parent operation and use the parent cleanup path.

mcp server keeps the values that move through the subsystem: policy, jobs, output_mutex, shared_http. Some members are pointers into caller- or subsystem-owned memory, so lifetime follows the API that populated them rather than the struct copy itself.

Enumeration Type Documentation

◆ mcp_tool_t

enum mcp_tool_t
Enumerator
MCP_FILE 
MCP_URL 
MCP_WEBCAM 
MCP_VISUALIZATION 

Definition at line 34 of file mcp.h.

mcp_tool_t
Definition mcp.h:34
@ MCP_VISUALIZATION
Definition mcp.h:34
@ MCP_URL
Definition mcp.h:34
@ MCP_FILE
Definition mcp.h:34
@ MCP_WEBCAM
Definition mcp.h:34

Function Documentation

◆ mcp_dispatch()

yyjson_mut_doc * mcp_dispatch ( mcp_server_t * server,
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")))
256 http_renderer_job_cancel(&s->jobs[i].renderer_job);
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 =
276 s->policy.restricted && s->policy.hosted
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
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

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_download_finalize()

asciichat_error_t mcp_download_finalize ( const mcp_policy_t * policy,
yyjson_mut_doc * doc,
int64_t now )

Definition at line 74 of file downloads.c.

74 {
75 yyjson_mut_val *result = yyjson_mut_obj_get(yyjson_mut_doc_get_root(doc), "result");
76 yyjson_mut_val *data = yyjson_mut_obj_get(result, "structuredContent");
77 yyjson_mut_val *files = yyjson_mut_obj_get(data, "saved_files");
78 yyjson_mut_val *first = yyjson_mut_arr_get_first(files);
79 const char *old = yyjson_mut_get_str(first);
80 if (!old || !*old)
81 return ASCIICHAT_OK;
82 const char *name = strrchr(old, '/');
83 const char *back = strrchr(old, '\\');
84 if (back && (!name || back > name))
85 name = back;
86 name = name ? name + 1 : old;
87 int64_t ignored;
88 if (!mcp_download_name(name, &ignored))
89 return SET_ERRNO(ERROR_INVALID_STATE, "Invalid generated download name");
90 const char *token = strchr(name, '-');
91 int64_t expires = now + 1800;
92 char new_name[128], new_path[PLATFORM_MAX_PATH_LENGTH], url[PLATFORM_MAX_PATH_LENGTH * 2];
93 int n = safe_snprintf(new_name, sizeof(new_name), "%lld%s", (long long)expires, token);
94 if (n < 0 || (size_t)n >= sizeof(new_name))
95 return SET_ERRNO(ERROR_INVALID_STATE, "Download name is too long");
96 n = safe_snprintf(new_path, sizeof(new_path), "%s/%s", policy->downloads_dir, new_name);
97 if (n < 0 || (size_t)n >= sizeof(new_path))
98 return SET_ERRNO(ERROR_INVALID_STATE, "Download path is too long");
99 const char *prefix = policy->downloads_url_prefix;
100 char fallback[128];
101 if (!prefix || !*prefix) {
102 SAFE_SNPRINTF(fallback, sizeof(fallback), "%s://127.0.0.1:%d",
103 policy->tls_cert && *policy->tls_cert ? "https" : "http", policy->port);
104 prefix = fallback;
105 }
106 size_t prefix_len = strlen(prefix);
107 n = safe_snprintf(url, sizeof(url), "%s%sdownloads/%s", prefix,
108 prefix_len && prefix[prefix_len - 1] == '/' ? "" : "/", new_name);
109 if (n < 0 || (size_t)n >= sizeof(url))
110 return SET_ERRNO(ERROR_INVALID_STATE, "Download URL is too long");
111 if (strcmp(old, new_path)) {
112 uv_fs_t fs;
113 int rc = uv_fs_link(NULL, &fs, old, new_path, NULL);
114 uv_fs_req_cleanup(&fs);
115 if (rc < 0)
116 return SET_ERRNO(ERROR_FILE_OPERATION, "Cannot publish download: %s", uv_strerror(rc));
117 uv_fs_unlink(NULL, &fs, old, NULL);
118 uv_fs_req_cleanup(&fs);
119 }
120 time_t t = (time_t)expires;
121 struct tm utc;
122 char timestamp[32];
123 if (platform_gtime(&t, &utc) != ASCIICHAT_OK || !strftime(timestamp, sizeof(timestamp), "%Y-%m-%dT%H:%M:%SZ", &utc))
124 return SET_ERRNO(ERROR_INVALID_STATE, "Cannot format download expiration");
125 yyjson_mut_arr_clear(files);
126 yyjson_mut_arr_add_strcpy(doc, files, url);
127 yyjson_mut_obj_add_strcpy(doc, data, "download_url", url);
128 yyjson_mut_obj_add_strcpy(doc, data, "expires_at", timestamp);
129 yyjson_mut_val *content = yyjson_mut_obj_get(result, "content");
130 yyjson_mut_val *item = yyjson_mut_arr_get_first(content);
131 char description[sizeof(url) + 80];
132 SAFE_SNPRINTF(description, sizeof(description), "Download: %s\nExpires: %s", url, timestamp);
133 yyjson_mut_obj_remove_str(item, "text");
134 yyjson_mut_obj_add_strcpy(doc, item, "text", description);
135 return ASCIICHAT_OK;
136}
#define SET_ERRNO(code,...)
bool mcp_download_name(const char *name, int64_t *expires)
Definition downloads.c:22
#define SAFE_SNPRINTF(buffer, buffer_size,...)
Definition common.h:507
#define PLATFORM_MAX_PATH_LENGTH
Definition common.h:95
@ ERROR_INVALID_STATE
@ ERROR_FILE_OPERATION
asciichat_error_t platform_gtime(const time_t *timer, struct tm *result)
Platform-safe gmtime wrapper.
const char * tls_cert
Definition mcp.h:40
char downloads_dir[PLATFORM_MAX_PATH_LENGTH]
Definition mcp.h:44
const char * downloads_url_prefix
Definition mcp.h:45
int port
Definition mcp.h:43

References ASCIICHAT_OK, mcp_policy_t::downloads_dir, mcp_policy_t::downloads_url_prefix, ERROR_FILE_OPERATION, ERROR_INVALID_STATE, mcp_download_name(), platform_gtime(), PLATFORM_MAX_PATH_LENGTH, mcp_policy_t::port, SAFE_SNPRINTF, safe_snprintf(), SET_ERRNO, and mcp_policy_t::tls_cert.

◆ mcp_download_name()

bool mcp_download_name ( const char * name,
int64_t * expires )

Definition at line 22 of file downloads.c.

22 {
23 if (!name || strlen(name) < 47)
24 return false;
25 const char *p = name;
26 while (isdigit((unsigned char)*p))
27 ++p;
28 if (p == name || p - name > 12 || *p++ != '-')
29 return false;
30 const char *digits_end = p - 1;
31 for (int i = 0; i < 32; ++i)
32 if (!((*p >= '0' && *p <= '9') || (*p >= 'a' && *p <= 'f')))
33 return false;
34 else
35 ++p;
36 if (*p++ != '.')
37 return false;
38 if (!mcp_output_mime(p))
39 return false;
40 char number[16];
41 size_t n = (size_t)(digits_end - name);
42 memcpy(number, name, n);
43 number[n] = '\0';
44 int64_t value = (int64_t)strtoll(number, NULL, 10);
45 if (value <= 0)
46 return false;
47 if (expires)
48 *expires = value;
49 return true;
50}
const char * mcp_output_mime(const char *extension)
Definition downloads.c:10

References mcp_output_mime().

Referenced by mcp_download_finalize(), and mcp_download_prune().

◆ mcp_download_prune()

void mcp_download_prune ( const char * directory,
int64_t now )

Definition at line 52 of file downloads.c.

52 {
53 uv_fs_t scan;
54 if (uv_fs_scandir(NULL, &scan, directory, 0, NULL) < 0) {
55 uv_fs_req_cleanup(&scan);
56 return;
57 }
58 uv_dirent_t entry;
59 while (uv_fs_scandir_next(&scan, &entry) == 0) {
60 int64_t expires;
61 if (!mcp_download_name(entry.name, &expires) || expires > now)
62 continue;
63 char path[PLATFORM_MAX_PATH_LENGTH];
64 int n = safe_snprintf(path, sizeof(path), "%s/%s", directory, entry.name);
65 if (n < 0 || (size_t)n >= sizeof(path))
66 continue;
67 uv_fs_t op;
68 uv_fs_unlink(NULL, &op, path, NULL);
69 uv_fs_req_cleanup(&op);
70 }
71 uv_fs_req_cleanup(&scan);
72}

References mcp_download_name(), PLATFORM_MAX_PATH_LENGTH, and safe_snprintf().

◆ 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_http_run()

asciichat_error_t mcp_http_run ( mcp_server_t * server)

Definition at line 233 of file http.c.

233 {
234 http_server_config_t config = {.bind_address = s->policy.address,
235 .bind_address6 = s->policy.address6,
236 .port = (uint16_t)s->policy.port,
237 .enable_http1 = true,
238 .enable_http2 = true,
239 .tls_cert_path = s->policy.tls_cert,
240 .tls_key_path = s->policy.tls_key,
241 .tls_key_password = s->policy.tls_password,
242 .max_connections = s->policy.max_jobs * 4U,
243 .max_request_body_bytes = MCP_MAX_INPUT,
244 .max_response_body_bytes = MCP_MAX_DOWNLOAD,
245 .on_request = shared_http_request,
246 .on_wakeup = shared_http_wakeup,
247 .user_data = s};
248 if (!config.max_connections)
249 config.max_connections = 1;
250 asciichat_error_t result = http_server_init(&s->shared_http, &config);
251 if (result != ASCIICHAT_OK)
252 return result;
253 result = http_server_start(s->shared_http);
254 if (result != ASCIICHAT_OK) {
255 http_server_destroy(&s->shared_http);
256 return result;
257 }
258 nat_upnp_context_t *mapping = NULL;
259 if (GET_OPTION(enable_upnp)) {
260 asciichat_error_t err = nat_upnp_open((uint16_t)s->policy.port, "ascii-chat MCP", &mapping);
261 if (err != ASCIICHAT_OK)
262 LOG_ERRNO_IF_SET("MCP port forwarding unavailable");
263 }
264 while (!atomic_load_bool(&s->stopping) && !APP_CALLBACK_BOOL(should_exit)) {
265 uv_sleep(16);
266 mcp_jobs_reap(s, false);
267 if (mapping && time_get_ns() >= mapping->refresh_at_ns) {
268 asciichat_error_t err = nat_upnp_refresh(mapping);
269 if (err != ASCIICHAT_OK)
270 LOG_ERRNO_IF_SET("MCP mapping renewal failed");
271 }
272 }
273 for (unsigned i = 0; i < s->policy.max_jobs; ++i)
274 if (s->jobs[i].renderer_job.started)
275 http_renderer_job_cancel(&s->jobs[i].renderer_job);
276 http_server_stop(s->shared_http);
277 http_server_destroy(&s->shared_http);
278 nat_upnp_close(&mapping);
279 mcp_jobs_reap(s, true);
280 return ASCIICHAT_OK;
281}
#define APP_CALLBACK_BOOL(callback_name)
#define LOG_ERRNO_IF_SET(message)
unsigned short uint16_t
Definition common.h:61
#define GET_OPTION(field)
Safely get a specific option field (lock-free read).
Definition options.h:1336
asciichat_error_t http_server_init(http_server_t **out, const http_server_config_t *cfg)
void http_server_stop(http_server_t *s)
void http_server_destroy(http_server_t **p)
asciichat_error_t http_server_start(http_server_t *s)
bool should_exit(void)
Definition main.c:249
#define MCP_MAX_INPUT
Definition mcp.h:29
#define MCP_MAX_DOWNLOAD
Definition mcp.h:31
void mcp_jobs_reap(mcp_server_t *server, bool all)
uint64_t refresh_at_ns
Next renewal or retry deadline.
Definition upnp.h:44
asciichat_error_t nat_upnp_refresh(nat_upnp_context_t *ctx)
Refresh port mapping (e.g., for long-running servers).
Definition upnp.c:316
void nat_upnp_close(nat_upnp_context_t **ctx)
Close port mapping and clean up.
Definition upnp.c:251
asciichat_error_t nat_upnp_open(uint16_t internal_port, const char *description, nat_upnp_context_t **ctx)
Discover and open port via UPnP.
Definition upnp.c:203
struct nat_upnp_context nat_upnp_context_t
Handle to UPnP context.

References mcp_policy_t::address, mcp_policy_t::address6, APP_CALLBACK_BOOL, ASCIICHAT_OK, GET_OPTION, http_renderer_job_cancel(), http_server_destroy(), http_server_init(), http_server_start(), http_server_stop(), mcp_server::jobs, LOG_ERRNO_IF_SET, http_server_config_t::max_connections, mcp_policy_t::max_jobs, mcp_jobs_reap(), MCP_MAX_DOWNLOAD, MCP_MAX_INPUT, nat_upnp_close(), nat_upnp_open(), nat_upnp_refresh(), mcp_server::policy, mcp_policy_t::port, nat_upnp_context::refresh_at_ns, mcp_job::renderer_job, mcp_server::shared_http, should_exit(), http_renderer_job::started, mcp_server::stopping, mcp_policy_t::tls_cert, mcp_policy_t::tls_key, and mcp_policy_t::tls_password.

Referenced by mcp_server_main().

◆ mcp_http_wake()

void mcp_http_wake ( mcp_server_t * server)

Definition at line 30 of file http.c.

30 {
31 if (s && s->shared_http)
32 (void)http_server_wakeup(s->shared_http);
33}
asciichat_error_t http_server_wakeup(http_server_t *s)

References http_server_wakeup(), and mcp_server::shared_http.

◆ mcp_jobs_reap()

void mcp_jobs_reap ( mcp_server_t * server,
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().

◆ mcp_output_mime()

const char * mcp_output_mime ( const char * extension)

Definition at line 10 of file downloads.c.

10 {
11 static const char *const mime[] = {"image/png", "image/jpeg", "image/jpeg", "image/bmp",
12 "image/webp", "image/gif", "video/mp4", "video/quicktime",
13 "video/x-matroska", "video/webm", "video/x-msvideo", "video/x-flv",
14 "video/ogg", "video/ogg", "video/3gpp"};
15 for (size_t i = 0; mcp_output_formats[i]; ++i)
16 if (!strcmp(extension, mcp_output_formats[i]))
17 return mime[i];
18 return NULL;
19}
const char *const mcp_output_formats[]
Definition downloads.c:7

References mcp_output_formats.

Referenced by mcp_download_name(), and mcp_request_parse().

◆ mcp_request_destroy()

void mcp_request_destroy ( http_renderer_process_request_t * request)

Definition at line 678 of file request.c.

678 {
679 for (size_t i = 0; i < r->argc; ++i)
680 SAFE_FREE(r->argv[i]);
681 SAFE_FREE(r->argv);
682 if (r->upload_dir[0]) {
683 asciichat_error_t e = platform_rmdir_recursive(r->upload_dir);
684 (void)e;
685 }
686 if (r->stage_dir[0]) {
687 asciichat_error_t e = platform_rmdir_recursive(r->stage_dir);
688 (void)e;
689 }
690 memset(r, 0, sizeof(*r));
691}
#define SAFE_FREE(ptr)
Definition common.h:377

References http_renderer_process_request_t::argc, http_renderer_process_request_t::argv, SAFE_FREE, http_renderer_process_request_t::stage_dir, and http_renderer_process_request_t::upload_dir.

Referenced by mcp_dispatch().

◆ mcp_request_parse()

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 at line 416 of file request.c.

417 {
418 memset(r, 0, sizeof(*r));
419 r->output_type = HTTP_RENDER_OUTPUT_TEXT;
420 r->options.width = 100;
421 r->options.height = 40;
422 r->options.color_mode = TERM_COLOR_NONE;
423 r->timeout_ms = 45000;
424 r->argv = SAFE_CALLOC(MCP_REQUEST_ARGV_CAPACITY, sizeof(char *), char **);
425 static const char *const fields[] = {"path", "data_base64", "mime_type", "url", "video_device",
426 "audio", "visualization", "options", "output_path", "timeout_ms",
427 "audio_device", "volume", "output_format", "output", NULL};
428 asciichat_error_t err = check_fields(a, fields);
429 if (err)
430 return err;
431 const char *source_fields[] = {"path", "data_base64", "mime_type", "url", "video_device",
432 "audio", "visualization", "audio_device", "volume"};
433 for (size_t i = 0; i < 9; ++i) {
434 bool allowed = tool == MCP_FILE ? i < 3 : tool == MCP_URL ? i == 3 : tool == MCP_WEBCAM ? i == 4 || i >= 7 : i >= 5;
435 if (!allowed && mcp_get(a, source_fields[i]))
436 return SET_ERRNO(ERROR_INVALID_PARAM, "Argument %s is not valid for this tool", source_fields[i]);
437 }
438 if (p->restricted && (tool == MCP_WEBCAM || mcp_get(a, "path") || mcp_get(a, "output_path") ||
439 mcp_get(a, "audio_device") || mcp_get(a, "volume")))
440 return SET_ERRNO(ERROR_INVALID_PARAM, "Local capability is unavailable in server mode");
441 yyjson_val *timeout = mcp_get(a, "timeout_ms");
442 if (timeout) {
443 if (!yyjson_is_int(timeout) || yyjson_get_sint(timeout) < 1000 || yyjson_get_sint(timeout) > 60000)
444 return SET_ERRNO(ERROR_INVALID_PARAM, "timeout_ms must be 1000–60000");
445 r->timeout_ms = (unsigned)yyjson_get_uint(timeout);
446 }
447 char exe[PLATFORM_MAX_PATH_LENGTH];
448 if (!platform_get_executable_path(exe, sizeof(exe)))
449 return SET_ERRNO(ERROR_FILE_OPERATION, "Cannot locate renderer executable");
450 yyjson_val *opts = mcp_get(a, "options");
451 if (opts && !yyjson_is_obj(opts))
452 return SET_ERRNO(ERROR_INVALID_PARAM, "options must be an object");
453 yyjson_val *format_value = mcp_get(a, "output_format"), *output_value = mcp_get(a, "output_path"),
454 *output_mode = mcp_get(a, "output");
455 if (output_mode && (!yyjson_is_str(output_mode) ||
456 (strcmp(yyjson_get_str(output_mode), "text") && strcmp(yyjson_get_str(output_mode), "files"))))
457 return SET_ERRNO(ERROR_INVALID_PARAM, "output must be text or files");
458 if (output_mode && !strcmp(yyjson_get_str(output_mode), "text") && output_value)
459 return SET_ERRNO(ERROR_INVALID_PARAM, "output=text cannot be combined with output_path");
460 if (output_mode && !strcmp(yyjson_get_str(output_mode), "files"))
461 r->output_type = HTTP_RENDER_OUTPUT_FILE;
462 const char *output_name = yyjson_is_str(output_value) ? yyjson_get_str(output_value) : "";
463 const char *output_ext = strrchr(output_name, '.');
464 bool gif_requested = yyjson_is_str(format_value) && !strcmp(yyjson_get_str(format_value), "gif");
465 bool gif_path = output_ext && !strcmp(output_ext, ".gif");
466 bool gif_output = gif_requested || gif_path;
467 bool media_tool = tool == MCP_URL || tool == MCP_FILE;
468 bool media_source = mcp_get(a, "url") || mcp_get(a, "path") || mcp_get(a, "data_base64");
469 bool snapshot_requested = mcp_get(opts, "snapshot") || mcp_get(opts, "snapshot-delay");
470 bool gif_auto = gif_output && media_tool && media_source && !snapshot_requested;
471 arg(r, exe);
472 arg(r, "--log-level=error");
473 arg(r, "--no-check-update");
474 arg(r, "mirror");
475 option(r, "width", "100");
476 option(r, "height", "40");
477 option(r, "color-mode", "none");
478 option(r, "strip-ansi", "true");
479 option(r, "snapshot", gif_auto ? "false" : "true");
480 if (!gif_auto && !yyjson_is_false(mcp_get(opts, "snapshot")))
481 option(r, "snapshot-delay", "0");
482 option(r, "splash-screen", "false");
483 option(r, "no-audio-playback", "true");
484 option(r, "audio", "false");
485 size_t i, max;
486 yyjson_val *k, *v;
487 if (opts)
488 yyjson_obj_foreach(opts, i, max, k, v) {
489 const char *name = yyjson_get_str(k);
490 const registry_entry_t *e = exposed(p, name);
491 if (strlen(name) != yyjson_get_len(k) || !e)
492 return SET_ERRNO(ERROR_INVALID_PARAM, "Unsupported option: %s", name);
494 bool omitted = false;
495 err = options_json_parse_value(name, v, buf, sizeof(buf), &omitted);
496 if (err)
497 return err;
498 if (omitted)
499 continue;
500 double n = yyjson_get_num(v);
501 if ((!strcmp(name, "width") && (n < e->metadata.numeric_range.min || n > 512)) ||
502 (!strcmp(name, "height") && (n < e->metadata.numeric_range.min || n > 256)) ||
503 (!strcmp(name, "snapshot-delay") && (n < 0 || n > 30)) || (!strcmp(name, "fps") && (n < 0 || n > 144)))
504 return SET_ERRNO(ERROR_INVALID_PARAM, "Option %s is out of bounds", name);
505 err = option(r, name, buf);
506 if (err)
507 return err;
508 if (!strcmp(name, "width"))
509 r->options.width = (int)n;
510 else if (!strcmp(name, "height"))
511 r->options.height = (int)n;
512 else if (!strcmp(name, "color-mode")) {
513 if (!strcmp(buf, "16"))
514 r->options.color_mode = TERM_COLOR_16;
515 else if (!strcmp(buf, "256"))
516 r->options.color_mode = TERM_COLOR_256;
517 else if (!strcmp(buf, "truecolor") || !strcmp(buf, "24bit"))
518 r->options.color_mode = TERM_COLOR_TRUECOLOR;
519 else
520 r->options.color_mode = TERM_COLOR_NONE;
521 } else if (!strcmp(name, "palette"))
522 SAFE_STRNCPY(r->options.palette, buf, sizeof(r->options.palette));
523 else if (!strcmp(name, "palette-chars"))
524 SAFE_STRNCPY(r->options.palette_chars, buf, sizeof(r->options.palette_chars));
525 }
526 bool synthetic = enabled(opts, "test-pattern"), test_audio = enabled(opts, "test-audio");
527 yyjson_val *audio_device = mcp_get(a, "audio_device"), *volume = mcp_get(a, "volume");
528 if (audio_device) {
529 if (!yyjson_is_int(audio_device) || yyjson_get_sint(audio_device) < -1 || yyjson_get_sint(audio_device) > INT_MAX)
530 return SET_ERRNO(ERROR_INVALID_PARAM, "audio_device must be -1 or a nonnegative device index");
532 SAFE_SNPRINTF(index, sizeof(index), "%d", (int)yyjson_get_sint(audio_device));
533 err = option(r, "microphone-index", index);
534 if (err)
535 return err;
536 }
537 if (volume) {
538 if (!yyjson_is_num(volume) || yyjson_get_num(volume) < 0 || yyjson_get_num(volume) > 1)
539 return SET_ERRNO(ERROR_INVALID_PARAM, "volume must be a number from 0 to 1");
541 bool omitted = false;
542 err = options_json_parse_value("microphone-volume", volume, value, sizeof(value), &omitted);
543 if (err)
544 return err;
545 if (mcp_get(opts, "microphone-volume") || mcp_get(opts, "ivolume"))
546 return SET_ERRNO(ERROR_INVALID_PARAM, "volume conflicts with options.microphone-volume/ivolume");
547 err = option(r, "microphone-volume", value);
548 if (err)
549 return err;
550 }
551 if (tool == MCP_FILE) {
552 if (synthetic || test_audio) {
553 if (mcp_get(a, "path") || mcp_get(a, "data_base64"))
554 return SET_ERRNO(ERROR_INVALID_PARAM, "Synthetic source conflicts with media");
555 if (!synthetic) {
556 option(r, "waveform", "true");
557 option(r, "audio", "true");
558 }
559 } else {
560 err = source_file(p, a, r);
561 if (err)
562 return err;
563 }
564 } else if (tool == MCP_URL) {
565 if (synthetic)
566 return SET_ERRNO(ERROR_INVALID_PARAM, "test-pattern conflicts with URL");
567 err = source_url(p, mcp_get(a, "url"), r);
568 if (err)
569 return err;
570 } else if (tool == MCP_WEBCAM) {
571 r->capture_webcam = !synthetic;
572 bool record_audio =
573 audio_device || volume || enabled(opts, "audio") ||
574 (mcp_get(a, "output_path") && yyjson_get_num(mcp_get(opts, "snapshot-delay")) > 0 && !mcp_get(opts, "audio"));
575 if (record_audio && mcp_get(opts, "audio") && !enabled(opts, "audio"))
576 return SET_ERRNO(ERROR_INVALID_PARAM, "Microphone selection conflicts with options.audio=false");
577 if (record_audio && !test_audio) {
578 option(r, "audio", "true");
579 option(r, "audio-source", "mic");
580 option(r, "audio-capture-source", "mic");
581 }
582 yyjson_val *dev = mcp_get(a, "video_device");
583 if (dev) {
584 if (!yyjson_is_int(dev) || yyjson_get_sint(dev) < 0 || yyjson_get_sint(dev) > 10)
585 return SET_ERRNO(ERROR_INVALID_PARAM, "video_device must be 0–10");
587 SAFE_SNPRINTF(b, sizeof(b), "%d", (int)yyjson_get_sint(dev));
588 option(r, "webcam-index", b);
589 }
590 } else {
591 if (synthetic)
592 return SET_ERRNO(ERROR_INVALID_PARAM, "test-pattern conflicts with audio visualization");
593 yyjson_val *visual = mcp_get(a, "visualization");
594 if (!yyjson_is_str(visual) || (strcmp(yyjson_get_str(visual), "waveform") && strcmp(yyjson_get_str(visual), "fft")))
595 return SET_ERRNO(ERROR_INVALID_PARAM, "visualization must be waveform or fft");
596 option(r, yyjson_get_str(visual), "true");
597 option(r, "audio", "true");
598 yyjson_val *audio = mcp_get(a, "audio");
599 static const char *const audio_fields[] = {"type", "path", "data_base64", "mime_type", "url", NULL};
600 if (audio) {
601 err = check_fields(audio, audio_fields);
602 if (err)
603 return err;
604 }
605 yyjson_val *type = mcp_get(audio, "type");
606 if (type && !yyjson_is_str(type))
607 return SET_ERRNO(ERROR_INVALID_PARAM, "audio.type must be a string");
608 const char *t = type ? yyjson_get_str(type) : (audio_device || volume ? "microphone" : "test");
609 if ((audio_device || volume) && strcmp(t, "microphone"))
610 return SET_ERRNO(ERROR_INVALID_PARAM, "audio_device and volume require microphone audio");
611 if (!strcmp(t, "test")) {
612 if (audio && yyjson_obj_size(audio) > 1)
613 return SET_ERRNO(ERROR_INVALID_PARAM, "Test audio has no source fields");
614 option(r, "test-audio", "true");
615 option(r, "audio-source", "all");
616 } else if (!strcmp(t, "file")) {
617 if (mcp_get(audio, "url") || mcp_get(a, "audio_device") || test_audio)
618 return SET_ERRNO(ERROR_INVALID_PARAM, "Conflicting audio sources");
619 err = source_file(p, audio, r);
620 if (err)
621 return err;
622 option(r, "audio-source", "media");
623 option(r, "audio-capture-source", "media");
624 } else if (!strcmp(t, "url")) {
625 if (mcp_get(audio, "path") || mcp_get(audio, "data_base64") || mcp_get(a, "audio_device") || test_audio)
626 return SET_ERRNO(ERROR_INVALID_PARAM, "Conflicting audio sources");
627 err = source_url(p, mcp_get(audio, "url"), r);
628 if (err)
629 return err;
630 option(r, "audio-source", "media");
631 option(r, "audio-capture-source", "media");
632 } else if (!strcmp(t, "microphone") && !p->restricted) {
633 if (mcp_get(audio, "path") || mcp_get(audio, "data_base64") || mcp_get(audio, "url") || test_audio)
634 return SET_ERRNO(ERROR_INVALID_PARAM, "Conflicting audio sources");
635 option(r, "audio-source", "mic");
636 option(r, "audio-capture-source", "mic");
637 } else
638 return SET_ERRNO(ERROR_INVALID_PARAM, "Unsupported audio source");
639 }
640 if (test_audio)
641 option(r, "audio", "true");
642 if (tool != MCP_WEBCAM && tool != MCP_VISUALIZATION && !test_audio)
643 option(r, "audio-capture-source", "media");
644 yyjson_val *format = mcp_get(a, "output_format");
645 if (format && (!p->restricted || !p->hosted))
646 return SET_ERRNO(ERROR_INVALID_PARAM, "output_format is available only in HTTP server mode");
647 if (p->restricted && p->hosted) {
648 const char *ext =
649 format && yyjson_is_str(format) ? yyjson_get_str(format)
650 : (yyjson_is_false(mcp_get(opts, "snapshot")) || yyjson_get_num(mcp_get(opts, "snapshot-delay")) > 0) ? "mp4"
651 : "png";
652 if ((format && !yyjson_is_str(format)) || !mcp_output_mime(ext) || !output_format_available(ext))
653 return SET_ERRNO(ERROR_INVALID_PARAM, "Unsupported output_format");
654 unsigned char random[16];
655 char token[33], path[PLATFORM_MAX_PATH_LENGTH];
656 randombytes_buf(random, sizeof(random));
657 sodium_bin2hex(token, sizeof(token), random, sizeof(random));
658 int path_len =
659 safe_snprintf(path, sizeof(path), "%s/%lld-%s.%s", p->downloads_dir, (long long)time(NULL) + 1800, token, ext);
660 if (path_len < 0 || (size_t)path_len >= sizeof(path))
661 return SET_ERRNO(ERROR_INVALID_PARAM, "Download path is too long");
662 err = stage_output(path, r);
663 if (err)
664 return err;
665 }
666 yyjson_val *output = mcp_get(a, "output_path");
667 if (output) {
668 if (!yyjson_is_str(output))
669 return SET_ERRNO(ERROR_INVALID_PARAM, "output_path must be a string");
670 err = stage_output(yyjson_get_str(output), r);
671 if (err)
672 return err;
673 }
674 if (output_mode && !strcmp(yyjson_get_str(output_mode), "files") && !r->output_path[0])
675 return SET_ERRNO(ERROR_INVALID_PARAM, "output=files requires output_path");
676 return ASCIICHAT_OK;
677}
bool enabled
Is filtering active?
Definition grep.c:62
#define SAFE_CALLOC(count, size, cast)
Definition common.h:275
@ ERROR_INVALID_PARAM
@ HTTP_RENDER_OUTPUT_FILE
@ HTTP_RENDER_OUTPUT_TEXT
asciichat_error_t options_json_parse_value(const char *name, yyjson_val *v, char *output, size_t output_size, bool *omitted)
static bool size_t index
Definition queries.h:8
#define MCP_SERVER_REQUEST_BUF_BUFFER_SIZE
Definition request.c:3
#define MCP_REQUEST_ARGV_CAPACITY
Definition request.c:9
#define MCP_SERVER_REQUEST_VALUE_BUFFER_SIZE
Definition request.c:5
#define MCP_SERVER_REQUEST_INDEX_BUFFER_SIZE
Definition request.c:4
#define MCP_SERVER_REQUEST_B_BUFFER_SIZE
Definition request.c:6
Registry entry - stores option definition with mode bitmask and metadata.

References http_renderer_process_request_t::argv, ASCIICHAT_OK, http_renderer_process_request_t::capture_webcam, http_render_options_t::color_mode, mcp_policy_t::downloads_dir, enabled, ERROR_FILE_OPERATION, ERROR_INVALID_PARAM, http_render_options_t::height, mcp_policy_t::hosted, HTTP_RENDER_OUTPUT_FILE, HTTP_RENDER_OUTPUT_TEXT, index, MCP_FILE, mcp_output_mime(), MCP_REQUEST_ARGV_CAPACITY, MCP_SERVER_REQUEST_B_BUFFER_SIZE, MCP_SERVER_REQUEST_BUF_BUFFER_SIZE, MCP_SERVER_REQUEST_INDEX_BUFFER_SIZE, MCP_SERVER_REQUEST_VALUE_BUFFER_SIZE, MCP_URL, MCP_VISUALIZATION, MCP_WEBCAM, http_renderer_process_request_t::options, options_json_parse_value(), http_renderer_process_request_t::output_path, http_renderer_process_request_t::output_type, http_render_options_t::palette, http_render_options_t::palette_chars, PLATFORM_MAX_PATH_LENGTH, mcp_policy_t::restricted, SAFE_CALLOC, SAFE_SNPRINTF, safe_snprintf(), SAFE_STRNCPY, SET_ERRNO, http_renderer_process_request_t::timeout_ms, and http_render_options_t::width.

Referenced by mcp_dispatch().

◆ mcp_server_main()

int mcp_server_main ( void )

Definition at line 21 of file mcp-server/main.c.

21 {
22 if (strcmp(GET_OPTION(mcp_transport), "http") && strcmp(GET_OPTION(mcp_transport), "stdio"))
23 return SET_ERRNO(ERROR_CONFIG, "--transport must be http or stdio");
24 mcp_server_t *s = SAFE_CALLOC(1, sizeof(*s), mcp_server_t *);
25 s->policy = (mcp_policy_t){.hosted = !strcmp(GET_OPTION(mcp_transport), "http"),
26 .restricted = GET_OPTION(mcp_server_restricted),
27 .address = GET_OPTION(address),
28 .address6 = GET_OPTION(address6),
29 .port = GET_OPTION(port),
30 .origin = GET_OPTION(http_origin),
31 .tls_cert = GET_OPTION(tls_cert),
32 .tls_key = GET_OPTION(tls_key),
33 .tls_password = GET_OPTION(tls_key_password),
34 .max_jobs = (unsigned)GET_OPTION(max_clients),
35 .downloads_url_prefix = GET_OPTION(mcp_downloads_url_prefix)};
37 const char *prefix = s->policy.downloads_url_prefix;
38 const char *authority = !strncmp(prefix, "https://", sizeof("https://") - 1) ? prefix + 8
39 : !strncmp(prefix, "http://", sizeof("http://") - 1) ? prefix + 7
40 : NULL;
41 if (!authority || !*authority || *authority == '/' || strpbrk(prefix, " \t\r\n?#")) {
42 SAFE_FREE(s);
43 return SET_ERRNO(ERROR_CONFIG, "--downloads-url-prefix must be an HTTP(S) URL without query or fragment");
44 }
45 }
46 if (s->policy.restricted && s->policy.hosted) {
47 const char *override = GET_OPTION(mcp_downloads_dir);
48 char *data_dir = NULL;
49 if (override && *override) {
50 SAFE_STRNCPY(s->policy.downloads_dir, override, sizeof(s->policy.downloads_dir));
51 } else {
52 data_dir = platform_get_data_dir();
53 if (!data_dir) {
54 SAFE_FREE(s);
55 return SET_ERRNO(ERROR_CONFIG, "Cannot determine MCP data directory");
56 }
57 SAFE_SNPRINTF(s->policy.downloads_dir, sizeof(s->policy.downloads_dir), "%s/downloads", data_dir);
58 SAFE_FREE(data_dir);
59 }
60 asciichat_error_t directory_error = platform_mkdir_recursive(s->policy.downloads_dir, 0700);
61 if (directory_error != ASCIICHAT_OK) {
62 SAFE_FREE(s);
63 return directory_error;
64 }
65 uv_fs_t resolved;
66 int rc = uv_fs_realpath(NULL, &resolved, s->policy.downloads_dir, NULL);
67 if (rc < 0 || strlen((const char *)resolved.ptr) >= sizeof(s->policy.downloads_dir)) {
68 uv_fs_req_cleanup(&resolved);
69 SAFE_FREE(s);
70 return SET_ERRNO(ERROR_CONFIG, "Cannot resolve MCP downloads directory");
71 }
72 SAFE_STRNCPY(s->policy.downloads_dir, (const char *)resolved.ptr, sizeof(s->policy.downloads_dir));
73 uv_fs_req_cleanup(&resolved);
74 }
76 unsigned available_cpus = uv_available_parallelism();
77 if (!available_cpus)
78 available_cpus = 1;
79 if (!s->policy.max_jobs || s->policy.max_jobs > available_cpus)
80 s->policy.max_jobs = available_cpus;
81 if (s->policy.hosted && (!!s->policy.tls_cert[0] != !!s->policy.tls_key[0])) {
82 asciichat_error_t err = SET_ERRNO(ERROR_CONFIG, "MCP HTTPS requires both --tls-cert and --tls-key");
83 log_error("MCP HTTPS requires both --tls-cert and --tls-key");
84 SAFE_FREE(s);
85 return err;
86 }
87 // Initialize immutable registry metadata before worker threads are started.
89 if (sodium_init() < 0 || mutex_init(&s->output_mutex, "mcp.output")) {
90 SAFE_FREE(s);
91 return SET_ERRNO(ERROR_INVALID_STATE, "Cannot initialize MCP server");
92 }
93 s->jobs = SAFE_CALLOC(s->policy.max_jobs, sizeof(*s->jobs), mcp_job_t *);
94 if (!s->jobs) {
95 mutex_destroy(&s->output_mutex);
96 SAFE_FREE(s);
97 return SET_ERRNO(ERROR_MEMORY, "Cannot allocate MCP render jobs");
98 }
99 log_info("MCP server started (%s, %s capabilities, %u render jobs)", s->policy.hosted ? "HTTP" : "stdio",
100 s->policy.restricted ? "restricted" : "local", s->policy.max_jobs);
101 if (s->policy.hosted) {
102 platform_systemd_notify_status("MCP HTTP listener started");
103 platform_systemd_notify_ready();
104 }
105 stats_runtime_set_provider(mcp_stats_view, s);
107 stats_runtime_set_provider(NULL, NULL);
108 for (unsigned i = 0; i < s->policy.max_jobs; ++i)
109 if (s->jobs[i].renderer_job.started)
111 mcp_jobs_reap(s, true);
113 mutex_destroy(&s->output_mutex);
114 SAFE_FREE(s->jobs);
115 SAFE_FREE(s);
116 return err;
117}
void registry_init_size(void)
Initialize registry size and metadata.
Definition core.c:108
@ ERROR_MEMORY
Definition error_codes.h:51
@ ERROR_CONFIG
Definition error_codes.h:52
#define log_error(...)
Log an ERROR message.
Definition log/log.h:634
#define log_info(...)
Log an INFO message.
Definition log/log.h:608
void log_set_terminal_output(bool enabled)
Control stderr output to terminal.
Definition log.c:746
asciichat_error_t mcp_http_run(mcp_server_t *s)
Definition http.c:233
struct mcp_server mcp_server_t
The useful part of mcp job is in server, message, response, request: together they describe what is p...
asciichat_error_t mcp_stdio_run(mcp_server_t *server)
Definition stdio.c:31
void mcp_stdio_release(void)
Definition stdio.c:13
void stats_runtime_set_provider(stats_provider_fn provider, void *data)
Definition runtime.c:126
const char * tls_key
Definition mcp.h:40
unsigned max_jobs
Definition mcp.h:41
bool restricted
Definition mcp.h:37
bool hosted
Definition mcp.h:36
mutex_t output_mutex
Definition mcp.h:83
mcp_job_t * jobs
Definition mcp.h:82
mcp_policy_t policy
Definition mcp.h:81

References ASCIICHAT_OK, mcp_policy_t::downloads_dir, mcp_policy_t::downloads_url_prefix, ERROR_CONFIG, ERROR_INVALID_STATE, ERROR_MEMORY, GET_OPTION, mcp_policy_t::hosted, http_renderer_job_cancel(), mcp_server::jobs, log_error, log_info, log_set_terminal_output(), mcp_policy_t::max_jobs, mcp_http_run(), mcp_jobs_reap(), mcp_stdio_release(), mcp_stdio_run(), mcp_server::output_mutex, mcp_server::policy, registry_init_size(), mcp_job::renderer_job, mcp_policy_t::restricted, SAFE_CALLOC, SAFE_FREE, SAFE_SNPRINTF, SAFE_STRNCPY, SET_ERRNO, http_renderer_job::started, stats_runtime_set_provider(), mcp_policy_t::tls_cert, and mcp_policy_t::tls_key.

◆ mcp_stdio_release()

void mcp_stdio_release ( void )

Definition at line 13 of file stdio.c.

13 {
14 if (protocol_fd >= 0) {
15 platform_close(protocol_fd);
16 protocol_fd = -1;
17 }
18}

Referenced by mcp_server_main().

◆ mcp_stdio_reserve()

asciichat_error_t mcp_stdio_reserve ( void )

Definition at line 7 of file stdio.c.

7 {
8 protocol_fd = platform_dup(STDOUT_FILENO);
9 if (protocol_fd < 0)
10 return SET_ERRNO(ERROR_FILE_OPERATION, "Cannot reserve MCP stdout");
11 return platform_dup2(STDERR_FILENO, STDOUT_FILENO);
12}
asciichat_error_t platform_dup2(int source_fd, int target_fd)
int platform_dup(int fd)

References ERROR_FILE_OPERATION, platform_dup(), platform_dup2(), and SET_ERRNO.

Referenced by main().

◆ mcp_stdio_run()

asciichat_error_t mcp_stdio_run ( mcp_server_t * server)

Definition at line 31 of file stdio.c.

31 {
32 char *line = SAFE_MALLOC(MCP_MAX_INPUT + 1, char *);
33 size_t n = 0;
34 bool overflow = false;
35 while (!atomic_load_bool(&s->stopping)) {
36 int c = fgetc(stdin);
37 if (c == EOF)
38 break;
39 if (c != '\n') {
40 if (n < MCP_MAX_INPUT)
41 line[n++] = (char)c;
42 else
43 overflow = true;
44 continue;
45 }
46 if (n && line[n - 1] == '\r')
47 --n;
48 if (!n && !overflow)
49 continue;
50 yyjson_doc *doc = overflow ? NULL : yyjson_read(line, n, YYJSON_READ_NOFLAG);
51 bool pending = false;
52 yyjson_mut_doc *response =
53 doc ? mcp_dispatch(s, doc, NULL, NULL, &pending)
54 : mcp_error(NULL, overflow ? -32600 : -32700, overflow ? "Request exceeds 24 MiB" : "Parse error");
55 if (response) {
56 mcp_stdio_send(s, response);
57 yyjson_mut_doc_free(response);
58 }
59 if (doc)
60 yyjson_doc_free(doc);
61 n = 0;
62 overflow = false;
63 mcp_jobs_reap(s, false);
64 }
65 SAFE_FREE(line);
66 for (unsigned i = 0; i < s->policy.max_jobs; ++i)
67 if (s->jobs[i].renderer_job.started)
68 http_renderer_job_cancel(&s->jobs[i].renderer_job);
69 mcp_jobs_reap(s, true);
70 return ASCIICHAT_OK;
71}
#define SAFE_MALLOC(size, cast)
Definition common.h:265
yyjson_mut_doc * mcp_error(yyjson_val *id, int code, const char *message)
yyjson_mut_doc * mcp_dispatch(mcp_server_t *server, yyjson_doc *message, void *connection, const char *session_id, bool *pending)
void mcp_stdio_send(mcp_server_t *s, yyjson_mut_doc *response)
Definition stdio.c:19

References ASCIICHAT_OK, http_renderer_job_cancel(), mcp_server::jobs, mcp_policy_t::max_jobs, mcp_dispatch(), mcp_error(), mcp_jobs_reap(), MCP_MAX_INPUT, mcp_stdio_send(), mcp_server::policy, mcp_job::renderer_job, SAFE_FREE, SAFE_MALLOC, http_renderer_job::started, and mcp_server::stopping.

Referenced by mcp_server_main().

◆ mcp_stdio_send()

void mcp_stdio_send ( mcp_server_t * server,
yyjson_mut_doc * response )

Definition at line 19 of file stdio.c.

19 {
20 size_t size;
21 char *json = yyjson_mut_write(response, 0, &size);
22 if (!json)
23 return;
24 mutex_lock(&s->output_mutex);
25 bool ok = platform_write_all(protocol_fd, json, size) == size && platform_write_all(protocol_fd, "\n", 1) == 1;
26 mutex_unlock(&s->output_mutex);
27 free(json); // yyjson owns allocations made by its default allocator.
28 if (!ok)
29 atomic_store_bool(&s->stopping, true);
30}
size_t platform_write_all(int fd, const void *buf, size_t count)
Write all bytes to a file descriptor, handling partial writes.

References mcp_server::output_mutex, platform_write_all(), and mcp_server::stopping.

Referenced by mcp_stdio_run().

◆ mcp_tools_schema()

yyjson_mut_val * mcp_tools_schema ( yyjson_mut_doc * doc,
const mcp_policy_t * policy )

Definition at line 158 of file request.c.

158 {
159 const char *names[] = {"render_file", "render_url", "render_webcam", "render_visualization"};
160 const char *descs[] = {
161 "Convert uploaded media or a local file into ASCII art. Source can be omitted for test-pattern/test-audio.",
162 "Convert URL media into ASCII art using the CLI media resolver.",
163 "Capture a local webcam as ASCII art; test-pattern avoids camera access.",
164 "Render waveform or FFT from test audio, uploaded audio, URL audio, or local audio/microphone."};
165 yyjson_mut_val *tools = yyjson_mut_arr(d);
166 for (int i = 0; i < 4; ++i) {
167 if (p->restricted && i == MCP_WEBCAM)
168 continue;
169 yyjson_mut_val *tool = yyjson_mut_obj(d), *s = object_schema(d), *props = yyjson_mut_obj_get(s, "properties");
170 yyjson_mut_obj_add_str(d, tool, "name", names[i]);
171 yyjson_mut_obj_add_str(d, tool, "description", descs[i]);
172 yyjson_mut_obj_add_val(d, props, "options", options_schema(d, p));
173 property(d, s, "timeout_ms", "integer", "Maximum job time, 1000–60000 ms; default 45000.");
174 if (p->restricted && p->hosted) {
175 property(d, s, "output_format", "string",
176 "Output format: png, jpg, jpeg, bmp, webp, gif, mp4, mov, mkv, webm, avi, flv, ogv, ogg or 3gp. "
177 "Defaults to png for a single frame or mp4 for a timed or continuous render. GIF renders media to EOF "
178 "unless snapshot or snapshot-delay is specified.");
179 yyjson_mut_val *available = yyjson_mut_arr(d);
180 for (size_t i = 0; mcp_output_formats[i]; ++i)
181 if (output_format_available(mcp_output_formats[i]))
182 yyjson_mut_arr_add_str(d, available, mcp_output_formats[i]);
183 yyjson_mut_obj_add_val(d, yyjson_mut_obj_get(props, "output_format"), "enum", available);
184 }
185 if (!p->restricted)
186 property(d, s, "output_path", "string",
187 "Save rendered ASCII art as an image/video, selected by extension. Existing files are never replaced.");
188 property(d, s, "output", "string", "Output mode: text or files. Defaults to text unless output_path is supplied.");
189 const char *output_modes[] = {"text", "files", NULL};
190 enum_strings(d, yyjson_mut_obj_get(props, "output"), output_modes);
191 if (i == MCP_FILE)
192 file_properties(d, s, !p->restricted);
193 if (i == MCP_URL) {
194 property(d, s, "url", "string", "HTTP/HTTPS media URL (local profile also accepts RTSP/RTMP).");
195 yyjson_mut_val *required = yyjson_mut_arr(d);
196 yyjson_mut_arr_add_str(d, required, "url");
197 yyjson_mut_obj_add_val(d, s, "required", required);
198 }
199 if (i == MCP_WEBCAM) {
200 property(d, s, "video_device", "integer", "Local webcam index, 0–10; default 0.");
201 yyjson_mut_val *device = yyjson_mut_obj_get(props, "video_device");
202 yyjson_mut_obj_add_int(d, device, "default", 0);
203 yyjson_mut_obj_add_int(d, device, "minimum", 0);
204 yyjson_mut_obj_add_int(d, device, "maximum", 10);
205 }
206 if (!p->restricted && (i == MCP_WEBCAM || i == MCP_VISUALIZATION)) {
207 property(d, s, "audio_device", "integer", "Microphone index; -1 or omitted selects the system default.");
208 yyjson_mut_val *device = yyjson_mut_obj_get(props, "audio_device");
209 yyjson_mut_obj_add_int(d, device, "default", -1);
210 yyjson_mut_obj_add_int(d, device, "minimum", -1);
211 yyjson_mut_obj_add_int(d, device, "maximum", INT_MAX);
212 yyjson_mut_val *volume = options_json_schema_for_option(d, "microphone-volume");
213 yyjson_mut_obj_remove_str(volume, "description");
214 yyjson_mut_obj_add_str(d, volume, "description", "Microphone volume multiplier, 0–1; 0 mutes, 1 is normal.");
215 yyjson_mut_obj_add_real(d, volume, "default", 1);
216 yyjson_mut_obj_add_real(d, volume, "minimum", 0);
217 yyjson_mut_obj_add_real(d, volume, "maximum", 1);
218 yyjson_mut_obj_add_val(d, props, "volume", volume);
219 }
220 if (i == MCP_VISUALIZATION) {
221 property(d, s, "visualization", "string", "waveform or fft");
222 const char *values[] = {"waveform", "fft", NULL};
223 enum_strings(d, yyjson_mut_obj_get(props, "visualization"), values);
224 yyjson_mut_val *audio = object_schema(d);
225 property(d, audio, "type", "string", "Audio source, default test.");
226 const char *types[] = {"test", "file", "url", "microphone", NULL};
227 const char *remote_types[] = {"test", "file", "url", NULL};
228 enum_strings(d, yyjson_mut_obj_get(yyjson_mut_obj_get(audio, "properties"), "type"),
229 p->restricted ? remote_types : types);
230 file_properties(d, audio, !p->restricted);
231 property(d, audio, "url", "string", "Audio URL.");
232 yyjson_mut_obj_add_val(d, props, "audio", audio);
233 yyjson_mut_val *req = yyjson_mut_arr(d);
234 yyjson_mut_arr_add_str(d, req, "visualization");
235 yyjson_mut_obj_add_val(d, s, "required", req);
236 }
237 yyjson_mut_val *out = object_schema(d);
238 property(d, out, "ascii", "string", "Rendered text; saved media may have no terminal text.");
239 property(d, out, "elapsed_ms", "integer", "Worker wall-clock duration.");
240 if (p->restricted && p->hosted) {
241 property(d, out, "download_url", "string", "Temporary URL for the rendered file.");
242 property(d, out, "expires_at", "string", "UTC expiration timestamp in RFC 3339 format.");
243 }
244 yyjson_mut_val *saved = yyjson_mut_obj(d), *items = yyjson_mut_obj(d);
245 yyjson_mut_obj_add_str(d, saved, "type", "array");
246 yyjson_mut_obj_add_str(d, items, "type", "string");
247 yyjson_mut_obj_add_val(d, saved, "items", items);
248 yyjson_mut_obj_add_val(d, yyjson_mut_obj_get(out, "properties"), "saved_files", saved);
249 yyjson_mut_val *required = yyjson_mut_arr(d);
250 yyjson_mut_arr_add_str(d, required, "ascii");
251 yyjson_mut_arr_add_str(d, required, "elapsed_ms");
252 yyjson_mut_arr_add_str(d, required, "saved_files");
253 yyjson_mut_obj_add_val(d, out, "required", required);
254 yyjson_mut_obj_add_val(d, tool, "outputSchema", out);
255 yyjson_mut_obj_add_val(d, tool, "inputSchema", s);
256 yyjson_mut_arr_add_val(tools, tool);
257 }
258 if (!p->restricted) {
259 yyjson_mut_val *tool = yyjson_mut_obj(d);
260 yyjson_mut_obj_add_str(d, tool, "name", "device_list");
261 yyjson_mut_obj_add_str(d, tool, "description",
262 "List local video and microphone device indices, names, and defaults.");
263 yyjson_mut_obj_add_val(d, tool, "inputSchema", object_schema(d));
264 yyjson_mut_val *annotations = yyjson_mut_obj(d);
265 yyjson_mut_obj_add_bool(d, annotations, "readOnlyHint", true);
266 yyjson_mut_obj_add_val(d, tool, "annotations", annotations);
267 yyjson_mut_arr_add_val(tools, tool);
268 }
269 return tools;
270}
yyjson_mut_val * options_json_schema_for_option(yyjson_mut_doc *d, const char *name)
static bool size_t gotel_query_t * out
Definition queries.h:8

References mcp_policy_t::hosted, MCP_FILE, mcp_output_formats, MCP_URL, MCP_VISUALIZATION, MCP_WEBCAM, options_json_schema_for_option(), out, and mcp_policy_t::restricted.

Referenced by mcp_dispatch().

Variable Documentation

◆ mcp_output_formats

const char* const mcp_output_formats[]
extern

Definition at line 7 of file downloads.c.

7 {"png", "jpg", "jpeg", "bmp", "webp", "gif", "mp4", "mov",
8 "mkv", "webm", "avi", "flv", "ogv", "ogg", "3gp", NULL};

Referenced by mcp_output_mime(), and mcp_tools_schema().