summaryrefslogtreecommitdiff
path: root/src/mcp/tools/write_message.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/mcp/tools/write_message.c')
-rw-r--r--src/mcp/tools/write_message.c169
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);