From 1efecaef1f867cb218366e2b4ab4b901fdd2835f Mon Sep 17 00:00:00 2001 From: tanaka takashi Date: Sat, 4 Jul 2026 14:54:45 +0900 Subject: 途中経過コミット MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/mcp/tools/common/common.c | 36 +++ src/mcp/tools/common/common.h | 3 + src/mcp/tools/common/context.c | 34 ++- src/mcp/tools/common/context.h | 4 + src/mcp/tools/common/history.c | 23 ++ src/mcp/tools/common/history.h | 6 + src/mcp/tools/common/memory_index.c | 219 +++++++++++++++ src/mcp/tools/common/memory_index.h | 24 ++ src/mcp/tools/common/registry.c | 5 +- src/mcp/tools/common/registry.h | 1 + src/mcp/tools/delete_message.c | 4 + src/mcp/tools/get_system_health.c | 19 +- src/mcp/tools/read_messages.c | 543 +++++++++++++++++++++++++++++++++++- src/mcp/tools/rebuild_index.c | 19 +- src/mcp/tools/search_messages.c | 134 ++++++++- src/mcp/tools/update_message.c | 97 +++++++ src/mcp/tools/write_message.c | 212 ++++++++++++-- 17 files changed, 1334 insertions(+), 49 deletions(-) create mode 100644 src/mcp/tools/common/memory_index.c create mode 100644 src/mcp/tools/common/memory_index.h create mode 100644 src/mcp/tools/update_message.c (limited to 'src/mcp/tools') diff --git a/src/mcp/tools/common/common.c b/src/mcp/tools/common/common.c index d6669f8..62f507f 100644 --- a/src/mcp/tools/common/common.c +++ b/src/mcp/tools/common/common.c @@ -68,6 +68,10 @@ tiny_mcp_tool_append_message(tiny_json_buffer_t *json, const tiny_message_t *mes tiny_json_string(json, tiny_role_name(message->role)); tiny_json_raw(json, ",\"content\":"); tiny_json_string(json, message->content); + tiny_json_raw(json, ",\"revision\":"); + tiny_json_u64(json, message->revision); + tiny_json_raw(json, ",\"updated_at\":"); + tiny_json_i64(json, message->updated_at_ms); tiny_json_raw(json, ",\"metadata\":{"); if (message->metadata.tool_call_id) { @@ -86,6 +90,38 @@ tiny_mcp_tool_append_message(tiny_json_buffer_t *json, const tiny_message_t *mes } tiny_json_raw(json, "],\"memory_tier\":"); tiny_json_string(json, tiny_tier_name(message->metadata.memory_tier)); + tiny_json_raw(json, ",\"memory_kind\":"); + tiny_json_string(json, tiny_kind_name(message->metadata.memory_kind)); + tiny_json_raw(json, ",\"importance\":"); + tiny_json_u64(json, message->metadata.importance); + if (message->metadata.summary) + { + tiny_json_raw(json, ",\"summary\":"); + tiny_json_string(json, message->metadata.summary); + } + tiny_json_raw(json, ",\"relations\":["); + for (index = 0; index < message->metadata.relation_count; ++index) + { + if (index) + { + tiny_json_raw(json, ","); + } + tiny_json_raw(json, "{\"type\":"); + tiny_json_string(json, tiny_relation_type_name(message->metadata.relations[index].type)); + tiny_json_raw(json, ",\"message_id\":"); + tiny_json_string(json, message->metadata.relations[index].message_id); + tiny_json_raw(json, "}"); + } + tiny_json_raw(json, "],\"source_message_ids\":["); + for (index = 0; index < message->metadata.source_count; ++index) + { + if (index) + { + tiny_json_raw(json, ","); + } + tiny_json_string(json, message->metadata.source_message_ids[index]); + } + tiny_json_raw(json, "]"); tiny_json_raw(json, "}}"); } diff --git a/src/mcp/tools/common/common.h b/src/mcp/tools/common/common.h index 07caea1..741b2ce 100644 --- a/src/mcp/tools/common/common.h +++ b/src/mcp/tools/common/common.h @@ -3,12 +3,15 @@ #include "domain/message.h" #include "mcp/json.h" +#include "domain/message.h" #include void tiny_mcp_tool_add_error(mcpc_ucbr_t *result, const char *code, const char *message); void tiny_mcp_tool_add_json(mcpc_ucbr_t *result, tiny_json_buffer_t *json); char *tiny_mcp_tool_get_string(const mcpc_tool_t *tool, const char *name, size_t maximum); void tiny_mcp_tool_append_message(tiny_json_buffer_t *json, const tiny_message_t *message); +bool +tiny_mcp_parse_metadata(const char *json, tiny_message_t *message, tiny_metadata_mask_t *present); mcpc_tool_t *tiny_mcp_tool_new(const char *name, const char *description, mcpc_tcallcb_t callback); bool tiny_mcp_tool_add_property(mcpc_tool_t *tool, diff --git a/src/mcp/tools/common/context.c b/src/mcp/tools/common/context.c index d79b5de..e833f70 100644 --- a/src/mcp/tools/common/context.c +++ b/src/mcp/tools/common/context.c @@ -43,10 +43,12 @@ tiny_mcp_tools_context_new(tiny_store_t *store) context->store = store; context->search_index = tiny_mcp_tools_build_search_index(store); context->history_index = tiny_history_index_build(store); - if (!context->search_index || !context->history_index) + context->memory_index = tiny_memory_index_build(store); + if (!context->search_index || !context->history_index || !context->memory_index) { tiny_search_index_free(context->search_index); tiny_history_index_free(context->history_index); + tiny_memory_index_free(context->memory_index); free(context); return NULL; } @@ -62,9 +64,39 @@ tiny_mcp_tools_context_free(tiny_mcp_tools_context_t *context) } tiny_search_index_free(context->search_index); tiny_history_index_free(context->history_index); + tiny_memory_index_free(context->memory_index); free(context); } +bool +tiny_mcp_tools_ensure_memory_index(tiny_mcp_tools_context_t *context) +{ + if (!context) + { + return false; + } + if (!context->memory_index) + { + context->memory_index = tiny_memory_index_build(context->store); + } + return context->memory_index != NULL; +} + +void +tiny_mcp_tools_invalidate_indexes(tiny_mcp_tools_context_t *context) +{ + if (!context) + { + return; + } + tiny_search_index_free(context->search_index); + tiny_history_index_free(context->history_index); + tiny_memory_index_free(context->memory_index); + context->search_index = NULL; + context->history_index = NULL; + context->memory_index = NULL; +} + bool tiny_mcp_tools_context_activate(tiny_mcp_tools_context_t *context) { diff --git a/src/mcp/tools/common/context.h b/src/mcp/tools/common/context.h index 2a6650a..bd167f2 100644 --- a/src/mcp/tools/common/context.h +++ b/src/mcp/tools/common/context.h @@ -3,6 +3,7 @@ #include "domain/search.h" #include "mcp/tools/common/history.h" +#include "mcp/tools/common/memory_index.h" #include "storage/store.h" typedef struct tiny_mcp_tools_context @@ -10,6 +11,7 @@ typedef struct tiny_mcp_tools_context tiny_store_t *store; tiny_search_index_t *search_index; tiny_history_index_t *history_index; + tiny_memory_index_t *memory_index; } tiny_mcp_tools_context_t; tiny_mcp_tools_context_t *tiny_mcp_tools_context_new(tiny_store_t *store); @@ -19,5 +21,7 @@ void tiny_mcp_tools_context_deactivate(tiny_mcp_tools_context_t *context); tiny_mcp_tools_context_t *tiny_mcp_tools_context_current(void); tiny_search_index_t *tiny_mcp_tools_build_search_index(const tiny_store_t *store); bool tiny_mcp_tools_ensure_history_index(tiny_mcp_tools_context_t *context); +bool tiny_mcp_tools_ensure_memory_index(tiny_mcp_tools_context_t *context); +void tiny_mcp_tools_invalidate_indexes(tiny_mcp_tools_context_t *context); #endif diff --git a/src/mcp/tools/common/history.c b/src/mcp/tools/common/history.c index 6254d8c..9f08e84 100644 --- a/src/mcp/tools/common/history.c +++ b/src/mcp/tools/common/history.c @@ -284,3 +284,26 @@ tiny_history_index_session(const tiny_history_index_t *index, } return index && session_id; } + +size_t +tiny_history_index_session_count(const tiny_history_index_t *index) +{ + return index ? index->count : 0; +} + +bool +tiny_history_index_session_at(const tiny_history_index_t *index, + size_t position, + const char **session_id, + const size_t **entries, + size_t *count) +{ + if (!index || position >= index->count || !session_id || !entries || !count) + { + return false; + } + *session_id = index->buckets[position].session_id; + *entries = index->buckets[position].entries; + *count = index->buckets[position].count; + return true; +} diff --git a/src/mcp/tools/common/history.h b/src/mcp/tools/common/history.h index ac08d99..d6f5708 100644 --- a/src/mcp/tools/common/history.h +++ b/src/mcp/tools/common/history.h @@ -13,5 +13,11 @@ bool tiny_history_index_session(const tiny_history_index_t *index, const char *session_id, const size_t **entries, size_t *count); +size_t tiny_history_index_session_count(const tiny_history_index_t *index); +bool tiny_history_index_session_at(const tiny_history_index_t *index, + size_t position, + const char **session_id, + const size_t **entries, + size_t *count); #endif diff --git a/src/mcp/tools/common/memory_index.c b/src/mcp/tools/common/memory_index.c new file mode 100644 index 0000000..36e430b --- /dev/null +++ b/src/mcp/tools/common/memory_index.c @@ -0,0 +1,219 @@ +#include "mcp/tools/common/memory_index.h" + +#include +#include +#include + +#include "base/allocator.h" + +#define EMPTY_SLOT SIZE_MAX + +typedef struct memory_bucket +{ + uint64_t hash; + char message_id[TINY_MESSAGE_ID_LENGTH + 1]; + size_t entry_index; + tiny_relation_reference_t *incoming; + size_t incoming_count; + size_t incoming_capacity; +} memory_bucket_t; + +struct tiny_memory_index +{ + memory_bucket_t *buckets; + size_t count; + size_t *slots; + size_t slot_capacity; +}; + +static uint64_t +id_hash(const char *value) +{ + uint64_t hash = UINT64_C(1469598103934665603); + while (*value) + { + hash ^= (unsigned char)*value++; + hash *= UINT64_C(1099511628211); + } + return hash; +} + +static size_t +slot_start(uint64_t hash, size_t capacity) +{ + return (size_t)(hash ^ (hash >> 32)) & (capacity - 1); +} + +static memory_bucket_t * +find_bucket(const tiny_memory_index_t *index, const char *message_id) +{ + uint64_t hash; + size_t slot, bucket; + if (!index || !message_id || !index->slot_capacity) + { + return NULL; + } + hash = id_hash(message_id); + slot = slot_start(hash, index->slot_capacity); + while ((bucket = index->slots[slot]) != EMPTY_SLOT) + { + if (bucket >= index->count) + { + return NULL; + } + if (index->buckets[bucket].hash == hash + && strcmp(index->buckets[bucket].message_id, message_id) == 0) + { + return &index->buckets[bucket]; + } + slot = (slot + 1) & (index->slot_capacity - 1); + } + return NULL; +} + +static bool +add_incoming(memory_bucket_t *bucket, size_t source, tiny_relation_type_t type) +{ + tiny_relation_reference_t *resized; + size_t capacity; + if (bucket->incoming_count == bucket->incoming_capacity) + { + capacity = bucket->incoming_capacity ? bucket->incoming_capacity * 2 : 4; + if (capacity < bucket->incoming_capacity + || !(resized = realloc(bucket->incoming, capacity * sizeof(*resized)))) + { + return false; + } + bucket->incoming = resized; + bucket->incoming_capacity = capacity; + } + bucket->incoming[bucket->incoming_count].source_index = source; + bucket->incoming[bucket->incoming_count].type = type; + bucket->incoming_count++; + return true; +} + +tiny_memory_index_t * +tiny_memory_index_build(const tiny_store_t *store) +{ + tiny_memory_index_t *index; + size_t count = tiny_store_entry_count(store), capacity = 64, position; + if (!store || count > SIZE_MAX / sizeof(memory_bucket_t)) + { + return NULL; + } + while (count > (capacity / 10) * 7) + { + if (capacity > SIZE_MAX / 2) + { + return NULL; + } + capacity *= 2; + } + index = calloc(1, sizeof(*index)); + if (!index || (count && !(index->buckets = calloc(count, sizeof(*index->buckets)))) + || !(index->slots = malloc(capacity * sizeof(*index->slots)))) + { + tiny_memory_index_free(index); + return NULL; + } + index->count = count; + index->slot_capacity = capacity; + for (position = 0; position < capacity; ++position) + { + index->slots[position] = EMPTY_SLOT; + } + for (position = 0; position < count; ++position) + { + const tiny_store_entry_t *entry = tiny_store_entry(store, position); + memory_bucket_t *bucket = &index->buckets[position]; + size_t slot; + memcpy(bucket->message_id, entry->message.message_id, sizeof(bucket->message_id)); + bucket->hash = id_hash(bucket->message_id); + bucket->entry_index = position; + slot = slot_start(bucket->hash, capacity); + while (index->slots[slot] != EMPTY_SLOT) + { + slot = (slot + 1) & (capacity - 1); + } + index->slots[slot] = position; + } + for (position = 0; position < count; ++position) + { + const tiny_store_entry_t *entry = tiny_store_entry(store, position); + size_t relation; + if (entry->deleted) + { + continue; + } + for (relation = 0; relation < entry->message.metadata.relation_count; ++relation) + { + const tiny_relation_t *value = &entry->message.metadata.relations[relation]; + memory_bucket_t *target = find_bucket(index, value->message_id); + if (!target || !add_incoming(target, position, value->type)) + { + tiny_memory_index_free(index); + return NULL; + } + } + } + return index; +} + +void +tiny_memory_index_free(tiny_memory_index_t *index) +{ + size_t position; + if (!index) + { + return; + } + for (position = 0; position < index->count; ++position) + { + free(index->buckets[position].incoming); + } + free(index->buckets); + free(index->slots); + free(index); +} + +bool +tiny_memory_index_find(const tiny_memory_index_t *index, + const char *message_id, + size_t *entry_index) +{ + memory_bucket_t *bucket; + if (!entry_index) + { + return false; + } + *entry_index = 0; + bucket = find_bucket(index, message_id); + if (bucket) + { + *entry_index = bucket->entry_index; + } + return bucket != NULL; +} + +bool +tiny_memory_index_incoming(const tiny_memory_index_t *index, + const char *message_id, + const tiny_relation_reference_t **references, + size_t *count) +{ + memory_bucket_t *bucket; + if (!references || !count) + { + return false; + } + *references = NULL; + *count = 0; + bucket = find_bucket(index, message_id); + if (bucket) + { + *references = bucket->incoming; + *count = bucket->incoming_count; + } + return bucket != NULL; +} diff --git a/src/mcp/tools/common/memory_index.h b/src/mcp/tools/common/memory_index.h new file mode 100644 index 0000000..fbe43a7 --- /dev/null +++ b/src/mcp/tools/common/memory_index.h @@ -0,0 +1,24 @@ +#ifndef TINY_CONVMEM_MCP_TOOLS_MEMORY_INDEX_H +#define TINY_CONVMEM_MCP_TOOLS_MEMORY_INDEX_H + +#include "storage/store.h" + +typedef struct tiny_relation_reference +{ + size_t source_index; + tiny_relation_type_t type; +} tiny_relation_reference_t; + +typedef struct tiny_memory_index tiny_memory_index_t; + +tiny_memory_index_t *tiny_memory_index_build(const tiny_store_t *store); +void tiny_memory_index_free(tiny_memory_index_t *index); +bool tiny_memory_index_find(const tiny_memory_index_t *index, + const char *message_id, + size_t *entry_index); +bool tiny_memory_index_incoming(const tiny_memory_index_t *index, + const char *message_id, + const tiny_relation_reference_t **references, + size_t *count); + +#endif diff --git a/src/mcp/tools/common/registry.c b/src/mcp/tools/common/registry.c index 9aa1dff..1bdafc3 100644 --- a/src/mcp/tools/common/registry.c +++ b/src/mcp/tools/common/registry.c @@ -6,6 +6,7 @@ bool tiny_mcp_tools_register(mcpc_server_t *server) { return tiny_mcp_register_write_message(server) && tiny_mcp_register_read_messages(server) - && tiny_mcp_register_search_messages(server) && tiny_mcp_register_delete_message(server) - && tiny_mcp_register_rebuild_index(server) && tiny_mcp_register_get_system_health(server); + && tiny_mcp_register_search_messages(server) && tiny_mcp_register_update_message(server) + && tiny_mcp_register_delete_message(server) && tiny_mcp_register_rebuild_index(server) + && tiny_mcp_register_get_system_health(server); } diff --git a/src/mcp/tools/common/registry.h b/src/mcp/tools/common/registry.h index bbc68d0..451d300 100644 --- a/src/mcp/tools/common/registry.h +++ b/src/mcp/tools/common/registry.h @@ -4,6 +4,7 @@ #include bool tiny_mcp_register_write_message(mcpc_server_t *server); +bool tiny_mcp_register_update_message(mcpc_server_t *server); bool tiny_mcp_register_read_messages(mcpc_server_t *server); bool tiny_mcp_register_search_messages(mcpc_server_t *server); bool tiny_mcp_register_delete_message(mcpc_server_t *server); diff --git a/src/mcp/tools/delete_message.c b/src/mcp/tools/delete_message.c index f90bbe3..9f70866 100644 --- a/src/mcp/tools/delete_message.c +++ b/src/mcp/tools/delete_message.c @@ -30,6 +30,10 @@ call_delete_message(const mcpc_tool_t *tool, mcpc_ucbr_t *result) } else { + if (status == TINY_STORE_OK) + { + tiny_mcp_tools_invalidate_indexes(context); + } mcpc_toolcall_result_add_text_printf8( result, (const char8_t *)(status == TINY_STORE_OK ? "{\"status\":\"OK\"}" diff --git a/src/mcp/tools/get_system_health.c b/src/mcp/tools/get_system_health.c index 5fd3fc8..f84511f 100644 --- a/src/mcp/tools/get_system_health.c +++ b/src/mcp/tools/get_system_health.c @@ -19,7 +19,9 @@ call_get_system_health(const mcpc_tool_t *tool, mcpc_ucbr_t *result) tiny_json_init(&json); tiny_json_raw(&json, "{\"status\":"); tiny_json_string(&json, health.last_error[0] ? "DEGRADED" : "OK"); - tiny_json_raw(&json, ",\"format_version\":1,\"log_size\":"); + tiny_json_raw(&json, ",\"format_version\":"); + tiny_json_u64(&json, health.format_version); + tiny_json_raw(&json, ",\"log_size\":"); tiny_json_u64(&json, health.log_size); tiny_json_raw(&json, ",\"snapshot_size\":"); tiny_json_u64(&json, health.snapshot_size); @@ -29,6 +31,21 @@ call_get_system_health(const mcpc_tool_t *tool, mcpc_ucbr_t *result) tiny_json_u64(&json, health.deleted_messages); tiny_json_raw(&json, ",\"last_sequence\":"); tiny_json_u64(&json, health.last_sequence); + tiny_json_raw(&json, ",\"update_records\":"); + tiny_json_u64(&json, health.update_records); + tiny_json_raw(&json, ",\"dead_bytes\":"); + tiny_json_u64(&json, health.dead_bytes); + tiny_json_raw(&json, ",\"io\":{\"write_bytes\":"); + tiny_json_u64(&json, health.write_bytes); + tiny_json_raw(&json, ",\"write_calls\":"); + tiny_json_u64(&json, health.write_calls); + tiny_json_raw(&json, ",\"flush_calls\":"); + tiny_json_u64(&json, health.flush_calls); + tiny_json_raw(&json, ",\"truncate_calls\":"); + tiny_json_u64(&json, health.truncate_calls); + tiny_json_raw(&json, ",\"replace_calls\":"); + tiny_json_u64(&json, health.replace_calls); + tiny_json_raw(&json, "}"); tiny_json_raw(&json, ",\"recovered_tail\":"); tiny_json_raw(&json, health.recovered_tail ? "true" : "false"); tiny_json_raw(&json, ",\"snapshot_valid\":"); diff --git a/src/mcp/tools/read_messages.c b/src/mcp/tools/read_messages.c index 44a026f..aa76ce8 100644 --- a/src/mcp/tools/read_messages.c +++ b/src/mcp/tools/read_messages.c @@ -4,11 +4,13 @@ #include "mcp/tools/common/context.h" #include +#include #include #include #include "base/allocator.h" #include "storage/codec.h" +#include "mjson.h" #define READ_DEFAULT_LIMIT 100 #define READ_MAX_LIMIT 1000 @@ -185,10 +187,493 @@ escaped_size(const char *value, size_t length) return total; } +typedef struct context_edge +{ + size_t entry_index; + tiny_relation_type_t type; + size_t depth; + bool incoming; + int64_t timestamp; + char message_id[TINY_MESSAGE_ID_LENGTH + 1]; +} context_edge_t; + +static bool +relation_type_mask(const char *json, uint32_t *mask) +{ + int offset = 0, key_offset, key_length, value_offset, value_length, value_type; + if (!json) + { + *mask = UINT32_MAX; + return true; + } + if (mjson_find(json, (int)strlen(json), "$", NULL, NULL) != MJSON_TOK_ARRAY) + { + return false; + } + *mask = 0; + while ((offset = mjson_next(json, + (int)strlen(json), + offset, + &key_offset, + &key_length, + &value_offset, + &value_length, + &value_type)) + != 0) + { + char value[24]; + int decoded; + tiny_relation_type_t type; + if (value_type != MJSON_TOK_STRING + || (decoded = mjson_get_string( + json + value_offset, value_length, "$", value, sizeof(value))) + <= 0 + || decoded >= (int)sizeof(value)) + { + return false; + } + value[decoded] = '\0'; + if (!tiny_relation_type_parse(value, &type)) + { + return false; + } + *mask |= UINT32_C(1) << (unsigned)type; + } + return true; +} + +static int +compare_context_edges(const void *left, const void *right) +{ + const context_edge_t *a = left, *b = right; + if (a->depth != b->depth) + return a->depth < b->depth ? -1 : 1; + if (a->type != b->type) + return a->type < b->type ? -1 : 1; + if (a->timestamp != b->timestamp) + return a->timestamp < b->timestamp ? -1 : 1; + return strcmp(a->message_id, b->message_id); +} + +static void +call_context_view(tiny_mcp_tools_context_t *context, const mcpc_tool_t *tool, mcpc_ucbr_t *result) +{ + char *message_id = tiny_mcp_tool_get_string(tool, "message_id", TINY_MESSAGE_ID_LENGTH); + char *types_json = tiny_mcp_tool_get_string(tool, "relation_types_json", 256); + int32_t before = 1, after = 1, relation_depth = 1; + mcpc_errcode_t before_status + = mcpc_tool_get_tpropval_i32(tool, (const char8_t *)"before", &before); + mcpc_errcode_t after_status = mcpc_tool_get_tpropval_i32(tool, (const char8_t *)"after", &after); + mcpc_errcode_t depth_status + = mcpc_tool_get_tpropval_i32(tool, (const char8_t *)"relation_depth", &relation_depth); + const tiny_store_entry_t *target = NULL; + const size_t *entries = NULL; + const tiny_relation_reference_t *incoming = NULL; + size_t session_entry_count = 0, position, target_position = SIZE_MAX, emitted, message_count = 1; + size_t incoming_count = 0; + size_t target_index, queue[50], queue_depth[50], queue_head = 0, queue_count = 0; + context_edge_t edges[49]; + size_t edge_count = 0; + bool *visited = NULL, truncated = false; + uint32_t type_mask; + tiny_json_buffer_t json; + if (!message_id || before < 0 || before > 10 || after < 0 || after > 10 + || relation_depth < 0 || relation_depth > 2 + || (before_status != MCPC_EC_0 && before_status != MCPC_EC_ANYDATA_NOTFOUND) + || (after_status != MCPC_EC_0 && after_status != MCPC_EC_ANYDATA_NOTFOUND) + || (depth_status != MCPC_EC_0 && depth_status != MCPC_EC_ANYDATA_NOTFOUND) + || !relation_type_mask(types_json, &type_mask) || !context + || !tiny_mcp_tools_ensure_memory_index(context) + || !tiny_memory_index_find(context->memory_index, message_id, &target_index)) + { + tiny_mcp_tool_add_error(result, "VALIDATION_ERROR", "invalid context view arguments"); + free(message_id); + free(types_json); + return; + } + target = tiny_store_entry(context->store, target_index); + if (!target || target->deleted || !tiny_mcp_tools_ensure_history_index(context) + || !tiny_history_index_session( + context->history_index, target->message.session_id, &entries, &session_entry_count)) + { + tiny_mcp_tool_add_error(result, "NOT_FOUND", "message context was not found"); + free(message_id); + free(types_json); + return; + } + for (position = 0; position < session_entry_count; ++position) + { + if (strcmp(tiny_store_entry(context->store, entries[position])->message.message_id, + message_id) + == 0) + { + target_position = position; + break; + } + } + if (target_position == SIZE_MAX + || !(visited = calloc(tiny_store_entry_count(context->store), sizeof(bool)))) + { + tiny_mcp_tool_add_error(result, "INTERNAL_ERROR", "context index allocation failed"); + free(message_id); + free(types_json); + return; + } + visited[target_index] = true; + queue[queue_count] = target_index; + queue_depth[queue_count++] = 0; + while (queue_head < queue_count && edge_count < 49) + { + size_t current_index = queue[queue_head]; + size_t depth = queue_depth[queue_head++]; + const tiny_store_entry_t *current = tiny_store_entry(context->store, current_index); + size_t relation; + if (depth >= (size_t)relation_depth) + { + continue; + } + for (relation = 0; relation < current->message.metadata.relation_count; ++relation) + { + const tiny_relation_t *value = ¤t->message.metadata.relations[relation]; + size_t related_index; + const tiny_store_entry_t *related; + if (!(type_mask & (UINT32_C(1) << (unsigned)value->type)) + || !tiny_memory_index_find(context->memory_index, value->message_id, &related_index) + || visited[related_index]) + { + continue; + } + related = tiny_store_entry(context->store, related_index); + visited[related_index] = true; + edges[edge_count].entry_index = related_index; + edges[edge_count].type = value->type; + edges[edge_count].depth = depth + 1; + edges[edge_count].incoming = false; + edges[edge_count].timestamp = related->message.timestamp_ms; + memcpy(edges[edge_count].message_id, + related->message.message_id, + sizeof(edges[edge_count].message_id)); + edge_count++; + if (!related->deleted && queue_count < 50) + { + queue[queue_count] = related_index; + queue_depth[queue_count++] = depth + 1; + } + if (edge_count == 49) + { + truncated = true; + break; + } + } + if (edge_count == 49) + { + break; + } + if (!tiny_memory_index_incoming( + context->memory_index, + current->message.message_id, + &incoming, + &incoming_count)) + { + continue; + } + for (relation = 0; relation < incoming_count && edge_count < 49; ++relation) + { + const tiny_store_entry_t *related + = tiny_store_entry(context->store, incoming[relation].source_index); + if (!(type_mask & (UINT32_C(1) << (unsigned)incoming[relation].type)) + || visited[incoming[relation].source_index]) + { + continue; + } + visited[incoming[relation].source_index] = true; + edges[edge_count].entry_index = incoming[relation].source_index; + edges[edge_count].type = incoming[relation].type; + edges[edge_count].depth = depth + 1; + edges[edge_count].incoming = true; + edges[edge_count].timestamp = related->message.timestamp_ms; + memcpy(edges[edge_count].message_id, + related->message.message_id, + sizeof(edges[edge_count].message_id)); + edge_count++; + if (!related->deleted && queue_count < 50) + { + queue[queue_count] = incoming[relation].source_index; + queue_depth[queue_count++] = depth + 1; + } + } + } + qsort(edges, edge_count, sizeof(*edges), compare_context_edges); + tiny_json_init(&json); + tiny_json_raw(&json, "{\"message\":"); + tiny_mcp_tool_append_message(&json, &target->message); + tiny_json_raw(&json, ",\"chronological_context\":{\"before\":["); + emitted = 0; + position = target_position > (size_t)before ? target_position - (size_t)before : 0; + for (; position < target_position; ++position) + { + const tiny_store_entry_t *entry = tiny_store_entry(context->store, entries[position]); + if (entry && !entry->deleted) + { + tiny_json_buffer_t item; + tiny_json_init(&item); + tiny_mcp_tool_append_message(&item, &entry->message); + if (message_count >= 50 || item.failed + || json.length + item.length + 256 > READ_RESPONSE_BUDGET) + { + truncated = true; + tiny_json_free(&item); + break; + } + if (emitted++) + { + tiny_json_raw(&json, ","); + } + tiny_json_raw_n(&json, item.data, item.length); + tiny_json_free(&item); + message_count++; + } + } + tiny_json_raw(&json, "],\"after\":["); + emitted = 0; + for (position = target_position + 1; + position < session_entry_count && emitted < (size_t)after; + ++position) + { + const tiny_store_entry_t *entry = tiny_store_entry(context->store, entries[position]); + if (entry && !entry->deleted) + { + tiny_json_buffer_t item; + tiny_json_init(&item); + tiny_mcp_tool_append_message(&item, &entry->message); + if (message_count >= 50 || item.failed + || json.length + item.length + 256 > READ_RESPONSE_BUDGET) + { + truncated = true; + tiny_json_free(&item); + break; + } + if (emitted++) + { + tiny_json_raw(&json, ","); + } + tiny_json_raw_n(&json, item.data, item.length); + tiny_json_free(&item); + message_count++; + } + } + tiny_json_raw(&json, "]},\"relations\":["); + emitted = 0; + for (position = 0; position < edge_count; ++position) + { + const context_edge_t *edge = &edges[position]; + const tiny_store_entry_t *related = tiny_store_entry(context->store, edge->entry_index); + tiny_json_buffer_t item; + tiny_json_init(&item); + tiny_json_raw(&item, "{\"type\":"); + tiny_json_string(&item, tiny_relation_type_name(edge->type)); + tiny_json_raw(&item, edge->incoming ? ",\"direction\":\"incoming\"" : ",\"direction\":\"outgoing\""); + tiny_json_raw(&item, ",\"depth\":"); + tiny_json_u64(&item, edge->depth); + if (related && !related->deleted) + { + tiny_json_raw(&item, ",\"message\":"); + tiny_mcp_tool_append_message(&item, &related->message); + } + else + { + tiny_json_raw(&item, ",\"message_id\":"); + tiny_json_string(&item, edge->message_id); + tiny_json_raw(&item, ",\"status\":\"deleted\""); + } + tiny_json_raw(&item, "}"); + if (message_count >= 50 || item.failed + || json.length + item.length + 256 > READ_RESPONSE_BUDGET) + { + truncated = true; + tiny_json_free(&item); + break; + } + if (emitted++) + { + tiny_json_raw(&json, ","); + } + tiny_json_raw_n(&json, item.data, item.length); + tiny_json_free(&item); + if (related && !related->deleted) + { + message_count++; + } + } + tiny_json_raw(&json, "],\"session\":{\"session_id\":"); + tiny_json_string(&json, target->message.session_id); + tiny_json_raw(&json, ",\"message_count\":"); + tiny_json_u64(&json, session_entry_count); + tiny_json_raw(&json, "},\"truncated\":"); + tiny_json_raw(&json, truncated ? "true" : "false"); + tiny_json_raw(&json, "}"); + tiny_mcp_tool_add_json(result, &json); + tiny_json_free(&json); + free(visited); + free(message_id); + free(types_json); +} + +static size_t +simple_cursor(const char *cursor, bool *valid) +{ + char *end = NULL; + unsigned long long value; + if (!cursor) + { + *valid = true; + return 0; + } + value = strtoull(cursor, &end, 10); + *valid = cursor[0] && end && !*end && value <= SIZE_MAX; + return *valid ? (size_t)value : 0; +} + +static void +call_sessions_view(tiny_mcp_tools_context_t *context, const mcpc_tool_t *tool, mcpc_ucbr_t *result) +{ + char *cursor = tiny_mcp_tool_get_string(tool, "cursor", CURSOR_TEXT_MAX); + int32_t limit = READ_DEFAULT_LIMIT; + bool valid; + size_t start = simple_cursor(cursor, &valid), index, unique = 0, emitted = 0; + tiny_json_buffer_t json; + mcpc_errcode_t limit_status = mcpc_tool_get_tpropval_i32(tool, (const char8_t *)"limit", &limit); + if (!context || !valid || limit < 1 || limit > READ_MAX_LIMIT + || (limit_status != MCPC_EC_0 && limit_status != MCPC_EC_ANYDATA_NOTFOUND)) + { + tiny_mcp_tool_add_error(result, "VALIDATION_ERROR", "invalid sessions view arguments"); + free(cursor); + return; + } + tiny_json_init(&json); + tiny_json_raw(&json, "{\"sessions\":["); + for (index = 0; index < tiny_store_entry_count(context->store) && emitted < (size_t)limit; + ++index) + { + const tiny_store_entry_t *entry = tiny_store_entry(context->store, index); + size_t prior, scan, count = 0; + int64_t started = INT64_MAX, ended = 0; + if (!entry || entry->deleted) + { + continue; + } + for (prior = 0; prior < index; ++prior) + { + const tiny_store_entry_t *candidate = tiny_store_entry(context->store, prior); + if (candidate && !candidate->deleted + && strcmp(candidate->message.session_id, entry->message.session_id) == 0) + { + break; + } + } + if (prior != index || unique++ < start) + { + continue; + } + for (scan = index; scan < tiny_store_entry_count(context->store); ++scan) + { + const tiny_store_entry_t *candidate = tiny_store_entry(context->store, scan); + if (candidate && !candidate->deleted + && strcmp(candidate->message.session_id, entry->message.session_id) == 0) + { + count++; + started = candidate->message.timestamp_ms < started ? candidate->message.timestamp_ms + : started; + ended = candidate->message.timestamp_ms > ended ? candidate->message.timestamp_ms + : ended; + } + } + if (emitted++) + { + tiny_json_raw(&json, ","); + } + tiny_json_raw(&json, "{\"session_id\":"); + tiny_json_string(&json, entry->message.session_id); + tiny_json_raw(&json, ",\"message_count\":"); + tiny_json_u64(&json, count); + tiny_json_raw(&json, ",\"started_at\":"); + tiny_json_i64(&json, started); + tiny_json_raw(&json, ",\"last_message_at\":"); + tiny_json_i64(&json, ended); + tiny_json_raw(&json, "}"); + } + tiny_json_raw(&json, "]"); + if (emitted == (size_t)limit) + { + char next[32]; + snprintf(next, sizeof(next), "%zu", start + emitted); + tiny_json_raw(&json, ",\"nextCursor\":"); + tiny_json_string(&json, next); + } + tiny_json_raw(&json, "}"); + tiny_mcp_tool_add_json(result, &json); + tiny_json_free(&json); + free(cursor); +} + +static void +call_export_view(tiny_mcp_tools_context_t *context, const mcpc_tool_t *tool, mcpc_ucbr_t *result) +{ + char *session = tiny_mcp_tool_get_string(tool, "session_id", TINY_SESSION_ID_MAX); + char *cursor = tiny_mcp_tool_get_string(tool, "cursor", CURSOR_TEXT_MAX); + int32_t limit = 500; + bool valid; + size_t index = simple_cursor(cursor, &valid), emitted = 0; + tiny_json_buffer_t json, lines; + mcpc_errcode_t limit_status = mcpc_tool_get_tpropval_i32(tool, (const char8_t *)"limit", &limit); + if (!context || !valid || limit < 1 || limit > 1000 + || (limit_status != MCPC_EC_0 && limit_status != MCPC_EC_ANYDATA_NOTFOUND)) + { + tiny_mcp_tool_add_error(result, "VALIDATION_ERROR", "invalid export view arguments"); + free(session); + free(cursor); + return; + } + tiny_json_init(&lines); + if (index == 0) + { + tiny_json_raw(&lines, "{\"type\":\"tiny-convmem-manifest\",\"format_version\":2}\n"); + } + for (; index < tiny_store_entry_count(context->store) && emitted < (size_t)limit; ++index) + { + const tiny_store_entry_t *entry = tiny_store_entry(context->store, index); + if (!entry || entry->deleted + || (session && session[0] && strcmp(session, entry->message.session_id) != 0)) + { + continue; + } + tiny_mcp_tool_append_message(&lines, &entry->message); + tiny_json_raw(&lines, "\n"); + emitted++; + } + tiny_json_init(&json); + tiny_json_raw(&json, "{\"jsonl\":"); + tiny_json_string(&json, lines.data ? lines.data : ""); + if (index < tiny_store_entry_count(context->store)) + { + char next[32]; + snprintf(next, sizeof(next), "%zu", index); + tiny_json_raw(&json, ",\"nextCursor\":"); + tiny_json_string(&json, next); + } + tiny_json_raw(&json, "}"); + tiny_mcp_tool_add_json(result, &json); + tiny_json_free(&json); + tiny_json_free(&lines); + free(session); + free(cursor); +} + static void call_read_messages(const mcpc_tool_t *tool, mcpc_ucbr_t *result) { tiny_mcp_tools_context_t *context = tiny_mcp_tools_context_current(); + char *view = tiny_mcp_tool_get_string(tool, "view", 16); char *session = tiny_mcp_tool_get_string(tool, "session_id", TINY_SESSION_ID_MAX); char *cursor_text = tiny_mcp_tool_get_string(tool, "cursor", CURSOR_TEXT_MAX); int32_t requested_limit = READ_DEFAULT_LIMIT; @@ -201,6 +686,40 @@ call_read_messages(const mcpc_tool_t *tool, mcpc_ucbr_t *result) const tiny_message_t *last = NULL; tiny_json_buffer_t json; + if (view && strcmp(view, "context") == 0) + { + call_context_view(context, tool, result); + free(view); + free(session); + free(cursor_text); + return; + } + if (view && strcmp(view, "sessions") == 0) + { + call_sessions_view(context, tool, result); + free(view); + free(session); + free(cursor_text); + return; + } + if (view && strcmp(view, "export") == 0) + { + call_export_view(context, tool, result); + free(view); + free(session); + free(cursor_text); + return; + } + if (view && strcmp(view, "history") != 0) + { + tiny_mcp_tool_add_error(result, "VALIDATION_ERROR", "unsupported read_messages view"); + free(view); + free(session); + free(cursor_text); + return; + } + free(view); + if (!context || !session || !session[0] || (limit_status != MCPC_EC_0 && limit_status != MCPC_EC_ANYDATA_NOTFOUND) || requested_limit < 1 || requested_limit > READ_MAX_LIMIT @@ -324,11 +843,33 @@ tiny_mcp_register_read_messages(mcpc_server_t *server) "Returns a paginated history for reconstructing that conversation.", call_read_messages); return tool + && tiny_mcp_tool_add_property( + tool, + "view", + "Read view: history (default), context, sessions, or export.", + MCPC_U8STR, + false) && tiny_mcp_tool_add_property(tool, "session_id", "Conversation session whose chronological history to read.", MCPC_U8STR, - true) + false) + && tiny_mcp_tool_add_property( + tool, "message_id", "Message ULID required by the context view.", MCPC_U8STR, false) + && tiny_mcp_tool_add_property( + tool, "before", "Context messages before the target, from 0 to 10.", MCPC_I32, false) + && tiny_mcp_tool_add_property( + tool, "after", "Context messages after the target, from 0 to 10.", MCPC_I32, false) + && tiny_mcp_tool_add_property(tool, + "relation_depth", + "Relation traversal depth from 0 to 2; defaults to 1.", + MCPC_I32, + false) + && tiny_mcp_tool_add_property(tool, + "relation_types_json", + "Optional JSON array string of relation types to follow.", + MCPC_U8STR, + false) && tiny_mcp_tool_add_property( tool, "limit", diff --git a/src/mcp/tools/rebuild_index.c b/src/mcp/tools/rebuild_index.c index e6dbf69..d9a5e87 100644 --- a/src/mcp/tools/rebuild_index.c +++ b/src/mcp/tools/rebuild_index.c @@ -10,8 +10,6 @@ call_rebuild_index(const mcpc_tool_t *tool, mcpc_ucbr_t *result) { tiny_mcp_tools_context_t *context = tiny_mcp_tools_context_current(); tiny_rebuild_report_t report; - tiny_search_index_t *rebuilt; - tiny_history_index_t *history; tiny_store_status_t status; tiny_json_buffer_t json; (void)tool; @@ -26,22 +24,7 @@ call_rebuild_index(const mcpc_tool_t *tool, mcpc_ucbr_t *result) tiny_mcp_tool_add_error(result, tiny_store_status_name(status), "index rebuild failed"); return; } - tiny_history_index_free(context->history_index); - context->history_index = NULL; - rebuilt = tiny_mcp_tools_build_search_index(context->store); - history = tiny_history_index_build(context->store); - if (!rebuilt || !history) - { - tiny_search_index_free(rebuilt); - tiny_history_index_free(history); - tiny_search_index_free(context->search_index); - context->search_index = NULL; - tiny_mcp_tool_add_error(result, "INTERNAL_ERROR", "search index allocation failed"); - return; - } - tiny_search_index_free(context->search_index); - context->search_index = rebuilt; - context->history_index = history; + tiny_mcp_tools_invalidate_indexes(context); tiny_json_init(&json); tiny_json_raw(&json, "{\"status\":\"OK\",\"messages_indexed\":"); tiny_json_u64(&json, report.messages_indexed); diff --git a/src/mcp/tools/search_messages.c b/src/mcp/tools/search_messages.c index 44db1ef..4b7f870 100644 --- a/src/mcp/tools/search_messages.c +++ b/src/mcp/tools/search_messages.c @@ -13,8 +13,31 @@ typedef struct result_item { const tiny_store_entry_t *entry; double score; + double content_score; + double summary_score; + double tag_score; } result_item_t; +static double +score_component(const char *query, const tiny_message_t *message, int component) +{ + tiny_message_t candidate = *message; + candidate.metadata.tags = NULL; + candidate.metadata.tag_count = 0; + candidate.metadata.summary = NULL; + if (component == 1) + { + candidate.content = message->metadata.summary ? message->metadata.summary : ""; + } + else if (component == 2) + { + candidate.content = ""; + candidate.metadata.tags = message->metadata.tags; + candidate.metadata.tag_count = message->metadata.tag_count; + } + return tiny_search_score(query, &candidate); +} + static int compare_results(const void *left, const void *right) { @@ -28,6 +51,22 @@ compare_results(const void *left, const void *right) { return 1; } + if (a->entry->message.metadata.importance > b->entry->message.metadata.importance) + { + return -1; + } + if (a->entry->message.metadata.importance < b->entry->message.metadata.importance) + { + return 1; + } + if (a->entry->message.updated_at_ms > b->entry->message.updated_at_ms) + { + return -1; + } + if (a->entry->message.updated_at_ms < b->entry->message.updated_at_ms) + { + return 1; + } if (a->entry->message.timestamp_ms > b->entry->message.timestamp_ms) { return -1; @@ -45,15 +84,27 @@ call_search_messages(const mcpc_tool_t *tool, mcpc_ucbr_t *result) tiny_mcp_tools_context_t *context = tiny_mcp_tools_context_current(); char *query = tiny_mcp_tool_get_string(tool, "query", TINY_QUERY_MAX); char *session = tiny_mcp_tool_get_string(tool, "session_id", TINY_SESSION_ID_MAX); + char *kind_text = tiny_mcp_tool_get_string(tool, "memory_kind", 24); + char *tier_text = tiny_mcp_tool_get_string(tool, "memory_tier", 16); + char *tag = tiny_mcp_tool_get_string(tool, "tag", TINY_TAG_MAX); int32_t requested_limit = 20; + int32_t minimum_importance = 0; mcpc_errcode_t limit_status = mcpc_tool_get_tpropval_i32(tool, (const char8_t *)"limit", &requested_limit); + mcpc_errcode_t importance_status + = mcpc_tool_get_tpropval_i32(tool, (const char8_t *)"min_importance", &minimum_importance); + tiny_memory_kind_t kind = TINY_KIND_CONVERSATION; + tiny_memory_tier_t tier = TINY_TIER_SHORT; size_t limit, index, count = 0, candidate_count = 0, *candidates = NULL; result_item_t *items = NULL; tiny_json_buffer_t json; bool valid = context && query && query[0] && (limit_status == MCPC_EC_0 || limit_status == MCPC_EC_ANYDATA_NOTFOUND) && requested_limit >= 1 && requested_limit <= 100; + valid = valid && minimum_importance >= 0 && minimum_importance <= 100 + && (importance_status == MCPC_EC_0 || importance_status == MCPC_EC_ANYDATA_NOTFOUND) + && (!kind_text || tiny_kind_parse(kind_text, &kind)) + && (!tier_text || tiny_tier_parse(tier_text, &tier)); if (!valid) { tiny_mcp_tool_add_error(result, "VALIDATION_ERROR", "query or limit is invalid"); @@ -90,11 +141,37 @@ call_search_messages(const mcpc_tool_t *tool, mcpc_ucbr_t *result) { continue; } + if ((kind_text && entry->message.metadata.memory_kind != kind) + || (tier_text && entry->message.metadata.memory_tier != tier) + || entry->message.metadata.importance < minimum_importance) + { + continue; + } + if (tag) + { + size_t tag_index; + for (tag_index = 0; tag_index < entry->message.metadata.tag_count; + ++tag_index) + { + if (strcmp(tag, entry->message.metadata.tags[tag_index]) == 0) + { + break; + } + } + if (tag_index == entry->message.metadata.tag_count) + { + continue; + } + } score = tiny_search_score(query, &entry->message); if (score > 0) { items[count].entry = entry; - items[count++].score = score; + items[count].score = score; + items[count].content_score = score_component(query, &entry->message, 0); + items[count].summary_score = score_component(query, &entry->message, 1); + items[count].tag_score = score_component(query, &entry->message, 2); + count++; } } qsort(items, count, sizeof(*items), compare_results); @@ -110,7 +187,47 @@ call_search_messages(const mcpc_tool_t *tool, mcpc_ucbr_t *result) { tiny_json_raw(&json, ","); } - tiny_mcp_tool_append_message(&json, &items[index].entry->message); + { + tiny_json_buffer_t item; + tiny_json_init(&item); + tiny_mcp_tool_append_message(&item, &items[index].entry->message); + if (item.length > 0) + { + item.length--; + item.data[item.length] = '\0'; + } + tiny_json_raw(&item, ",\"score\":"); + tiny_json_number(&item, items[index].score); + tiny_json_raw(&item, ",\"score_breakdown\":{\"content\":"); + tiny_json_number(&item, items[index].content_score); + tiny_json_raw(&item, ",\"summary\":"); + tiny_json_number(&item, items[index].summary_score); + tiny_json_raw(&item, ",\"tags\":"); + tiny_json_number(&item, items[index].tag_score); + tiny_json_raw(&item, ",\"importance\":0},\"matched_fields\":["); + { + bool matched = false; + if (items[index].content_score > 0) + { + tiny_json_raw(&item, "\"content\""); + matched = true; + } + if (items[index].summary_score > 0) + { + tiny_json_raw(&item, matched ? ",\"summary\"" : "\"summary\""); + matched = true; + } + if (items[index].tag_score > 0) + { + tiny_json_raw(&item, matched ? ",\"tags\"" : "\"tags\""); + } + } + tiny_json_raw(&item, "],\"relation_count\":"); + tiny_json_u64(&item, items[index].entry->message.metadata.relation_count); + tiny_json_raw(&item, "}"); + tiny_json_raw_n(&json, item.data, item.length); + tiny_json_free(&item); + } } tiny_json_raw(&json, "]}"); tiny_mcp_tool_add_json(result, &json); @@ -122,6 +239,9 @@ call_search_messages(const mcpc_tool_t *tool, mcpc_ucbr_t *result) free(candidates); free(query); free(session); + free(kind_text); + free(tier_text); + free(tag); } bool @@ -145,6 +265,16 @@ tiny_mcp_register_search_messages(mcpc_server_t *server) "Conversation session to search; omit to search across all sessions.", MCPC_U8STR, false) + && tiny_mcp_tool_add_property( + tool, "memory_kind", "Optional exact memory kind filter.", MCPC_U8STR, false) + && tiny_mcp_tool_add_property( + tool, "memory_tier", "Optional exact memory tier filter.", MCPC_U8STR, false) + && tiny_mcp_tool_add_property(tool, "tag", "Optional exact tag filter.", MCPC_U8STR, false) + && tiny_mcp_tool_add_property(tool, + "min_importance", + "Minimum importance from 0 to 100; defaults to 0.", + MCPC_I32, + false) && tiny_mcp_tool_add_property( tool, "limit", diff --git a/src/mcp/tools/update_message.c b/src/mcp/tools/update_message.c new file mode 100644 index 0000000..aca1bed --- /dev/null +++ b/src/mcp/tools/update_message.c @@ -0,0 +1,97 @@ +#include "mcp/tools/common/registry.h" + +#include "mcp/tools/common/common.h" +#include "mcp/tools/common/context.h" + +#include +#include + +static void +call_update_message(const mcpc_tool_t *tool, mcpc_ucbr_t *result) +{ + tiny_mcp_tools_context_t *context = tiny_mcp_tools_context_current(); + char *message_id = tiny_mcp_tool_get_string(tool, "message_id", TINY_MESSAGE_ID_LENGTH); + char *content = tiny_mcp_tool_get_string(tool, "content", TINY_CONTENT_MAX); + char *metadata_json = tiny_mcp_tool_get_string(tool, "metadata_json", TINY_CONTENT_MAX); + int32_t expected = 0; + mcpc_errcode_t expected_status + = mcpc_tool_get_tpropval_i32(tool, (const char8_t *)"expected_revision", &expected); + tiny_message_t parsed, updated; + tiny_message_patch_t patch; + tiny_store_status_t status; + tiny_json_buffer_t json; + bool changed = false; + tiny_metadata_mask_t metadata_mask = 0; + tiny_message_init(&parsed); + tiny_message_init(&updated); + patch.content = content; + patch.metadata = metadata_json ? &parsed.metadata : NULL; + patch.metadata_mask = 0; + patch.expected_revision = (uint64_t)expected; + patch.has_expected_revision = expected_status == MCPC_EC_0; + if (!context || !message_id || (!content && !metadata_json) + || (metadata_json && !tiny_mcp_parse_metadata(metadata_json, &parsed, &metadata_mask)) + || (expected_status != MCPC_EC_0 && expected_status != MCPC_EC_ANYDATA_NOTFOUND) + || (patch.has_expected_revision && expected < 1)) + { + tiny_mcp_tool_add_error(result, "VALIDATION_ERROR", "invalid update_message arguments"); + } + else + { + patch.metadata_mask = metadata_mask; + status = tiny_store_update(context->store, message_id, &patch, &updated, &changed); + if (status != TINY_STORE_OK) + { + tiny_mcp_tool_add_error( + result, tiny_store_status_name(status), "message was not updated"); + } + else + { + if (changed) + { + tiny_mcp_tools_invalidate_indexes(context); + } + tiny_json_init(&json); + tiny_json_raw(&json, "{\"status\":\"OK\",\"changed\":"); + tiny_json_raw(&json, changed ? "true" : "false"); + tiny_json_raw(&json, ",\"message\":"); + tiny_mcp_tool_append_message(&json, &updated); + tiny_json_raw(&json, "}"); + tiny_mcp_tool_add_json(result, &json); + tiny_json_free(&json); + } + } + free(message_id); + free(content); + free(metadata_json); + tiny_message_free(&parsed); + tiny_message_free(&updated); +} + +bool +tiny_mcp_register_update_message(mcpc_server_t *server) +{ + mcpc_tool_t *tool = tiny_mcp_tool_new("update_message", + "Update message content or metadata while preserving " + "identity, session, role, and creation time.", + call_update_message); + return tool + && tiny_mcp_tool_add_property( + tool, "message_id", "ULID of the live message to update.", MCPC_U8STR, true) + && tiny_mcp_tool_add_property(tool, + "content", + "Replacement UTF-8 content; omit to retain content.", + MCPC_U8STR, + false) + && tiny_mcp_tool_add_property(tool, + "metadata_json", + "Replacement metadata JSON; omit to retain metadata.", + MCPC_U8STR, + false) + && tiny_mcp_tool_add_property(tool, + "expected_revision", + "Optional positive revision for optimistic concurrency.", + MCPC_I32, + false) + && mcpc_server_add_tool(server, tool) == MCPC_EC_0; +} diff --git a/src/mcp/tools/write_message.c b/src/mcp/tools/write_message.c index 7df0058..3d7cb29 100644 --- a/src/mcp/tools/write_message.c +++ b/src/mcp/tools/write_message.c @@ -34,7 +34,9 @@ metadata_has_only_known_fields(const char *json, int length) } key[decoded] = '\0'; if (strcmp(key, "tool_call_id") != 0 && strcmp(key, "tags") != 0 - && strcmp(key, "memory_tier") != 0) + && strcmp(key, "memory_tier") != 0 && strcmp(key, "memory_kind") != 0 + && strcmp(key, "importance") != 0 && strcmp(key, "summary") != 0 + && strcmp(key, "relations") != 0 && strcmp(key, "source_message_ids") != 0) { return false; } @@ -42,14 +44,20 @@ metadata_has_only_known_fields(const char *json, int length) return true; } -static bool -parse_metadata(const char *json, tiny_message_t *message) +bool +tiny_mcp_parse_metadata(const char *json, tiny_message_t *message, tiny_metadata_mask_t *present) { - const char *tool_call = NULL, *tier = NULL, *tags = NULL; - int tool_call_len = 0, tier_len = 0, tags_len = 0; + const char *tool_call = NULL, *tier = NULL, *tags = NULL, *kind = NULL, *summary = NULL; + const char *relations = NULL, *sources = NULL; + int tool_call_len = 0, tier_len = 0, tags_len = 0, kind_len = 0, summary_len = 0; + int relations_len = 0, sources_len = 0; int offset = 0, key_offset, key_length, value_offset, value_length, value_type; int length = (int)strlen(json); int tool_call_type, tier_type, tags_type; + int kind_type, summary_type, importance_type; + int relations_type, sources_type; + double importance = 50; + tiny_metadata_mask_t fields = 0; if (strcmp(json, "{}") == 0) { return true; @@ -62,12 +70,176 @@ parse_metadata(const char *json, tiny_message_t *message) 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); tags_type = mjson_find(json, length, "$.tags", &tags, &tags_len); + kind_type = mjson_find(json, length, "$.memory_kind", &kind, &kind_len); + summary_type = mjson_find(json, length, "$.summary", &summary, &summary_len); + 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)) + || (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; } + 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; + fields |= kind_type != MJSON_TOK_INVALID ? TINY_METADATA_KIND : 0; + fields |= importance_type != MJSON_TOK_INVALID ? TINY_METADATA_IMPORTANCE : 0; + fields |= summary_type != MJSON_TOK_INVALID ? TINY_METADATA_SUMMARY : 0; + fields |= relations_type != MJSON_TOK_INVALID ? TINY_METADATA_RELATIONS : 0; + fields |= sources_type != MJSON_TOK_INVALID ? TINY_METADATA_SOURCES : 0; + if (relations_type == MJSON_TOK_ARRAY) + { + offset = 0; + while ((offset = mjson_next(relations, + relations_len, + offset, + &key_offset, + &key_length, + &value_offset, + &value_length, + &value_type)) + != 0) + { + tiny_relation_t relation; + char type[24]; + int decoded_type, decoded_id; + tiny_relation_t *resized; + if (value_type != MJSON_TOK_OBJECT + || message->metadata.relation_count >= TINY_RELATION_COUNT_MAX) + { + return false; + } + decoded_type = mjson_get_string( + relations + value_offset, value_length, "$.type", type, sizeof(type)); + decoded_id = mjson_get_string(relations + value_offset, + value_length, + "$.message_id", + relation.message_id, + sizeof(relation.message_id)); + if (decoded_type <= 0 || decoded_type >= (int)sizeof(type) + || decoded_id != TINY_MESSAGE_ID_LENGTH) + { + return false; + } + 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; + } + resized = realloc(message->metadata.relations, + (message->metadata.relation_count + 1) * sizeof(*resized)); + if (!resized) + { + return false; + } + message->metadata.relations = resized; + message->metadata.relations[message->metadata.relation_count++] = relation; + } + } + if (sources_type == MJSON_TOK_ARRAY) + { + offset = 0; + while ((offset = mjson_next(sources, + sources_len, + offset, + &key_offset, + &key_length, + &value_offset, + &value_length, + &value_type)) + != 0) + { + char *id; + char **resized; + int decoded; + if (value_type != MJSON_TOK_STRING + || message->metadata.source_count >= TINY_SOURCE_COUNT_MAX) + { + return false; + } + id = malloc(TINY_MESSAGE_ID_LENGTH + 1); + if (!id) + { + return false; + } + 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; + } + id[decoded] = '\0'; + if (!tiny_message_id_valid(id)) + { + free(id); + return false; + } + resized = realloc(message->metadata.source_message_ids, + (message->metadata.source_count + 1) * sizeof(*resized)); + if (!resized) + { + free(id); + return false; + } + 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 < 0 || importance > 100 || importance != (double)(uint8_t)importance) + { + return false; + } + message->metadata.importance = (uint8_t)importance; + } + if (kind_type == MJSON_TOK_STRING) + { + char value[24]; + int decoded = mjson_get_string(json, length, "$.memory_kind", value, sizeof(value)); + if (decoded <= 0 || decoded >= (int)sizeof(value)) + { + return false; + } + value[decoded] = '\0'; + if (!tiny_kind_parse(value, &message->metadata.memory_kind)) + { + return false; + } + } + if (summary_type == MJSON_TOK_STRING) + { + int decoded; + message->metadata.summary = malloc(TINY_SUMMARY_MAX + 1); + if (!message->metadata.summary) + { + return false; + } + 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; + } + message->metadata.summary[decoded] = '\0'; + if (decoded == 0) + { + free(message->metadata.summary); + message->metadata.summary = NULL; + } + } if (tool_call_type == MJSON_TOK_STRING) { int decoded; @@ -144,7 +316,11 @@ parse_metadata(const char *json, tiny_message_t *message) message->metadata.tags[message->metadata.tag_count++] = tag; } } - return true; + if (present) + { + *present = fields; + } + return tiny_metadata_valid(&message->metadata); } static void @@ -163,7 +339,7 @@ call_write_message(const mcpc_tool_t *tool, mcpc_ucbr_t *result) 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) - && parse_metadata(metadata ? metadata : "{}", &input); + && tiny_mcp_parse_metadata(metadata ? metadata : "{}", &input, NULL); if (!valid) { tiny_mcp_tool_add_error(result, "VALIDATION_ERROR", "invalid write_message arguments"); @@ -178,21 +354,7 @@ call_write_message(const mcpc_tool_t *tool, mcpc_ucbr_t *result) } else { - if (context->search_index - && !tiny_search_index_add( - context->search_index, &written, tiny_store_entry_count(context->store) - 1)) - { - tiny_search_index_free(context->search_index); - context->search_index = NULL; - } - if (context->history_index - && !tiny_history_index_add(context->history_index, - context->store, - tiny_store_entry_count(context->store) - 1)) - { - tiny_history_index_free(context->history_index); - context->history_index = NULL; - } + tiny_mcp_tools_invalidate_indexes(context); tiny_json_init(&json); tiny_json_raw(&json, "{\"status\":\"OK\",\"message\":"); tiny_mcp_tool_append_message(&json, &written); @@ -225,7 +387,9 @@ tiny_mcp_register_write_message(mcpc_server_t *server) && tiny_mcp_tool_add_property(tool, "metadata_json", "Optional JSON object string containing tool_call_id, tags, " - "and/or memory_tier; defaults to {}.", + "memory_tier, memory_kind, importance, summary, relations, " + "and/or source_message_ids; " + "defaults to {}.", MCPC_U8STR, false) && mcpc_server_add_tool(server, tool) == MCPC_EC_0; -- cgit v1.2.3