summaryrefslogtreecommitdiff
path: root/src/mcp/tools/read_messages.c
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/read_messages.c
parent34e00404e9561dcaf8d2d99df7470757a4fee915 (diff)
途中経過コミット
Diffstat (limited to 'src/mcp/tools/read_messages.c')
-rw-r--r--src/mcp/tools/read_messages.c543
1 files changed, 542 insertions, 1 deletions
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",