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/README.md | 22 +- src/mcp/json.c | 61 ++- src/mcp/server.c | 762 ++------------------------------------ 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 +++++++++++ 16 files changed, 942 insertions(+), 764 deletions(-) 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') 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 #include +#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 -#include -#include -#include - -#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", ¶ms, &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 + +#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