summaryrefslogtreecommitdiff
path: root/src/mcp/tools
diff options
context:
space:
mode:
authortanaka takashi <isazaka@gmail.com>2026-07-04 14:54:45 +0900
committertanaka takashi <isazaka@gmail.com>2026-07-04 14:54:45 +0900
commit1efecaef1f867cb218366e2b4ab4b901fdd2835f (patch)
tree3080a3d80a1a0671e8e82819d3f785465aace162 /src/mcp/tools
parent34e00404e9561dcaf8d2d99df7470757a4fee915 (diff)
途中経過コミット
Diffstat (limited to 'src/mcp/tools')
-rw-r--r--src/mcp/tools/common/common.c36
-rw-r--r--src/mcp/tools/common/common.h3
-rw-r--r--src/mcp/tools/common/context.c34
-rw-r--r--src/mcp/tools/common/context.h4
-rw-r--r--src/mcp/tools/common/history.c23
-rw-r--r--src/mcp/tools/common/history.h6
-rw-r--r--src/mcp/tools/common/memory_index.c219
-rw-r--r--src/mcp/tools/common/memory_index.h24
-rw-r--r--src/mcp/tools/common/registry.c5
-rw-r--r--src/mcp/tools/common/registry.h1
-rw-r--r--src/mcp/tools/delete_message.c4
-rw-r--r--src/mcp/tools/get_system_health.c19
-rw-r--r--src/mcp/tools/read_messages.c543
-rw-r--r--src/mcp/tools/rebuild_index.c19
-rw-r--r--src/mcp/tools/search_messages.c134
-rw-r--r--src/mcp/tools/update_message.c97
-rw-r--r--src/mcp/tools/write_message.c212
17 files changed, 1334 insertions, 49 deletions
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 <mcpc/mcpc.h>
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,10 +64,40 @@ 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)
{
if (!context || active_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 <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+
+#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 <mcpc/mcpc.h>
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 <stdint.h>
+#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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 = &current->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
@@ -146,6 +266,16 @@ tiny_mcp_register_search_messages(mcpc_server_t *server)
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",
"Maximum ranked matches to return, from 1 to 100; defaults to 20.",
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 <stdint.h>
+#include <stdlib.h>
+
+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;