Skip to content
Open
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
388 changes: 388 additions & 0 deletions tests/mcp-tools.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,388 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { registerConnectTool } from "../src/tools/connect.js";
import { registerExecuteTool } from "../src/tools/execute.js";
import { registerDisconnectTool } from "../src/tools/disconnect.js";
import { registerListTool } from "../src/tools/list.js";

/**
* MCP tool registration tests.
*
* These tests verify that each SSH tool is properly registered with the MCP
* server, including correct tool names, titles, descriptions, and input schema
* fields. The tests access the server's internal _registeredTools map to
* inspect what was registered without needing a transport connection.
*/

// ---------------------------------------------------------------------------
// Minimal test runner (matches project convention)
// ---------------------------------------------------------------------------

interface TestResult {
name: string;
passed: boolean;
error?: string;
}

const results: TestResult[] = [];
let currentDescribe = "";

function describe(name: string, fn: () => void) {
currentDescribe = name;
fn();
currentDescribe = "";
}

function it(name: string, fn: () => void) {
const fullName = currentDescribe ? `${currentDescribe} > ${name}` : name;
try {
fn();
results.push({ name: fullName, passed: true });
} catch (err: any) {
results.push({ name: fullName, passed: false, error: err.message });
}
}

function expect<T>(actual: T) {
return {
toBe(expected: T) {
if (actual !== expected) {
throw new Error(`Expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
}
},
toEqual(expected: T) {
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
throw new Error(
`Expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`
);
}
},
toBeTruthy() {
if (!actual) {
throw new Error(`Expected truthy value, got ${JSON.stringify(actual)}`);
}
},
toBeFalsy() {
if (actual) {
throw new Error(`Expected falsy value, got ${JSON.stringify(actual)}`);
}
},
toBeUndefined() {
if (actual !== undefined) {
throw new Error(`Expected undefined, got ${JSON.stringify(actual)}`);
}
},
toBeDefined() {
if (actual === undefined) {
throw new Error(`Expected defined value, got undefined`);
}
},
toContain(item: any) {
if (!Array.isArray(actual) || !actual.includes(item)) {
throw new Error(
`Expected ${JSON.stringify(actual)} to contain ${JSON.stringify(item)}`
);
}
},
toBeGreaterThanOrEqual(expected: number) {
if (typeof actual !== "number" || actual < expected) {
throw new Error(`Expected ${actual} >= ${expected}`);
}
},
};
}

// ---------------------------------------------------------------------------
// Helper: access the internal _registeredTools from the McpServer instance
// ---------------------------------------------------------------------------

function getRegisteredTools(server: McpServer): Record<string, any> {
return (server as any)._registeredTools;
}

function getToolNames(server: McpServer): string[] {
return Object.keys(getRegisteredTools(server));
}

// ---------------------------------------------------------------------------
// Setup: create a server and register all tools
// ---------------------------------------------------------------------------

const server = new McpServer({
name: "ssh-mcp-server-test",
version: "1.0.0",
});

registerConnectTool(server);
registerExecuteTool(server);
registerDisconnectTool(server);
registerListTool(server);

const tools = getRegisteredTools(server);

// ---------------------------------------------------------------------------
// Tests: all four tools are registered
// ---------------------------------------------------------------------------

describe("tool registration", () => {
it("registers exactly four tools", () => {
expect(getToolNames(server).length).toBe(4);
});

it("registers the ssh_connect tool", () => {
expect(tools["ssh_connect"]).toBeDefined();
});

it("registers the ssh_execute tool", () => {
expect(tools["ssh_execute"]).toBeDefined();
});

it("registers the ssh_disconnect tool", () => {
expect(tools["ssh_disconnect"]).toBeDefined();
});

it("registers the ssh_list_connections tool", () => {
expect(tools["ssh_list_connections"]).toBeDefined();
});
});

// ---------------------------------------------------------------------------
// Tests: ssh_connect tool metadata and schema
// ---------------------------------------------------------------------------

describe("ssh_connect tool", () => {
const tool = tools["ssh_connect"];

it("has the correct title", () => {
expect(tool.title).toBe("SSH Connect");
});

it("has a non-empty description", () => {
expect(typeof tool.description).toBe("string");
expect((tool.description as string).length > 0).toBe(true);
});

it("has an inputSchema defined", () => {
expect(tool.inputSchema).toBeDefined();
});

it("has a required 'host' field in its input schema", () => {
const schema = tool.inputSchema;
expect(schema.host).toBeDefined();
});

it("has optional 'username' field in its input schema", () => {
const schema = tool.inputSchema;
expect(schema.username).toBeDefined();
});

it("has optional 'port' field in its input schema", () => {
const schema = tool.inputSchema;
expect(schema.port).toBeDefined();
});

it("has optional 'password' field in its input schema", () => {
const schema = tool.inputSchema;
expect(schema.password).toBeDefined();
});

it("has optional 'privateKeyPath' field in its input schema", () => {
const schema = tool.inputSchema;
expect(schema.privateKeyPath).toBeDefined();
});

it("has optional 'passphrase' field in its input schema", () => {
const schema = tool.inputSchema;
expect(schema.passphrase).toBeDefined();
});

it("has optional 'useAgent' field in its input schema", () => {
const schema = tool.inputSchema;
expect(schema.useAgent).toBeDefined();
});

it("has optional 'connectTimeout' field in its input schema", () => {
const schema = tool.inputSchema;
expect(schema.connectTimeout).toBeDefined();
});

it("has optional 'connectionId' field in its input schema", () => {
const schema = tool.inputSchema;
expect(schema.connectionId).toBeDefined();
});

it("has exactly 9 fields in its input schema", () => {
const fieldCount = Object.keys(tool.inputSchema).length;
expect(fieldCount).toBe(9);
});

it("is enabled by default", () => {
expect(tool.enabled).toBe(true);
});
});

// ---------------------------------------------------------------------------
// Tests: ssh_execute tool metadata and schema
// ---------------------------------------------------------------------------

describe("ssh_execute tool", () => {
const tool = tools["ssh_execute"];

it("has the correct title", () => {
expect(tool.title).toBe("SSH Execute");
});

it("has a non-empty description mentioning output cap", () => {
expect(typeof tool.description).toBe("string");
expect((tool.description as string).includes("1MB")).toBe(true);
});

it("has a required 'connectionId' field in its input schema", () => {
expect(tool.inputSchema.connectionId).toBeDefined();
});

it("has a required 'command' field in its input schema", () => {
expect(tool.inputSchema.command).toBeDefined();
});

it("has optional 'cwd' field in its input schema", () => {
expect(tool.inputSchema.cwd).toBeDefined();
});

it("has optional 'timeout' field in its input schema", () => {
expect(tool.inputSchema.timeout).toBeDefined();
});

it("has optional 'errOnNonZero' field in its input schema", () => {
expect(tool.inputSchema.errOnNonZero).toBeDefined();
});

it("has exactly 5 fields in its input schema", () => {
const fieldCount = Object.keys(tool.inputSchema).length;
expect(fieldCount).toBe(5);
});

it("is enabled by default", () => {
expect(tool.enabled).toBe(true);
});
});

// ---------------------------------------------------------------------------
// Tests: ssh_disconnect tool metadata and schema
// ---------------------------------------------------------------------------

describe("ssh_disconnect tool", () => {
const tool = tools["ssh_disconnect"];

it("has the correct title", () => {
expect(tool.title).toBe("SSH Disconnect");
});

it("has a non-empty description", () => {
expect(typeof tool.description).toBe("string");
expect((tool.description as string).length > 0).toBe(true);
});

it("has a required 'connectionId' field in its input schema", () => {
expect(tool.inputSchema.connectionId).toBeDefined();
});

it("has exactly 1 field in its input schema", () => {
const fieldCount = Object.keys(tool.inputSchema).length;
expect(fieldCount).toBe(1);
});

it("is enabled by default", () => {
expect(tool.enabled).toBe(true);
});
});

// ---------------------------------------------------------------------------
// Tests: ssh_list_connections tool metadata and schema
// ---------------------------------------------------------------------------

describe("ssh_list_connections tool", () => {
const tool = tools["ssh_list_connections"];

it("has the correct title", () => {
expect(tool.title).toBe("SSH List Connections");
});

it("has a non-empty description", () => {
expect(typeof tool.description).toBe("string");
expect((tool.description as string).length > 0).toBe(true);
});

it("has an empty input schema (no parameters required)", () => {
const fieldCount = Object.keys(tool.inputSchema).length;
expect(fieldCount).toBe(0);
});

it("is enabled by default", () => {
expect(tool.enabled).toBe(true);
});
});

// ---------------------------------------------------------------------------
// Tests: duplicate registration prevention
// ---------------------------------------------------------------------------

describe("duplicate registration prevention", () => {
it("throws when registering the same tool name twice", () => {
const freshServer = new McpServer({
name: "duplicate-test",
version: "1.0.0",
});
registerConnectTool(freshServer);
let threw = false;
try {
registerConnectTool(freshServer);
} catch {
threw = true;
}
expect(threw).toBe(true);
});
});

// ---------------------------------------------------------------------------
// Tests: tool names follow naming convention
// ---------------------------------------------------------------------------

describe("tool naming convention", () => {
it("all tool names start with 'ssh_' prefix", () => {
const names = getToolNames(server);
for (const name of names) {
expect(name.startsWith("ssh_")).toBe(true);
}
});

it("tool names use snake_case", () => {
const names = getToolNames(server);
const snakeCasePattern = /^[a-z][a-z0-9_]*$/;
for (const name of names) {
expect(snakeCasePattern.test(name)).toBe(true);
}
});
});

// ---------------------------------------------------------------------------
// Run and report
// ---------------------------------------------------------------------------

const passed = results.filter((r) => r.passed).length;
const failed = results.filter((r) => !r.passed).length;

console.log(`\nTest Results: ${passed} passed, ${failed} failed, ${results.length} total\n`);

for (const r of results) {
const icon = r.passed ? "PASS" : "FAIL";
console.log(` [${icon}] ${r.name}`);
if (!r.passed && r.error) {
console.log(` ${r.error}`);
}
}

console.log();

if (failed > 0) {
process.exit(1);
}