onto-mcp 0.4.0 → 0.4.2
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.
|
|
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, {
|
|
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
|
|
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
|
|
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
|
|
75
|
-
if (
|
|
74
|
+
const before = probeRegistered(spec, runner, entry);
|
|
75
|
+
if (before === "present" && !options.force) {
|
|
76
76
|
return {
|
|
77
|
-
|
|
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 (
|
|
84
|
-
runner.run(spec.cli, spec.removeArgs(entry)); // best effort
|
|
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
|
|
85
|
+
const result = runner.run(spec.cli, spec.addArgs(entry), spec.commandEnv);
|
|
86
|
+
if (result.status !== 0) {
|
|
88
87
|
return {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
97
|
-
displayName: spec.displayName,
|
|
109
|
+
...base,
|
|
98
110
|
outcome: "failed",
|
|
99
|
-
detail:
|
|
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
|
-
/**
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
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 ??
|
|
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/dist/mcp/server.js
CHANGED
|
@@ -507,6 +507,9 @@ function toReviewRequest(input) {
|
|
|
507
507
|
return request;
|
|
508
508
|
}
|
|
509
509
|
function formatToolResult(data) {
|
|
510
|
+
// Per MCP, `structuredContent` must be a JSON object. Callers that produce a
|
|
511
|
+
// top-level array (e.g. a list of domains/profiles) must wrap it in an object
|
|
512
|
+
// before calling this, or strict MCP clients reject the result.
|
|
510
513
|
const text = JSON.stringify(data, null, 2);
|
|
511
514
|
return {
|
|
512
515
|
content: [{ type: "text", text }],
|
|
@@ -963,11 +966,15 @@ async function callTool(name, args, options = {}) {
|
|
|
963
966
|
return formatToolResult(await reviewApi.listLenses());
|
|
964
967
|
case "onto.list_domains": {
|
|
965
968
|
const parsed = OntoListDomainsToolInputSchema.parse(args ?? {});
|
|
966
|
-
|
|
969
|
+
const domains = await reviewApi.listDomains(parsed.projectRoot);
|
|
970
|
+
// Wrap the array so structuredContent is a JSON object (MCP requirement).
|
|
971
|
+
return formatToolResult({ domains });
|
|
967
972
|
}
|
|
968
973
|
case "onto.list_source_profiles": {
|
|
969
974
|
const parsed = OntoListSourceProfilesToolInputSchema.parse(args ?? {});
|
|
970
|
-
|
|
975
|
+
const sourceProfiles = await reconstructApi.listSourceProfiles(parsed.projectRoot);
|
|
976
|
+
// Wrap the array so structuredContent is a JSON object (MCP requirement).
|
|
977
|
+
return formatToolResult({ sourceProfiles });
|
|
971
978
|
}
|
|
972
979
|
case "onto.observe_source": {
|
|
973
980
|
const parsed = OntoObserveSourceToolInputSchema.parse(args);
|
package/package.json
CHANGED