summaryrefslogtreecommitdiff
path: root/src/mcp
diff options
context:
space:
mode:
authortanaka takashi <isazaka@gmail.com>2026-06-29 21:43:54 +0900
committertanaka takashi <isazaka@gmail.com>2026-06-30 01:23:51 +0900
commit4bc721c3bf7589b519c5730d093f704ce56e3f09 (patch)
tree38da055ab15fd70f5c92584560ea73486e5ddce0 /src/mcp
parent4c6a831aa2a57e2bcc20aa3fc2f1b7172f5be285 (diff)
refactor: MCP層をmcpc APIベースに刷新
- 独自JSON-RPC処理をmcpcへ置き換え - 各MCPツールを個別ソースへ分離 - 共通コンテキストと登録処理を整理 - mcpc互換修正と統合テストを更新
Diffstat (limited to 'src/mcp')
-rw-r--r--src/mcp/README.md22
-rw-r--r--src/mcp/json.c61
-rw-r--r--src/mcp/server.c762
-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
16 files changed, 942 insertions, 764 deletions
diff --git a/src/mcp/README.md b/src/mcp/README.md
index dc7ac46..ed008a0 100644
--- a/src/mcp/README.md
+++ b/src/mcp/README.md
@@ -1,14 +1,14 @@
-# MCP compatibility boundary
+# MCP adapter
-The pinned mcpc revision cannot describe required versus optional properties,
-nested objects/arrays, 64-bit values, or application callback context. The
-project therefore keeps mcpc immutable and implements the stdio dispatch and
-tool schemas in `server.c`, using mcpc's bundled `mjson` parser for raw JSON.
+`server.c` owns only the mcpc server lifecycle. Tool schemas and callbacks live
+under `tools/`, one source file per memory tool. Shared registration, callback
+context, and response helpers live under `tools/common/`. mcpc owns stdio
+framing, MCP initialization, dispatch, and result envelopes.
-This boundary supports the MCP 2024-11-05 initialize, tools/list, and tools/call
-flow needed by the server. Tool success and application errors are returned as
-JSON serialized into TextContent; application errors also set `isError`.
+The pinned mcpc API supports integer and UTF-8 string properties, but not nested
+or optional properties. Consequently every advertised property must be sent.
+Use an empty `session_id` where that filter is optional, and pass write metadata
+as a JSON object string in `metadata_json` (`{}` when no metadata is needed).
-If mcpc is upgraded, this boundary should be removed only after its integration
-tests demonstrate equivalent optional/object/array schema and callback-state
-behavior.
+Tool success and application errors are JSON serialized into MCP TextContent;
+application errors also set `isError`.
diff --git a/src/mcp/json.c b/src/mcp/json.c
index dcef91b..62f31c9 100644
--- a/src/mcp/json.c
+++ b/src/mcp/json.c
@@ -4,11 +4,17 @@
#include <stdlib.h>
#include <string.h>
+#include "base/allocator.h"
+
static bool
reserve(tiny_json_buffer_t *buffer, size_t addition)
{
size_t needed, capacity;
char *resized;
+ if (buffer == NULL)
+ {
+ return false;
+ }
if (buffer->failed || addition > SIZE_MAX - buffer->length - 1)
{
return false;
@@ -42,11 +48,18 @@ reserve(tiny_json_buffer_t *buffer, size_t addition)
void
tiny_json_init(tiny_json_buffer_t *buffer)
{
- memset(buffer, 0, sizeof(*buffer));
+ if (buffer != NULL)
+ {
+ memset(buffer, 0, sizeof(*buffer));
+ }
}
void
tiny_json_free(tiny_json_buffer_t *buffer)
{
+ if (buffer == NULL)
+ {
+ return;
+ }
free(buffer->data);
tiny_json_init(buffer);
}
@@ -54,7 +67,7 @@ tiny_json_free(tiny_json_buffer_t *buffer)
bool
tiny_json_raw_n(tiny_json_buffer_t *buffer, const char *value, size_t length)
{
- if (!reserve(buffer, length))
+ if ((value == NULL && length != 0) || !reserve(buffer, length))
{
return false;
}
@@ -66,14 +79,14 @@ tiny_json_raw_n(tiny_json_buffer_t *buffer, const char *value, size_t length)
bool
tiny_json_raw(tiny_json_buffer_t *buffer, const char *value)
{
- return tiny_json_raw_n(buffer, value, strlen(value));
+ return value != NULL && tiny_json_raw_n(buffer, value, strlen(value));
}
bool
tiny_json_string(tiny_json_buffer_t *buffer, const char *value)
{
const unsigned char *position = (const unsigned char *)value;
- if (!tiny_json_raw(buffer, "\""))
+ if (value == NULL || !tiny_json_raw(buffer, "\""))
{
return false;
}
@@ -85,36 +98,60 @@ tiny_json_string(tiny_json_buffer_t *buffer, const char *value)
escaped[0] = '\\';
escaped[1] = (char)*position;
escaped[2] = '\0';
- tiny_json_raw(buffer, escaped);
+ if (!tiny_json_raw(buffer, escaped))
+ {
+ return false;
+ }
}
else if (*position == '\b')
{
- tiny_json_raw(buffer, "\\b");
+ if (!tiny_json_raw(buffer, "\\b"))
+ {
+ return false;
+ }
}
else if (*position == '\f')
{
- tiny_json_raw(buffer, "\\f");
+ if (!tiny_json_raw(buffer, "\\f"))
+ {
+ return false;
+ }
}
else if (*position == '\n')
{
- tiny_json_raw(buffer, "\\n");
+ if (!tiny_json_raw(buffer, "\\n"))
+ {
+ return false;
+ }
}
else if (*position == '\r')
{
- tiny_json_raw(buffer, "\\r");
+ if (!tiny_json_raw(buffer, "\\r"))
+ {
+ return false;
+ }
}
else if (*position == '\t')
{
- tiny_json_raw(buffer, "\\t");
+ if (!tiny_json_raw(buffer, "\\t"))
+ {
+ return false;
+ }
}
else if (*position < 0x20)
{
snprintf(escaped, sizeof(escaped), "\\u%04x", *position);
- tiny_json_raw(buffer, escaped);
+ if (!tiny_json_raw(buffer, escaped))
+ {
+ return false;
+ }
}
else
{
- tiny_json_raw_n(buffer, (const char *)position, 1);
+ if (!tiny_json_raw_n(buffer, (const char *)position, 1))
+ {
+ return false;
+ }
}
++position;
}
diff --git a/src/mcp/server.c b/src/mcp/server.c
index 80d8307..a0ff436 100644
--- a/src/mcp/server.c
+++ b/src/mcp/server.c
@@ -1,757 +1,37 @@
#include "mcp/server.h"
-#include "domain/search.h"
-#include "mcp/json.h"
-#include "mjson.h"
+#include "mcp/tools/common/tools.h"
-#include <math.h>
-#include <stdint.h>
-#include <stdlib.h>
-#include <string.h>
-
-#define MCP_PROTOCOL_VERSION "2024-11-05"
-
-typedef struct result_item
-{
- const tiny_store_entry_t *entry;
- double score;
-} result_item_t;
-
-static char *
-read_line(FILE *input)
-{
- size_t length = 0, capacity = 4096;
- int character;
- char *line = malloc(capacity);
- if (!line)
- {
- return NULL;
- }
- while ((character = fgetc(input)) != EOF)
- {
- if (length + 1 >= capacity)
- {
- char *r;
- capacity *= 2;
- r = realloc(line, capacity);
- if (!r)
- {
- free(line);
- return NULL;
- }
- line = r;
- }
- line[length++] = (char)character;
- if (character == '\n')
- {
- break;
- }
- }
- if (length == 0 && character == EOF)
- {
- free(line);
- return NULL;
- }
- line[length] = '\0';
- return line;
-}
-
-static char *
-json_string_value(
- const char *json, int length, const char *path, size_t maximum, bool required, bool *valid)
-{
- const char *token = NULL;
- int token_length = 0;
- int type = mjson_find(json, length, path, &token, &token_length);
- char *value;
- int decoded;
- if (type == MJSON_TOK_INVALID)
- {
- if (required)
- {
- *valid = false;
- }
- return NULL;
- }
- if (type != MJSON_TOK_STRING || token_length < 2 || (size_t)token_length > maximum * 6 + 2)
- {
- *valid = false;
- return NULL;
- }
- value = malloc(maximum + 1);
- if (!value)
- {
- *valid = false;
- return NULL;
- }
- decoded = mjson_get_string(json, length, path, value, (int)maximum + 1);
- if (decoded < 0 || (size_t)decoded > maximum || !tiny_utf8_valid(value, (size_t)decoded))
- {
- free(value);
- *valid = false;
- return NULL;
- }
- value[decoded] = '\0';
- return value;
-}
-
-static bool
-object_has_only(const char *json, int length, const char *const *allowed, size_t allowed_count)
-{
- int offset = 0, koff, klen, voff, vlen, vtype;
- while ((offset = mjson_next(json, length, offset, &koff, &klen, &voff, &vlen, &vtype)) != 0)
- {
- char key[64];
- int decoded;
- size_t index;
- if (klen <= 0)
- {
- return false;
- }
- decoded = mjson_get_string(json + koff, klen, "$", key, sizeof(key));
- if (decoded <= 0 || decoded >= (int)sizeof(key))
- {
- return false;
- }
- key[decoded] = '\0';
- for (index = 0; index < allowed_count; ++index)
- {
- if (strcmp(key, allowed[index]) == 0)
- {
- break;
- }
- }
- if (index == allowed_count)
- {
- return false;
- }
- }
- return true;
-}
-
-static void
-free_input_message(tiny_message_t *message)
-{
- tiny_message_free(message);
-}
-
-static bool
-parse_tags(const char *json, int length, tiny_message_t *message)
-{
- const char *array = NULL;
- int array_length = 0;
- int type = mjson_find(json, length, "$.metadata.tags", &array, &array_length);
- int offset = 0, koff, klen, voff, vlen, vtype;
- if (type == MJSON_TOK_INVALID)
- {
- return true;
- }
- if (type != MJSON_TOK_ARRAY)
- {
- return false;
- }
- while ((offset = mjson_next(array, array_length, offset, &koff, &klen, &voff, &vlen, &vtype))
- != 0)
- {
- char *tag;
- int decoded;
- char **resized;
- if (vtype != 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(array + voff, vlen, "$", 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(char *));
- if (!resized)
- {
- free(tag);
- return false;
- }
- message->metadata.tags = resized;
- message->metadata.tags[message->metadata.tag_count++] = tag;
- }
- return true;
-}
-
-static bool
-parse_write(const char *args, int length, tiny_message_t *message)
-{
- static const char *const top_keys[] = { "session_id", "role", "content", "metadata" };
- static const char *const metadata_keys[] = { "tool_call_id", "tags", "memory_tier" };
- bool valid = true;
- char *role = NULL, *tier = NULL;
- const char *metadata = NULL;
- int metadata_length = 0, metadata_type;
- tiny_message_init(message);
- if (!object_has_only(args, length, top_keys, sizeof(top_keys) / sizeof(top_keys[0])))
- {
- valid = false;
- }
- metadata_type = mjson_find(args, length, "$.metadata", &metadata, &metadata_length);
- if (metadata_type != MJSON_TOK_INVALID
- && (metadata_type != MJSON_TOK_OBJECT
- || !object_has_only(metadata,
- metadata_length,
- metadata_keys,
- sizeof(metadata_keys) / sizeof(metadata_keys[0]))))
- {
- valid = false;
- }
- message->session_id
- = json_string_value(args, length, "$.session_id", TINY_SESSION_ID_MAX, true, &valid);
- role = json_string_value(args, length, "$.role", 16, true, &valid);
- message->content = json_string_value(args, length, "$.content", TINY_CONTENT_MAX, true, &valid);
- message->metadata.tool_call_id = json_string_value(
- args, length, "$.metadata.tool_call_id", TINY_TOOL_CALL_ID_MAX, false, &valid);
- tier = json_string_value(args, length, "$.metadata.memory_tier", 16, false, &valid);
- if (!valid || !message->session_id || !message->content || !role || message->session_id[0] == '\0'
- || message->content[0] == '\0' || !tiny_role_parse(role, &message->role)
- || (tier && !tiny_tier_parse(tier, &message->metadata.memory_tier))
- || !parse_tags(args, length, message))
- {
- valid = false;
- }
- free(role);
- free(tier);
- if (!valid)
- {
- free_input_message(message);
- }
- return valid;
-}
-
-static void
-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, "}}");
-}
-
-static void
-send_json_result(
- FILE *output, const char *id, int id_length, tiny_json_buffer_t *inner, bool is_error)
-{
- tiny_json_buffer_t response;
- tiny_json_init(&response);
- tiny_json_raw(&response, "{\"jsonrpc\":\"2.0\",\"id\":");
- tiny_json_raw_n(&response, id, (size_t)id_length);
- tiny_json_raw(&response, ",\"result\":{\"content\":[{\"type\":\"text\",\"text\":");
- tiny_json_string(&response, inner->data ? inner->data : "{}");
- tiny_json_raw(&response, "}]");
- if (is_error)
- {
- tiny_json_raw(&response, ",\"isError\":true");
- }
- tiny_json_raw(&response, "}}\n");
- if (!response.failed)
- {
- fwrite(response.data, 1, response.length, output);
- fflush(output);
- }
- tiny_json_free(&response);
-}
-
-static void
-send_error(FILE *output, const char *id, int id_length, const char *code, const char *message)
-{
- tiny_json_buffer_t inner;
- tiny_json_init(&inner);
- tiny_json_raw(&inner, "{\"status\":\"ERROR\",\"error\":{\"code\":");
- tiny_json_string(&inner, code);
- tiny_json_raw(&inner, ",\"message\":");
- tiny_json_string(&inner, message);
- tiny_json_raw(&inner, "}}");
- send_json_result(output, id, id_length, &inner, true);
- tiny_json_free(&inner);
-}
-
-static int
-compare_history(const void *left, const void *right)
-{
- const tiny_store_entry_t *a = *(const tiny_store_entry_t *const *)left,
- *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 int
-compare_results(const void *l, const void *r)
+int
+tiny_convmem_server_run(FILE *input, FILE *output, tiny_store_t *store)
{
- const result_item_t *a = l, *b = r;
- if (a->score > b->score)
- {
- return -1;
- }
- if (a->score < b->score)
+ tiny_mcp_tools_context_t *context;
+ mcpc_server_t *server;
+ mcpc_errcode_t status;
+ if (!input || !output || !store)
{
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)
+ context = tiny_mcp_tools_context_new(store);
+ if (!context)
{
return 1;
}
- return strcmp(a->entry->message.message_id, b->entry->message.message_id);
-}
-
-static tiny_search_index_t *
-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)
+ server = mcpc_server_new_iostrm(input, output);
+ if (!server || mcpc_server_set_nament(server, (const char8_t *)"tiny-convmem-mcp") != MCPC_EC_0
+ || mcpc_server_capa_enable_tool(server) != MCPC_EC_0 || !tiny_mcp_tools_register(server)
+ || !tiny_mcp_tools_context_activate(context))
{
- if (!tiny_search_index_add(index, &tiny_store_entry(store, position)->message, position))
- {
- tiny_search_index_free(index);
- return NULL;
- }
- }
- return index;
-}
-
-static void
-handle_call(FILE *output,
- const char *id,
- int id_length,
- const char *name,
- const char *args,
- int args_length,
- tiny_store_t *store,
- tiny_search_index_t **search_index)
-{
- tiny_json_buffer_t result;
- tiny_store_status_t status;
- tiny_json_init(&result);
- if (strcmp(name, "write_message") == 0)
- {
- tiny_message_t input, written;
- if (!parse_write(args, args_length, &input))
- {
- send_error(output, id, id_length, "VALIDATION_ERROR", "invalid write_message arguments");
- return;
- }
- tiny_message_init(&written);
- status = tiny_store_write(store, &input, &written);
- tiny_message_free(&input);
- if (status != TINY_STORE_OK)
- {
- send_error(
- output, id, id_length, tiny_store_status_name(status), "message was not written");
- return;
- }
- if (*search_index
- && !tiny_search_index_add(*search_index, &written, tiny_store_entry_count(store) - 1))
- {
- tiny_search_index_free(*search_index);
- *search_index = NULL;
- }
- tiny_json_raw(&result, "{\"status\":\"OK\",\"message\":");
- append_message(&result, &written);
- tiny_json_raw(&result, "}");
- tiny_message_free(&written);
- }
- else if (strcmp(name, "read_messages") == 0)
- {
- static const char *const keys[] = { "session_id" };
- bool valid = object_has_only(args, args_length, keys, 1);
- char *session
- = json_string_value(args, args_length, "$.session_id", TINY_SESSION_ID_MAX, true, &valid);
- size_t i, count = 0;
- const tiny_store_entry_t **items;
- if (!valid || !session || !session[0])
- {
- free(session);
- send_error(output, id, id_length, "VALIDATION_ERROR", "session_id is required");
- return;
- }
- items = malloc(tiny_store_entry_count(store) * sizeof(*items));
- if (!items)
- {
- free(session);
- send_error(output, id, id_length, "INTERNAL_ERROR", "out of memory");
- return;
- }
- for (i = 0; i < tiny_store_entry_count(store); ++i)
- {
- const tiny_store_entry_t *e = tiny_store_entry(store, i);
- if (!e->deleted && strcmp(e->message.session_id, session) == 0)
- {
- items[count++] = e;
- }
- }
- qsort(items, count, sizeof(*items), compare_history);
- tiny_json_raw(&result, "{\"history\":[");
- for (i = 0; i < count; ++i)
- {
- if (i)
- {
- tiny_json_raw(&result, ",");
- }
- append_message(&result, &items[i]->message);
- }
- tiny_json_raw(&result, "]}");
- free(items);
- free(session);
- }
- else if (strcmp(name, "search_messages") == 0)
- {
- static const char *const keys[] = { "query", "session_id", "limit" };
- bool valid = object_has_only(args, args_length, keys, 3);
- char *query = json_string_value(args, args_length, "$.query", TINY_QUERY_MAX, true, &valid);
- char *session = json_string_value(
- args, args_length, "$.session_id", TINY_SESSION_ID_MAX, false, &valid);
- double number = 20;
- size_t limit = 20, i, count = 0, candidate_count = 0, *candidates = NULL;
- result_item_t *items;
- if (mjson_find(args, args_length, "$.limit", NULL, NULL) != MJSON_TOK_INVALID)
- {
- if (!mjson_get_number(args, args_length, "$.limit", &number) || number < 1 || number > 100
- || floor(number) != number)
- {
- valid = false;
- }
- else
- {
- limit = (size_t)number;
- }
- }
- if (!valid || !query || !query[0])
- {
- free(query);
- free(session);
- send_error(output, id, id_length, "VALIDATION_ERROR", "query or limit is invalid");
- return;
- }
- if (!*search_index)
+ if (server)
{
- *search_index = build_search_index(store);
+ mcpc_server_close(server);
}
- if (!*search_index
- || !tiny_search_index_candidates(*search_index, query, &candidates, &candidate_count))
- {
- free(query);
- free(session);
- send_error(output, id, id_length, "INTERNAL_ERROR", "search index allocation failed");
- return;
- }
- items = malloc((candidate_count ? candidate_count : 1) * sizeof(*items));
- if (!items)
- {
- free(candidates);
- free(query);
- free(session);
- send_error(output, id, id_length, "INTERNAL_ERROR", "out of memory");
- return;
- }
- for (i = 0; i < candidate_count; ++i)
- {
- const tiny_store_entry_t *e = tiny_store_entry(store, candidates[i]);
- double score;
- if (!e || e->deleted || (session && strcmp(e->message.session_id, session) != 0))
- {
- continue;
- }
- score = tiny_search_score(query, &e->message);
- if (score > 0)
- {
- items[count].entry = e;
- items[count].score = score;
- count++;
- }
- }
- qsort(items, count, sizeof(*items), compare_results);
- if (count > limit)
- {
- count = limit;
- }
- tiny_json_raw(&result, "{\"results\":[");
- for (i = 0; i < count; ++i)
- {
- if (i)
- {
- tiny_json_raw(&result, ",");
- }
- append_message(&result, &items[i].entry->message);
- }
- tiny_json_raw(&result, "]}");
- free(candidates);
- free(items);
- free(query);
- free(session);
- }
- else if (strcmp(name, "delete_message") == 0)
- {
- static const char *const keys[] = { "message_id", "session_id" };
- bool valid = object_has_only(args, args_length, keys, 2);
- char *mid = json_string_value(
- args, args_length, "$.message_id", TINY_MESSAGE_ID_LENGTH, true, &valid);
- char *session = json_string_value(
- args, args_length, "$.session_id", TINY_SESSION_ID_MAX, false, &valid);
- if (!valid || !mid || !tiny_message_id_valid(mid))
- {
- free(mid);
- free(session);
- send_error(output,
- id,
- id_length,
- "VALIDATION_ERROR",
- "message_id must be a canonical 26-character ULID");
- return;
- }
- status = tiny_store_delete(store, mid, session);
- free(mid);
- free(session);
- if (status != TINY_STORE_OK && status != TINY_STORE_NOT_FOUND)
- {
- send_error(output,
- id,
- id_length,
- tiny_store_status_name(status),
- "delete record was not written");
- return;
- }
- tiny_json_raw(&result,
- status == TINY_STORE_OK ? "{\"status\":\"OK\"}" : "{\"status\":\"NOT_FOUND\"}");
- }
- else if (strcmp(name, "rebuild_index") == 0)
- {
- tiny_rebuild_report_t report;
- tiny_search_index_t *rebuilt;
- if (!object_has_only(args, args_length, NULL, 0))
- {
- send_error(output, id, id_length, "VALIDATION_ERROR", "rebuild_index takes no arguments");
- return;
- }
- status = tiny_store_rebuild(store, &report);
- if (status != TINY_STORE_OK)
- {
- send_error(output, id, id_length, tiny_store_status_name(status), "index rebuild failed");
- return;
- }
- rebuilt = build_search_index(store);
- if (!rebuilt)
- {
- tiny_search_index_free(*search_index);
- *search_index = NULL;
- send_error(output,
- id,
- id_length,
- "INTERNAL_ERROR",
- "log was compacted but the search index could not be allocated");
- return;
- }
- tiny_search_index_free(*search_index);
- *search_index = rebuilt;
- tiny_json_raw(&result, "{\"status\":\"OK\",\"messages_indexed\":");
- tiny_json_u64(&result, report.messages_indexed);
- tiny_json_raw(&result, ",\"records_removed\":");
- tiny_json_u64(&result, report.records_removed);
- tiny_json_raw(&result, ",\"bytes_before\":");
- tiny_json_u64(&result, report.bytes_before);
- tiny_json_raw(&result, ",\"bytes_after\":");
- tiny_json_u64(&result, report.bytes_after);
- tiny_json_raw(&result, "}");
- }
- else if (strcmp(name, "get_system_health") == 0)
- {
- tiny_store_health_t health;
- if (!object_has_only(args, args_length, NULL, 0))
- {
- send_error(
- output, id, id_length, "VALIDATION_ERROR", "get_system_health takes no arguments");
- return;
- }
- tiny_store_health(store, &health);
- tiny_json_raw(&result, "{\"status\":");
- tiny_json_string(&result, health.last_error[0] ? "DEGRADED" : "OK");
- tiny_json_raw(&result, ",\"format_version\":1,\"log_size\":");
- tiny_json_u64(&result, health.log_size);
- tiny_json_raw(&result, ",\"snapshot_size\":");
- tiny_json_u64(&result, health.snapshot_size);
- tiny_json_raw(&result, ",\"active_messages\":");
- tiny_json_u64(&result, health.active_messages);
- tiny_json_raw(&result, ",\"deleted_messages\":");
- tiny_json_u64(&result, health.deleted_messages);
- tiny_json_raw(&result, ",\"last_sequence\":");
- tiny_json_u64(&result, health.last_sequence);
- tiny_json_raw(&result, ",\"recovered_tail\":");
- tiny_json_raw(&result, health.recovered_tail ? "true" : "false");
- tiny_json_raw(&result, ",\"snapshot_valid\":");
- tiny_json_raw(&result, health.snapshot_valid ? "true" : "false");
- tiny_json_raw(&result, ",\"last_error\":");
- tiny_json_string(&result, health.last_error);
- tiny_json_raw(&result, "}");
- }
- else
- {
- send_error(output, id, id_length, "VALIDATION_ERROR", "unknown tool");
- return;
- }
- if (result.failed)
- {
- send_error(output, id, id_length, "INTERNAL_ERROR", "response allocation failed");
- }
- else
- {
- send_json_result(output, id, id_length, &result, false);
- }
- tiny_json_free(&result);
-}
-
-static const char *tools_json
- = "{\"tools\":["
- "{\"name\":\"write_message\",\"description\":\"Durably append a conversation "
- "message\",\"inputSchema\":{\"type\":\"object\",\"properties\":{\"session_id\":{\"type\":"
- "\"string\",\"maxLength\":256},\"role\":{\"type\":\"string\",\"enum\":[\"user\","
- "\"assistant\",\"system\"]},\"content\":{\"type\":\"string\",\"maxLength\":1048576},"
- "\"metadata\":{\"type\":\"object\",\"properties\":{\"tool_call_id\":{\"type\":\"string\","
- "\"maxLength\":256},\"tags\":{\"type\":\"array\",\"maxItems\":32,\"items\":{\"type\":"
- "\"string\",\"maxLength\":128}},\"memory_tier\":{\"type\":\"string\",\"enum\":[\"short\","
- "\"mid\",\"long\",\"discard\"]}},\"additionalProperties\":false}},\"required\":[\"session_"
- "id\",\"role\",\"content\"],\"additionalProperties\":false}},"
- "{\"name\":\"read_messages\",\"description\":\"Read a session in chronological "
- "order\",\"inputSchema\":{\"type\":\"object\",\"properties\":{\"session_id\":{\"type\":"
- "\"string\",\"maxLength\":256}},\"required\":[\"session_id\"],\"additionalProperties\":false}"
- "},"
- "{\"name\":\"search_messages\",\"description\":\"Search message content and "
- "tags\",\"inputSchema\":{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\","
- "\"maxLength\":4096},\"session_id\":{\"type\":\"string\",\"maxLength\":256},\"limit\":{"
- "\"type\":\"integer\",\"minimum\":1,\"maximum\":100}},\"required\":[\"query\"],"
- "\"additionalProperties\":false}},"
- "{\"name\":\"delete_message\",\"description\":\"Durably tombstone a "
- "message\",\"inputSchema\":{\"type\":\"object\",\"properties\":{\"message_id\":{\"type\":"
- "\"string\",\"minLength\":26,\"maxLength\":26},\"session_id\":{\"type\":\"string\","
- "\"maxLength\":256}},\"required\":[\"message_id\"],\"additionalProperties\":false}},"
- "{\"name\":\"rebuild_index\",\"description\":\"Compact the log and rebuild the "
- "snapshot\",\"inputSchema\":{\"type\":\"object\",\"properties\":{},\"additionalProperties\":"
- "false}},"
- "{\"name\":\"get_system_health\",\"description\":\"Report persistence health and "
- "counters\",\"inputSchema\":{\"type\":\"object\",\"properties\":{},\"additionalProperties\":"
- "false}}]}";
-
-int
-tiny_convmem_server_run(FILE *input, FILE *output, tiny_store_t *store)
-{
- char *line;
- tiny_search_index_t *search_index = build_search_index(store);
- if (!search_index)
- {
+ tiny_mcp_tools_context_free(context);
return 1;
}
- while ((line = read_line(input)) != NULL)
- {
- int length = (int)strlen(line);
- const char *method = NULL, *id = NULL, *params = NULL;
- int ml = 0, il = 0, pl = 0;
- if (mjson_find(line, length, "$.method", &method, &ml) != MJSON_TOK_STRING)
- {
- free(line);
- continue;
- }
- mjson_find(line, length, "$.id", &id, &il);
- mjson_find(line, length, "$.params", &params, &pl);
- {
- char method_value[64];
- int decoded
- = mjson_get_string(line, length, "$.method", method_value, sizeof(method_value));
- if (decoded < 0)
- {
- free(line);
- continue;
- }
- method_value[decoded] = '\0';
- if (strcmp(method_value, "initialize") == 0 && id)
- {
- fprintf(output,
- "{\"jsonrpc\":\"2.0\",\"id\":%.*s,\"result\":{\"protocolVersion\":\"%s\","
- "\"capabilities\":{\"tools\":{}},\"serverInfo\":{\"name\":\"tiny-convmem-mcp\","
- "\"version\":\"0.1.0\"}}}\n",
- il,
- id,
- MCP_PROTOCOL_VERSION);
- }
- else if (strcmp(method_value, "tools/list") == 0 && id)
- {
- fprintf(
- output, "{\"jsonrpc\":\"2.0\",\"id\":%.*s,\"result\":%s}\n", il, id, tools_json);
- }
- else if (strcmp(method_value, "tools/call") == 0 && id && params)
- {
- char name[64];
- const char *args = NULL;
- int al = 0;
- int n = mjson_get_string(params, pl, "$.name", name, sizeof(name));
- if (n > 0)
- {
- name[n] = '\0';
- if (mjson_find(params, pl, "$.arguments", &args, &al) == MJSON_TOK_OBJECT)
- {
- handle_call(output, id, il, name, args, al, store, &search_index);
- }
- else
- {
- send_error(output, id, il, "VALIDATION_ERROR", "arguments must be an object");
- }
- }
- }
- fflush(output);
- }
- free(line);
- }
- tiny_search_index_free(search_index);
- return 0;
+ status = mcpc_server_start(server);
+ tiny_mcp_tools_context_deactivate(context);
+ mcpc_server_close(server);
+ tiny_mcp_tools_context_free(context);
+ return status == MCPC_EC_0 ? 0 : 1;
}
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;
+}