diff options
Diffstat (limited to 'src/mcp/tools/write_message.c')
| -rw-r--r-- | src/mcp/tools/write_message.c | 169 |
1 files changed, 120 insertions, 49 deletions
diff --git a/src/mcp/tools/write_message.c b/src/mcp/tools/write_message.c index 3d7cb29..51fd05b 100644 --- a/src/mcp/tools/write_message.c +++ b/src/mcp/tools/write_message.c @@ -9,8 +9,8 @@ #include "base/allocator.h" -static bool -metadata_has_only_known_fields(const char *json, int length) +static tiny_metadata_parse_status_t +metadata_check_known_fields(const char *json, int length, const char **error) { int offset = 0, key_offset, key_length, value_offset, value_length, value_type; while ((offset = mjson_next(json, @@ -30,7 +30,8 @@ metadata_has_only_known_fields(const char *json, int length) (void)value_type; if (decoded <= 0 || decoded >= (int)sizeof(key)) { - return false; + *error = "metadata_json contains an invalid field name"; + return TINY_METADATA_PARSE_INVALID; } key[decoded] = '\0'; if (strcmp(key, "tool_call_id") != 0 && strcmp(key, "tags") != 0 @@ -38,14 +39,18 @@ metadata_has_only_known_fields(const char *json, int length) && strcmp(key, "importance") != 0 && strcmp(key, "summary") != 0 && strcmp(key, "relations") != 0 && strcmp(key, "source_message_ids") != 0) { - return false; + *error = "metadata_json contains an unknown field"; + return TINY_METADATA_PARSE_INVALID; } } - return true; + return TINY_METADATA_PARSE_OK; } -bool -tiny_mcp_parse_metadata(const char *json, tiny_message_t *message, tiny_metadata_mask_t *present) +tiny_metadata_parse_status_t +tiny_mcp_parse_metadata(const char *json, + tiny_message_t *message, + tiny_metadata_mask_t *present, + const char **error) { const char *tool_call = NULL, *tier = NULL, *tags = NULL, *kind = NULL, *summary = NULL; const char *relations = NULL, *sources = NULL; @@ -58,14 +63,26 @@ tiny_mcp_parse_metadata(const char *json, tiny_message_t *message, tiny_metadata int relations_type, sources_type; double importance = 50; tiny_metadata_mask_t fields = 0; + if (error) + { + *error = NULL; + } + if (!json || !message || !error) + { + return TINY_METADATA_PARSE_INVALID; + } if (strcmp(json, "{}") == 0) { - return true; + return TINY_METADATA_PARSE_OK; } - if (mjson_find(json, length, "$", NULL, NULL) != MJSON_TOK_OBJECT - || !metadata_has_only_known_fields(json, length)) + if (mjson_find(json, length, "$", NULL, NULL) != MJSON_TOK_OBJECT) { - return false; + *error = "metadata_json must be a JSON object"; + return TINY_METADATA_PARSE_INVALID; + } + if (metadata_check_known_fields(json, length, error) != TINY_METADATA_PARSE_OK) + { + return TINY_METADATA_PARSE_INVALID; } tool_call_type = mjson_find(json, length, "$.tool_call_id", &tool_call, &tool_call_len); tier_type = mjson_find(json, length, "$.memory_tier", &tier, &tier_len); @@ -75,17 +92,25 @@ tiny_mcp_parse_metadata(const char *json, tiny_message_t *message, tiny_metadata importance_type = mjson_find(json, length, "$.importance", NULL, NULL); relations_type = mjson_find(json, length, "$.relations", &relations, &relations_len); sources_type = mjson_find(json, length, "$.source_message_ids", &sources, &sources_len); - if ((tool_call_type != MJSON_TOK_INVALID && tool_call_type != MJSON_TOK_STRING) - || (tier_type != MJSON_TOK_INVALID && tier_type != MJSON_TOK_STRING) - || (tags_type != MJSON_TOK_INVALID && tags_type != MJSON_TOK_ARRAY) - || (kind_type != MJSON_TOK_INVALID && kind_type != MJSON_TOK_STRING) - || (summary_type != MJSON_TOK_INVALID && summary_type != MJSON_TOK_STRING) - || (importance_type != MJSON_TOK_INVALID && importance_type != MJSON_TOK_NUMBER) - || (relations_type != MJSON_TOK_INVALID && relations_type != MJSON_TOK_ARRAY) - || (sources_type != MJSON_TOK_INVALID && sources_type != MJSON_TOK_ARRAY)) - { - return false; - } +#define REQUIRE_METADATA_TYPE(actual, expected, field, type_name) \ + do \ + { \ + if ((actual) != MJSON_TOK_INVALID && (actual) != (expected)) \ + { \ + *error = "metadata_json." field " must be " type_name; \ + return TINY_METADATA_PARSE_INVALID; \ + } \ + } \ + while (0) + REQUIRE_METADATA_TYPE(tool_call_type, MJSON_TOK_STRING, "tool_call_id", "a string"); + REQUIRE_METADATA_TYPE(tier_type, MJSON_TOK_STRING, "memory_tier", "a string"); + REQUIRE_METADATA_TYPE(tags_type, MJSON_TOK_ARRAY, "tags", "an array"); + REQUIRE_METADATA_TYPE(kind_type, MJSON_TOK_STRING, "memory_kind", "a string"); + REQUIRE_METADATA_TYPE(summary_type, MJSON_TOK_STRING, "summary", "a string"); + REQUIRE_METADATA_TYPE(importance_type, MJSON_TOK_NUMBER, "importance", "an integer"); + REQUIRE_METADATA_TYPE(relations_type, MJSON_TOK_ARRAY, "relations", "an array"); + REQUIRE_METADATA_TYPE(sources_type, MJSON_TOK_ARRAY, "source_message_ids", "an array"); +#undef REQUIRE_METADATA_TYPE fields |= tool_call_type != MJSON_TOK_INVALID ? TINY_METADATA_TOOL_CALL_ID : 0; fields |= tags_type != MJSON_TOK_INVALID ? TINY_METADATA_TAGS : 0; fields |= tier_type != MJSON_TOK_INVALID ? TINY_METADATA_TIER : 0; @@ -114,7 +139,8 @@ tiny_mcp_parse_metadata(const char *json, tiny_message_t *message, tiny_metadata if (value_type != MJSON_TOK_OBJECT || message->metadata.relation_count >= TINY_RELATION_COUNT_MAX) { - return false; + *error = "metadata_json.relations must contain at most 32 objects"; + return TINY_METADATA_PARSE_INVALID; } decoded_type = mjson_get_string( relations + value_offset, value_length, "$.type", type, sizeof(type)); @@ -126,20 +152,23 @@ tiny_mcp_parse_metadata(const char *json, tiny_message_t *message, tiny_metadata if (decoded_type <= 0 || decoded_type >= (int)sizeof(type) || decoded_id != TINY_MESSAGE_ID_LENGTH) { - return false; + *error = "metadata_json.relations entries require type and a 26-character message_id"; + return TINY_METADATA_PARSE_INVALID; } type[decoded_type] = '\0'; relation.message_id[decoded_id] = '\0'; if (!tiny_relation_type_parse(type, &relation.type) || !tiny_message_id_valid(relation.message_id)) { - return false; + *error = "metadata_json.relations contains an invalid type or message_id"; + return TINY_METADATA_PARSE_INVALID; } resized = realloc(message->metadata.relations, (message->metadata.relation_count + 1) * sizeof(*resized)); if (!resized) { - return false; + *error = "metadata_json allocation failed"; + return TINY_METADATA_PARSE_NO_MEMORY; } message->metadata.relations = resized; message->metadata.relations[message->metadata.relation_count++] = relation; @@ -164,43 +193,53 @@ tiny_mcp_parse_metadata(const char *json, tiny_message_t *message, tiny_metadata if (value_type != MJSON_TOK_STRING || message->metadata.source_count >= TINY_SOURCE_COUNT_MAX) { - return false; + *error = "metadata_json.source_message_ids must contain at most 64 strings"; + return TINY_METADATA_PARSE_INVALID; } id = malloc(TINY_MESSAGE_ID_LENGTH + 1); if (!id) { - return false; + *error = "metadata_json allocation failed"; + return TINY_METADATA_PARSE_NO_MEMORY; } decoded = mjson_get_string( sources + value_offset, value_length, "$", id, TINY_MESSAGE_ID_LENGTH + 1); if (decoded != TINY_MESSAGE_ID_LENGTH) { free(id); - return false; + *error = "metadata_json.source_message_ids entries must be 26-character ULIDs"; + return TINY_METADATA_PARSE_INVALID; } id[decoded] = '\0'; if (!tiny_message_id_valid(id)) { free(id); - return false; + *error = "metadata_json.source_message_ids contains an invalid ULID"; + return TINY_METADATA_PARSE_INVALID; } resized = realloc(message->metadata.source_message_ids, (message->metadata.source_count + 1) * sizeof(*resized)); if (!resized) { free(id); - return false; + *error = "metadata_json allocation failed"; + return TINY_METADATA_PARSE_NO_MEMORY; } message->metadata.source_message_ids = resized; message->metadata.source_message_ids[message->metadata.source_count++] = id; } } - if (importance_type == MJSON_TOK_NUMBER - && mjson_get_number(json, length, "$.importance", &importance) > 0) + if (importance_type == MJSON_TOK_NUMBER) { + if (mjson_get_number(json, length, "$.importance", &importance) <= 0) + { + *error = "metadata_json.importance must be an integer from 0 to 100"; + return TINY_METADATA_PARSE_INVALID; + } if (importance < 0 || importance > 100 || importance != (double)(uint8_t)importance) { - return false; + *error = "metadata_json.importance must be an integer from 0 to 100"; + return TINY_METADATA_PARSE_INVALID; } message->metadata.importance = (uint8_t)importance; } @@ -210,12 +249,14 @@ tiny_mcp_parse_metadata(const char *json, tiny_message_t *message, tiny_metadata int decoded = mjson_get_string(json, length, "$.memory_kind", value, sizeof(value)); if (decoded <= 0 || decoded >= (int)sizeof(value)) { - return false; + *error = "metadata_json.memory_kind contains an invalid value"; + return TINY_METADATA_PARSE_INVALID; } value[decoded] = '\0'; if (!tiny_kind_parse(value, &message->metadata.memory_kind)) { - return false; + *error = "metadata_json.memory_kind contains an invalid value"; + return TINY_METADATA_PARSE_INVALID; } } if (summary_type == MJSON_TOK_STRING) @@ -224,14 +265,16 @@ tiny_mcp_parse_metadata(const char *json, tiny_message_t *message, tiny_metadata message->metadata.summary = malloc(TINY_SUMMARY_MAX + 1); if (!message->metadata.summary) { - return false; + *error = "metadata_json allocation failed"; + return TINY_METADATA_PARSE_NO_MEMORY; } decoded = mjson_get_string( json, length, "$.summary", message->metadata.summary, TINY_SUMMARY_MAX + 1); if (decoded < 0 || decoded > TINY_SUMMARY_MAX || (decoded && !tiny_utf8_valid(message->metadata.summary, (size_t)decoded))) { - return false; + *error = "metadata_json.summary must be valid UTF-8 up to 4096 bytes"; + return TINY_METADATA_PARSE_INVALID; } message->metadata.summary[decoded] = '\0'; if (decoded == 0) @@ -246,7 +289,8 @@ tiny_mcp_parse_metadata(const char *json, tiny_message_t *message, tiny_metadata message->metadata.tool_call_id = malloc(TINY_TOOL_CALL_ID_MAX + 1); if (!message->metadata.tool_call_id) { - return false; + *error = "metadata_json allocation failed"; + return TINY_METADATA_PARSE_NO_MEMORY; } decoded = mjson_get_string(json, length, @@ -256,7 +300,8 @@ tiny_mcp_parse_metadata(const char *json, tiny_message_t *message, tiny_metadata if (decoded < 0 || decoded > TINY_TOOL_CALL_ID_MAX || !tiny_utf8_valid(message->metadata.tool_call_id, (size_t)decoded)) { - return false; + *error = "metadata_json.tool_call_id must be valid UTF-8 up to 256 bytes"; + return TINY_METADATA_PARSE_INVALID; } message->metadata.tool_call_id[decoded] = '\0'; } @@ -266,16 +311,19 @@ tiny_mcp_parse_metadata(const char *json, tiny_message_t *message, tiny_metadata int decoded = mjson_get_string(json, length, "$.memory_tier", tier_value, sizeof(tier_value)); if (decoded < 0 || decoded >= (int)sizeof(tier_value)) { - return false; + *error = "metadata_json.memory_tier contains an invalid value"; + return TINY_METADATA_PARSE_INVALID; } tier_value[decoded] = '\0'; if (!tiny_tier_parse(tier_value, &message->metadata.memory_tier)) { - return false; + *error = "metadata_json.memory_tier contains an invalid value"; + return TINY_METADATA_PARSE_INVALID; } } if (tags_type == MJSON_TOK_ARRAY) { + offset = 0; while ((offset = mjson_next(tags, tags_len, offset, @@ -291,18 +339,21 @@ tiny_mcp_parse_metadata(const char *json, tiny_message_t *message, tiny_metadata int decoded; if (value_type != MJSON_TOK_STRING || message->metadata.tag_count >= TINY_TAG_COUNT_MAX) { - return false; + *error = "metadata_json.tags must contain at most 32 strings"; + return TINY_METADATA_PARSE_INVALID; } tag = malloc(TINY_TAG_MAX + 1); if (!tag) { - return false; + *error = "metadata_json allocation failed"; + return TINY_METADATA_PARSE_NO_MEMORY; } decoded = mjson_get_string(tags + value_offset, value_length, "$", tag, TINY_TAG_MAX + 1); if (decoded <= 0 || decoded > TINY_TAG_MAX || !tiny_utf8_valid(tag, (size_t)decoded)) { free(tag); - return false; + *error = "metadata_json.tags entries must be non-empty valid UTF-8 up to 128 bytes"; + return TINY_METADATA_PARSE_INVALID; } tag[decoded] = '\0'; resized = realloc(message->metadata.tags, @@ -310,7 +361,8 @@ tiny_mcp_parse_metadata(const char *json, tiny_message_t *message, tiny_metadata if (!resized) { free(tag); - return false; + *error = "metadata_json allocation failed"; + return TINY_METADATA_PARSE_NO_MEMORY; } message->metadata.tags = resized; message->metadata.tags[message->metadata.tag_count++] = tag; @@ -320,7 +372,12 @@ tiny_mcp_parse_metadata(const char *json, tiny_message_t *message, tiny_metadata { *present = fields; } - return tiny_metadata_valid(&message->metadata); + if (!tiny_metadata_valid(&message->metadata)) + { + *error = "metadata_json contains duplicate or inconsistent metadata values"; + return TINY_METADATA_PARSE_INVALID; + } + return TINY_METADATA_PARSE_OK; } static void @@ -330,6 +387,8 @@ call_write_message(const mcpc_tool_t *tool, mcpc_ucbr_t *result) tiny_message_t input, written; tiny_store_status_t status; char *role = NULL, *metadata = NULL; + const char *metadata_error = NULL; + tiny_metadata_parse_status_t metadata_status = TINY_METADATA_PARSE_OK; tiny_json_buffer_t json; tiny_message_init(&input); tiny_message_init(&written); @@ -338,12 +397,24 @@ call_write_message(const mcpc_tool_t *tool, mcpc_ucbr_t *result) input.content = tiny_mcp_tool_get_string(tool, "content", TINY_CONTENT_MAX); metadata = tiny_mcp_tool_get_string(tool, "metadata_json", TINY_CONTENT_MAX); bool valid = context && input.session_id && input.session_id[0] && role && input.content - && input.content[0] && tiny_role_parse(role, &input.role) - && tiny_mcp_parse_metadata(metadata ? metadata : "{}", &input, NULL); + && input.content[0] && tiny_role_parse(role, &input.role); + if (valid) + { + metadata_status + = tiny_mcp_parse_metadata(metadata ? metadata : "{}", &input, NULL, &metadata_error); + } if (!valid) { tiny_mcp_tool_add_error(result, "VALIDATION_ERROR", "invalid write_message arguments"); } + else if (metadata_status == TINY_METADATA_PARSE_NO_MEMORY) + { + tiny_mcp_tool_add_error(result, "INTERNAL_ERROR", metadata_error); + } + else if (metadata_status == TINY_METADATA_PARSE_INVALID) + { + tiny_mcp_tool_add_error(result, "VALIDATION_ERROR", metadata_error); + } else { status = tiny_store_write(context->store, &input, &written); |
