From 4bc721c3bf7589b519c5730d093f704ce56e3f09 Mon Sep 17 00:00:00 2001 From: tanaka takashi Date: Mon, 29 Jun 2026 21:43:54 +0900 Subject: refactor: MCP層をmcpc APIベースに刷新 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 独自JSON-RPC処理をmcpcへ置き換え - 各MCPツールを個別ソースへ分離 - 共通コンテキストと登録処理を整理 - mcpc互換修正と統合テストを更新 --- src/mcp/tools/common/common.c | 117 +++++++++++++++++++++ src/mcp/tools/common/common.h | 19 ++++ src/mcp/tools/common/context.c | 88 ++++++++++++++++ src/mcp/tools/common/context.h | 20 ++++ src/mcp/tools/common/registry.c | 11 ++ src/mcp/tools/common/registry.h | 13 +++ src/mcp/tools/common/tools.h | 10 ++ src/mcp/tools/delete_message.c | 50 +++++++++ src/mcp/tools/get_system_health.c | 49 +++++++++ src/mcp/tools/read_messages.c | 80 ++++++++++++++ src/mcp/tools/rebuild_index.c | 57 ++++++++++ src/mcp/tools/search_messages.c | 134 ++++++++++++++++++++++++ src/mcp/tools/write_message.c | 213 ++++++++++++++++++++++++++++++++++++++ 13 files changed, 861 insertions(+) create mode 100644 src/mcp/tools/common/common.c create mode 100644 src/mcp/tools/common/common.h create mode 100644 src/mcp/tools/common/context.c create mode 100644 src/mcp/tools/common/context.h create mode 100644 src/mcp/tools/common/registry.c create mode 100644 src/mcp/tools/common/registry.h create mode 100644 src/mcp/tools/common/tools.h create mode 100644 src/mcp/tools/delete_message.c create mode 100644 src/mcp/tools/get_system_health.c create mode 100644 src/mcp/tools/read_messages.c create mode 100644 src/mcp/tools/rebuild_index.c create mode 100644 src/mcp/tools/search_messages.c create mode 100644 src/mcp/tools/write_message.c (limited to 'src/mcp/tools') diff --git a/src/mcp/tools/common/common.c b/src/mcp/tools/common/common.c new file mode 100644 index 0000000..71b6f68 --- /dev/null +++ b/src/mcp/tools/common/common.c @@ -0,0 +1,117 @@ +#include "mcp/tools/common/common.h" + +#include + +#include "base/allocator.h" + +void +tiny_mcp_tool_add_error(mcpc_ucbr_t *result, const char *code, const char *message) +{ + tiny_json_buffer_t json; + tiny_json_init(&json); + tiny_json_raw(&json, "{\"status\":\"ERROR\",\"error\":{\"code\":"); + tiny_json_string(&json, code); + tiny_json_raw(&json, ",\"message\":"); + tiny_json_string(&json, message); + tiny_json_raw(&json, "}}"); + mcpc_toolcall_result_add_errmsg_printf8( + result, (const char8_t *)"%s", json.data ? json.data : "{}"); + tiny_json_free(&json); +} + +void +tiny_mcp_tool_add_json(mcpc_ucbr_t *result, tiny_json_buffer_t *json) +{ + if (json->failed) + { + tiny_mcp_tool_add_error(result, "INTERNAL_ERROR", "response allocation failed"); + return; + } + mcpc_toolcall_result_add_text_printf8( + result, (const char8_t *)"%s", json->data ? json->data : "{}"); +} + +char * +tiny_mcp_tool_get_string(const mcpc_tool_t *tool, const char *name, size_t maximum) +{ + char *value = malloc(maximum + 1); + size_t length = 0; + if (!value + || mcpc_tool_get_tpropval_u8str( + tool, (const char8_t *)name, (char8_t *)value, maximum + 1, &length) + != MCPC_EC_0 + || length > maximum) + { + free(value); + return NULL; + } + value[length] = '\0'; + if (!tiny_utf8_valid(value, length)) + { + free(value); + return NULL; + } + return value; +} + +void +tiny_mcp_tool_append_message(tiny_json_buffer_t *json, const tiny_message_t *message) +{ + size_t index; + tiny_json_raw(json, "{\"message_id\":"); + tiny_json_string(json, message->message_id); + tiny_json_raw(json, ",\"session_id\":"); + tiny_json_string(json, message->session_id); + tiny_json_raw(json, ",\"timestamp\":"); + tiny_json_i64(json, message->timestamp_ms); + tiny_json_raw(json, ",\"role\":"); + tiny_json_string(json, tiny_role_name(message->role)); + tiny_json_raw(json, ",\"content\":"); + tiny_json_string(json, message->content); + tiny_json_raw(json, ",\"metadata\":{"); + if (message->metadata.tool_call_id) + { + tiny_json_raw(json, "\"tool_call_id\":"); + tiny_json_string(json, message->metadata.tool_call_id); + tiny_json_raw(json, ","); + } + tiny_json_raw(json, "\"tags\":["); + for (index = 0; index < message->metadata.tag_count; ++index) + { + if (index) + { + tiny_json_raw(json, ","); + } + tiny_json_string(json, message->metadata.tags[index]); + } + tiny_json_raw(json, "],\"memory_tier\":"); + tiny_json_string(json, tiny_tier_name(message->metadata.memory_tier)); + tiny_json_raw(json, "}}"); +} + +mcpc_tool_t * +tiny_mcp_tool_new(const char *name, const char *description, mcpc_tcallcb_t callback) +{ + mcpc_tool_t *tool = mcpc_tool_new2((const char8_t *)name, (const char8_t *)description); + if (!tool || mcpc_tool_set_call_cb(tool, callback) != MCPC_EC_0) + { + return NULL; + } + return tool; +} + +bool +tiny_mcp_tool_add_property(mcpc_tool_t *tool, + const char *name, + const char *description, + mcpc_anytype_t type) +{ + mcpc_toolprop_t *property + = mcpc_toolprop_new2((const char8_t *)name, (const char8_t *)description, type); + if (!property) + { + return false; + } + mcpc_tool_add_toolprop(tool, property); + return true; +} diff --git a/src/mcp/tools/common/common.h b/src/mcp/tools/common/common.h new file mode 100644 index 0000000..6b1cc96 --- /dev/null +++ b/src/mcp/tools/common/common.h @@ -0,0 +1,19 @@ +#ifndef TINY_CONVMEM_MCP_TOOLS_COMMON_H +#define TINY_CONVMEM_MCP_TOOLS_COMMON_H + +#include "domain/message.h" +#include "mcp/json.h" +#include + +void tiny_mcp_tool_add_error(mcpc_ucbr_t *result, const char *code, const char *message); +void tiny_mcp_tool_add_json(mcpc_ucbr_t *result, tiny_json_buffer_t *json); +char *tiny_mcp_tool_get_string(const mcpc_tool_t *tool, const char *name, size_t maximum); +void tiny_mcp_tool_append_message(tiny_json_buffer_t *json, const tiny_message_t *message); + +mcpc_tool_t *tiny_mcp_tool_new(const char *name, const char *description, mcpc_tcallcb_t callback); +bool tiny_mcp_tool_add_property(mcpc_tool_t *tool, + const char *name, + const char *description, + mcpc_anytype_t type); + +#endif diff --git a/src/mcp/tools/common/context.c b/src/mcp/tools/common/context.c new file mode 100644 index 0000000..1a469e0 --- /dev/null +++ b/src/mcp/tools/common/context.c @@ -0,0 +1,88 @@ +#include "mcp/tools/common/context.h" + +#include + +#include "base/allocator.h" + +/* mcpc's callback API has no user-data parameter. The stdio server is single-instance. */ +static tiny_mcp_tools_context_t *active_context; + +tiny_search_index_t * +tiny_mcp_tools_build_search_index(const tiny_store_t *store) +{ + tiny_search_index_t *index = tiny_search_index_create(); + size_t position; + if (!index) + { + return NULL; + } + for (position = 0; position < tiny_store_entry_count(store); ++position) + { + if (!tiny_search_index_add(index, &tiny_store_entry(store, position)->message, position)) + { + tiny_search_index_free(index); + return NULL; + } + } + return index; +} + +tiny_mcp_tools_context_t * +tiny_mcp_tools_context_new(tiny_store_t *store) +{ + tiny_mcp_tools_context_t *context; + if (!store) + { + return NULL; + } + context = malloc(sizeof(*context)); + if (!context) + { + return NULL; + } + context->store = store; + context->search_index = tiny_mcp_tools_build_search_index(store); + if (!context->search_index) + { + free(context); + return NULL; + } + return context; +} + +void +tiny_mcp_tools_context_free(tiny_mcp_tools_context_t *context) +{ + if (!context) + { + return; + } + tiny_search_index_free(context->search_index); + free(context); +} + +bool +tiny_mcp_tools_context_activate(tiny_mcp_tools_context_t *context) +{ + if (!context || active_context) + { + return false; + } + active_context = context; + return true; +} + +void +tiny_mcp_tools_context_deactivate(tiny_mcp_tools_context_t *context) +{ + if (active_context == context) + { + active_context = NULL; + } +} + +tiny_mcp_tools_context_t * +tiny_mcp_tools_context_current(void) +{ + return active_context; +} diff --git a/src/mcp/tools/common/context.h b/src/mcp/tools/common/context.h new file mode 100644 index 0000000..0847d4b --- /dev/null +++ b/src/mcp/tools/common/context.h @@ -0,0 +1,20 @@ +#ifndef TINY_CONVMEM_MCP_TOOLS_CONTEXT_H +#define TINY_CONVMEM_MCP_TOOLS_CONTEXT_H + +#include "domain/search.h" +#include "storage/store.h" + +typedef struct tiny_mcp_tools_context +{ + tiny_store_t *store; + tiny_search_index_t *search_index; +} tiny_mcp_tools_context_t; + +tiny_mcp_tools_context_t *tiny_mcp_tools_context_new(tiny_store_t *store); +void tiny_mcp_tools_context_free(tiny_mcp_tools_context_t *context); +bool tiny_mcp_tools_context_activate(tiny_mcp_tools_context_t *context); +void tiny_mcp_tools_context_deactivate(tiny_mcp_tools_context_t *context); +tiny_mcp_tools_context_t *tiny_mcp_tools_context_current(void); +tiny_search_index_t *tiny_mcp_tools_build_search_index(const tiny_store_t *store); + +#endif diff --git a/src/mcp/tools/common/registry.c b/src/mcp/tools/common/registry.c new file mode 100644 index 0000000..9aa1dff --- /dev/null +++ b/src/mcp/tools/common/registry.c @@ -0,0 +1,11 @@ +#include "mcp/tools/common/tools.h" + +#include "mcp/tools/common/registry.h" + +bool +tiny_mcp_tools_register(mcpc_server_t *server) +{ + return tiny_mcp_register_write_message(server) && tiny_mcp_register_read_messages(server) + && tiny_mcp_register_search_messages(server) && tiny_mcp_register_delete_message(server) + && tiny_mcp_register_rebuild_index(server) && tiny_mcp_register_get_system_health(server); +} diff --git a/src/mcp/tools/common/registry.h b/src/mcp/tools/common/registry.h new file mode 100644 index 0000000..bbc68d0 --- /dev/null +++ b/src/mcp/tools/common/registry.h @@ -0,0 +1,13 @@ +#ifndef TINY_CONVMEM_MCP_TOOLS_REGISTRY_H +#define TINY_CONVMEM_MCP_TOOLS_REGISTRY_H + +#include + +bool tiny_mcp_register_write_message(mcpc_server_t *server); +bool tiny_mcp_register_read_messages(mcpc_server_t *server); +bool tiny_mcp_register_search_messages(mcpc_server_t *server); +bool tiny_mcp_register_delete_message(mcpc_server_t *server); +bool tiny_mcp_register_rebuild_index(mcpc_server_t *server); +bool tiny_mcp_register_get_system_health(mcpc_server_t *server); + +#endif diff --git a/src/mcp/tools/common/tools.h b/src/mcp/tools/common/tools.h new file mode 100644 index 0000000..b06321c --- /dev/null +++ b/src/mcp/tools/common/tools.h @@ -0,0 +1,10 @@ +#ifndef TINY_CONVMEM_MCP_TOOLS_H +#define TINY_CONVMEM_MCP_TOOLS_H + +#include "mcp/tools/common/context.h" +#define MCPC_C23PTCH_UCHAR2 1 +#include + +bool tiny_mcp_tools_register(mcpc_server_t *server); + +#endif diff --git a/src/mcp/tools/delete_message.c b/src/mcp/tools/delete_message.c new file mode 100644 index 0000000..ae924d7 --- /dev/null +++ b/src/mcp/tools/delete_message.c @@ -0,0 +1,50 @@ +#include "mcp/tools/common/registry.h" + +#include "mcp/tools/common/common.h" +#include "mcp/tools/common/context.h" + +#include + +#include "base/allocator.h" + +static void +call_delete_message(const mcpc_tool_t *tool, mcpc_ucbr_t *result) +{ + tiny_mcp_tools_context_t *context = tiny_mcp_tools_context_current(); + char *message_id = tiny_mcp_tool_get_string(tool, "message_id", TINY_MESSAGE_ID_LENGTH); + char *session = tiny_mcp_tool_get_string(tool, "session_id", TINY_SESSION_ID_MAX); + tiny_store_status_t status; + if (!context || !message_id || !session || !tiny_message_id_valid(message_id)) + { + tiny_mcp_tool_add_error( + result, "VALIDATION_ERROR", "message_id must be a canonical 26-character ULID"); + } + else + { + status = tiny_store_delete(context->store, message_id, session[0] ? session : NULL); + if (status != TINY_STORE_OK && status != TINY_STORE_NOT_FOUND) + { + tiny_mcp_tool_add_error( + result, tiny_store_status_name(status), "delete record was not written"); + } + else + { + mcpc_toolcall_result_add_text_printf8( + result, + (const char8_t *)(status == TINY_STORE_OK ? "{\"status\":\"OK\"}" + : "{\"status\":\"NOT_FOUND\"}")); + } + } + free(message_id); + free(session); +} + +bool +tiny_mcp_register_delete_message(mcpc_server_t *server) +{ + mcpc_tool_t *tool = tiny_mcp_tool_new("delete_message", "Delete a message", call_delete_message); + return tool && tiny_mcp_tool_add_property(tool, "message_id", "Message ULID", MCPC_U8STR) + && tiny_mcp_tool_add_property( + tool, "session_id", "Session ID; empty if omitted", MCPC_U8STR) + && mcpc_server_add_tool(server, tool) == MCPC_EC_0; +} diff --git a/src/mcp/tools/get_system_health.c b/src/mcp/tools/get_system_health.c new file mode 100644 index 0000000..eb02090 --- /dev/null +++ b/src/mcp/tools/get_system_health.c @@ -0,0 +1,49 @@ +#include "mcp/tools/common/registry.h" + +#include "mcp/tools/common/common.h" +#include "mcp/tools/common/context.h" + +static void +call_get_system_health(const mcpc_tool_t *tool, mcpc_ucbr_t *result) +{ + tiny_mcp_tools_context_t *context = tiny_mcp_tools_context_current(); + tiny_store_health_t health; + tiny_json_buffer_t json; + (void)tool; + if (!context) + { + tiny_mcp_tool_add_error(result, "INTERNAL_ERROR", "tool context is unavailable"); + return; + } + tiny_store_health(context->store, &health); + tiny_json_init(&json); + tiny_json_raw(&json, "{\"status\":"); + tiny_json_string(&json, health.last_error[0] ? "DEGRADED" : "OK"); + tiny_json_raw(&json, ",\"format_version\":1,\"log_size\":"); + tiny_json_u64(&json, health.log_size); + tiny_json_raw(&json, ",\"snapshot_size\":"); + tiny_json_u64(&json, health.snapshot_size); + tiny_json_raw(&json, ",\"active_messages\":"); + tiny_json_u64(&json, health.active_messages); + tiny_json_raw(&json, ",\"deleted_messages\":"); + tiny_json_u64(&json, health.deleted_messages); + tiny_json_raw(&json, ",\"last_sequence\":"); + tiny_json_u64(&json, health.last_sequence); + tiny_json_raw(&json, ",\"recovered_tail\":"); + tiny_json_raw(&json, health.recovered_tail ? "true" : "false"); + tiny_json_raw(&json, ",\"snapshot_valid\":"); + tiny_json_raw(&json, health.snapshot_valid ? "true" : "false"); + tiny_json_raw(&json, ",\"last_error\":"); + tiny_json_string(&json, health.last_error); + tiny_json_raw(&json, "}"); + tiny_mcp_tool_add_json(result, &json); + tiny_json_free(&json); +} + +bool +tiny_mcp_register_get_system_health(mcpc_server_t *server) +{ + mcpc_tool_t *tool + = tiny_mcp_tool_new("get_system_health", "Report storage health", call_get_system_health); + return tool && mcpc_server_add_tool(server, tool) == MCPC_EC_0; +} diff --git a/src/mcp/tools/read_messages.c b/src/mcp/tools/read_messages.c new file mode 100644 index 0000000..55696d6 --- /dev/null +++ b/src/mcp/tools/read_messages.c @@ -0,0 +1,80 @@ +#include "mcp/tools/common/registry.h" + +#include "mcp/tools/common/common.h" +#include "mcp/tools/common/context.h" + +#include +#include + +#include "base/allocator.h" + +static int +compare_history(const void *left, const void *right) +{ + const tiny_store_entry_t *a = *(const tiny_store_entry_t *const *)left; + const tiny_store_entry_t *b = *(const tiny_store_entry_t *const *)right; + if (a->message.timestamp_ms < b->message.timestamp_ms) + { + return -1; + } + if (a->message.timestamp_ms > b->message.timestamp_ms) + { + return 1; + } + return strcmp(a->message.message_id, b->message.message_id); +} + +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); + const tiny_store_entry_t **items = NULL; + size_t index, count = 0; + tiny_json_buffer_t json; + if (!context || !session || !session[0]) + { + free(session); + tiny_mcp_tool_add_error(result, "VALIDATION_ERROR", "session_id is required"); + return; + } + items = malloc((tiny_store_entry_count(context->store) + 1) * sizeof(*items)); + if (!items) + { + free(session); + tiny_mcp_tool_add_error(result, "INTERNAL_ERROR", "out of memory"); + return; + } + for (index = 0; index < tiny_store_entry_count(context->store); ++index) + { + const tiny_store_entry_t *entry = tiny_store_entry(context->store, index); + if (!entry->deleted && strcmp(entry->message.session_id, session) == 0) + { + items[count++] = entry; + } + } + qsort(items, count, sizeof(*items), compare_history); + tiny_json_init(&json); + tiny_json_raw(&json, "{\"history\":["); + for (index = 0; index < count; ++index) + { + if (index) + { + tiny_json_raw(&json, ","); + } + tiny_mcp_tool_append_message(&json, &items[index]->message); + } + tiny_json_raw(&json, "]}"); + tiny_mcp_tool_add_json(result, &json); + tiny_json_free(&json); + free(items); + free(session); +} + +bool +tiny_mcp_register_read_messages(mcpc_server_t *server) +{ + mcpc_tool_t *tool = tiny_mcp_tool_new("read_messages", "Read a session", call_read_messages); + return tool && tiny_mcp_tool_add_property(tool, "session_id", "Session ID", MCPC_U8STR) + && mcpc_server_add_tool(server, tool) == MCPC_EC_0; +} diff --git a/src/mcp/tools/rebuild_index.c b/src/mcp/tools/rebuild_index.c new file mode 100644 index 0000000..69f8341 --- /dev/null +++ b/src/mcp/tools/rebuild_index.c @@ -0,0 +1,57 @@ +#include "mcp/tools/common/registry.h" + +#include "mcp/tools/common/common.h" +#include "mcp/tools/common/context.h" + +#include "base/allocator.h" + +static void +call_rebuild_index(const mcpc_tool_t *tool, mcpc_ucbr_t *result) +{ + tiny_mcp_tools_context_t *context = tiny_mcp_tools_context_current(); + tiny_rebuild_report_t report; + tiny_search_index_t *rebuilt; + tiny_store_status_t status; + tiny_json_buffer_t json; + (void)tool; + if (!context) + { + tiny_mcp_tool_add_error(result, "INTERNAL_ERROR", "tool context is unavailable"); + return; + } + status = tiny_store_rebuild(context->store, &report); + if (status != TINY_STORE_OK) + { + tiny_mcp_tool_add_error(result, tiny_store_status_name(status), "index rebuild failed"); + return; + } + rebuilt = tiny_mcp_tools_build_search_index(context->store); + if (!rebuilt) + { + tiny_search_index_free(context->search_index); + context->search_index = NULL; + tiny_mcp_tool_add_error(result, "INTERNAL_ERROR", "search index allocation failed"); + return; + } + tiny_search_index_free(context->search_index); + context->search_index = rebuilt; + tiny_json_init(&json); + tiny_json_raw(&json, "{\"status\":\"OK\",\"messages_indexed\":"); + tiny_json_u64(&json, report.messages_indexed); + tiny_json_raw(&json, ",\"records_removed\":"); + tiny_json_u64(&json, report.records_removed); + tiny_json_raw(&json, ",\"bytes_before\":"); + tiny_json_u64(&json, report.bytes_before); + tiny_json_raw(&json, ",\"bytes_after\":"); + tiny_json_u64(&json, report.bytes_after); + tiny_json_raw(&json, "}"); + tiny_mcp_tool_add_json(result, &json); + tiny_json_free(&json); +} + +bool +tiny_mcp_register_rebuild_index(mcpc_server_t *server) +{ + mcpc_tool_t *tool = tiny_mcp_tool_new("rebuild_index", "Compact and rebuild", call_rebuild_index); + return tool && mcpc_server_add_tool(server, tool) == MCPC_EC_0; +} diff --git a/src/mcp/tools/search_messages.c b/src/mcp/tools/search_messages.c new file mode 100644 index 0000000..166c167 --- /dev/null +++ b/src/mcp/tools/search_messages.c @@ -0,0 +1,134 @@ +#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" + +typedef struct result_item +{ + const tiny_store_entry_t *entry; + double score; +} result_item_t; + +static int +compare_results(const void *left, const void *right) +{ + const result_item_t *a = left; + const result_item_t *b = right; + if (a->score > b->score) + { + return -1; + } + if (a->score < b->score) + { + return 1; + } + if (a->entry->message.timestamp_ms > b->entry->message.timestamp_ms) + { + return -1; + } + if (a->entry->message.timestamp_ms < b->entry->message.timestamp_ms) + { + return 1; + } + return strcmp(a->entry->message.message_id, b->entry->message.message_id); +} + +static void +call_search_messages(const mcpc_tool_t *tool, mcpc_ucbr_t *result) +{ + tiny_mcp_tools_context_t *context = tiny_mcp_tools_context_current(); + char *query = tiny_mcp_tool_get_string(tool, "query", TINY_QUERY_MAX); + char *session = tiny_mcp_tool_get_string(tool, "session_id", TINY_SESSION_ID_MAX); + int32_t requested_limit = 0; + size_t limit, index, count = 0, candidate_count = 0, *candidates = NULL; + result_item_t *items = NULL; + tiny_json_buffer_t json; + bool valid + = context && query && query[0] && session + && mcpc_tool_get_tpropval_i32(tool, (const char8_t *)"limit", &requested_limit) == MCPC_EC_0 + && requested_limit >= 1 && requested_limit <= 100; + if (!valid) + { + tiny_mcp_tool_add_error(result, "VALIDATION_ERROR", "query or limit is invalid"); + } + else + { + limit = (size_t)requested_limit; + if (!context->search_index) + { + context->search_index = tiny_mcp_tools_build_search_index(context->store); + } + if (!context->search_index + || !tiny_search_index_candidates( + context->search_index, query, &candidates, &candidate_count)) + { + tiny_mcp_tool_add_error(result, "INTERNAL_ERROR", "search index allocation failed"); + } + else + { + items = malloc((candidate_count + 1) * sizeof(*items)); + if (!items) + { + tiny_mcp_tool_add_error(result, "INTERNAL_ERROR", "out of memory"); + } + else + { + for (index = 0; index < candidate_count; ++index) + { + const tiny_store_entry_t *entry + = tiny_store_entry(context->store, candidates[index]); + double score; + if (!entry || entry->deleted + || (session[0] && strcmp(entry->message.session_id, session) != 0)) + { + continue; + } + score = tiny_search_score(query, &entry->message); + if (score > 0) + { + items[count].entry = entry; + items[count++].score = score; + } + } + qsort(items, count, sizeof(*items), compare_results); + if (count > limit) + { + count = limit; + } + tiny_json_init(&json); + tiny_json_raw(&json, "{\"results\":["); + for (index = 0; index < count; ++index) + { + if (index) + { + tiny_json_raw(&json, ","); + } + tiny_mcp_tool_append_message(&json, &items[index].entry->message); + } + tiny_json_raw(&json, "]}"); + tiny_mcp_tool_add_json(result, &json); + tiny_json_free(&json); + } + } + } + free(items); + free(candidates); + free(query); + free(session); +} + +bool +tiny_mcp_register_search_messages(mcpc_server_t *server) +{ + mcpc_tool_t *tool = tiny_mcp_tool_new("search_messages", "Search messages", call_search_messages); + return tool && tiny_mcp_tool_add_property(tool, "query", "Search text", MCPC_U8STR) + && tiny_mcp_tool_add_property(tool, "session_id", "Session ID; empty for all", MCPC_U8STR) + && tiny_mcp_tool_add_property(tool, "limit", "Result limit from 1 to 100", MCPC_I32) + && mcpc_server_add_tool(server, tool) == MCPC_EC_0; +} diff --git a/src/mcp/tools/write_message.c b/src/mcp/tools/write_message.c new file mode 100644 index 0000000..1d7ece2 --- /dev/null +++ b/src/mcp/tools/write_message.c @@ -0,0 +1,213 @@ +#include "mcp/tools/common/registry.h" + +#include "mcp/tools/common/common.h" +#include "mcp/tools/common/context.h" +#include "mjson.h" + +#include +#include + +#include "base/allocator.h" + +static bool +metadata_has_only_known_fields(const char *json, int length) +{ + int offset = 0, key_offset, key_length, value_offset, value_length, value_type; + while ((offset = mjson_next(json, + length, + offset, + &key_offset, + &key_length, + &value_offset, + &value_length, + &value_type)) + != 0) + { + char key[32]; + int decoded = mjson_get_string(json + key_offset, key_length, "$", key, sizeof(key)); + (void)value_offset; + (void)value_length; + (void)value_type; + if (decoded <= 0 || decoded >= (int)sizeof(key)) + { + return false; + } + key[decoded] = '\0'; + if (strcmp(key, "tool_call_id") != 0 && strcmp(key, "tags") != 0 + && strcmp(key, "memory_tier") != 0) + { + return false; + } + } + return true; +} + +static bool +parse_metadata(const char *json, tiny_message_t *message) +{ + const char *tool_call = NULL, *tier = NULL, *tags = NULL; + int tool_call_len = 0, tier_len = 0, tags_len = 0; + int offset = 0, key_offset, key_length, value_offset, value_length, value_type; + int length = (int)strlen(json); + int tool_call_type, tier_type, tags_type; + if (strcmp(json, "{}") == 0) + { + return true; + } + if (mjson_find(json, length, "$", NULL, NULL) != MJSON_TOK_OBJECT + || !metadata_has_only_known_fields(json, length)) + { + return false; + } + tool_call_type = mjson_find(json, length, "$.tool_call_id", &tool_call, &tool_call_len); + tier_type = mjson_find(json, length, "$.memory_tier", &tier, &tier_len); + tags_type = mjson_find(json, length, "$.tags", &tags, &tags_len); + if ((tool_call_type != MJSON_TOK_INVALID && tool_call_type != MJSON_TOK_STRING) + || (tier_type != MJSON_TOK_INVALID && tier_type != MJSON_TOK_STRING) + || (tags_type != MJSON_TOK_INVALID && tags_type != MJSON_TOK_ARRAY)) + { + return false; + } + if (tool_call_type == MJSON_TOK_STRING) + { + int decoded; + message->metadata.tool_call_id = malloc(TINY_TOOL_CALL_ID_MAX + 1); + if (!message->metadata.tool_call_id) + { + return false; + } + decoded = mjson_get_string(json, + length, + "$.tool_call_id", + message->metadata.tool_call_id, + TINY_TOOL_CALL_ID_MAX + 1); + if (decoded < 0 || decoded > TINY_TOOL_CALL_ID_MAX + || !tiny_utf8_valid(message->metadata.tool_call_id, (size_t)decoded)) + { + return false; + } + message->metadata.tool_call_id[decoded] = '\0'; + } + if (tier_type == MJSON_TOK_STRING) + { + char tier_value[16]; + int decoded = mjson_get_string(json, length, "$.memory_tier", tier_value, sizeof(tier_value)); + if (decoded < 0 || decoded >= (int)sizeof(tier_value)) + { + return false; + } + tier_value[decoded] = '\0'; + if (!tiny_tier_parse(tier_value, &message->metadata.memory_tier)) + { + return false; + } + } + if (tags_type == MJSON_TOK_ARRAY) + { + while ((offset = mjson_next(tags, + tags_len, + offset, + &key_offset, + &key_length, + &value_offset, + &value_length, + &value_type)) + != 0) + { + char *tag; + char **resized; + int decoded; + if (value_type != MJSON_TOK_STRING || message->metadata.tag_count >= TINY_TAG_COUNT_MAX) + { + return false; + } + tag = malloc(TINY_TAG_MAX + 1); + if (!tag) + { + return false; + } + decoded = mjson_get_string(tags + value_offset, value_length, "$", tag, TINY_TAG_MAX + 1); + if (decoded <= 0 || decoded > TINY_TAG_MAX || !tiny_utf8_valid(tag, (size_t)decoded)) + { + free(tag); + return false; + } + tag[decoded] = '\0'; + resized = realloc(message->metadata.tags, + (message->metadata.tag_count + 1) * sizeof(*message->metadata.tags)); + if (!resized) + { + free(tag); + return false; + } + message->metadata.tags = resized; + message->metadata.tags[message->metadata.tag_count++] = tag; + } + } + return true; +} + +static void +call_write_message(const mcpc_tool_t *tool, mcpc_ucbr_t *result) +{ + tiny_mcp_tools_context_t *context = tiny_mcp_tools_context_current(); + tiny_message_t input, written; + tiny_store_status_t status; + char *role = NULL, *metadata = NULL; + tiny_json_buffer_t json; + tiny_message_init(&input); + tiny_message_init(&written); + input.session_id = tiny_mcp_tool_get_string(tool, "session_id", TINY_SESSION_ID_MAX); + role = tiny_mcp_tool_get_string(tool, "role", 16); + input.content = tiny_mcp_tool_get_string(tool, "content", TINY_CONTENT_MAX); + metadata = tiny_mcp_tool_get_string(tool, "metadata_json", TINY_CONTENT_MAX); + bool valid = context && input.session_id && input.session_id[0] && role && input.content + && input.content[0] && metadata && tiny_role_parse(role, &input.role) + && parse_metadata(metadata, &input); + if (!valid) + { + tiny_mcp_tool_add_error(result, "VALIDATION_ERROR", "invalid write_message arguments"); + } + else + { + status = tiny_store_write(context->store, &input, &written); + if (status != TINY_STORE_OK) + { + tiny_mcp_tool_add_error( + result, tiny_store_status_name(status), "message was not written"); + } + else + { + if (context->search_index + && !tiny_search_index_add( + context->search_index, &written, tiny_store_entry_count(context->store) - 1)) + { + tiny_search_index_free(context->search_index); + context->search_index = NULL; + } + tiny_json_init(&json); + tiny_json_raw(&json, "{\"status\":\"OK\",\"message\":"); + tiny_mcp_tool_append_message(&json, &written); + tiny_json_raw(&json, "}"); + tiny_mcp_tool_add_json(result, &json); + tiny_json_free(&json); + } + } + free(role); + free(metadata); + tiny_message_free(&input); + tiny_message_free(&written); +} + +bool +tiny_mcp_register_write_message(mcpc_server_t *server) +{ + mcpc_tool_t *tool + = tiny_mcp_tool_new("write_message", "Append a conversation message", call_write_message); + return tool && tiny_mcp_tool_add_property(tool, "session_id", "Session ID", MCPC_U8STR) + && tiny_mcp_tool_add_property(tool, "role", "user, assistant, or system", MCPC_U8STR) + && tiny_mcp_tool_add_property(tool, "content", "Message text", MCPC_U8STR) + && tiny_mcp_tool_add_property( + tool, "metadata_json", "Metadata object JSON; use {} when empty", MCPC_U8STR) + && mcpc_server_add_tool(server, tool) == MCPC_EC_0; +} -- cgit v1.2.3