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
447450struct 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
454461cbm_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
20372154char * 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