fix(adapters/omp): self-register MCP server on plugin load (#677) (#872)

The `omp plugin install context-mode` path wires the extension factory
so routing hooks fire, but never creates an mcp.json entry — so the 11
ctx_* tools stay unreachable even though curl/wget are hard-blocked.

Register the server on plugin load, only when absent (never clobbering a
user's existing entry). Spawn via `node <plugin>/server.bundle.mjs`
rather than the `context-mode` bin: under the plugin install the package
lives in ~/.omp/plugins/node_modules and its bin is not on PATH.

Takes effect on the next OMP restart, same as the manual mcp.json
workaround the issue documents.
This commit is contained in:
Ben Younes
2026-06-23 23:33:21 +03:00
committed by GitHub
parent ddda95c70d
commit e0c3bb2afa
3 changed files with 123 additions and 2 deletions
+2
View File
@@ -1067,6 +1067,8 @@ Full configs: [`configs/kiro/mcp.json`](configs/kiro/mcp.json) | [`configs/kiro/
Both should show `context-mode` as `enabled`.
> The plugin self-registers its MCP server in `~/.omp/agent/mcp.json` on first load (spawned as `node <plugin>/server.bundle.mjs`, since the plugin-install package directory is not on `PATH`), so the 11 `ctx_*` tools become reachable after the restart in step 2 — no manual `mcp.json` edit needed ([#677](https://github.com/mksglu/context-mode/issues/677)). An existing `context-mode` entry is never overwritten; remove it if you want the plugin to re-register the bundled path.
**Install — manual plugin path (if `omp plugin install` is unavailable):**
OMP loads anything listed under `~/.omp/plugins/package.json` `dependencies` whose own `package.json` carries an `omp` (or `pi`) field. New plugins default to enabled — the lock file at `~/.omp/plugins/omp-plugins.lock.json` is only consulted when a plugin needs to be explicitly **disabled** (loader skips `runtimeState && !runtimeState.enabled` per [`extensibility/plugins/loader.ts:89-94`](https://github.com/can1357/oh-my-pi/blob/main/packages/coding-agent/src/extensibility/plugins/loader.ts)). So the manual install is two commands:
+61 -2
View File
@@ -25,8 +25,9 @@
*/
import { createHash } from "node:crypto";
import { mkdirSync } from "node:fs";
import { join } from "node:path";
import { existsSync, mkdirSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { resolveSessionDbPath, SessionDB } from "../../session/db.js";
import { extractEvents } from "../../session/extract.js";
@@ -74,6 +75,59 @@ let _sessionId = "";
const _ompAdapter = new OMPAdapter();
// ── MCP self-registration (issue #677) ───────────────────
// The `omp plugin install context-mode` path wires THIS extension factory
// (so routing hooks fire), but never creates the MCP config — so the 11
// `ctx_*` tools stay unreachable even though curl/wget are blocked. Register
// the server ourselves on plugin load, ONLY when absent (never clobber a
// user's existing entry). Takes effect on the next OMP restart, same as the
// manual mcp.json workaround the issue documents.
const MCP_SERVER_NAME = "context-mode";
// plugin.js ships at <pkg>/build/adapters/omp/plugin.js; the MCP server
// bundle sits at the package root (<pkg>/server.bundle.mjs) — three up.
const SERVER_BUNDLE_RELATIVE = "../../../server.bundle.mjs";
function resolveServerBundle(): string | null {
try {
const here = dirname(fileURLToPath(import.meta.url));
const bundle = resolve(here, SERVER_BUNDLE_RELATIVE);
return existsSync(bundle) ? bundle : null;
} catch {
return null;
}
}
/**
* Ensure `~/.omp/agent/mcp.json` registers the context-mode MCP server.
*
* Uses `node <abs>/server.bundle.mjs` rather than the `context-mode` bin:
* under the plugin install the package lives in `~/.omp/plugins/node_modules`
* and its bin is NOT on PATH, so the bare command would fail to spawn (the
* exact symptom reported on issue #677). Best effort — never throws, never
* breaks plugin load.
*/
function ensureMcpServerRegistered(): void {
try {
const bundle = resolveServerBundle();
if (!bundle) return; // bundle missing → nothing safe to register
const settings = _ompAdapter.readSettings() ?? {};
const mcpServers =
(settings.mcpServers as Record<string, unknown> | undefined) ?? {};
if (MCP_SERVER_NAME in mcpServers) return; // already present — don't clobber
mcpServers[MCP_SERVER_NAME] = {
type: "stdio",
command: "node",
args: [bundle],
};
settings.mcpServers = mcpServers;
_ompAdapter.writeSettings(settings as Record<string, unknown>);
} catch {
// best effort — a registration failure must never break plugin load
}
}
function getSessionDir(): string {
const dir = _ompAdapter.getSessionDir();
mkdirSync(dir, { recursive: true });
@@ -189,6 +243,11 @@ export default function ompPlugin(pi: MinimalHookAPI): void {
// earlier `OMP_PROJECT_DIR` read was an EM mistake — no upstream code
// ever sets it. Drop it; fall through PI_PROJECT_DIR → cwd().
const projectDir = process.env.PI_PROJECT_DIR || process.cwd();
// Self-register the MCP server so `ctx_*` tools are reachable under the
// plugin install path, not just the manual MCP-only path (issue #677).
ensureMcpServerRegistered();
const db = getOrCreateDB(projectDir);
// ── 1. session_start — initialize session row ─────────
+60
View File
@@ -447,4 +447,64 @@ describe("OMP plugin", () => {
}
});
});
// ═══════════════════════════════════════════════════════════
// Slice 5: MCP self-registration (issue #677)
// ═══════════════════════════════════════════════════════════
// The `omp plugin install` path wires the extension factory (routing
// hooks fire) but never creates mcp.json, so the `ctx_*` tools stay
// unreachable. The plugin must self-register the MCP server on load.
describe("Slice 5: MCP self-registration (issue #677)", () => {
it("registers the context-mode MCP server in mcp.json when absent", async () => {
const { OMPAdapter } = await import("../../src/adapters/omp/index.js");
const { rmSync } = await import("node:fs");
const adapter = new OMPAdapter();
// Clean slate — drop any mcp.json a prior test in this suite wrote.
try {
rmSync(adapter.getSettingsPath(), { force: true });
} catch {
/* best effort */
}
await registerOmpPlugin(api);
const settings = adapter.readSettings();
const server = (settings?.mcpServers as Record<string, unknown> | undefined)?.[
"context-mode"
] as { command?: string; args?: unknown[] } | undefined;
expect(
server,
"context-mode must be registered in mcp.json after plugin load",
).toBeDefined();
// Plugin install puts the package in ~/.omp/plugins/node_modules where
// the `context-mode` bin is NOT on PATH — must spawn via node + bundle.
expect(server?.command).toBe("node");
expect(Array.isArray(server?.args)).toBe(true);
expect(String(server?.args?.[0])).toMatch(/server\.bundle\.mjs$/);
});
it("does NOT clobber an existing context-mode mcp.json entry", async () => {
const { OMPAdapter } = await import("../../src/adapters/omp/index.js");
const adapter = new OMPAdapter();
// User-supplied entry (e.g. the commenter's own working config) that
// must survive plugin load untouched.
adapter.writeSettings({
mcpServers: {
"context-mode": { command: "context-mode", args: ["--user-custom"] },
},
});
await registerOmpPlugin(api);
const settings = adapter.readSettings();
const server = (settings?.mcpServers as Record<string, unknown>)["context-mode"] as {
command?: string;
args?: unknown[];
};
expect(server.command).toBe("context-mode");
expect(server.args).toEqual(["--user-custom"]);
});
});
});