#include "mcp/tools/common/registry.h" #include "mcp/tools/common/common.h" #include "mcp/tools/common/context.h" #include #include #include #include "base/allocator.h" #include "storage/codec.h" #define READ_DEFAULT_LIMIT 100 #define READ_MAX_LIMIT 1000 #define READ_RESPONSE_BUDGET (8U * 1024U * 1024U - 64U * 1024U) #define CURSOR_BYTES 47U #define CURSOR_TEXT_MAX 64U 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(const char *input, unsigned char output[CURSOR_BYTES]) { size_t length = strlen(input), source = 0, target = 0; uint32_t accumulator = 0; unsigned bits = 0; if (length != 63) { return false; } while (source < 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 >= CURSOR_BYTES) { return false; } output[target++] = (unsigned char)(accumulator >> bits); accumulator &= bits ? (UINT32_C(1) << bits) - 1 : 0; } } return target == CURSOR_BYTES && accumulator == 0; } static bool cursor_encode(const char *session, const tiny_message_t *message, char output[CURSOR_TEXT_MAX]) { unsigned char data[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, CURSOR_TEXT_MAX) == 63; } static bool cursor_decode(const char *session, const char *value, history_cursor_t *cursor) { unsigned char data[CURSOR_BYTES]; if (!value || !value[0] || !base64url_decode(value, data) || 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; } 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 *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; 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 (!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[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 a session page", call_read_messages); return tool && tiny_mcp_tool_add_property(tool, "session_id", "Session ID", MCPC_U8STR, true) && tiny_mcp_tool_add_property( tool, "limit", "Optional page limit from 1 to 1000; defaults to 100", MCPC_I32, false) && tiny_mcp_tool_add_property( tool, "cursor", "Optional opaque page cursor", MCPC_U8STR, false) && mcpc_server_add_tool(server, tool) == MCPC_EC_0; }