summaryrefslogtreecommitdiff
path: root/src/mcp/tools
diff options
context:
space:
mode:
Diffstat (limited to 'src/mcp/tools')
-rw-r--r--src/mcp/tools/common/common.c117
-rw-r--r--src/mcp/tools/common/common.h19
-rw-r--r--src/mcp/tools/common/context.c88
-rw-r--r--src/mcp/tools/common/context.h20
-rw-r--r--src/mcp/tools/common/registry.c11
-rw-r--r--src/mcp/tools/common/registry.h13
-rw-r--r--src/mcp/tools/common/tools.h10
-rw-r--r--src/mcp/tools/delete_message.c50
-rw-r--r--src/mcp/tools/get_system_health.c49
-rw-r--r--src/mcp/tools/read_messages.c80
-rw-r--r--src/mcp/tools/rebuild_index.c57
-rw-r--r--src/mcp/tools/search_messages.c134
-rw-r--r--src/mcp/tools/write_message.c213
13 files changed, 861 insertions, 0 deletions
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 <stdlib.h>
+
+#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 <mcpc/mcpc.h>
+
+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 <stdlib.h>
+
+#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 <mcpc/mcpc.h>
+
+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 <mcpc/mcpc.h>
+
+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 <stdlib.h>
+
+#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 <stdlib.h>
+#include <string.h>
+
+#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 <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+
+#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 <stdlib.h>
+#include <string.h>
+
+#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;
+}