ascii-chat
Binary v0.15.83-53-gb711d8720
Library lib/v0.3.2-701-gb711d8720
Video chat in your terminal
Loading...
Searching...
No Matches
request.c
Go to the documentation of this file.
1
2/* Storage capacities in elements; text buffers include the terminating NUL. */
3#define MCP_SERVER_REQUEST_BUF_BUFFER_SIZE 4097
4#define MCP_SERVER_REQUEST_INDEX_BUFFER_SIZE 16
5#define MCP_SERVER_REQUEST_VALUE_BUFFER_SIZE 64
6#define MCP_SERVER_REQUEST_B_BUFFER_SIZE 16
7
8/* Existing buffer budgets in bytes, including NUL for text storage. */
9#define MCP_REQUEST_ARGV_CAPACITY 256
10#define MCP_REQUEST_RESERVED_ARGS 6
11
12#include "mcp.h"
16#include <libplatform/filesystem.h>
17#include <sodium.h>
18#include <libavcodec/avcodec.h>
19#include <uv.h>
20#include <string.h>
21#include <stdlib.h>
22#include <limits.h>
23#include <time.h>
24
25// Exposure is explicit; types, descriptions, enums and CLI parsers remain in the registry.
26static const char *const shared_options[] = {"width",
27 "height",
28 "color-mode",
29 "color-filter",
30 "render-mode",
31 "renderer",
32 "stabilize",
33 "palette",
34 "palette-chars",
35 "stretch",
36 "fps",
37 "snapshot",
38 "snapshot-delay",
39 "matrix",
40 "face-follow",
41 "flip-x",
42 "flip-y",
43 "zalgo",
44 "utf8",
45 "strip-ansi",
46 "loop",
47 "seek",
48 "test-pattern",
49 "test-audio",
50 "audio",
51 "audio-spectral-agc",
52 "audio-pitch-correct",
53 "spectral-audio",
54 "audio-spectral-gate",
55 "audio-multiband",
56 "audio-adaptive-eq",
57 "audio-fft-size",
58 "microphone-volume",
59 "ivolume",
60 "speakers-volume",
61 "volume",
62 "encode-audio",
63 "no-encode-audio",
64 "no-audio-mixer",
65 "render-theme",
66 "render-font-size",
67 NULL};
68static const char *const local_options[] = {"render-font", "yt-dlp-options", NULL};
69static bool in_names(const char *name, const char *const *names) {
70 for (size_t i = 0; names[i]; ++i)
71 if (!strcmp(name, names[i]))
72 return true;
73 return false;
74}
75static const registry_entry_t *exposed(const mcp_policy_t *policy, const char *name) {
76 if (!in_names(name, shared_options) && !(!policy->restricted && in_names(name, local_options)))
77 return NULL;
78 return registry_find_entry_by_name(name);
79}
80static yyjson_mut_val *object_schema(yyjson_mut_doc *d) {
81 yyjson_mut_val *v = yyjson_mut_obj(d);
82 yyjson_mut_obj_add_str(d, v, "type", "object");
83 yyjson_mut_obj_add_bool(d, v, "additionalProperties", false);
84 yyjson_mut_obj_add_val(d, v, "properties", yyjson_mut_obj(d));
85 return v;
86}
87static void property(yyjson_mut_doc *d, yyjson_mut_val *s, const char *name, const char *type,
88 const char *description) {
89 yyjson_mut_val *v = yyjson_mut_obj(d);
90 yyjson_mut_obj_add_str(d, v, "type", type);
91 yyjson_mut_obj_add_str(d, v, "description", description);
92 yyjson_mut_obj_add_val(d, yyjson_mut_obj_get(s, "properties"), name, v);
93}
94static void enum_strings(yyjson_mut_doc *d, yyjson_mut_val *v, const char *const *values) {
95 yyjson_mut_val *a = yyjson_mut_arr(d);
96 for (size_t i = 0; values[i]; ++i)
97 yyjson_mut_arr_add_str(d, a, values[i]);
98 yyjson_mut_obj_add_val(d, v, "enum", a);
99}
100static bool output_format_available(const char *format) {
101 const char *codec = NULL;
102 if (!strcmp(format, "png"))
103 codec = "png";
104 else if (!strcmp(format, "jpg") || !strcmp(format, "jpeg"))
105 codec = "mjpeg";
106 else if (!strcmp(format, "bmp"))
107 codec = "bmp";
108 else if (!strcmp(format, "webp"))
109 codec = "libwebp";
110 else if (!strcmp(format, "gif"))
111 codec = "gif";
112 else if (!strcmp(format, "avi"))
113 codec = "mpeg4";
114 else if (!strcmp(format, "ogv") || !strcmp(format, "ogg"))
115 codec = "libtheora";
116 else if (!strcmp(format, "webm"))
117 return avcodec_find_encoder_by_name("libvpx-vp9") || avcodec_find_encoder_by_name("libvpx") ||
118 avcodec_find_encoder_by_name("vp9") || avcodec_find_encoder_by_name("libvpx-vp8") ||
119 avcodec_find_encoder_by_name("vp8");
120 else
121 return avcodec_find_encoder_by_name("libx264") || avcodec_find_encoder_by_name("libopenh264") ||
122 avcodec_find_encoder_by_name("h264_mf") || avcodec_find_encoder(AV_CODEC_ID_H264);
123 return avcodec_find_encoder_by_name(codec) != NULL;
124}
125
126static yyjson_mut_val *options_schema(yyjson_mut_doc *d, const mcp_policy_t *p) {
127 yyjson_mut_val *s = object_schema(d), *props = yyjson_mut_obj_get(s, "properties");
129 const char *const *lists[] = {shared_options, local_options};
130 for (size_t list = 0; list < (p->restricted ? 1u : 2u); ++list)
131 for (size_t i = 0; lists[list][i]; ++i) {
132 const registry_entry_t *e = exposed(p, lists[list][i]);
133 if (!e)
134 continue;
135 yyjson_mut_val *v = options_json_schema_for_option(d, e->long_name);
136 yyjson_mut_obj_add_val(d, props, e->long_name, v);
137 if (!strcmp(e->long_name, "width") || !strcmp(e->long_name, "height")) {
138 yyjson_mut_obj_add_int(d, v, "minimum", e->metadata.numeric_range.min);
139 yyjson_mut_obj_add_int(d, v, "maximum", !strcmp(e->long_name, "width") ? 512 : 256);
140 }
141 if (!strcmp(e->long_name, "snapshot"))
142 yyjson_mut_obj_add_bool(d, v, "default", true);
143 if (!strcmp(e->long_name, "snapshot-delay")) {
144 yyjson_mut_obj_add_real(d, v, "minimum", 0);
145 yyjson_mut_obj_add_real(d, v, "maximum", 30);
146 yyjson_mut_obj_add_real(d, v, "default", 0);
147 }
148 }
149 return s;
150}
151static void file_properties(yyjson_mut_doc *d, yyjson_mut_val *s, bool local) {
152 if (local)
153 property(d, s, "path", "string", "Local media path. Mutually exclusive with data_base64.");
154 property(d, s, "data_base64", "string",
155 "Base64 encoded media bytes; client must read/encode the file. Maximum decoded size 16 MiB.");
156 property(d, s, "mime_type", "string", "Media MIME type, e.g. image/png, video/mp4, audio/wav.");
157}
158yyjson_mut_val *mcp_tools_schema(yyjson_mut_doc *d, const mcp_policy_t *p) {
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}
271static asciichat_error_t arg(http_renderer_process_request_t *r, const char *value) {
273 return SET_ERRNO(ERROR_INVALID_PARAM, "Too many or oversized options");
274 r->argv[r->argc++] = platform_strdup(value);
275 r->argv[r->argc] = NULL;
276 return ASCIICHAT_OK;
277}
278static asciichat_error_t option(http_renderer_process_request_t *r, const char *name, const char *value) {
279 size_t n = strlen(name) + strlen(value) + 4;
280 char *s = SAFE_MALLOC(n, char *);
281 SAFE_SNPRINTF(s, n, "--%s=%s", name, value);
282 asciichat_error_t err = arg(r, s);
283 SAFE_FREE(s);
285 if (err == ASCIICHAT_OK && entry && entry->mode_bitmask == OPTION_MODE_BINARY) {
286 size_t last = r->argc - 1;
287 for (size_t i = 1; i < last; ++i)
288 if (!strcmp(r->argv[i], "mirror")) {
289 char *binary = r->argv[last];
290 memmove(&r->argv[i + 1], &r->argv[i], (last - i) * sizeof(char *));
291 r->argv[i] = binary;
292 break;
293 }
294 }
295 return err;
296}
297static bool enabled(yyjson_val *opts, const char *name) {
298 yyjson_val *v = mcp_get(opts, name);
299 return v && !(yyjson_is_bool(v) && !yyjson_get_bool(v));
300}
301static asciichat_error_t check_fields(yyjson_val *obj, const char *const *allowed) {
302 if (!yyjson_is_obj(obj))
303 return SET_ERRNO(ERROR_INVALID_PARAM, "Arguments must be an object");
304 size_t i, max;
305 yyjson_val *k, *v;
306 yyjson_obj_foreach(obj, i, max, k, v) {
307 if (strlen(yyjson_get_str(k)) != yyjson_get_len(k) || !in_names(yyjson_get_str(k), allowed))
308 return SET_ERRNO(ERROR_INVALID_PARAM, "Unknown argument: %s", yyjson_get_str(k));
309 if (yyjson_is_str(v) && strlen(yyjson_get_str(v)) != yyjson_get_len(v))
310 return SET_ERRNO(ERROR_INVALID_PARAM, "Embedded NUL in argument");
311 }
312 return ASCIICHAT_OK;
313}
314static asciichat_error_t source_file(const mcp_policy_t *p, yyjson_val *a, http_renderer_process_request_t *r) {
315 yyjson_val *path = mcp_get(a, "path"), *data = mcp_get(a, "data_base64"), *mime = mcp_get(a, "mime_type");
316 if ((path != NULL) == (data != NULL))
317 return SET_ERRNO(ERROR_INVALID_PARAM, "Provide exactly one of path or data_base64");
318 if (path) {
319 if (p->restricted)
320 return SET_ERRNO(ERROR_INVALID_PARAM, "Local paths are unavailable in server mode");
321 if (!yyjson_is_str(path) || !*yyjson_get_str(path) || !strcmp(yyjson_get_str(path), "-") ||
322 strstr(yyjson_get_str(path), "://"))
323 return SET_ERRNO(ERROR_INVALID_PARAM, "path must be a local file, not stdin or a URL");
324 SAFE_STRNCPY(r->source_path, yyjson_get_str(path), sizeof(r->source_path));
325 return option(r, "file", yyjson_get_str(path));
326 }
327 if (!yyjson_is_str(data) || yyjson_get_len(data) == 0 || yyjson_get_len(data) > MCP_MAX_INPUT - 4096)
328 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid or oversized base64 media");
329 if (mime && !yyjson_is_str(mime))
330 return SET_ERRNO(ERROR_INVALID_PARAM, "mime_type must be a string");
331 asciichat_error_t err = platform_mkdtemp(r->upload_dir, sizeof(r->upload_dir), "ascii-mcp-input");
332 if (err != ASCIICHAT_OK)
333 return err;
334 char file[PLATFORM_MAX_PATH_LENGTH];
335 SAFE_SNPRINTF(file, sizeof(file), "%s/media", r->upload_dir);
336 size_t cap = yyjson_get_len(data) / 4 * 3 + 3, len = 0;
337 unsigned char *bytes = SAFE_MALLOC(cap, unsigned char *);
338 int rc = sodium_base642bin(bytes, cap, yyjson_get_str(data), yyjson_get_len(data), "\r\n\t ", &len, NULL,
339 sodium_base64_VARIANT_ORIGINAL);
340 if (rc || !len || len > 16 * 1024 * 1024) {
341 SAFE_FREE(bytes);
342 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid base64 or media exceeds 16 MiB");
343 }
344 FILE *f = platform_fopen("mcp-upload", file, "wb");
345 if (!f) {
346 SAFE_FREE(bytes);
347 return SET_ERRNO(ERROR_FILE_OPERATION, "Cannot stage uploaded media");
348 }
349 bool wrote = fwrite(bytes, 1, len, f) == len;
350 if (fclose(f) != 0)
351 wrote = false;
352 SAFE_FREE(bytes);
353 if (!wrote)
354 return SET_ERRNO(ERROR_FILE_OPERATION, "Cannot write uploaded media");
355 SAFE_STRNCPY(r->source_path, file, sizeof(r->source_path));
356 return option(r, "file", file);
357}
358static asciichat_error_t source_url(const mcp_policy_t *p, yyjson_val *v, http_renderer_process_request_t *r) {
359 if (!yyjson_is_str(v) || !*yyjson_get_str(v) || yyjson_get_len(v) > 4000)
360 return SET_ERRNO(ERROR_INVALID_PARAM, "url must be a nonempty string");
361 const char *url = yyjson_get_str(v);
362 SAFE_STRNCPY(r->source_url, url, sizeof(r->source_url));
363 if (strncmp(url, "https://", sizeof("https://") - 1) && strncmp(url, "http://", sizeof("http://") - 1) &&
364 (p->restricted ||
365 (strncmp(url, "rtsp://", sizeof("rtsp://") - 1) && strncmp(url, "rtmp://", sizeof("rtmp://") - 1))))
366 return SET_ERRNO(ERROR_INVALID_PARAM, "Unsupported URL scheme");
367 return option(r, "url", url);
368}
369static asciichat_error_t stage_output(const char *path, http_renderer_process_request_t *r) {
370 if (!path || !*path || strlen(path) > 3500 || !strcmp(path, "-") || strchr(path, '\n'))
371 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid output_path");
372 uv_fs_t fs;
373 int rc = uv_fs_lstat(NULL, &fs, path, NULL);
374 uv_fs_req_cleanup(&fs);
375 if (rc >= 0)
376 return SET_ERRNO(ERROR_INVALID_PARAM, "Output already exists: %s", path);
377 if (rc != UV_ENOENT)
378 return SET_ERRNO(ERROR_FILE_OPERATION, "Cannot inspect output destination");
379 char parent[PLATFORM_MAX_PATH_LENGTH];
380 SAFE_STRNCPY(parent, path, sizeof(parent));
381 char *slash = strrchr(parent, '/'), *back = strrchr(parent, '\\');
382 if (back && (!slash || back > slash))
383 slash = back;
384 const char *filename = path;
385 if (slash) {
386 filename = path + (slash - parent) + 1;
387 if (slash == parent)
388 slash[1] = '\0';
389 else
390 *slash = '\0';
391 } else
392 SAFE_STRNCPY(parent, ".", sizeof(parent));
393 const char *ext = strrchr(filename, '.');
394 if (!ext || !mcp_output_mime(ext + 1) || !output_format_available(ext + 1))
395 return SET_ERRNO(ERROR_INVALID_PARAM, "Unsupported output extension");
396 rc = uv_fs_realpath(NULL, &fs, parent, NULL);
397 if (rc < 0) {
398 uv_fs_req_cleanup(&fs);
399 return SET_ERRNO(ERROR_FILE_OPERATION, "Output parent directory does not exist");
400 }
401 SAFE_SNPRINTF(r->output_path, sizeof(r->output_path), "%s/%s", (char *)fs.ptr, filename);
402 SAFE_SNPRINTF(r->stage_dir, sizeof(r->stage_dir), "%s/.ascii-mcp-XXXXXX", (char *)fs.ptr);
403 uv_fs_req_cleanup(&fs);
404 rc = uv_fs_mkdtemp(NULL, &fs, r->stage_dir, NULL);
405 if (rc < 0) {
406 r->stage_dir[0] = '\0';
407 uv_fs_req_cleanup(&fs);
408 return SET_ERRNO(ERROR_FILE_OPERATION, "Cannot stage output beside destination");
409 }
410 SAFE_STRNCPY(r->stage_dir, fs.path, sizeof(r->stage_dir));
411 uv_fs_req_cleanup(&fs);
412 SAFE_SNPRINTF(r->stage_file, sizeof(r->stage_file), "%s/render%s", r->stage_dir, ext);
414 return option(r, "render-file", r->stage_file);
415}
418 memset(r, 0, sizeof(*r));
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"))
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"))
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}
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 SET_ERRNO(code,...)
const registry_entry_t * registry_find_entry_by_name(const char *long_name)
Get a registry entry by long name.
Definition core.c:127
void registry_init_size(void)
Initialize registry size and metadata.
Definition core.c:108
Internal helper functions and data for registry implementation.
const char *const mcp_output_formats[]
Definition downloads.c:7
const char * mcp_output_mime(const char *extension)
Definition downloads.c:10
bool enabled
Is filtering active?
Definition grep.c:62
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:423
#define SAFE_FREE(ptr)
Definition common.h:377
#define SAFE_MALLOC(size, cast)
Definition common.h:265
#define SAFE_SNPRINTF(buffer, buffer_size,...)
Definition common.h:507
#define SAFE_CALLOC(count, size, cast)
Definition common.h:275
#define PLATFORM_MAX_PATH_LENGTH
Definition common.h:95
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
@ ERROR_INVALID_PARAM
@ OPTION_MODE_BINARY
Binary-level options (parsed before mode detection).
Definition options.h:945
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Platform-safe snprintf wrapper.
@ HTTP_RENDER_OUTPUT_FILE
@ HTTP_RENDER_OUTPUT_TEXT
Registry-backed JSON schemas and conversion through CLI option handlers.
#define MCP_MAX_INPUT
Definition mcp.h:29
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
yyjson_mut_val * options_json_schema_for_option(yyjson_mut_doc *d, const char *name)
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
static bool size_t gotel_query_t * out
Definition queries.h:8
Central registry of all command-line options with mode applicability.
#define MCP_SERVER_REQUEST_BUF_BUFFER_SIZE
Definition request.c:3
#define MCP_REQUEST_ARGV_CAPACITY
Definition request.c:9
void mcp_request_destroy(http_renderer_process_request_t *r)
Definition request.c:678
#define MCP_SERVER_REQUEST_VALUE_BUFFER_SIZE
Definition request.c:5
#define MCP_SERVER_REQUEST_INDEX_BUFFER_SIZE
Definition request.c:4
#define MCP_REQUEST_RESERVED_ARGS
Definition request.c:10
asciichat_error_t mcp_request_parse(const mcp_policy_t *p, mcp_tool_t tool, yyjson_val *a, http_renderer_process_request_t *r)
Definition request.c:416
yyjson_mut_val * mcp_tools_schema(yyjson_mut_doc *d, const mcp_policy_t *p)
Definition request.c:158
#define MCP_SERVER_REQUEST_B_BUFFER_SIZE
Definition request.c:6
🔤 String Manipulation and Shell Escaping Utilities
char palette_chars[OPTIONS_BUFF_SIZE]
char palette[OPTIONS_BUFF_SIZE]
char upload_dir[PLATFORM_MAX_PATH_LENGTH]
http_render_output_type_t output_type
char stage_file[PLATFORM_MAX_PATH_LENGTH]
char stage_dir[PLATFORM_MAX_PATH_LENGTH]
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]
char downloads_dir[PLATFORM_MAX_PATH_LENGTH]
Definition mcp.h:44
bool restricted
Definition mcp.h:37
bool hosted
Definition mcp.h:36
int min
Minimum value (or 0 if no limit).
Definition builder.h:207
struct option_metadata_t::@061372117173071240020206164036331043262170062306 numeric_range
Registry entry - stores option definition with mode bitmask and metadata.
const char * long_name
option_mode_bitmask_t mode_bitmask
option_metadata_t metadata
Enum values, numeric ranges, examples.
⏱️ High-precision timing utilities using sokol_time.h and uthash