Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
Tool,
ToolSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { realpathSync } from "node:fs";
import * as fs from "node:fs/promises";
import * as path from "node:path";
import * as os from "node:os";
Expand Down Expand Up @@ -864,10 +865,17 @@ async function runServer() {
}

// Only start the stdio server when executed directly as a binary, not when
// imported by tests. process.argv[1] is the entrypoint script path.
const isMain =
process.argv[1] &&
fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
// imported by tests. npm installs bin entries as SYMLINKS (node_modules/.bin,
// npx cache), so argv[1] must be realpath'd before comparing — path.resolve
// alone breaks `npx @kazuph/mcp-taskmanager`.
const isMain = (() => {
if (!process.argv[1]) return false;
try {
return fileURLToPath(import.meta.url) === realpathSync(process.argv[1]);
} catch {
return false;
}
})();

if (isMain) {
runServer().catch((error) => {
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@kazuph/mcp-taskmanager",
"version": "1.1.0",
"version": "1.1.1",
"description": "Model Context Protocol server for Task Management",
"author": "kazuph (https://x.com/kazuph)",
"main": "dist/index.js",
Expand Down
103 changes: 103 additions & 0 deletions tests/bin-entrypoint.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { spawn } from "node:child_process";
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { fileURLToPath } from "node:url";
import { afterAll, beforeAll, describe, expect, it } from "vitest";

// npm installs bin entries as symlinks (node_modules/.bin, the npx cache).
// v1.1.0 shipped an entrypoint guard that compared path.resolve(argv[1])
// against the real module path, so the server silently never started when
// launched through a symlink. This test reproduces that exact launch shape.

const distEntry = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
"../dist/index.js"
);

let tmpDir: string;
let symlinkPath: string;

function runServerOnce(
entry: string,
taskFile: string
): Promise<{ stdoutLines: string[]; stderr: string }> {
return new Promise((resolve, reject) => {
const p = spawn("node", [entry], {
env: { ...process.env, TASK_MANAGER_FILE_PATH: taskFile },
});
let out = "";
let err = "";
p.stdout.on("data", (d) => {
out += d.toString();
});
p.stderr.on("data", (d) => {
err += d.toString();
});
p.on("error", reject);
p.stdin.write(
`${JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "initialize",
params: {
protocolVersion: "2024-11-05",
capabilities: {},
clientInfo: { name: "bin-test", version: "1.0" },
},
})}\n`
);
setTimeout(() => {
p.stdin.write(
`${JSON.stringify({
jsonrpc: "2.0",
id: 2,
method: "tools/list",
params: {},
})}\n`
);
}, 300);
setTimeout(() => {
p.kill();
resolve({ stdoutLines: out.trim().split("\n").filter(Boolean), stderr: err });
}, 900);
Comment on lines +60 to +63
});
}

describe("bin entrypoint", () => {
beforeAll(async () => {
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "mcp-tm-bin-"));
symlinkPath = path.join(tmpDir, "mcp-taskmanager");
await fs.symlink(distEntry, symlinkPath);
});
Comment on lines +68 to +72

afterAll(async () => {
await fs.rm(tmpDir, { recursive: true, force: true });
});

it("starts the server when launched through a symlink (npx-style)", async () => {
const { stdoutLines, stderr } = await runServerOnce(
symlinkPath,
path.join(tmpDir, "via-symlink", "tasks.json")
);
expect(stderr).toContain("Task Manager MCP Server running");
const tools = stdoutLines
.map((l) => {
try {
return JSON.parse(l);
} catch {
return null;
}
})
.find((m) => m && m.id === 2);
expect(tools?.result?.tools?.length).toBe(10);
});

it("starts the server when launched via the direct path", async () => {
const { stderr } = await runServerOnce(
distEntry,
path.join(tmpDir, "direct", "tasks.json")
);
expect(stderr).toContain("Task Manager MCP Server running");
});
});
Loading