onto-mcp 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -100,12 +100,26 @@ Mechanism per host:
100
100
  | Cursor | edits `mcpServers.onto` in `~/.cursor/mcp.json` |
101
101
 
102
102
  For the CLI-backed hosts, `onto register` prefers the official CLI and falls back
103
- to printing manual instructions when it is not on PATH. JSON edits preserve any
103
+ to printing manual instructions when it is not on PATH. It verifies the result
104
+ after `mcp add` and reports `failed` (not a false `registered`) if the CLI exits
105
+ successfully but the server is not listed afterward — e.g. when `claude` on PATH
106
+ is an alias/wrapper or points at the wrong profile. JSON edits preserve any
104
107
  servers already present and are idempotent (re-running reports `skipped`).
105
108
  Registration writes only host-owned config; it never writes onto runtime data.
106
109
  Restart the host app after registering to pick up the new server. Override the
107
110
  launched command or server name with `--command <cmd>` / `--name <id>`.
108
111
 
112
+ **Claude Code profiles.** Claude Code stores MCP servers per config directory
113
+ (`CLAUDE_CONFIG_DIR`). If you run multiple profiles (e.g. `~/.claude`,
114
+ `~/.claude-1`), target one explicitly so registration lands in the right place:
115
+
116
+ ```bash
117
+ onto register --hosts claude-code --claude-config-dir ~/.claude-1 --yes
118
+ ```
119
+
120
+ When `--claude-config-dir` is omitted, an ambient `CLAUDE_CONFIG_DIR` is honored
121
+ (shown in the plan), otherwise the claude default `~/.claude` is used.
122
+
109
123
  For project-local installs, add `onto-mcp` to the project and run the local
110
124
  binary:
111
125
 
@@ -2,9 +2,12 @@ import { execFileSync } from "node:child_process";
2
2
  import { isCommandOnPath } from "./path-scan.js";
3
3
  export const defaultCommandRunner = {
4
4
  exists: (command) => isCommandOnPath(command),
5
- run: (command, args) => {
5
+ run: (command, args, env) => {
6
6
  try {
7
- const stdout = execFileSync(command, args, { encoding: "utf8" });
7
+ const stdout = execFileSync(command, args, {
8
+ encoding: "utf8",
9
+ ...(env ? { env: { ...process.env, ...env } } : {}),
10
+ });
8
11
  return { status: 0, stdout, stderr: "" };
9
12
  }
10
13
  catch (error) {
@@ -21,21 +24,22 @@ export const defaultCommandRunner = {
21
24
  }
22
25
  },
23
26
  };
24
- function isAlreadyRegistered(spec, runner, entry) {
25
- const result = runner.run(spec.cli, spec.listArgs());
27
+ function probeRegistered(spec, runner, entry) {
28
+ const result = runner.run(spec.cli, spec.listArgs(), spec.commandEnv);
26
29
  if (result.status !== 0)
27
- return false;
30
+ return "unknown";
28
31
  const haystack = `${result.stdout}\n${result.stderr}`;
29
32
  // CLIs print one server name per line; a word-boundary match avoids
30
33
  // false positives from substrings of other server names.
31
34
  const pattern = new RegExp(`(^|[^\\w-])${escapeRegExp(entry.name)}([^\\w-]|$)`, "m");
32
- return pattern.test(haystack);
35
+ return pattern.test(haystack) ? "present" : "absent";
33
36
  }
34
37
  function escapeRegExp(value) {
35
38
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
36
39
  }
37
40
  export function createCliHost(spec, runner = defaultCommandRunner) {
38
41
  const detect = () => (runner.exists(spec.cli) ? "cli" : "absent");
42
+ const withNote = (summary) => spec.targetNote ? `${summary} (${spec.targetNote})` : summary;
39
43
  return {
40
44
  id: spec.id,
41
45
  displayName: spec.displayName,
@@ -58,52 +62,70 @@ export function createCliHost(spec, runner = defaultCommandRunner) {
58
62
  displayName: spec.displayName,
59
63
  detection,
60
64
  method: "cli",
61
- summary: `Run: ${commandLine}`,
65
+ summary: withNote(`Run: ${commandLine}`),
62
66
  commandLine,
63
67
  };
64
68
  },
65
69
  async apply(entry, options) {
70
+ const base = { hostId: spec.id, displayName: spec.displayName };
66
71
  if (detect() === "absent") {
67
- return {
68
- hostId: spec.id,
69
- displayName: spec.displayName,
70
- outcome: "manual",
71
- detail: spec.manualInstructions(entry),
72
- };
72
+ return { ...base, outcome: "manual", detail: spec.manualInstructions(entry) };
73
73
  }
74
- const exists = isAlreadyRegistered(spec, runner, entry);
75
- if (exists && !options.force) {
74
+ const before = probeRegistered(spec, runner, entry);
75
+ if (before === "present" && !options.force) {
76
76
  return {
77
- hostId: spec.id,
78
- displayName: spec.displayName,
77
+ ...base,
79
78
  outcome: "skipped",
80
79
  detail: `${entry.name} already registered (use --force to re-add)`,
81
80
  };
82
81
  }
83
- if (exists && options.force) {
84
- runner.run(spec.cli, spec.removeArgs(entry)); // best effort; ignore failure
82
+ if (before === "present" && options.force) {
83
+ runner.run(spec.cli, spec.removeArgs(entry), spec.commandEnv); // best effort
85
84
  }
86
- const result = runner.run(spec.cli, spec.addArgs(entry));
87
- if (result.status === 0) {
85
+ const result = runner.run(spec.cli, spec.addArgs(entry), spec.commandEnv);
86
+ if (result.status !== 0) {
88
87
  return {
89
- hostId: spec.id,
90
- displayName: spec.displayName,
91
- outcome: exists ? "updated" : "registered",
92
- detail: `${spec.cli} ${spec.addArgs(entry).join(" ")}`,
88
+ ...base,
89
+ outcome: "failed",
90
+ detail: (result.stderr || result.stdout || "command failed").trim(),
91
+ };
92
+ }
93
+ // Verify the add actually took effect. A CLI that exits 0 without
94
+ // registering (aliased/wrapper `claude`, wrong profile) must not be
95
+ // reported as success.
96
+ const after = probeRegistered(spec, runner, entry);
97
+ const outcome = before === "present" ? "updated" : "registered";
98
+ if (after === "present") {
99
+ return { ...base, outcome, detail: `${spec.cli} ${spec.addArgs(entry).join(" ")}` };
100
+ }
101
+ if (after === "unknown") {
102
+ return {
103
+ ...base,
104
+ outcome,
105
+ detail: `${spec.cli} ${spec.addArgs(entry).join(" ")} (could not verify via ${spec.cli} mcp list)`,
93
106
  };
94
107
  }
95
108
  return {
96
- hostId: spec.id,
97
- displayName: spec.displayName,
109
+ ...base,
98
110
  outcome: "failed",
99
- detail: (result.stderr || result.stdout || "command failed").trim(),
111
+ detail: `${spec.cli} accepted the command but ${entry.name} is not listed afterward. ` +
112
+ `The ${spec.cli} on PATH may be an alias/wrapper or target a different profile` +
113
+ (spec.targetNote ? ` (${spec.targetNote})` : "") +
114
+ `. Try registering against the real CLI/profile directly.`,
100
115
  };
101
116
  },
102
117
  };
103
118
  }
104
- /** Claude Code — registers at user scope so it applies across all projects. */
105
- export function createClaudeCodeHost(runner) {
106
- return createCliHost({
119
+ /**
120
+ * Claude Code — registers at user scope so it applies across all projects.
121
+ *
122
+ * Config-dir resolution: explicit `configDir` wins; otherwise an ambient
123
+ * `CLAUDE_CONFIG_DIR` is honored (and shown in the plan); otherwise the claude
124
+ * default (`~/.claude`) applies.
125
+ */
126
+ export function createClaudeCodeHost(options = {}) {
127
+ const effectiveDir = options.configDir ?? process.env.CLAUDE_CONFIG_DIR;
128
+ const spec = {
107
129
  id: "claude-code",
108
130
  displayName: "Claude Code",
109
131
  cli: "claude",
@@ -121,7 +143,10 @@ export function createClaudeCodeHost(runner) {
121
143
  listArgs: () => ["mcp", "list"],
122
144
  manualInstructions: (entry) => `claude CLI not found. Install Claude Code, then run:\n` +
123
145
  ` claude mcp add ${entry.name} -s user -- ${entry.command} ${entry.args.join(" ")}`,
124
- }, runner);
146
+ ...(effectiveDir ? { commandEnv: { CLAUDE_CONFIG_DIR: effectiveDir } } : {}),
147
+ targetNote: effectiveDir ? `config dir: ${effectiveDir}` : "config dir: claude default (~/.claude)",
148
+ };
149
+ return createCliHost(spec, options.runner);
125
150
  }
126
151
  /** Codex CLI. */
127
152
  export function createCodexHost(runner) {
@@ -4,9 +4,9 @@ import { claudeDesktopConfigPath, createJsonConfigHost, cursorConfigPath, } from
4
4
  * The four supported hosts in display order. CLI-backed hosts (Claude Code,
5
5
  * Codex) come first; JSON-config hosts (Claude Desktop, Cursor) follow.
6
6
  */
7
- export function getDefaultHostTargets() {
7
+ export function getDefaultHostTargets(options = {}) {
8
8
  return [
9
- createClaudeCodeHost(),
9
+ createClaudeCodeHost(options.claudeConfigDir ? { configDir: options.claudeConfigDir } : {}),
10
10
  createCodexHost(),
11
11
  createJsonConfigHost({
12
12
  id: "claude-desktop",
@@ -18,6 +18,8 @@ const USAGE = [
18
18
  " --force Re-register CLI hosts even if already present",
19
19
  " --name <id> MCP server name (default: onto)",
20
20
  " --command <cmd> Executable the host launches (default: onto)",
21
+ " --claude-config-dir <path> Target a Claude Code profile (sets",
22
+ " CLAUDE_CONFIG_DIR; default: ambient env or ~/.claude)",
21
23
  " --help, -h Show this help",
22
24
  ].join("\n");
23
25
  export function parseRegisterArgs(argv) {
@@ -30,6 +32,7 @@ export function parseRegisterArgs(argv) {
30
32
  help: false,
31
33
  name: "onto",
32
34
  command: "onto",
35
+ claudeConfigDir: undefined,
33
36
  unknownFlags: [],
34
37
  invalidHosts: [],
35
38
  };
@@ -76,6 +79,9 @@ export function parseRegisterArgs(argv) {
76
79
  case "--command":
77
80
  parsed.command = argv[++i] ?? parsed.command;
78
81
  break;
82
+ case "--claude-config-dir":
83
+ parsed.claudeConfigDir = argv[++i] ?? parsed.claudeConfigDir;
84
+ break;
79
85
  default:
80
86
  parsed.unknownFlags.push(arg);
81
87
  break;
@@ -133,7 +139,8 @@ export async function runRegister(argv, deps = {}) {
133
139
  `Valid: ${ALL_HOST_IDS.join(", ")}`);
134
140
  return 1;
135
141
  }
136
- const targets = deps.targets ?? getDefaultHostTargets();
142
+ const targets = deps.targets ??
143
+ getDefaultHostTargets(parsed.claudeConfigDir ? { claudeConfigDir: parsed.claudeConfigDir } : {});
137
144
  const isTty = deps.isTty ?? Boolean(process.stdin.isTTY);
138
145
  if (parsed.list) {
139
146
  console.log("Host detection:");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "onto-mcp",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "MCP-native ontology review runtime with context-isolated lenses and controlled deliberation",
5
5
  "homepage": "https://github.com/kangminlee-maker/onto-mcp#readme",
6
6
  "bugs": {