diff --git a/src/cli/cli.c b/src/cli/cli.c index 51fbafc02..edf5472ab 100644 --- a/src/cli/cli.c +++ b/src/cli/cli.c @@ -13700,6 +13700,39 @@ static void cli_add_typed(yyjson_mut_doc *out, yyjson_mut_val *obj, const char * arr = yyjson_mut_arr(out); yyjson_mut_obj_add(obj, yyjson_mut_strcpy(out, key), arr); } + /* Accept the JSON-array spelling shown by MCP schemas in addition to + * repeated flags. This keeps `--semantic-query '["a","b"]'` from + * becoming one opaque keyword while preserving the established + * `--semantic-query a --semantic-query b` form. Both spellings may be + * mixed and repeated; items accumulate in argument order. Only a + * well-formed array of strings is expanded (an empty `[]` adds no + * items); anything else keeps the previous literal-string behaviour. */ + yyjson_doc *array_doc = + have_value && value && value[0] == '[' ? yyjson_read(value, strlen(value), 0) : NULL; + yyjson_val *array_root = array_doc ? yyjson_doc_get_root(array_doc) : NULL; + bool string_array = array_root && yyjson_is_arr(array_root); + if (string_array) { + size_t idx; + size_t count; + yyjson_val *item; + yyjson_arr_foreach(array_root, idx, count, item) { + if (!yyjson_is_str(item)) { + string_array = false; + break; + } + } + } + if (string_array) { + size_t idx; + size_t count; + yyjson_val *item; + yyjson_arr_foreach(array_root, idx, count, item) { + yyjson_mut_arr_add_strcpy(out, arr, yyjson_get_str(item)); + } + yyjson_doc_free(array_doc); + return; + } + yyjson_doc_free(array_doc); yyjson_mut_arr_add_strcpy(out, arr, have_value ? value : ""); return; } diff --git a/tests/test_cli.c b/tests/test_cli.c index 06ebb3841..cf65e364e 100644 --- a/tests/test_cli.c +++ b/tests/test_cli.c @@ -15235,6 +15235,80 @@ TEST(cli_build_args_json_repeated_array_issue680) { PASS(); } +/* An array-typed flag also accepts the JSON-array spelling shown by the MCP + * schemas, expanding it into individual items instead of one opaque keyword. */ +TEST(cli_build_args_json_array_literal) { + char *err = NULL; + char *argv[] = {"--semantic-query", "[\"create\",\"billing\",\"account\"]"}; + char *json = cbm_cli_build_args_json("search_graph", 2, argv, &err); + ASSERT_NOT_NULL(json); + ASSERT(strstr(json, "\"semantic_query\":[\"create\",\"billing\",\"account\"]") != NULL); + free(json); + PASS(); +} + +/* An explicit empty JSON array contributes no items; it must not turn into + * the literal keyword "[]". */ +TEST(cli_build_args_json_array_literal_empty) { + char *err = NULL; + char *argv[] = {"--semantic-query", "[]"}; + char *json = cbm_cli_build_args_json("search_graph", 2, argv, &err); + ASSERT_NOT_NULL(json); + ASSERT(strstr(json, "\"semantic_query\":[]") != NULL); + ASSERT(strstr(json, "\"[]\"") == NULL); + free(json); + PASS(); +} + +/* Malformed JSON keeps the pre-existing behaviour: the value stays one + * literal string and no error is raised. */ +TEST(cli_build_args_json_array_literal_malformed) { + char *err = NULL; + char *argv[] = {"--semantic-query", "[\"create\","}; + char *json = cbm_cli_build_args_json("search_graph", 2, argv, &err); + ASSERT_NOT_NULL(json); + ASSERT_NULL(err); + ASSERT(strstr(json, "\"semantic_query\":[\"[\\\"create\\\",\"]") != NULL); + free(json); + PASS(); +} + +/* Only arrays of strings are expanded; other well-formed JSON arrays keep + * the literal-string behaviour. */ +TEST(cli_build_args_json_array_literal_non_string_items) { + char *err = NULL; + char *argv[] = {"--semantic-query", "[1,2]"}; + char *json = cbm_cli_build_args_json("search_graph", 2, argv, &err); + ASSERT_NOT_NULL(json); + ASSERT(strstr(json, "\"semantic_query\":[\"[1,2]\"]") != NULL); + free(json); + PASS(); +} + +/* Repeated flags and array literals may be mixed; items accumulate in + * argument order. */ +TEST(cli_build_args_json_array_literal_mixed_with_repeated) { + char *err = NULL; + char *argv[] = {"--semantic-query", "send", "--semantic-query", "[\"publish\",\"emit\"]", + "--semantic-query", "notify"}; + char *json = cbm_cli_build_args_json("search_graph", 6, argv, &err); + ASSERT_NOT_NULL(json); + ASSERT(strstr(json, "\"semantic_query\":[\"send\",\"publish\",\"emit\",\"notify\"]") != NULL); + free(json); + PASS(); +} + +/* A repeated array literal accumulates into one array. */ +TEST(cli_build_args_json_array_literal_repeated) { + char *err = NULL; + char *argv[] = {"--semantic-query", "[\"send\"]", "--semantic-query", "[\"publish\"]"}; + char *json = cbm_cli_build_args_json("search_graph", 4, argv, &err); + ASSERT_NOT_NULL(json); + ASSERT(strstr(json, "\"semantic_query\":[\"send\",\"publish\"]") != NULL); + free(json); + PASS(); +} + /* kebab-case flag names map to snake_case JSON keys. */ TEST(cli_build_args_json_kebab_to_snake_issue680) { char *err = NULL; @@ -16349,6 +16423,12 @@ SUITE(cli) { RUN_TEST(cli_build_args_json_bare_boolean_issue680); RUN_TEST(cli_build_args_json_unknown_flag_rejected); RUN_TEST(cli_build_args_json_repeated_array_issue680); + RUN_TEST(cli_build_args_json_array_literal); + RUN_TEST(cli_build_args_json_array_literal_empty); + RUN_TEST(cli_build_args_json_array_literal_malformed); + RUN_TEST(cli_build_args_json_array_literal_non_string_items); + RUN_TEST(cli_build_args_json_array_literal_mixed_with_repeated); + RUN_TEST(cli_build_args_json_array_literal_repeated); RUN_TEST(cli_build_args_json_kebab_to_snake_issue680); RUN_TEST(cli_build_args_json_key_equals_value_issue680); RUN_TEST(cli_build_args_json_bad_positional_errors_issue680);