#include "mcp/tools/common/registry.h" #include "mcp/tools/common/common.h" #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 #define READ_RESPONSE_BUDGET (8U * 1024U * 1024U - 64U * 1024U) #define HISTORY_CURSOR_BYTES 47U #define HISTORY_CURSOR_TEXT_MAX 64U #define SESSION_CURSOR_BYTES_MAX (1U + 8U + 4U + TINY_SESSION_ID_MAX + 4U) #define SESSION_CURSOR_TEXT_MAX (((SESSION_CURSOR_BYTES_MAX * 4U + 2U) / 3U) + 1U) typedef struct history_cursor { int64_t timestamp; char message_id[27]; } history_cursor_t; static uint64_t session_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 base64url_encode(const unsigned char *input, size_t length, char *output, size_t capacity) { static const char alphabet[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; size_t source = 0, target = 0; while (source < length) { uint32_t value = (uint32_t)input[source++] << 16; unsigned available = 1; if (source < length) { value |= (uint32_t)input[source++] << 8; available++; } if (source < length) { value |= input[source++]; available++; } if (target + available + 1 >= capacity) { return 0; } output[target++] = alphabet[(value >> 18) & 63]; output[target++] = alphabet[(value >> 12) & 63]; if (available > 1) { output[target++] = alphabet[(value >> 6) & 63]; } if (available > 2) { output[target++] = alphabet[value & 63]; } } output[target] = '\0'; return target; } static int base64_value(unsigned char value) { if (value >= 'A' && value <= 'Z') { return value - 'A'; } if (value >= 'a' && value <= 'z') { return value - 'a' + 26; } if (value >= '0' && value <= '9') { return value - '0' + 52; } if (value == '-') { return 62; } if (value == '_') { return 63; } return -1; } static bool base64url_decode_n(const char *input, unsigned char *output, size_t capacity, size_t *length) { size_t input_length = strlen(input), source = 0, target = 0; uint32_t accumulator = 0; unsigned bits = 0; if (!output || !length || input_length == 0 || input_length % 4 == 1) { return false; } while (source < input_length) { int value = base64_value((unsigned char)input[source++]); if (value < 0) { return false; } accumulator = (accumulator << 6) | (uint32_t)value; bits += 6; if (bits >= 8) { bits -= 8; if (target >= capacity) { return false; } output[target++] = (unsigned char)(accumulator >> bits); accumulator &= bits ? (UINT32_C(1) << bits) - 1 : 0; } } *length = target; return accumulator == 0; } static bool cursor_encode(const char *session, const tiny_message_t *message, char output[HISTORY_CURSOR_TEXT_MAX]) { unsigned char data[HISTORY_CURSOR_BYTES] = { 0 }; data[0] = 1; tiny_codec_put_u64(data + 1, session_hash(session)); tiny_codec_put_u64(data + 9, (uint64_t)message->timestamp_ms); memcpy(data + 17, message->message_id, 26); tiny_codec_put_u32(data + 43, tiny_codec_crc32(data, 43)); return base64url_encode(data, sizeof(data), output, HISTORY_CURSOR_TEXT_MAX) == 63; } static bool cursor_decode(const char *session, const char *value, history_cursor_t *cursor) { unsigned char data[HISTORY_CURSOR_BYTES]; size_t length = 0; if (!value || strlen(value) != 63 || !base64url_decode_n(value, data, sizeof(data), &length) || length != HISTORY_CURSOR_BYTES || data[0] != 1 || tiny_codec_get_u64(data + 1) != session_hash(session) || tiny_codec_get_u32(data + 43) != tiny_codec_crc32(data, 43)) { return false; } memcpy(cursor->message_id, data + 17, 26); cursor->message_id[26] = '\0'; cursor->timestamp = (int64_t)tiny_codec_get_u64(data + 9); return cursor->timestamp >= 0 && tiny_message_id_valid(cursor->message_id); } static int compare_cursor(const tiny_message_t *message, const history_cursor_t *cursor) { if (message->timestamp_ms < cursor->timestamp) { return -1; } if (message->timestamp_ms > cursor->timestamp) { return 1; } return strcmp(message->message_id, cursor->message_id); } static size_t escaped_size(const char *value, size_t length) { size_t position, total = 0; for (position = 0; position < length; ++position) { unsigned char byte = (unsigned char)value[position]; total += byte < 0x20 ? 6 : (byte == '"' || byte == '\\' ? 2 : 1); } 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 bool session_cursor_encode(int64_t last_message_at, const char *session_id, char output[SESSION_CURSOR_TEXT_MAX]) { unsigned char data[SESSION_CURSOR_BYTES_MAX] = { 0 }; size_t session_length = strlen(session_id); size_t data_length; if (session_length == 0 || session_length > TINY_SESSION_ID_MAX) { return false; } data_length = 1U + 8U + 4U + session_length + 4U; data[0] = 2; tiny_codec_put_u64(data + 1, (uint64_t)last_message_at); tiny_codec_put_u32(data + 9, (uint32_t)session_length); memcpy(data + 13, session_id, session_length); tiny_codec_put_u32(data + data_length - 4U, tiny_codec_crc32(data, data_length - 4U)); return base64url_encode(data, data_length, output, SESSION_CURSOR_TEXT_MAX) > 0; } static bool session_cursor_decode(const char *cursor, int64_t *last_message_at, char session_id[TINY_SESSION_ID_MAX + 1]) { unsigned char data[SESSION_CURSOR_BYTES_MAX]; size_t decoded_length = 0, session_length; if (!cursor || !cursor[0] || !base64url_decode_n(cursor, data, sizeof(data), &decoded_length) || decoded_length < 18U || data[0] != 2) { return false; } *last_message_at = (int64_t)tiny_codec_get_u64(data + 1); session_length = tiny_codec_get_u32(data + 9); if (*last_message_at < 0 || session_length == 0 || session_length > TINY_SESSION_ID_MAX || decoded_length != 1U + 8U + 4U + session_length + 4U || tiny_codec_crc32(data, decoded_length - 4U) != tiny_codec_get_u32(data + decoded_length - 4U)) { return false; } memcpy(session_id, data + 13, session_length); session_id[session_length] = '\0'; return tiny_utf8_valid(session_id, session_length); } static int compare_session_by_last_then_id(const void *left, const void *right) { const tiny_history_index_session_info_t *a = left; const tiny_history_index_session_info_t *b = right; if (a->last_message_at != b->last_message_at) { return a->last_message_at < b->last_message_at ? -1 : 1; } return strcmp(a->session_id, b->session_id); } 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", SESSION_CURSOR_TEXT_MAX - 1U); int32_t limit = READ_DEFAULT_LIMIT; tiny_json_buffer_t json; mcpc_errcode_t limit_status = mcpc_tool_get_tpropval_i32(tool, (const char8_t *)"limit", &limit); int64_t cursor_last = 0; char cursor_session[TINY_SESSION_ID_MAX + 1] = { 0 }; bool has_cursor = cursor && cursor[0]; size_t emitted = 0, index, position, last_emitted_position = SIZE_MAX, outer_size = 256; bool more = false; const tiny_history_index_t *history; tiny_history_index_session_info_t *sessions = NULL; size_t session_count = 0; bool cursor_ok = !has_cursor || session_cursor_decode(cursor, &cursor_last, cursor_session); if (!context || !cursor_ok || limit < 1 || limit > READ_MAX_LIMIT || (limit_status != MCPC_EC_0 && limit_status != MCPC_EC_ANYDATA_NOTFOUND) || !tiny_mcp_tools_ensure_history_index(context) || !(history = context->history_index)) { tiny_mcp_tool_add_error(result, "VALIDATION_ERROR", "invalid sessions view arguments"); free(cursor); return; } session_count = tiny_history_index_session_count(history); if (session_count > 0) { sessions = malloc(session_count * sizeof(*sessions)); if (!sessions) { tiny_mcp_tool_add_error(result, "INTERNAL_ERROR", "sessions allocation failed"); free(cursor); return; } for (index = 0; index < session_count; ++index) { const char *session_id; const size_t *entries; size_t count; if (!tiny_history_index_session_at(history, index, &session_id, &entries, &count)) { free(sessions); tiny_mcp_tool_add_error(result, "INTERNAL_ERROR", "session enumeration failed"); free(cursor); return; } sessions[index].session_id = session_id; sessions[index].last_message_at = count > 0 ? tiny_store_entry(context->store, entries[count - 1])->message.timestamp_ms : 0; } qsort(sessions, session_count, sizeof(*sessions), compare_session_by_last_then_id); } tiny_json_init(&json); tiny_json_raw(&json, "{\"sessions\":["); for (position = 0; position < session_count && emitted < (size_t)limit; ++position) { const char *session_id = sessions[position].session_id; const size_t *entries; size_t count, representative_index = SIZE_MAX; int64_t started = INT64_MAX, ended = 0; int kind_counts[6] = { 0 }; const tiny_store_entry_t *representative = NULL; const char *summary = NULL; if (has_cursor && (sessions[position].last_message_at < cursor_last || (sessions[position].last_message_at == cursor_last && strcmp(session_id, cursor_session) <= 0))) { continue; } if (!tiny_history_index_session(history, session_id, &entries, &count)) { continue; } for (index = 0; index < count; ++index) { const tiny_store_entry_t *entry = tiny_store_entry(context->store, entries[index]); if (!entry || entry->deleted) { continue; } kind_counts[entry->message.metadata.memory_kind - 1]++; started = entry->message.timestamp_ms < started ? entry->message.timestamp_ms : started; ended = entry->message.timestamp_ms > ended ? entry->message.timestamp_ms : ended; representative_index = entries[index]; representative = entry; } if (representative_index == SIZE_MAX) { continue; } summary = representative->message.metadata.summary ? representative->message.metadata.summary : representative->message.content; { tiny_json_buffer_t item; size_t item_size; tiny_json_init(&item); tiny_json_raw(&item, "{\"session_id\":"); tiny_json_string(&item, session_id); tiny_json_raw(&item, ",\"message_count\":"); tiny_json_u64(&item, count); tiny_json_raw(&item, ",\"kind_counts\":{\"conversation\":"); tiny_json_i64(&item, kind_counts[0]); tiny_json_raw(&item, ",\"episode\":"); tiny_json_i64(&item, kind_counts[1]); tiny_json_raw(&item, ",\"semantic\":"); tiny_json_i64(&item, kind_counts[2]); tiny_json_raw(&item, ",\"procedure\":"); tiny_json_i64(&item, kind_counts[3]); tiny_json_raw(&item, ",\"preference\":"); tiny_json_i64(&item, kind_counts[4]); tiny_json_raw(&item, ",\"summary\":"); tiny_json_i64(&item, kind_counts[5]); tiny_json_raw(&item, "},\"representative_summary\":"); tiny_json_string(&item, summary); tiny_json_raw(&item, ",\"started_at\":"); tiny_json_i64(&item, started); tiny_json_raw(&item, ",\"last_message_at\":"); tiny_json_i64(&item, ended); tiny_json_raw(&item, "}"); item_size = item.failed ? 0U : escaped_size(item.data, item.length) + (emitted ? 2U : 0U); if (item.failed || (emitted && outer_size + item_size > READ_RESPONSE_BUDGET)) { if (item.failed) { json.failed = true; } else { more = 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); outer_size += item_size; emitted++; last_emitted_position = position; } } tiny_json_raw(&json, "]"); if (!more && emitted == (size_t)limit && position < session_count) { more = true; } if (more && last_emitted_position != SIZE_MAX) { char next[SESSION_CURSOR_TEXT_MAX]; if (session_cursor_encode(sessions[last_emitted_position].last_message_at, sessions[last_emitted_position].session_id, next)) { tiny_json_raw(&json, ",\"nextCursor\":"); tiny_json_string(&json, next); } } tiny_json_raw(&json, "}"); if (json.failed) { tiny_mcp_tool_add_error(result, "INTERNAL_ERROR", "sessions response allocation failed"); } else { tiny_mcp_tool_add_json(result, &json); } tiny_json_free(&json); free(sessions); 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", SESSION_CURSOR_TEXT_MAX - 1U); int32_t requested_limit = READ_DEFAULT_LIMIT; mcpc_errcode_t limit_status = mcpc_tool_get_tpropval_i32(tool, (const char8_t *)"limit", &requested_limit); const size_t *entries = NULL; size_t entry_count = 0, position = 0, emitted = 0, outer_size = 256; history_cursor_t cursor; bool has_cursor = cursor_text != NULL, more = false; 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, "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 || (has_cursor && !cursor_decode(session, cursor_text, &cursor))) { tiny_mcp_tool_add_error(result, "VALIDATION_ERROR", "invalid read_messages arguments"); free(session); free(cursor_text); return; } if (!tiny_mcp_tools_ensure_history_index(context) || !tiny_history_index_session(context->history_index, session, &entries, &entry_count)) { tiny_mcp_tool_add_error(result, "INTERNAL_ERROR", "history index allocation failed"); free(session); free(cursor_text); return; } if (has_cursor) { size_t low = 0, high = entry_count; while (low < high) { size_t middle = low + (high - low) / 2; const tiny_store_entry_t *entry = tiny_store_entry(context->store, entries[middle]); if (compare_cursor(&entry->message, &cursor) <= 0) { low = middle + 1; } else { high = middle; } } position = low; } tiny_json_init(&json); tiny_json_raw(&json, "{\"history\":["); while (position < entry_count && emitted < (size_t)requested_limit) { const tiny_store_entry_t *entry = tiny_store_entry(context->store, entries[position]); tiny_json_buffer_t item; size_t item_size; if (!entry || entry->deleted) { position++; continue; } tiny_json_init(&item); tiny_mcp_tool_append_message(&item, &entry->message); item_size = escaped_size(item.data, item.length) + (emitted ? 2 : 0); if (item.failed || (emitted && outer_size + item_size > READ_RESPONSE_BUDGET)) { tiny_json_free(&item); if (item.failed) { json.failed = true; } more = true; break; } if (emitted) { tiny_json_raw(&json, ","); } tiny_json_raw_n(&json, item.data, item.length); outer_size += item_size; emitted++; last = &entry->message; position++; tiny_json_free(&item); } if (!more) { size_t lookahead = position; while (lookahead < entry_count) { const tiny_store_entry_t *entry = tiny_store_entry(context->store, entries[lookahead++]); if (entry && !entry->deleted) { more = true; break; } } } tiny_json_raw(&json, "]"); if (more && last) { char next_cursor[HISTORY_CURSOR_TEXT_MAX]; if (!cursor_encode(session, last, next_cursor)) { json.failed = true; } else { tiny_json_raw(&json, ",\"nextCursor\":"); tiny_json_string(&json, next_cursor); } } tiny_json_raw(&json, "}"); if (json.failed) { tiny_mcp_tool_add_error(result, "INTERNAL_ERROR", "response allocation failed"); } else { tiny_mcp_tool_add_json(result, &json); } tiny_json_free(&json); free(session); free(cursor_text); } bool tiny_mcp_register_read_messages(mcpc_server_t *server) { mcpc_tool_t *tool = tiny_mcp_tool_new("read_messages", "Read persisted messages from one known session in chronological order. " "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, or sessions.", MCPC_U8STR, false) && tiny_mcp_tool_add_property(tool, "session_id", "Conversation session whose chronological history to read.", MCPC_U8STR, 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", "Maximum messages to return in this page, from 1 to 1000; defaults to 100.", MCPC_I32, false) && tiny_mcp_tool_add_property( tool, "cursor", "Opaque nextCursor returned by a previous read_messages call; " "omit for the first page.", MCPC_U8STR, false) && mcpc_server_add_tool(server, tool) == MCPC_EC_0; }