summaryrefslogtreecommitdiff
path: root/src/mcp/tools
diff options
context:
space:
mode:
Diffstat (limited to 'src/mcp/tools')
-rw-r--r--src/mcp/tools/read_messages.c152
1 files changed, 150 insertions, 2 deletions
diff --git a/src/mcp/tools/read_messages.c b/src/mcp/tools/read_messages.c
index 131c513..e217aa2 100644
--- a/src/mcp/tools/read_messages.c
+++ b/src/mcp/tools/read_messages.c
@@ -15,6 +15,8 @@
#define READ_DEFAULT_LIMIT 100
#define READ_MAX_LIMIT 1000
#define READ_RESPONSE_BUDGET (8U * 1024U * 1024U - 64U * 1024U)
+#define CONTENT_RANGE_DEFAULT_LIMIT 4096
+#define CONTENT_RANGE_MAX_LIMIT 65536
#define HISTORY_CURSOR_BYTES 47U
#define HISTORY_CURSOR_TEXT_MAX 64U
#define SESSION_CURSOR_BYTES_MAX (1U + 8U + 4U + TINY_SESSION_ID_MAX + 4U)
@@ -26,6 +28,120 @@ typedef struct history_cursor
char message_id[27];
} history_cursor_t;
+static bool
+utf8_boundary(const char *content, size_t length, size_t offset)
+{
+ return offset == 0 || offset == length || ((unsigned char)content[offset] & 0xc0U) != 0x80U;
+}
+
+static void
+call_content_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);
+ int32_t requested_offset = 0, requested_limit = CONTENT_RANGE_DEFAULT_LIMIT;
+ int32_t expected_revision = 0;
+ mcpc_errcode_t offset_status
+ = mcpc_tool_get_tpropval_i32(tool, (const char8_t *)"content_offset", &requested_offset);
+ mcpc_errcode_t limit_status
+ = mcpc_tool_get_tpropval_i32(tool, (const char8_t *)"content_limit", &requested_limit);
+ mcpc_errcode_t revision_status
+ = mcpc_tool_get_tpropval_i32(tool, (const char8_t *)"expected_revision", &expected_revision);
+ const tiny_store_entry_t *entry = NULL;
+ size_t entry_index = 0, content_length = 0, offset = 0, end = 0, chunk_length = 0;
+ char *chunk = NULL;
+ tiny_json_buffer_t json;
+ bool valid = context && message_id && tiny_message_id_valid(message_id)
+ && (offset_status == MCPC_EC_0 || offset_status == MCPC_EC_ANYDATA_NOTFOUND)
+ && requested_offset >= 0
+ && (limit_status == MCPC_EC_0 || limit_status == MCPC_EC_ANYDATA_NOTFOUND)
+ && requested_limit >= 1 && requested_limit <= CONTENT_RANGE_MAX_LIMIT
+ && (revision_status == MCPC_EC_0 || revision_status == MCPC_EC_ANYDATA_NOTFOUND)
+ && (revision_status != MCPC_EC_0 || expected_revision >= 1);
+ if (!valid)
+ {
+ tiny_mcp_tool_add_error(result,
+ "VALIDATION_ERROR",
+ "message_id, content_offset, content_limit or expected_revision is "
+ "invalid");
+ }
+ else if (!tiny_mcp_tools_ensure_memory_index(context))
+ {
+ tiny_mcp_tool_add_error(result, "INTERNAL_ERROR", "message index allocation failed");
+ }
+ else if (!tiny_memory_index_find(context->memory_index, message_id, &entry_index)
+ || !(entry = tiny_store_entry(context->store, entry_index)) || entry->deleted)
+ {
+ tiny_mcp_tool_add_error(result, "NOT_FOUND", "message was not found");
+ }
+ else if (revision_status == MCPC_EC_0 && entry->message.revision != (uint64_t)expected_revision)
+ {
+ tiny_mcp_tool_add_error(result, "CONFLICT", "message revision does not match");
+ }
+ else
+ {
+ content_length = strlen(entry->message.content);
+ offset = (size_t)requested_offset;
+ if (offset > content_length || !utf8_boundary(entry->message.content, content_length, offset))
+ {
+ tiny_mcp_tool_add_error(result,
+ "VALIDATION_ERROR",
+ "content_offset is outside content or not a UTF-8 boundary");
+ }
+ else
+ {
+ end = offset + (size_t)requested_limit;
+ if (end > content_length)
+ {
+ end = content_length;
+ }
+ while (end > offset && !utf8_boundary(entry->message.content, content_length, end))
+ {
+ --end;
+ }
+ if (end == offset && end < content_length)
+ {
+ do
+ {
+ ++end;
+ }
+ while (end < content_length
+ && !utf8_boundary(entry->message.content, content_length, end));
+ }
+ chunk_length = end - offset;
+ chunk = malloc(chunk_length + 1);
+ if (!chunk)
+ {
+ tiny_mcp_tool_add_error(result, "INTERNAL_ERROR", "out of memory");
+ }
+ else
+ {
+ memcpy(chunk, entry->message.content + offset, chunk_length);
+ chunk[chunk_length] = '\0';
+ tiny_json_init(&json);
+ tiny_json_raw(&json, "{\"content_range\":{\"message_id\":");
+ tiny_json_string(&json, entry->message.message_id);
+ tiny_json_raw(&json, ",\"revision\":");
+ tiny_json_u64(&json, entry->message.revision);
+ tiny_json_raw(&json, ",\"total_bytes\":");
+ tiny_json_u64(&json, content_length);
+ tiny_json_raw(&json, ",\"content_offset\":");
+ tiny_json_u64(&json, offset);
+ tiny_json_raw(&json, ",\"next_content_offset\":");
+ tiny_json_u64(&json, end);
+ tiny_json_raw(&json, ",\"complete\":");
+ tiny_json_raw(&json, end == content_length ? "true" : "false");
+ tiny_json_raw(&json, ",\"content\":");
+ tiny_json_string(&json, chunk);
+ tiny_json_raw(&json, "}}");
+ tiny_mcp_tool_add_json(result, &json);
+ tiny_json_free(&json);
+ }
+ }
+ }
+ free(chunk);
+ free(message_id);
+}
+
static uint64_t
session_hash(const char *value)
{
@@ -800,6 +916,14 @@ 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, "content") == 0)
+ {
+ call_content_view(context, tool, result);
+ free(view);
+ free(session);
+ free(cursor_text);
+ return;
+ }
if (view && strcmp(view, "context") == 0)
{
call_context_view(context, tool, result);
@@ -950,14 +1074,38 @@ tiny_mcp_register_read_messages(mcpc_server_t *server)
call_read_messages);
return tool
&& tiny_mcp_tool_add_property(
- tool, "view", "Read view: history (default), context, or sessions.", MCPC_U8STR, false)
+ tool,
+ "view",
+ "Read view: history (default), content, 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 content and context views.",
+ MCPC_U8STR,
+ false)
+ && tiny_mcp_tool_add_property(tool,
+ "content_offset",
+ "UTF-8 byte offset for the content view; defaults to 0.",
+ MCPC_I32,
+ false)
&& tiny_mcp_tool_add_property(
- tool, "message_id", "Message ULID required by the context view.", MCPC_U8STR, false)
+ tool,
+ "content_limit",
+ "Target content chunk bytes from 1 to 65536; defaults to 4096.",
+ MCPC_I32,
+ false)
+ && tiny_mcp_tool_add_property(
+ tool,
+ "expected_revision",
+ "Optional positive revision for a consistent multi-call content read.",
+ MCPC_I32,
+ 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(