Skip to content

Commit 0b8a456

Browse files
committed
Restore startup update check: notify on first tool call if newer release exists
Background thread checks api.github.com/repos/.../releases/latest on initialize, stores one-shot notice prepended to the first tools/call response content array. Ported from Go internal/tools/tools.go checkForUpdate + addUpdateNotice pattern.
1 parent 442cb95 commit 0b8a456

File tree

1 file changed

+123
-4
lines changed

1 file changed

+123
-4
lines changed

src/mcp/mcp.c

Lines changed: 123 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,12 @@
1111
#include "store/store.h"
1212
#include "cypher/cypher.h"
1313
#include "pipeline/pipeline.h"
14+
#include "cli/cli.h"
1415
#include "foundation/platform.h"
1516
#include "foundation/compat.h"
1617
#include "foundation/compat_fs.h"
18+
#include "foundation/compat_thread.h"
19+
#include "foundation/log.h"
1720

1821
#ifdef _WIN32
1922
#include <process.h> /* _getpid */
@@ -445,10 +448,14 @@ bool cbm_mcp_get_bool_arg(const char *args_json, const char *key) {
445448
* ══════════════════════════════════════════════════════════════════ */
446449

447450
struct cbm_mcp_server {
448-
cbm_store_t *store; /* currently open project store (or NULL) */
449-
bool owns_store; /* true if we opened the store */
450-
char *current_project; /* which project store is open for (heap) */
451-
time_t store_last_used; /* last time resolve_store was called for a named project */
451+
cbm_store_t *store; /* currently open project store (or NULL) */
452+
bool owns_store; /* true if we opened the store */
453+
char *current_project; /* which project store is open for (heap) */
454+
time_t store_last_used; /* last time resolve_store was called for a named project */
455+
char update_notice[256]; /* one-shot update notice, cleared after first injection */
456+
bool update_checked; /* true after background check has been launched */
457+
cbm_thread_t update_tid; /* background update check thread */
458+
bool update_thread_active; /* true if update thread was started and needs joining */
452459
};
453460

454461
cbm_mcp_server_t *cbm_mcp_server_new(const char *store_path) {
@@ -486,6 +493,9 @@ void cbm_mcp_server_free(cbm_mcp_server_t *srv) {
486493
if (!srv) {
487494
return;
488495
}
496+
if (srv->update_thread_active) {
497+
cbm_thread_join(&srv->update_tid);
498+
}
489499
if (srv->owns_store && srv->store) {
490500
cbm_store_close(srv->store);
491501
}
@@ -2032,6 +2042,113 @@ char *cbm_mcp_handle_tool(cbm_mcp_server_t *srv, const char *tool_name, const ch
20322042
return cbm_mcp_text_result(msg, true);
20332043
}
20342044

2045+
/* ── Background update check ──────────────────────────────────── */
2046+
2047+
#define UPDATE_CHECK_URL "https://api.github.com/repos/DeusData/codebase-memory-mcp/releases/latest"
2048+
2049+
static void *update_check_thread(void *arg) {
2050+
cbm_mcp_server_t *srv = (cbm_mcp_server_t *)arg;
2051+
2052+
/* Use curl with 5s timeout to fetch latest release tag */
2053+
FILE *fp = cbm_popen("curl -sf --max-time 5 -H 'Accept: application/vnd.github+json' "
2054+
"'" UPDATE_CHECK_URL "' 2>/dev/null",
2055+
"r");
2056+
if (!fp) {
2057+
srv->update_checked = true;
2058+
return NULL;
2059+
}
2060+
2061+
char buf[4096];
2062+
size_t total = 0;
2063+
while (total < sizeof(buf) - 1) {
2064+
size_t n = fread(buf + total, 1, sizeof(buf) - 1 - total, fp);
2065+
if (n == 0) {
2066+
break;
2067+
}
2068+
total += n;
2069+
}
2070+
buf[total] = '\0';
2071+
cbm_pclose(fp);
2072+
2073+
/* Parse tag_name from JSON response */
2074+
yyjson_doc *doc = yyjson_read(buf, total, 0);
2075+
if (!doc) {
2076+
srv->update_checked = true;
2077+
return NULL;
2078+
}
2079+
2080+
yyjson_val *root = yyjson_doc_get_root(doc);
2081+
yyjson_val *tag = yyjson_obj_get(root, "tag_name");
2082+
const char *tag_str = yyjson_get_str(tag);
2083+
2084+
if (tag_str) {
2085+
const char *current = cbm_cli_get_version();
2086+
if (cbm_compare_versions(tag_str, current) > 0) {
2087+
snprintf(srv->update_notice, sizeof(srv->update_notice),
2088+
"Update available: %s -> %s -- run: codebase-memory-mcp update", current,
2089+
tag_str);
2090+
cbm_log_info("update.available", "current", current, "latest", tag_str);
2091+
}
2092+
}
2093+
2094+
yyjson_doc_free(doc);
2095+
srv->update_checked = true;
2096+
return NULL;
2097+
}
2098+
2099+
static void start_update_check(cbm_mcp_server_t *srv) {
2100+
if (srv->update_checked) {
2101+
return;
2102+
}
2103+
srv->update_checked = true; /* prevent double-launch */
2104+
if (cbm_thread_create(&srv->update_tid, 0, update_check_thread, srv) == 0) {
2105+
srv->update_thread_active = true;
2106+
}
2107+
}
2108+
2109+
/* Prepend update notice to a tool result, then clear it (one-shot). */
2110+
static char *inject_update_notice(cbm_mcp_server_t *srv, char *result_json) {
2111+
if (srv->update_notice[0] == '\0') {
2112+
return result_json;
2113+
}
2114+
2115+
/* Parse existing result, prepend notice text, rebuild */
2116+
yyjson_doc *doc = yyjson_read(result_json, strlen(result_json), 0);
2117+
if (!doc) {
2118+
return result_json;
2119+
}
2120+
2121+
yyjson_mut_doc *mdoc = yyjson_mut_doc_new(NULL);
2122+
yyjson_mut_val *root = yyjson_val_mut_copy(mdoc, yyjson_doc_get_root(doc));
2123+
yyjson_doc_free(doc);
2124+
if (!root) {
2125+
yyjson_mut_doc_free(mdoc);
2126+
return result_json;
2127+
}
2128+
yyjson_mut_doc_set_root(mdoc, root);
2129+
2130+
/* Find the "content" array */
2131+
yyjson_mut_val *content = yyjson_mut_obj_get(root, "content");
2132+
if (content && yyjson_mut_is_arr(content)) {
2133+
/* Prepend a text content item with the update notice */
2134+
yyjson_mut_val *notice_item = yyjson_mut_obj(mdoc);
2135+
yyjson_mut_obj_add_str(mdoc, notice_item, "type", "text");
2136+
yyjson_mut_obj_add_str(mdoc, notice_item, "text", srv->update_notice);
2137+
yyjson_mut_arr_prepend(content, notice_item);
2138+
}
2139+
2140+
size_t len;
2141+
char *new_json = yyjson_mut_write(mdoc, YYJSON_WRITE_ALLOW_INVALID_UNICODE, &len);
2142+
yyjson_mut_doc_free(mdoc);
2143+
2144+
if (new_json) {
2145+
free(result_json);
2146+
srv->update_notice[0] = '\0'; /* clear — one-shot */
2147+
return new_json;
2148+
}
2149+
return result_json;
2150+
}
2151+
20352152
/* ── Server request handler ───────────────────────────────────── */
20362153

20372154
char *cbm_mcp_server_handle(cbm_mcp_server_t *srv, const char *line) {
@@ -2050,6 +2167,7 @@ char *cbm_mcp_server_handle(cbm_mcp_server_t *srv, const char *line) {
20502167

20512168
if (strcmp(req.method, "initialize") == 0) {
20522169
result_json = cbm_mcp_initialize_response();
2170+
start_update_check(srv);
20532171
} else if (strcmp(req.method, "tools/list") == 0) {
20542172
result_json = cbm_mcp_tools_list();
20552173
} else if (strcmp(req.method, "tools/call") == 0) {
@@ -2058,6 +2176,7 @@ char *cbm_mcp_server_handle(cbm_mcp_server_t *srv, const char *line) {
20582176
req.params_raw ? cbm_mcp_get_arguments(req.params_raw) : heap_strdup("{}");
20592177

20602178
result_json = cbm_mcp_handle_tool(srv, tool_name, tool_args);
2179+
result_json = inject_update_notice(srv, result_json);
20612180
free(tool_name);
20622181
free(tool_args);
20632182
} else {

0 commit comments

Comments
 (0)