Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 93 additions & 13 deletions src/mcp/mcp.c
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ enum {
#include "pipeline/artifact.h"

#ifdef _WIN32
#include <direct.h>
#include <io.h>
#include <process.h>
#define getpid _getpid
#else
Expand Down Expand Up @@ -720,6 +722,59 @@ char *cbm_mcp_get_string_arg(const char *args_json, const char *key) {
return result;
}

static char *canonicalize_repo_path_if_exists(char *repo_path) {
if (!repo_path) {
return NULL;
}
bool root_syntax = true;
for (const char *p = repo_path; *p; p++) {
if (*p != '/' && *p != '\\' && *p != ':') {
root_syntax = false;
break;
}
}
if (root_syntax) {
return repo_path;
}

char real[CBM_SZ_4K];
#ifdef _WIN32
if (_access(repo_path, 0) == 0 && _fullpath(real, repo_path, sizeof(real))) {
cbm_normalize_path_sep(real);
char *canonical = heap_strdup(real);
if (canonical) {
free(repo_path);
return canonical;
}
}
#else
if (realpath(repo_path, real)) {
cbm_normalize_path_sep(real);
char *canonical = heap_strdup(real);
if (canonical) {
free(repo_path);
return canonical;
}
}
#endif

return repo_path;
}

static char *normalize_project_arg(char *project) {
if (!project || (!strchr(project, '/') && !strchr(project, '\\'))) {
return project;
}

project = canonicalize_repo_path_if_exists(project);
char *normalized = cbm_project_name_from_path(project);
if (normalized) {
free(project);
return normalized;
}
return project;
}

/* Resolve the project argument, accepting the canonical "project" key plus the
* aliases a caller naturally reaches for (#640): list_projects surfaces the
* field as "name" and the not-found hint says "pass the project name", so
Expand All @@ -737,7 +792,7 @@ static char *get_project_arg(const char *args_json) {
if (!p) {
p = cbm_mcp_get_string_arg(args_json, "projectName");
}
return p;
return normalize_project_arg(p);
}

int cbm_mcp_get_int_arg(const char *args_json, const char *key, int default_val) {
Expand Down Expand Up @@ -1163,6 +1218,26 @@ static char *build_no_store_error(const char *project) {
} \
} while (0)

static bool project_has_adr(cbm_store_t *store, const char *project, const char *root_path) {
if (store && project) {
cbm_adr_t adr;
memset(&adr, 0, sizeof(adr));
if (cbm_store_adr_get(store, project, &adr) == CBM_STORE_OK) {
cbm_store_adr_free(&adr);
return true;
}
}

if (!root_path) {
return false;
}

char adr_path[CBM_SZ_4K];
snprintf(adr_path, sizeof(adr_path), "%s/.codebase-memory/adr.md", root_path);
struct stat adr_st;
return stat(adr_path, &adr_st) == 0;
}

/* ── Tool handler implementations ─────────────────────────────── */

/* Return true if filename is a valid project .db file (not temp/internal).
Expand Down Expand Up @@ -1411,10 +1486,7 @@ static char *handle_get_graph_schema(cbm_mcp_server_t *srv, const char *args) {
/* Check ADR presence */
cbm_project_t proj_info = {0};
if (cbm_store_get_project(store, project, &proj_info) == 0 && proj_info.root_path) {
char adr_path[CBM_SZ_4K];
snprintf(adr_path, sizeof(adr_path), "%s/.codebase-memory/adr.md", proj_info.root_path);
struct stat adr_st;
bool adr_exists = (stat(adr_path, &adr_st) == 0);
bool adr_exists = project_has_adr(store, project, proj_info.root_path);
yyjson_mut_obj_add_bool(doc, root, "adr_present", adr_exists);
if (!adr_exists) {
yyjson_mut_obj_add_str(
Expand Down Expand Up @@ -3204,17 +3276,14 @@ static bool build_index_success_response(cbm_mcp_server_t *srv, yyjson_mut_doc *
}
}

char adr_path[CBM_SZ_4K];
snprintf(adr_path, sizeof(adr_path), "%s/.codebase-memory/adr.md", repo_path);
struct stat adr_st;
bool adr_exists = (stat(adr_path, &adr_st) == 0);
bool adr_exists = project_has_adr(store, project_name, repo_path);
yyjson_mut_obj_add_bool(doc, root, "adr_present", adr_exists);
if (!adr_exists && !degraded) {
yyjson_mut_obj_add_str(
doc, root, "adr_hint",
"Project indexed. Consider creating an Architecture Decision Record: "
"explore the codebase with get_architecture(aspects=['all']), then use "
"manage_adr(mode='store') to persist architectural insights across sessions.");
"manage_adr(mode='update') to persist architectural insights across sessions.");
}

bool has_artifact = cbm_artifact_exists(repo_path);
Expand All @@ -3240,6 +3309,8 @@ static char *handle_index_repository(cbm_mcp_server_t *srv, const char *args) {
return cbm_mcp_text_result("repo_path is required", true);
}

repo_path = canonicalize_repo_path_if_exists(repo_path);

if (mode_str && strcmp(mode_str, "cross-repo-intelligence") == 0) {
free(mode_str);
free(name_override);
Expand Down Expand Up @@ -4678,10 +4749,13 @@ static char *handle_detect_changes(cbm_mcp_server_t *srv, const char *args) {

char *root_path = get_project_root(srv, project);
if (!root_path) {
char *err = build_no_store_error(project);
char *res = cbm_mcp_text_result(err, true);
free(err);
free(project);
free(base_branch);
free(scope);
return cbm_mcp_text_result("project not found", true);
return res;
}

if (!validate_search_path_arg(root_path)) {
Expand Down Expand Up @@ -4896,10 +4970,13 @@ static char *handle_manage_adr(cbm_mcp_server_t *srv, const char *args) {
* the UI are visible to each other (#256). */
cbm_store_t *resolved = resolve_store(srv, project);
if (!resolved) {
char *err = build_no_store_error(project);
char *res = cbm_mcp_text_result(err, true);
free(err);
free(project);
free(mode_str);
free(content);
return cbm_mcp_text_result("project not found", true);
return res;
}

/* resolve_store opens file-backed projects READ-ONLY (query stores must
Expand All @@ -4915,10 +4992,13 @@ static char *handle_manage_adr(cbm_mcp_server_t *srv, const char *args) {
if (resolved_db_path) {
owned_rw = cbm_store_open_path(resolved_db_path);
if (!owned_rw) {
char *err = build_no_store_error(project);
char *res = cbm_mcp_text_result(err, true);
free(err);
free(project);
free(mode_str);
free(content);
return cbm_mcp_text_result("project not found", true);
return res;
}
store = owned_rw;
}
Expand Down
38 changes: 37 additions & 1 deletion src/pipeline/fqn.c
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h> // strdup
#ifdef _WIN32
#include <direct.h>
#include <io.h>
#endif

/* Maximum path segments in a FQN (CBM_SZ_256 slots total, -2 for project + name) */
#define FQN_MAX_PATH_SEGS 254
Expand Down Expand Up @@ -384,13 +388,45 @@ static char *fqn_bound_name_len(char *name) {
return name;
}

static bool path_is_root_syntax(const char *path) {
if (!path || !path[0]) {
return false;
}
for (const char *p = path; *p; p++) {
if (*p != '/' && *p != '\\' && *p != ':') {
return false;
}
}
return true;
}

char *cbm_project_name_from_path(const char *abs_path) {
if (!abs_path || !abs_path[0]) {
return strdup("root");
}
if (path_is_root_syntax(abs_path)) {
return strdup("root");
}

char real[CBM_SZ_4K];
const char *name_path = abs_path;
#ifdef _WIN32
if (_access(abs_path, 0) == 0 && _fullpath(real, abs_path, sizeof(real))) {
cbm_normalize_path_sep(real);
name_path = real;
}
#else
if (realpath(abs_path, real)) {
cbm_normalize_path_sep(real);
name_path = real;
}
#endif

/* Work on mutable copy */
char *path = strdup(abs_path);
char *path = strdup(name_path);
if (!path) {
return NULL;
}
size_t len = strlen(path);

/* Normalize path separators */
Expand Down
Loading
Loading