fix(mcp-npm): v0.1.1 skip npm shim when resolving Go binary
npx installs a bin also named opsxdb-mcp; the launcher now only execs native ELF/Mach-O/PE binaries so it no longer recurses into itself.
This commit is contained in:
1 parent
979efb606d
commit
0aeee2b692
3 files changed
+79
-9
No files matched your search
@@ -2,14 +2,16 @@
|
|||||||
|
|
||||||
MCP (Model Context Protocol) launcher for OpsxDB.
|
MCP (Model Context Protocol) launcher for OpsxDB.
|
||||||
|
|
||||||
This package ships a small Node CLI that **spawns the Go `opsxdb-mcp` binary** over stdio. Build the binary from the OpsxDB repo, then point the launcher at it.
|
This package ships a small Node CLI that **spawns the Go `opsxdb-mcp` binary** over stdio. Build the binary from the OpsxDB repo, then point the launcher at it (or put the Go binary on `PATH`).
|
||||||
|
|
||||||
|
> The npm bin is also named `opsxdb-mcp`. The launcher only execs a **native** Go/ELF/Mach-O/PE binary, so it will not recurse into its own npm shim.
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm i -g @rlwj-lab/opsxdb-mcp
|
npm i -g @rlwj-lab/opsxdb-mcp
|
||||||
# or
|
# or
|
||||||
npx -y @rlwj-lab/opsxdb-mcp
|
npx -y @rlwj-lab/opsxdb-mcp@0.1.1
|
||||||
```
|
```
|
||||||
|
|
||||||
## Build the Go binary
|
## Build the Go binary
|
||||||
@@ -26,6 +28,8 @@ Put `bin/opsxdb-mcp` on `PATH`, or set:
|
|||||||
export OPSXDB_MCP_BIN=/absolute/path/to/opsxdb-mcp
|
export OPSXDB_MCP_BIN=/absolute/path/to/opsxdb-mcp
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If the Go binary is missing, the CLI exits with a clear error (not `command not found`).
|
||||||
|
|
||||||
## Cursor / Claude Desktop
|
## Cursor / Claude Desktop
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|||||||
+72
-6
@@ -7,11 +7,16 @@ const os = require('os');
|
|||||||
/**
|
/**
|
||||||
* Resolve the OpsxDB Go MCP binary path.
|
* Resolve the OpsxDB Go MCP binary path.
|
||||||
* Order: OPSXDB_MCP_BIN → PATH → common install locations.
|
* Order: OPSXDB_MCP_BIN → PATH → common install locations.
|
||||||
|
* Skips this package's Node launcher and any #! script shims (npm/npx bin
|
||||||
|
* is also named opsxdb-mcp and must not be mistaken for the Go server).
|
||||||
* @returns {string|null}
|
* @returns {string|null}
|
||||||
*/
|
*/
|
||||||
function resolveBinary() {
|
function resolveBinary() {
|
||||||
const envBin = process.env.OPSXDB_MCP_BIN;
|
const envBin = process.env.OPSXDB_MCP_BIN;
|
||||||
if (envBin && isExecutable(envBin)) return path.resolve(envBin);
|
if (envBin) {
|
||||||
|
const resolved = path.resolve(envBin);
|
||||||
|
if (isGoMcpBinary(resolved)) return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
const onPath = findOnPath('opsxdb-mcp');
|
const onPath = findOnPath('opsxdb-mcp');
|
||||||
if (onPath) return onPath;
|
if (onPath) return onPath;
|
||||||
@@ -25,7 +30,7 @@ function resolveBinary() {
|
|||||||
'/opt/homebrew/bin/opsxdb-mcp',
|
'/opt/homebrew/bin/opsxdb-mcp',
|
||||||
];
|
];
|
||||||
for (const c of candidates) {
|
for (const c of candidates) {
|
||||||
if (isExecutable(c)) return c;
|
if (isGoMcpBinary(c)) return c;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -35,16 +40,38 @@ function findOnPath(name) {
|
|||||||
for (const dir of pathEnv.split(path.delimiter)) {
|
for (const dir of pathEnv.split(path.delimiter)) {
|
||||||
if (!dir) continue;
|
if (!dir) continue;
|
||||||
const p = path.join(dir, name);
|
const p = path.join(dir, name);
|
||||||
if (isExecutable(p)) return p;
|
if (isGoMcpBinary(p)) return p;
|
||||||
if (process.platform === 'win32' && isExecutable(p + '.exe')) return p + '.exe';
|
if (process.platform === 'win32') {
|
||||||
|
const pe = p + '.exe';
|
||||||
|
if (isGoMcpBinary(pe)) return pe;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isExecutable(filePath) {
|
function selfCliPath() {
|
||||||
|
return path.resolve(__dirname, '..', 'bin', 'cli.js');
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSameFile(a, b) {
|
||||||
try {
|
try {
|
||||||
|
return fs.realpathSync(a) === fs.realpathSync(b);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True only for a real Go/native opsxdb-mcp binary (ELF / Mach-O / PE),
|
||||||
|
* not npm bin shims or this package's Node cli.js.
|
||||||
|
*/
|
||||||
|
function isGoMcpBinary(filePath) {
|
||||||
|
try {
|
||||||
|
if (!filePath) return false;
|
||||||
const st = fs.statSync(filePath);
|
const st = fs.statSync(filePath);
|
||||||
if (!st.isFile()) return false;
|
if (!st.isFile()) return false;
|
||||||
|
if (isSameFile(filePath, selfCliPath())) return false;
|
||||||
|
if (!isLikelyNativeBinary(filePath)) return false;
|
||||||
fs.accessSync(filePath, fs.constants.X_OK);
|
fs.accessSync(filePath, fs.constants.X_OK);
|
||||||
return true;
|
return true;
|
||||||
} catch {
|
} catch {
|
||||||
@@ -52,6 +79,44 @@ function isExecutable(filePath) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isLikelyNativeBinary(filePath) {
|
||||||
|
let fd;
|
||||||
|
try {
|
||||||
|
fd = fs.openSync(filePath, 'r');
|
||||||
|
const buf = Buffer.alloc(8);
|
||||||
|
const n = fs.readSync(fd, buf, 0, 8, 0);
|
||||||
|
if (n < 2) return false;
|
||||||
|
// Reject scripts (npm shims, node shebang)
|
||||||
|
if (buf[0] === 0x23 && buf[1] === 0x21) return false; // #!
|
||||||
|
// ELF
|
||||||
|
if (n >= 4 && buf[0] === 0x7f && buf[1] === 0x45 && buf[2] === 0x4c && buf[3] === 0x46) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// PE
|
||||||
|
if (buf[0] === 0x4d && buf[1] === 0x5a) return true;
|
||||||
|
if (n < 4) return false;
|
||||||
|
const be = buf.readUInt32BE(0);
|
||||||
|
const le = buf.readUInt32LE(0);
|
||||||
|
// Mach-O 64 / 32 (both endians) + fat
|
||||||
|
const macho = new Set([
|
||||||
|
0xfeedfacf, 0xcffaedfe, 0xfeedface, 0xcefaedfe,
|
||||||
|
0xcafebabe, 0xbebafeca, 0xcafebabf, 0xbfbafeca,
|
||||||
|
]);
|
||||||
|
if (macho.has(be) || macho.has(le)) return true;
|
||||||
|
return false;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
if (fd !== undefined) {
|
||||||
|
try {
|
||||||
|
fs.closeSync(fd);
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function missingBinaryMessage() {
|
function missingBinaryMessage() {
|
||||||
return `opsxdb-mcp: Go binary not found.
|
return `opsxdb-mcp: Go binary not found.
|
||||||
|
|
||||||
@@ -63,7 +128,7 @@ Build it from the opsxdb repo:
|
|||||||
cd server && go build -o ../bin/opsxdb-mcp ./cmd/opsxdb-mcp
|
cd server && go build -o ../bin/opsxdb-mcp ./cmd/opsxdb-mcp
|
||||||
|
|
||||||
Then either:
|
Then either:
|
||||||
- put bin/opsxdb-mcp on your PATH, or
|
- put that binary on your PATH (ahead of any npm shim), or
|
||||||
- export OPSXDB_MCP_BIN=/absolute/path/to/opsxdb-mcp
|
- export OPSXDB_MCP_BIN=/absolute/path/to/opsxdb-mcp
|
||||||
|
|
||||||
Cursor / Claude Desktop example:
|
Cursor / Claude Desktop example:
|
||||||
@@ -89,4 +154,5 @@ Config files (same as native binary):
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
resolveBinary,
|
resolveBinary,
|
||||||
missingBinaryMessage,
|
missingBinaryMessage,
|
||||||
|
isGoMcpBinary,
|
||||||
};
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@rlwj-lab/opsxdb-mcp",
|
"name": "@rlwj-lab/opsxdb-mcp",
|
||||||
"version": "0.1.0",
|
"version": "0.1.1",
|
||||||
"description": "MCP server for opsxdb",
|
"description": "MCP server for opsxdb",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|||||||
Reference in new issue
Block a user