@minhpnq1807/contextos 0.1.3 → 0.1.6

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/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.6
4
+
5
+ - Adds a transparent stdio MCP telemetry proxy that records `tools/call` events while forwarding requests to the original MCP server.
6
+ - `ctx install` now wraps supported local MCP servers, including `code-review-graph` and `agentmemory`, so runtime-only tool rules can be measured from real MCP usage.
7
+
8
+ ## 0.1.5
9
+
10
+ - Sanitizes stale Stop reports at display time so previously recorded system-user rules no longer appear in `ctx report` or `ctx evidence` after upgrading.
11
+ - Filters system-user rules again inside the Stop hook to protect reports created from older prompt contexts.
12
+
13
+ ## 0.1.4
14
+
15
+ - Filters host/session user rules such as `sudo -u user`, `sudo su - user`, and "commands must run as user X" before scoring and injection.
16
+ - Prevents system-user setup instructions from inflating `unknown` outcomes or skewing ContextOS efficiency reports.
17
+
3
18
  ## 0.1.3
4
19
 
5
20
  - Separates runtime prompt/report/stats files per workspace under `~/.ctx/contextos/workspaces/<workspace-id>`.
package/README.md CHANGED
@@ -53,7 +53,7 @@ With ContextOS, each prompt gets a compact block:
53
53
  ```text
54
54
  ## Critical ContextOS rules
55
55
  - Use code-review-graph before reading files.
56
- - All shell commands must run as minh_dev.
56
+ - Check upload moderation flows before editing approval code.
57
57
 
58
58
  ## Suggested files to check
59
59
  - services/content-service/src/infrastructure/services/content-moderation.service.ts
@@ -66,6 +66,7 @@ With ContextOS, each prompt gets a compact block:
66
66
  - Registers a `ctx-mcp` MCP server that owns model loading and semantic scoring.
67
67
  - Reads the active `AGENTS.md` chain for the current workspace.
68
68
  - Scores rules by relevance to the user prompt.
69
+ - Filters host/session setup rules such as "run commands as user X" or `sudo -u user` because they are environment instructions, not project guidance.
69
70
  - Finds likely relevant files with a hybrid retriever:
70
71
  - first, local prompt/file heuristics create seed candidates;
71
72
  - then, if `.code-review-graph/graph.db` exists, ContextOS queries `code-review-graph` semantic search and re-ranks graph-backed matches;
@@ -104,6 +105,7 @@ node bin/ctx.js install
104
105
  3. Downloads and caches the required local MiniLM embedding model under `~/.ctx/contextos/models`.
105
106
  4. Warms `~/.ctx/contextos/embeddings.db` for AGENTS rules and project file paths.
106
107
  5. Registers the `ctx-mcp` MCP server and merges ContextOS global hooks into `$CODEX_HOME/hooks.json`.
108
+ 6. Wraps supported local MCP servers, currently `code-review-graph` and `agentmemory`, with a transparent telemetry proxy so `tools/call` events can be measured.
107
109
 
108
110
  Restart Codex after installing.
109
111
 
@@ -288,7 +290,9 @@ unknown = the rule was relevant, but the diff does not prove either way
288
290
 
289
291
  For runtime-only rules, ContextOS also checks `telemetry.jsonl` for hook-visible tool names, MCP server names, and command metadata. A rule like "use code-review-graph before reading files" can be marked `followed` when telemetry contains a matching `code-review-graph` signal.
290
292
 
291
- Example `unknown`: a rule says shell commands must run as a specific OS user, but neither git diff nor hook telemetry records that user identity. ContextOS cannot prove the rule was followed from available evidence alone.
293
+ `ctx install` wraps supported stdio MCP servers with a transparent proxy. The proxy forwards MCP JSON-RPC unchanged, but records `tools/call` requests such as `code-review-graph.detect_changes_tool` to workspace telemetry. This is what lets ContextOS prove runtime rules that cannot be seen in git diff.
294
+
295
+ Host/session setup rules such as "run shell commands as user X", `sudo su - user`, `sudo -i -u user`, and `sudo -u user` are filtered before scoring. They are not injected and do not count toward `unknown` outcomes because they describe the agent runtime environment rather than project behavior.
292
296
 
293
297
  ## Development
294
298
 
package/bin/ctx.js CHANGED
@@ -5,7 +5,7 @@ import { fileURLToPath } from "node:url";
5
5
  import { execFileSync } from "node:child_process";
6
6
 
7
7
  import { readAgentsChain } from "../plugins/ctx/lib/reader.js";
8
- import { parseRules, scoreRules } from "../plugins/ctx/lib/analyzer.js";
8
+ import { filterActionableRules, parseRules, scoreRules } from "../plugins/ctx/lib/analyzer.js";
9
9
  import { scheduleContext } from "../plugins/ctx/lib/scheduler.js";
10
10
  import { formatEvidence, formatReport } from "../plugins/ctx/lib/reporter.js";
11
11
  import { installGlobalHooks } from "../plugins/ctx/lib/global-hooks.js";
@@ -14,6 +14,7 @@ import { modelCacheDir, warmRuleEmbeddings } from "../plugins/ctx/lib/embedding-
14
14
  import { warmFileEmbeddings } from "../plugins/ctx/lib/file-embedding-retriever.js";
15
15
  import { scoreContext } from "../plugins/ctx/lib/score-context.js";
16
16
  import { defaultDataRoot, workspaceDataDir, workspaceMarkerPath } from "../plugins/ctx/lib/workspace-data.js";
17
+ import { installMcpTelemetryProxies } from "../plugins/ctx/lib/mcp-proxy-install.js";
17
18
 
18
19
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
19
20
  const rootDir = path.resolve(__dirname, "..");
@@ -101,6 +102,7 @@ async function install({ copy = false, inject = true } = {}) {
101
102
  runCodex(["plugin", "marketplace", "add", marketplaceRoot]);
102
103
  runCodex(["plugin", "add", "ctx@contextos"]);
103
104
  runCodex(["mcp", "add", "ctx-mcp", "--", "node", path.join(marketplaceRoot, "plugins", "ctx", "mcp", "server.js")]);
105
+ const proxyResult = installMcpTelemetryProxies({ codexHome: codexHome(), marketplaceRoot });
104
106
  const hooksPath = installGlobalHooks({ codexHome: codexHome(), marketplaceRoot, injectPromptContext: inject });
105
107
 
106
108
  console.log("Preparing required local embedding model...");
@@ -109,6 +111,7 @@ async function install({ copy = false, inject = true } = {}) {
109
111
  console.log(`Stable marketplace root: ${marketplaceRoot}`);
110
112
  console.log(`Installed ContextOS global hooks to ${hooksPath}`);
111
113
  console.log("Installed ctx-mcp MCP server.");
114
+ console.log(`MCP telemetry proxies: ${proxyResult.wrapped.length ? proxyResult.wrapped.map((item) => item.name).join(", ") : "none changed"}`);
112
115
  console.log(`Embedding model cache: ${modelCacheDir(contextOSDataDir())}`);
113
116
  console.log(`Embedding vectors cache: ${warmResult.cachePath}`);
114
117
  console.log(`File path embeddings warmed: ${warmResult.fileCount || 0}`);
@@ -222,7 +225,7 @@ async function debug(task) {
222
225
  async function warmEmbeddings(task) {
223
226
  const cwd = process.cwd();
224
227
  const merged = readAgentsChain({ cwd });
225
- const rules = scoreRules(parseRules(merged.content), task, []);
228
+ const rules = scoreRules(filterActionableRules(parseRules(merged.content)), task, []);
226
229
  const result = await warmRuleEmbeddings({
227
230
  rules,
228
231
  task,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minhpnq1807/contextos",
3
- "version": "0.1.3",
3
+ "version": "0.1.6",
4
4
  "description": "Task-aware AGENTS.md context injection and compliance reporting for Codex.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ctx",
3
- "version": "0.1.0",
3
+ "version": "0.1.6",
4
4
  "description": "Inject task-relevant AGENTS.md rules into Codex through plugin hooks.",
5
5
  "author": {
6
6
  "name": "ContextOS"
@@ -48,6 +48,20 @@ const SEMANTIC_ALIASES = {
48
48
 
49
49
  const MODERATION_TOKENS = new Set(["moderation", "moderate", "content-moderation", "approval", "approved", "reject", "rejected", "needs_review"]);
50
50
 
51
+ const SYSTEM_USER_RULE_PATTERNS = [
52
+ /\ball\s+shell\s+commands?\s+must\s+run\s+as\b/i,
53
+ /\bcommands?\s+must\s+run\s+as\b/i,
54
+ /\bstrictly\s+follow\s+this\s+sequence\b/i,
55
+ /\bswitch\s+the\s+user\s+context\b/i,
56
+ /\bdo\s+not\s+prefix\b.*\bsudo\s+-u\b/i,
57
+ /\bsudo\s+su\s+-\s*[a-z_][a-z0-9_-]*\b/i,
58
+ /\bsudo\s+-i\s+-u\s+[a-z_][a-z0-9_-]*\b/i,
59
+ /\bsudo\s+-u\s+[a-z_][a-z0-9_-]*\b/i,
60
+ /\bsu\s+-\s+[a-z_][a-z0-9_-]*\b/i,
61
+ /[/\\]\.codex[/\\]RTK\.md\b/i,
62
+ /\bminh_dev\b/i
63
+ ];
64
+
51
65
  export function tokenize(value) {
52
66
  const normalized = String(value || "")
53
67
  .toLowerCase()
@@ -138,6 +152,17 @@ export function parseRules(markdown) {
138
152
  return dedupeRules(rules);
139
153
  }
140
154
 
155
+ export function filterActionableRules(rules = []) {
156
+ return rules
157
+ .filter((rule) => !isSystemUserRule(rule))
158
+ .map((rule, index) => ({ ...rule, id: `r${index + 1}`, originalOrder: index }));
159
+ }
160
+
161
+ export function isSystemUserRule(rule) {
162
+ const content = typeof rule === "string" ? rule : rule?.content;
163
+ return SYSTEM_USER_RULE_PATTERNS.some((pattern) => pattern.test(String(content || "")));
164
+ }
165
+
141
166
  function dedupeRules(rules) {
142
167
  const seen = new Set();
143
168
  return rules.filter((rule) => {
@@ -0,0 +1,118 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ const DEFAULT_TARGETS = new Set(["code-review-graph", "agentmemory"]);
5
+
6
+ export function installMcpTelemetryProxies({ codexHome, marketplaceRoot, targets = DEFAULT_TARGETS } = {}) {
7
+ const configPath = path.join(codexHome, "config.toml");
8
+ if (!fs.existsSync(configPath)) return { wrapped: [], skipped: [], configPath };
9
+
10
+ const original = fs.readFileSync(configPath, "utf8");
11
+ const proxyPath = path.join(marketplaceRoot, "plugins", "ctx", "mcp", "proxy.js");
12
+ const result = rewriteMcpTelemetryProxies(original, { proxyPath, targets });
13
+ if (result.content !== original) {
14
+ fs.writeFileSync(configPath, result.content, "utf8");
15
+ }
16
+ return { ...result, configPath };
17
+ }
18
+
19
+ export function rewriteMcpTelemetryProxies(toml, { proxyPath, targets = DEFAULT_TARGETS } = {}) {
20
+ const lines = String(toml || "").split(/\r?\n/);
21
+ const sections = findMcpServerSections(lines);
22
+ const wrapped = [];
23
+ const skipped = [];
24
+
25
+ for (const section of sections.reverse()) {
26
+ if (!targets.has(section.name)) {
27
+ skipped.push({ name: section.name, reason: "not-targeted" });
28
+ continue;
29
+ }
30
+
31
+ const body = lines.slice(section.start + 1, section.end);
32
+ const command = findStringValue(body, "command");
33
+ const args = findArrayValue(body, "args") || [];
34
+ if (!command) {
35
+ skipped.push({ name: section.name, reason: "missing-command" });
36
+ continue;
37
+ }
38
+ if (command === "node" && args[0] === proxyPath) {
39
+ skipped.push({ name: section.name, reason: "already-wrapped" });
40
+ continue;
41
+ }
42
+
43
+ const nextBody = replaceOrInsertServerField(
44
+ replaceOrInsertServerField(body, "command", tomlString("node")),
45
+ "args",
46
+ tomlArray([proxyPath, "--name", section.name, "--", command, ...args])
47
+ );
48
+ lines.splice(section.start + 1, section.end - section.start - 1, ...nextBody);
49
+ wrapped.push({ name: section.name, command, args });
50
+ }
51
+
52
+ return { content: lines.join("\n"), wrapped: wrapped.reverse(), skipped: skipped.reverse() };
53
+ }
54
+
55
+ function findMcpServerSections(lines) {
56
+ const sections = [];
57
+ for (let index = 0; index < lines.length; index += 1) {
58
+ const match = lines[index].match(/^\[mcp_servers\.([^\].]+)\]\s*$/);
59
+ if (!match) continue;
60
+ let end = lines.length;
61
+ for (let cursor = index + 1; cursor < lines.length; cursor += 1) {
62
+ if (/^\[/.test(lines[cursor])) {
63
+ end = cursor;
64
+ break;
65
+ }
66
+ }
67
+ sections.push({ name: unquoteTomlKey(match[1]), start: index, end });
68
+ }
69
+ return sections;
70
+ }
71
+
72
+ function replaceOrInsertServerField(body, key, value) {
73
+ const next = [...body];
74
+ const index = next.findIndex((line) => new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`).test(line));
75
+ const line = `${key} = ${value}`;
76
+ if (index >= 0) next[index] = line;
77
+ else next.unshift(line);
78
+ return next;
79
+ }
80
+
81
+ function findStringValue(lines, key) {
82
+ const line = lines.find((item) => new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`).test(item));
83
+ if (!line) return null;
84
+ const match = line.match(/=\s*"((?:\\.|[^"\\])*)"/);
85
+ return match ? unescapeTomlString(match[1]) : null;
86
+ }
87
+
88
+ function findArrayValue(lines, key) {
89
+ const line = lines.find((item) => new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`).test(item));
90
+ if (!line) return null;
91
+ const arrayMatch = line.match(/=\s*\[(.*)\]\s*$/);
92
+ if (!arrayMatch) return null;
93
+ const values = [];
94
+ const pattern = /"((?:\\.|[^"\\])*)"/g;
95
+ let match;
96
+ while ((match = pattern.exec(arrayMatch[1]))) values.push(unescapeTomlString(match[1]));
97
+ return values;
98
+ }
99
+
100
+ function tomlArray(values) {
101
+ return `[${values.map(tomlString).join(", ")}]`;
102
+ }
103
+
104
+ function tomlString(value) {
105
+ return `"${String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
106
+ }
107
+
108
+ function unescapeTomlString(value) {
109
+ return String(value).replace(/\\"/g, '"').replace(/\\\\/g, "\\");
110
+ }
111
+
112
+ function unquoteTomlKey(value) {
113
+ return value.replace(/^"|"$/g, "");
114
+ }
115
+
116
+ function escapeRegExp(value) {
117
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
118
+ }
@@ -1,7 +1,10 @@
1
+ import { isSystemUserRule } from "./analyzer.js";
2
+
1
3
  export function buildReport({ cwd, prompt, relevantFiles, scheduled, gitSnapshot, compliance, runtimeEvidence }) {
2
- const followed = compliance.filter((item) => item.status === "followed");
3
- const ignored = compliance.filter((item) => item.status === "ignored");
4
- const unknown = compliance.filter((item) => item.status === "unknown");
4
+ const actionableCompliance = compliance.filter((item) => !isSystemUserRule(item.rule));
5
+ const followed = actionableCompliance.filter((item) => item.status === "followed");
6
+ const ignored = actionableCompliance.filter((item) => item.status === "ignored");
7
+ const unknown = actionableCompliance.filter((item) => item.status === "unknown");
5
8
  const measured = followed.length + ignored.length;
6
9
  const efficiencyScore = measured ? Math.round((followed.length / measured) * 100) : null;
7
10
 
@@ -24,6 +27,7 @@ export function buildReport({ cwd, prompt, relevantFiles, scheduled, gitSnapshot
24
27
  }
25
28
 
26
29
  export function formatReport(report) {
30
+ report = sanitizeReport(report);
27
31
  const lines = [];
28
32
  lines.push("ContextOS report");
29
33
  lines.push(`Efficiency: ${report.efficiencyScore == null ? "unknown" : `${report.efficiencyScore}%`}`);
@@ -55,6 +59,7 @@ export function formatReport(report) {
55
59
  }
56
60
 
57
61
  export function formatEvidence(report) {
62
+ report = sanitizeReport(report);
58
63
  const lines = [];
59
64
  lines.push("ContextOS evidence");
60
65
  lines.push(`Prompt: ${report.prompt || "(empty)"}`);
@@ -119,3 +124,20 @@ function summarizeRuntimeEvidence(runtimeEvidence = {}) {
119
124
  sources: (runtimeEvidence.sources || []).slice(0, 10)
120
125
  };
121
126
  }
127
+
128
+ function sanitizeReport(report = {}) {
129
+ const followed = (report.followed || []).filter((item) => !isSystemUserRule(item.rule));
130
+ const ignored = (report.ignored || []).filter((item) => !isSystemUserRule(item.rule));
131
+ const unknown = (report.unknown || []).filter((item) => !isSystemUserRule(item.rule));
132
+ const measured = followed.length + ignored.length;
133
+ return {
134
+ ...report,
135
+ injectedRuleCount: followed.length + ignored.length + unknown.length,
136
+ followed,
137
+ ignored,
138
+ unknown,
139
+ measuredRuleCount: measured,
140
+ unknownRuleCount: unknown.length,
141
+ efficiencyScore: measured ? Math.round((followed.length / measured) * 100) : null
142
+ };
143
+ }
@@ -1,7 +1,7 @@
1
1
  import path from "node:path";
2
2
 
3
3
  import { readAgentsChain } from "./reader.js";
4
- import { parseRules, scoreRules, findRelevantFiles } from "./analyzer.js";
4
+ import { filterActionableRules, parseRules, scoreRules, findRelevantFiles } from "./analyzer.js";
5
5
  import { enhanceRuleScoresWithEmbeddings } from "./embedding-scorer.js";
6
6
 
7
7
  export async function scoreContext({
@@ -15,7 +15,8 @@ export async function scoreContext({
15
15
  } = {}) {
16
16
  const started = Date.now();
17
17
  const merged = readAgentsChain({ cwd });
18
- const parsedRules = parseRules(merged.content);
18
+ const rawRules = parseRules(merged.content);
19
+ const parsedRules = filterActionableRules(rawRules);
19
20
  const baseScoredRules = scoreRules(parsedRules, prompt, openFiles);
20
21
  const embedding = await enhanceRuleScoresWithEmbeddings(baseScoredRules, prompt, {
21
22
  dataDir,
@@ -47,6 +48,7 @@ export async function scoreContext({
47
48
  model: embedding.model,
48
49
  cachePath: embedding.cachePath,
49
50
  rulesParsed: parsedRules.length,
51
+ rulesFiltered: rawRules.length - parsedRules.length,
50
52
  rulesInjected: scoredRules.filter((rule) => Number(rule.score || 0) >= 0.1).length,
51
53
  filesSuggested: suggestedFiles.length,
52
54
  sources: merged.sources.map((source) => path.relative(cwd, source))
@@ -4,14 +4,27 @@ import { appendJsonLine, readJsonFile, writeJsonFile } from "./fs-utils.js";
4
4
  import { readGitSnapshot, checkCompliance } from "./measure.js";
5
5
  import { buildReport, formatReport } from "./reporter.js";
6
6
  import { loadRuntimeEvidence } from "./telemetry.js";
7
+ import { filterActionableRules } from "./analyzer.js";
7
8
 
8
9
  export function handleStopPayload(payload, { contextPath, reportPath, historyPath, telemetryPath } = {}) {
9
10
  const cwd = payload.cwd || payload.working_directory || process.cwd();
10
11
  const promptContext = contextPath && fs.existsSync(contextPath) ? readJsonFile(contextPath) : null;
11
- const scheduledRules = [
12
+ const rawScheduledRules = [
12
13
  ...(promptContext?.scheduled?.highRules || []),
13
14
  ...(promptContext?.scheduled?.midRules || [])
14
15
  ];
16
+ const scheduledRules = filterActionableRules(rawScheduledRules);
17
+ const scheduled = promptContext?.scheduled
18
+ ? {
19
+ ...promptContext.scheduled,
20
+ highRules: filterActionableRules(promptContext.scheduled.highRules || []),
21
+ midRules: filterActionableRules(promptContext.scheduled.midRules || []),
22
+ droppedRules: [
23
+ ...(promptContext.scheduled.droppedRules || []),
24
+ ...rawScheduledRules.filter((rule) => !scheduledRules.some((item) => item.content === rule.content && item.sourcePath === rule.sourcePath))
25
+ ]
26
+ }
27
+ : null;
15
28
  const gitSnapshot = readGitSnapshot({ cwd });
16
29
  const runtimeEvidence = loadRuntimeEvidence({
17
30
  telemetryPath,
@@ -28,7 +41,7 @@ export function handleStopPayload(payload, { contextPath, reportPath, historyPat
28
41
  cwd,
29
42
  prompt: promptContext?.prompt || "",
30
43
  relevantFiles: promptContext?.relevantFiles || [],
31
- scheduled: promptContext?.scheduled || null,
44
+ scheduled,
32
45
  gitSnapshot,
33
46
  compliance,
34
47
  runtimeEvidence
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from "node:child_process";
3
+ import path from "node:path";
4
+
5
+ import { appendTelemetry } from "../lib/telemetry.js";
6
+ import { workspaceDataDir } from "../lib/workspace-data.js";
7
+
8
+ const { serverName, command, args } = parseArgs(process.argv.slice(2));
9
+ const cwd = process.cwd();
10
+ const telemetryPath = path.join(workspaceDataDir({ cwd }), "telemetry.jsonl");
11
+ let inspectBuffer = "";
12
+
13
+ const child = spawn(command, args, {
14
+ cwd,
15
+ env: process.env,
16
+ stdio: ["pipe", "pipe", "pipe"]
17
+ });
18
+
19
+ process.stdin.on("data", (chunk) => {
20
+ inspectClientChunk(chunk);
21
+ child.stdin.write(chunk);
22
+ });
23
+
24
+ process.stdin.on("end", () => {
25
+ child.stdin.end();
26
+ });
27
+
28
+ child.stdout.on("data", (chunk) => {
29
+ process.stdout.write(chunk);
30
+ });
31
+
32
+ child.stderr.on("data", (chunk) => {
33
+ process.stderr.write(chunk);
34
+ });
35
+
36
+ child.on("error", (error) => {
37
+ process.stderr.write(`contextos mcp proxy failed to start ${serverName}: ${error?.message || String(error)}\n`);
38
+ process.exitCode = 1;
39
+ });
40
+
41
+ child.on("exit", (code, signal) => {
42
+ if (signal) process.kill(process.pid, signal);
43
+ else process.exit(code ?? 0);
44
+ });
45
+
46
+ function inspectClientChunk(chunk) {
47
+ inspectBuffer += chunk.toString("utf8");
48
+ const lines = inspectBuffer.split(/\r?\n/);
49
+ inspectBuffer = lines.pop() || "";
50
+ for (const line of lines.filter(Boolean)) {
51
+ let message;
52
+ try {
53
+ message = JSON.parse(line);
54
+ } catch {
55
+ continue;
56
+ }
57
+ if (message?.method !== "tools/call") continue;
58
+ const toolName = message.params?.name || "unknown";
59
+ appendTelemetry({
60
+ telemetryPath,
61
+ event: "McpToolCall",
62
+ payload: {
63
+ cwd,
64
+ mcp: serverName,
65
+ server: serverName,
66
+ toolName: `${serverName}.${toolName}`,
67
+ tool: toolName,
68
+ method: message.method
69
+ }
70
+ });
71
+ }
72
+ }
73
+
74
+ function parseArgs(argv) {
75
+ const separator = argv.indexOf("--");
76
+ if (separator < 0) usage();
77
+
78
+ const before = argv.slice(0, separator);
79
+ const after = argv.slice(separator + 1);
80
+ const nameIndex = before.indexOf("--name");
81
+ const serverName = nameIndex >= 0 ? before[nameIndex + 1] : null;
82
+ const command = after[0];
83
+ const args = after.slice(1);
84
+
85
+ if (!serverName || !command) usage();
86
+ return { serverName, command, args };
87
+ }
88
+
89
+ function usage() {
90
+ process.stderr.write("Usage: node proxy.js --name <mcp-server-name> -- <command> [...args]\n");
91
+ process.exit(2);
92
+ }