Skip to content
171 changes: 166 additions & 5 deletions src/cli/client_adapter.c
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

#include "foundation/constants.h"
#include "mcp/mcp.h"
#include "yyjson/yyjson.h"

#include <stdbool.h>
#include <stdio.h>
Expand Down Expand Up @@ -138,6 +139,167 @@ static void emit_header(adapter_sb_t *sb, const char *client) {
"// are overwritten; edit outside it, or remove the markers to take ownership.\n");
}

static void append_typebox_type(adapter_sb_t *sb, yyjson_val *val, const char *description,
bool include_description) {
yyjson_val *type_val = yyjson_obj_get(val, "type");
yyjson_val *enum_val = yyjson_obj_get(val, "enum");
yyjson_val *items_val = yyjson_obj_get(val, "items");
const char *type_str = yyjson_get_str(type_val);

if (type_str && strcmp(type_str, "string") == 0 && enum_val && yyjson_is_arr(enum_val) &&
yyjson_arr_size(enum_val) > 0) {
sb_append(sb, "Type.Union([");
size_t enum_idx = 0;
size_t enum_max = yyjson_arr_size(enum_val);
bool first_enum = true;
for (enum_idx = 0; enum_idx < enum_max; enum_idx++) {
yyjson_val *enum_item = yyjson_arr_get(enum_val, enum_idx);
const char *enum_str = yyjson_get_str(enum_item);
if (!enum_str) {
continue;
}
if (!first_enum) {
sb_append(sb, ", ");
}
first_enum = false;
sb_append(sb, "Type.Literal(");
sb_append_js_string(sb, enum_str);
sb_append(sb, ")");
}
if (first_enum) {
sb_append(sb, "Type.String()");
} else {
sb_append(sb, "]");
if (include_description && description) {
sb_append(sb, ", { description: ");
sb_append_js_string(sb, description);
sb_append(sb, " }");
}
sb_append(sb, ")");
return;
}
sb_append(sb, ")");
return;
}

const char *typebox_name = "Any";
if (type_str && strcmp(type_str, "string") == 0) {
typebox_name = "String";
} else if (type_str && strcmp(type_str, "integer") == 0) {
typebox_name = "Integer";
} else if (type_str && strcmp(type_str, "number") == 0) {
typebox_name = "Number";
} else if (type_str && strcmp(type_str, "boolean") == 0) {
typebox_name = "Boolean";
}

if (type_str && strcmp(type_str, "array") == 0) {
sb_append(sb, "Type.Array(");
if (items_val) {
append_typebox_type(sb, items_val, NULL, false);
} else {
sb_append(sb, "Type.Any()");
}
if (include_description && description) {
sb_append(sb, ", { description: ");
sb_append_js_string(sb, description);
sb_append(sb, " }");
}
sb_append(sb, ")");
return;
}

sb_append(sb, "Type.");
sb_append(sb, typebox_name);
sb_append(sb, "(");
if (include_description && description) {
sb_append(sb, "{ description: ");
sb_append_js_string(sb, description);
sb_append(sb, " }");
}
sb_append(sb, ")");
}

static bool typebox_property_is_required(yyjson_val *required, const char *prop_name) {
if (!required || !yyjson_is_arr(required)) {
return false;
}
size_t idx = 0;
size_t max = yyjson_arr_size(required);
for (idx = 0; idx < max; idx++) {
const char *required_name = yyjson_get_str(yyjson_arr_get(required, idx));
if (required_name && strcmp(required_name, prop_name) == 0) {
return true;
}
}
return false;
}

static void convert_json_schema_to_typebox(adapter_sb_t *sb, const char *schema_str) {
if (!schema_str) {
sb_append(sb, "Type.Object({})");
return;
}

/* Parse the JSON schema to extract properties and convert to TypeBox */
yyjson_doc *doc = yyjson_read(schema_str, strlen(schema_str), 0);
if (!doc) {
sb_append(sb, "Type.Object({})");
return;
}

yyjson_val *root = yyjson_doc_get_root(doc);
if (!root) {
sb_append(sb, "Type.Object({})");
yyjson_doc_free(doc);
return;
}

yyjson_val *properties = yyjson_obj_get(root, "properties");
yyjson_val *required = yyjson_obj_get(root, "required");
if (!properties) {
sb_append(sb, "Type.Object({})");
yyjson_doc_free(doc);
return;
}

sb_append(sb, "Type.Object({\n");

bool first_prop = true;
size_t idx = 0, max = 0;
yyjson_val *key = NULL, *val = NULL;

yyjson_obj_foreach(properties, idx, max, key, val) {
const char *prop_name = yyjson_get_str(key);
if (!prop_name) {
continue;
}

if (!first_prop) {
sb_append(sb, ",\n");
}
first_prop = false;

sb_append(sb, " ");
sb_append(sb, prop_name);
sb_append(sb, ": ");

yyjson_val *description_val = yyjson_obj_get(val, "description");
const char *description = yyjson_get_str(description_val);
bool is_required = typebox_property_is_required(required, prop_name);
if (!is_required) {
sb_append(sb, "Type.Optional(");
}
append_typebox_type(sb, val, description, true);
if (!is_required) {
sb_append(sb, ")");
}
}

sb_append(sb, "\n })");
yyjson_doc_free(doc);
}

char *cbm_client_adapter_pi(const char *binary_path) {
if (!binary_path || !binary_path[0]) {
return NULL;
Expand All @@ -157,7 +319,8 @@ char *cbm_client_adapter_pi(const char *binary_path) {
* (pods CLI) or an old AgentTool arity cannot be mistaken for this file. */
sb_append(&sb, "// Target: @earendil-works/pi-coding-agent >= 0.74.0 (verified 0.84.2)\n"
"// ToolDefinition.execute(toolCallId, params, signal, onUpdate, ctx)\n");
sb_append(&sb, "import { spawn } from 'node:child_process';\n\n");
sb_append(&sb, "import { spawn } from 'node:child_process';\n");
sb_append(&sb, "import { Type } from 'typebox';\n\n");
sb_append(&sb, "const BIN = '");
sb_append(&sb, bin);
sb_append(&sb, "';\n\n");
Expand Down Expand Up @@ -224,10 +387,8 @@ char *cbm_client_adapter_pi(const char *binary_path) {
sb_append(&sb, ",\n description: ");
sb_append_js_string(&sb, description ? description : "");
sb_append(&sb, ",\n parameters: ");
/* input_schema is compact JSON, which is a valid JavaScript object
* literal; embedding it directly keeps the generated module free of a
* JSON.parse indirection and of any escaping drift. */
sb_append(&sb, schema ? schema : "{}");
/* Convert JSON schema to TypeBox format */
convert_json_schema_to_typebox(&sb, schema);
/* 0.84.2 calls execute(toolCallId, params, signal, onUpdate, ctx).
* The previous (args, ctx) shape bound the call id as the MCP args. */
sb_append(&sb, ",\n execute: async (toolCallId, params, signal, _onUpdate, ctx) => {\n");
Expand Down
9 changes: 7 additions & 2 deletions tests/test_agent_clients.c
Original file line number Diff line number Diff line change
Expand Up @@ -1174,8 +1174,13 @@ TEST(client_adapter_pi_emits_parameters_and_execute) {
ASSERT_NOT_NULL(strstr(js, "result.content"));
ASSERT_NULL(strstr(js, "run: (args, ctx)"));
ASSERT_NOT_NULL(strstr(js, "parameters:"));
/* The registry input_schema is embedded as a JSON object literal. */
ASSERT_NOT_NULL(strstr(js, "\"type\":\"object\""));
/* TypeBox parameters schema is embedded with Type.Object syntax. */
ASSERT_NOT_NULL(strstr(js, "Type.Object("));
ASSERT_NOT_NULL(strstr(js, "Type.String("));
/* A numeric registry property must remain numeric in the generated schema. */
ASSERT_NOT_NULL(strstr(js, "depth: Type.Optional(Type.Integer("));
/* TypeBox import should be present. */
ASSERT_NOT_NULL(strstr(js, "import { Type } from 'typebox';"));
/* Raw JSON output is required so the bridge can parse the MCP result; the
* human-readable path would leave `call` with nothing to JSON.parse. */
ASSERT_NOT_NULL(strstr(js, "'cli', '--json'"));
Expand Down
Loading