Skip to content

Commit 0d24e84

Browse files
committed
fix(extension): MCP server reads --workspace flag; live hooks state; drop footer Reindex/Reset
Three issues from real Cursor install testing: **1. axme_context resolves to /home/$USER instead of the workspace.** When Cursor spawns the MCP server via cursor.mcp.registerServer, the cwd it picks is the user's home directory — not the workspace root — so the server's process.cwd() defaults defaultProjectPath to /home/$USER. Calls to axme_context without an explicit project_path then look up `.axme-code/` in the wrong place and report "project not initialised", even after a successful setup in the real workspace. The user's diagnostic made this obvious: "STORAGE ROOT: /home/georgeb/.axme-code, которого не существует" while the project's `.axme-code/` was sitting in /tmp/. Fix: src/server.ts now resolves its root in this order: 1. --workspace <abs> CLI flag (passed by the Cursor extension at registration time — see registerMcpServer) 2. AXME_WORKSPACE env var (escape hatch for hand-spawned servers) 3. process.cwd() (preserves Claude Code CLI behaviour where the binary is spawned from the project root and cwd already points there) Extension passes workspaceFolders[0].uri.fsPath to registerMcpServer so the server's defaultProjectPath always matches what the user thinks is "the project". Verified via stdio smoke test: server reports `Project: /tmp/axme-cwd-test` after --workspace /tmp/axme-cwd-test. **2. Sidebar showed "hooks missing" when they were installed.** The activation ActivationReport's hooks-step flag captures one moment in time. After reinstall / reset cycles the sidebar's `S.hooksOk` could stay stale until the next window reload. Added extension/src/hooks-state.ts which reads ~/.cursor/hooks.json on demand and confirms preToolUse / postToolUse / sessionEnd entries each contain `axme-code`. Sidebar pushes this live value both on attach and on every resolveWebviewView, so the report's snapshot is no longer the source of truth. **3. Reindex / Reset buttons in the sidebar footer fired without obvious feedback** and (per user feedback) were doing destructive-ish things from a place the user expected to be lightweight monitoring. Removed both from the sidebar footer. Healthcheck stays — it's a read-only webview. The commands are still registered and reachable via the Command Palette ("AXME: Reindex semantic search" / "AXME: Reset"), where the modal confirmation flow makes their effect more obviously deliberate. Verified: npm test → 608 / 608 pass; self-test 6 / 6 pass on rebuilt bundled binary; vsce package → 533 KB .vsix. #!axme pr=none repo=AxmeAI/axme-code
1 parent 98dfbcc commit 0d24e84

5 files changed

Lines changed: 110 additions & 12 deletions

File tree

extension/src/extension.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,14 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
117117
}
118118

119119
// ---- Step 3: MCP registration ------------------------------------------
120+
// We need the workspace folder BEFORE Step 6 — pass it to mcp-register so
121+
// the server's --workspace flag points at the real project, not Cursor's
122+
// home-dir cwd. Without this, axme_context called with no project_path
123+
// defaults to /home/$USER and misses the workspace's .axme-code/ entirely.
124+
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
125+
const workspaceRoot = workspaceFolder?.uri.fsPath;
120126
await runStep(report, "mcp", () => "registered", async () => {
121-
const disposable = await registerMcpServer(binary);
127+
const disposable = await registerMcpServer(binary, workspaceRoot);
122128
context.subscriptions.push(disposable);
123129
});
124130

@@ -157,8 +163,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
157163
// ---- Step 6: setup offer (non-blocking, fire-and-forget) ---------------
158164
// Setup is the user's job, not part of activation. We only record whether
159165
// the workspace is already initialised; the offer toast fires async and
160-
// the user can decline.
161-
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
166+
// the user can decline. workspaceFolder was resolved earlier in Step 3.
162167
if (workspaceFolder) {
163168
const initialized = isAxmeInitialized();
164169
if (initialized) {

extension/src/hooks-state.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/**
2+
* Live read of ~/.cursor/hooks.json to confirm AXME entries are present.
3+
*
4+
* The sidebar previously inferred "hooks ok / missing" from the activation
5+
* ActivationReport, which captured one moment in time. After a user
6+
* reinstalls or reset and reinstalls hooks, the report stays stale until
7+
* the next window reload. Reading the file on demand is cheap and never
8+
* lies — if the entries exist on disk, the hook will fire next tool call.
9+
*/
10+
11+
import { existsSync, readFileSync } from "node:fs";
12+
import { homedir } from "node:os";
13+
import { join } from "node:path";
14+
15+
export function hooksAreInstalled(): boolean {
16+
const p = join(homedir(), ".cursor", "hooks.json");
17+
if (!existsSync(p)) return false;
18+
try {
19+
const raw = readFileSync(p, "utf-8");
20+
// We don't fully parse the schema here — a substring match on the
21+
// binary name and any of the three lifecycle hook keys is enough to
22+
// tell "AXME left its fingerprints in this file". installUserHooks
23+
// is the canonical writer; if it has run successfully, all three
24+
// keys will be present with our command path.
25+
const parsed = JSON.parse(raw);
26+
const hooks = parsed?.hooks;
27+
if (!hooks || typeof hooks !== "object") return false;
28+
for (const kind of ["preToolUse", "postToolUse", "sessionEnd"] as const) {
29+
const list = hooks[kind];
30+
if (!Array.isArray(list)) return false;
31+
const hasAxme = list.some(
32+
(entry: { command?: unknown }) =>
33+
typeof entry?.command === "string" && entry.command.includes("axme-code"),
34+
);
35+
if (!hasAxme) return false;
36+
}
37+
return true;
38+
} catch {
39+
return false;
40+
}
41+
}

extension/src/mcp-register.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@ function getCursorMcpApi(): CursorMcpApi | undefined {
3131
return v.cursor?.mcp;
3232
}
3333

34-
export async function registerMcpServer(binary: string): Promise<vscode.Disposable> {
34+
export async function registerMcpServer(
35+
binary: string,
36+
workspaceRoot: string | undefined,
37+
): Promise<vscode.Disposable> {
3538
const cursor = getCursorMcpApi();
3639
if (!cursor?.registerServer) {
3740
throw new Error(
@@ -40,11 +43,18 @@ export async function registerMcpServer(binary: string): Promise<vscode.Disposab
4043
"that lacks vscode.cursor.mcp.",
4144
);
4245
}
46+
// Pass --workspace explicitly. Cursor spawns the MCP server from the
47+
// user's home dir regardless of which workspace is open, so the server's
48+
// process.cwd() is useless for resolving `.axme-code/`. Without this
49+
// flag, axme_context defaults project_path to /home/$USER and reports
50+
// "project not initialised" even after a successful setup in the real
51+
// workspace. The server's resolveServerRoot() reads this flag.
52+
const args = workspaceRoot ? ["serve", "--workspace", workspaceRoot] : ["serve"];
4353
cursor.registerServer({
4454
name: "axme",
45-
server: { command: binary, args: ["serve"], env: {} },
55+
server: { command: binary, args, env: {} },
4656
});
47-
log(`MCP: registered 'axme' (binary=${binary})`);
57+
log(`MCP: registered 'axme' (binary=${binary}, workspace=${workspaceRoot ?? "(none)"})`);
4858
// Cursor needs ~3s to process the registration before tools become
4959
// available to the chat agent. Verified empirically against the
5060
// browser-devtools-mcp reference implementation.

extension/src/sidebar-webview.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { KbWatcher, KbCounts, readCounts } from "./kb-watcher.js";
2121
import { readBacklog, BacklogItemLite } from "./backlog-reader.js";
2222
import { readActiveSession, ActiveSession } from "./session-tracker.js";
2323
import { detectCurrentMode } from "./auditor-auth.js";
24+
import { hooksAreInstalled } from "./hooks-state.js";
2425
import { log } from "./log.js";
2526

2627
/**
@@ -98,6 +99,10 @@ export class AxmeSidebarProvider implements vscode.WebviewViewProvider {
9899
this.push({ counts, backlog: readBacklog(workspaceRoot).slice(0, 5) });
99100
});
100101
}
102+
// Push the live disk state for hooks. This overrides the activation
103+
// report's snapshot (which can be stale after a reinstall or after
104+
// installUserHooks's first-run race).
105+
this.push({ hooksOk: hooksAreInstalled() });
101106
// Fire-and-forget auditor credential probe so the sidebar can render
102107
// the "Configure credential…" banner accurately on first open.
103108
void this.refreshAuthState();
@@ -118,10 +123,20 @@ export class AxmeSidebarProvider implements vscode.WebviewViewProvider {
118123
webviewView.webview.html = this.renderHtml(webviewView.webview);
119124
webviewView.webview.onDidReceiveMessage((m: SidebarMessage) => this.onMessage(m));
120125

121-
// Push the initial snapshot once webview is alive.
126+
// Push the initial snapshot once webview is alive. Hooks state is
127+
// read from disk on every reveal so reinstalls / external edits to
128+
// ~/.cursor/hooks.json show up the next time the sidebar comes into
129+
// view, not just at activation time.
122130
const counts = this.workspaceRoot ? readCounts(this.workspaceRoot) : emptyCounts();
123131
const backlog = this.workspaceRoot ? readBacklog(this.workspaceRoot).slice(0, 5) : [];
124-
this.push({ ...this.initialState, counts, backlog, warnTokens: SESSION_WARN_TOKENS, ...this.pendingState });
132+
this.push({
133+
...this.initialState,
134+
counts,
135+
backlog,
136+
hooksOk: hooksAreInstalled(),
137+
warnTokens: SESSION_WARN_TOKENS,
138+
...this.pendingState,
139+
});
125140
this.pendingState = {};
126141

127142
// Session polling — only runs while the view is visible. VS Code fires
@@ -234,9 +249,14 @@ export class AxmeSidebarProvider implements vscode.WebviewViewProvider {
234249
235250
<footer>
236251
<button class="link" data-cmd="axme.showStatus">Healthcheck…</button>
237-
<button class="link" data-cmd="axme.reindex">Reindex</button>
238-
<button class="link" data-cmd="axme.reset">Reset</button>
239252
</footer>
253+
<!--
254+
Reindex / Reset removed from the sidebar footer per user feedback —
255+
they ran a destructive or hard-to-undo flow without enough visible
256+
feedback inline. The commands stay registered and accessible via
257+
the Command Palette (AXME: Reindex semantic search / AXME: Reset),
258+
where the modal-driven flow is more obviously a deliberate action.
259+
-->
240260
241261
<script nonce="${nonce}">${SIDEBAR_JS}</script>
242262
</body>

src/server.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,31 @@ import {
4141
import { logEvent } from "./storage/worklog.js";
4242
import { spawnDetachedAuditWorker } from "./audit-spawner.js";
4343

44-
// --- Server state (detected at startup from cwd) ---
44+
// --- Server state (detected at startup from --workspace flag or cwd) ---
45+
//
46+
// When the Cursor extension registers the MCP server it cannot control the
47+
// cwd Cursor uses to spawn us — empirically Cursor spawns from the user's
48+
// home directory regardless of which workspace is open. Falling back to
49+
// cwd makes the server report a wrong `defaultProjectPath` and tools like
50+
// axme_context (called without explicit project_path) look up
51+
// `.axme-code/` in the wrong place. The fix is the extension passing
52+
// `--workspace <abs>` at registration time so the server has the right
53+
// root from the first tool call.
54+
//
55+
// Resolution order:
56+
// 1. --workspace <path> CLI flag (passed by the Cursor extension)
57+
// 2. AXME_WORKSPACE env var (escape hatch for hand-spawned servers)
58+
// 3. process.cwd() (legacy Claude Code CLI behaviour — server spawns
59+
// from the project root so cwd already points at the right place)
60+
function resolveServerRoot(): string {
61+
const argv = process.argv;
62+
const flagIdx = argv.indexOf("--workspace");
63+
if (flagIdx > -1 && argv[flagIdx + 1]) return argv[flagIdx + 1];
64+
if (process.env.AXME_WORKSPACE) return process.env.AXME_WORKSPACE;
65+
return process.cwd();
66+
}
4567

46-
const serverCwd = process.cwd();
68+
const serverCwd = resolveServerRoot();
4769
const serverHasGit = existsSync(join(serverCwd, ".git"));
4870
const serverWorkspace = detectWorkspace(serverCwd);
4971
const isWorkspace = serverHasGit ? false : serverWorkspace.type !== "single";

0 commit comments

Comments
 (0)