diff options
| author | tanaka takashi <isazaka@gmail.com> | 2026-08-29 09:15:24 +0900 |
|---|---|---|
| committer | tanaka takashi <isazaka@gmail.com> | 2026-08-29 09:15:24 +0900 |
| commit | 5b063df8868ef8cdc2a6aab7a670f911bc0ea9a4 (patch) | |
| tree | ad42c80733be664ccee5dcf3211ba3bb63328acf /src | |
| parent | d57577637c1c1a16176d63d5a89e9eec3c1e4d7c (diff) | |
大きなメッセージをMCPホストのtool result上限に触れず取得できるよう、read_messagesへcontent viewを追加する。UTF-8 byte単位のcontent_offset/content_limitとnext_content_offsetにより、文字境界を壊さず反復取得できる。
各chunkにrevisionとtotal_bytesを返し、expected_revisionで取得途中の更新をCONFLICTとして検出する。未知または削除済みmessageはNOT_FOUND、不正な範囲はVALIDATION_ERRORとする。読み取りには既存memory indexを利用し、永続data、revision、I/O counterを変更しない。
新規MCP toolは増やさず、従来の7 tool構成とhistory/context/sessionsの契約を維持する。storage recordとsnapshot形式も変更しないためmigrationとrebuildは不要。
README、設計書、MCPリファレンスを更新し、多言語UTF-8本文の完全再構成、境界・limit・revision・削除済みmessage、read-only I/O、既存view回帰をprotocol testで検証する。
Tests: ctest --test-dir build/range-read-test -C Release --output-on-failure (20/20 passed)
Diffstat (limited to 'src')
| -rw-r--r-- | src/mcp/tools/read_messages.c | 152 |
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( |
