diff --git a/deno.json b/deno.json
index 32c3222..737c551 100644
--- a/deno.json
+++ b/deno.json
@@ -17,7 +17,10 @@
"proxy:start": "deno run --allow-net --allow-env --allow-read --allow-run --allow-sys --allow-ffi src/proxy.ts",
"proxy:watch": "deno run --watch --allow-net --allow-env --allow-read --allow-run --allow-sys --allow-ffi src/proxy.ts",
"client:start": "deno run --allow-net --allow-env --allow-read --allow-run --allow-sys --allow-ffi src/client.ts",
- "client:watch": "deno run --watch --allow-net --allow-env --allow-read --allow-run --allow-sys --allow-ffi src/client.ts"
+ "client:watch": "deno run --watch --allow-net --allow-env --allow-read --allow-run --allow-sys --allow-ffi src/client.ts",
+ "test": "deno test --allow-net --allow-env --allow-read --allow-run --allow-sys tests/",
+ "test:watch": "deno test --watch --allow-net --allow-env --allow-read --allow-run --allow-sys tests/",
+ "test:coverage": "deno test --coverage=coverage --allow-net --allow-env --allow-read --allow-run --allow-sys tests/ && deno coverage coverage"
},
"imports": {
"std/": "https://deno.land/std@0.220.0/",
diff --git a/deno.lock b/deno.lock
index 79c512e..141499b 100644
--- a/deno.lock
+++ b/deno.lock
@@ -1357,6 +1357,61 @@
"integrity": "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg=="
}
},
+ "remote": {
+ "https://deno.land/std@0.218.2/assert/_constants.ts": "a271e8ef5a573f1df8e822a6eb9d09df064ad66a4390f21b3e31f820a38e0975",
+ "https://deno.land/std@0.218.2/assert/_diff.ts": "dcc63d94ca289aec80644030cf88ccbf7acaa6fbd7b0f22add93616b36593840",
+ "https://deno.land/std@0.218.2/assert/_format.ts": "0ba808961bf678437fb486b56405b6fefad2cf87b5809667c781ddee8c32aff4",
+ "https://deno.land/std@0.218.2/assert/assert_equals.ts": "4497c56fe7d2993b0d447926702802fc0becb44e319079e8eca39b482ee01b4e",
+ "https://deno.land/std@0.218.2/assert/assert_is_error.ts": "6596f2b5ba89ba2fe9b074f75e9318cda97a2381e59d476812e30077fbdb6ed2",
+ "https://deno.land/std@0.218.2/assert/assert_rejects.ts": "5206ac37d883797d9504e3915a0c7b692df6efcdefff3889cc14bb5a325641dd",
+ "https://deno.land/std@0.218.2/assert/assertion_error.ts": "9f689a101ee586c4ce92f52fa7ddd362e86434ffdf1f848e45987dc7689976b8",
+ "https://deno.land/std@0.218.2/assert/equal.ts": "fae5e8a52a11d3ac694bbe1a53e13a7969e3f60791262312e91a3e741ae519e2",
+ "https://deno.land/std@0.218.2/fmt/colors.ts": "d239d84620b921ea520125d778947881f62c50e78deef2657073840b8af9559a",
+ "https://deno.land/std@0.218.2/testing/mock.ts": "dc9e58f88f7e746edd0c551443a39096c75895a0fdcd7db62777e47787be6226",
+ "https://deno.land/std@0.220.0/assert/_constants.ts": "a271e8ef5a573f1df8e822a6eb9d09df064ad66a4390f21b3e31f820a38e0975",
+ "https://deno.land/std@0.220.0/assert/_diff.ts": "4bf42969aa8b1a33aaf23eb8e478b011bfaa31b82d85d2ff4b5c4662d8780d2b",
+ "https://deno.land/std@0.220.0/assert/_format.ts": "0ba808961bf678437fb486b56405b6fefad2cf87b5809667c781ddee8c32aff4",
+ "https://deno.land/std@0.220.0/assert/assert.ts": "bec068b2fccdd434c138a555b19a2c2393b71dfaada02b7d568a01541e67cdc5",
+ "https://deno.land/std@0.220.0/assert/assert_almost_equals.ts": "8b96b7385cc117668b0720115eb6ee73d04c9bcb2f5d2344d674918c9113688f",
+ "https://deno.land/std@0.220.0/assert/assert_array_includes.ts": "1688d76317fd45b7e93ef9e2765f112fdf2b7c9821016cdfb380b9445374aed1",
+ "https://deno.land/std@0.220.0/assert/assert_equals.ts": "4497c56fe7d2993b0d447926702802fc0becb44e319079e8eca39b482ee01b4e",
+ "https://deno.land/std@0.220.0/assert/assert_exists.ts": "24a7bf965e634f909242cd09fbaf38bde6b791128ece08e33ab08586a7cc55c9",
+ "https://deno.land/std@0.220.0/assert/assert_false.ts": "6f382568e5128c0f855e5f7dbda8624c1ed9af4fcc33ef4a9afeeedcdce99769",
+ "https://deno.land/std@0.220.0/assert/assert_greater.ts": "4945cf5729f1a38874d7e589e0fe5cc5cd5abe5573ca2ddca9d3791aa891856c",
+ "https://deno.land/std@0.220.0/assert/assert_greater_or_equal.ts": "573ed8823283b8d94b7443eb69a849a3c369a8eb9666b2d1db50c33763a5d219",
+ "https://deno.land/std@0.220.0/assert/assert_instance_of.ts": "72dc1faff1e248692d873c89382fa1579dd7b53b56d52f37f9874a75b11ba444",
+ "https://deno.land/std@0.220.0/assert/assert_is_error.ts": "6596f2b5ba89ba2fe9b074f75e9318cda97a2381e59d476812e30077fbdb6ed2",
+ "https://deno.land/std@0.220.0/assert/assert_less.ts": "2b4b3fe7910f65f7be52212f19c3977ecb8ba5b2d6d0a296c83cde42920bb005",
+ "https://deno.land/std@0.220.0/assert/assert_less_or_equal.ts": "b93d212fe669fbde959e35b3437ac9a4468f2e6b77377e7b6ea2cfdd825d38a0",
+ "https://deno.land/std@0.220.0/assert/assert_match.ts": "ec2d9680ed3e7b9746ec57ec923a17eef6d476202f339ad91d22277d7f1d16e1",
+ "https://deno.land/std@0.220.0/assert/assert_not_equals.ts": "ac86413ab70ffb14fdfc41740ba579a983fe355ba0ce4a9ab685e6b8e7f6a250",
+ "https://deno.land/std@0.220.0/assert/assert_not_instance_of.ts": "8f720d92d83775c40b2542a8d76c60c2d4aeddaf8713c8d11df8984af2604931",
+ "https://deno.land/std@0.220.0/assert/assert_not_match.ts": "b4b7c77f146963e2b673c1ce4846473703409eb93f5ab0eb60f6e6f8aeffe39f",
+ "https://deno.land/std@0.220.0/assert/assert_not_strict_equals.ts": "da0b8ab60a45d5a9371088378e5313f624799470c3b54c76e8b8abeec40a77be",
+ "https://deno.land/std@0.220.0/assert/assert_object_match.ts": "e85e5eef62a56ce364c3afdd27978ccab979288a3e772e6855c270a7b118fa49",
+ "https://deno.land/std@0.220.0/assert/assert_rejects.ts": "5206ac37d883797d9504e3915a0c7b692df6efcdefff3889cc14bb5a325641dd",
+ "https://deno.land/std@0.220.0/assert/assert_strict_equals.ts": "0425a98f70badccb151644c902384c12771a93e65f8ff610244b8147b03a2366",
+ "https://deno.land/std@0.220.0/assert/assert_string_includes.ts": "dfb072a890167146f8e5bdd6fde887ce4657098e9f71f12716ef37f35fb6f4a7",
+ "https://deno.land/std@0.220.0/assert/assert_throws.ts": "31f3c061338aec2c2c33731973d58ccd4f14e42f355501541409ee958d2eb8e5",
+ "https://deno.land/std@0.220.0/assert/assertion_error.ts": "9f689a101ee586c4ce92f52fa7ddd362e86434ffdf1f848e45987dc7689976b8",
+ "https://deno.land/std@0.220.0/assert/equal.ts": "fae5e8a52a11d3ac694bbe1a53e13a7969e3f60791262312e91a3e741ae519e2",
+ "https://deno.land/std@0.220.0/assert/fail.ts": "f310e51992bac8e54f5fd8e44d098638434b2edb802383690e0d7a9be1979f1c",
+ "https://deno.land/std@0.220.0/assert/mod.ts": "7e41449e77a31fef91534379716971bebcfc12686e143d38ada5438e04d4a90e",
+ "https://deno.land/std@0.220.0/assert/unimplemented.ts": "47ca67d1c6dc53abd0bd729b71a31e0825fc452dbcd4fde4ca06789d5644e7fd",
+ "https://deno.land/std@0.220.0/assert/unreachable.ts": "3670816a4ab3214349acb6730e3e6f5299021234657eefe05b48092f3848c270",
+ "https://deno.land/std@0.220.0/async/delay.ts": "8e1d18fe8b28ff95885e2bc54eccec1713f57f756053576d8228e6ca110793ad",
+ "https://deno.land/std@0.220.0/data_structures/_binary_search_node.ts": "ce1da11601fef0638df4d1e53c377f791f96913383277389286b390685d76c07",
+ "https://deno.land/std@0.220.0/data_structures/_red_black_node.ts": "4af8d3c5ac5f119d8058269259c46ea22ead567246cacde04584a83e43a9d2ea",
+ "https://deno.land/std@0.220.0/data_structures/binary_search_tree.ts": "2dd43d97ce5f5a4bdba11b075eb458db33e9143f50997b0eebf02912cb44f5d5",
+ "https://deno.land/std@0.220.0/data_structures/comparators.ts": "74e64752f005f03614d9bd4912ea64c58d2e663b5d8c9dba6e2e2997260f51eb",
+ "https://deno.land/std@0.220.0/data_structures/red_black_tree.ts": "2222be0c46842fc932e2c8589a66dced9e6eae180914807c5c55d1aa4c8c1b9b",
+ "https://deno.land/std@0.220.0/fmt/colors.ts": "d239d84620b921ea520125d778947881f62c50e78deef2657073840b8af9559a",
+ "https://deno.land/std@0.220.0/testing/_test_suite.ts": "f10a8a6338b60c403f07a76f3f46bdc9f1e1a820c0a1decddeb2949f7a8a0546",
+ "https://deno.land/std@0.220.0/testing/_time.ts": "fefd1ff35b50a410db9b0e7227e05163e1b172c88afd0d2071df0125958c3ff3",
+ "https://deno.land/std@0.220.0/testing/bdd.ts": "7a8ac58eded80e6fefa7cf7538927e88781cf5f247c04b35261c3213316e2dd0",
+ "https://deno.land/std@0.220.0/testing/mock.ts": "a963181c2860b6ba3eb60e08b62c164d33cf5da7cd445893499b2efda20074db",
+ "https://deno.land/std@0.220.0/testing/time.ts": "0d25e0f15eded2d66c9ed37d16c3188b16cc1aefa58be4a4753afb7750e72cb0"
+ },
"workspace": {
"dependencies": [
"npm:@modelcontextprotocol/sdk@*"
diff --git a/implmentation_plan.md b/implmentation_plan.md
index 7f77e45..50f2c0f 100644
--- a/implmentation_plan.md
+++ b/implmentation_plan.md
@@ -38,8 +38,8 @@ Here is a plan to transform your Node.js CLI package into a Deno CLI project, fo
- [x] Evaluate replacing `npm:express` with a native Deno HTTP server solution (e.g., `Deno.serve` or from `std/http`).
- [x] Evaluate replacing `npm:open` with a Deno equivalent or platform-specific commands.
8. **Implement Testing:**
- - [ ] Add unit tests for key utility functions (e.g., in `utils.ts`, `mcp-auth-config.ts`).
- - [ ] Add integration tests for the core proxy (`proxy.ts`) and client (`client.ts`) functionality.
+ - [x] Add unit tests for key utility functions (e.g., in `utils.ts`, `mcp-auth-config.ts`).
+ - [x] Add integration tests for the core proxy (`proxy.ts`) and client (`client.ts`) functionality.
9. **Enhance Documentation:**
- [ ] Update `README.md` with Deno-specific installation, usage, and contribution guidelines.
- [ ] Add comprehensive TSDoc comments to all exported functions, classes, and interfaces.
diff --git a/src/lib/deno-http-server.ts b/src/lib/deno-http-server.ts
index 3ebb0c2..2804e14 100644
--- a/src/lib/deno-http-server.ts
+++ b/src/lib/deno-http-server.ts
@@ -1,3 +1,4 @@
+///
import type { Server } from "node:http";
// Simple type definitions for our server
@@ -27,9 +28,9 @@ export class DenoHttpServer {
// Create a simple request object that mimics Express req
const query: Record = {};
- for (const [key, value] of searchParams) {
+ searchParams.forEach((value, key) => {
query[key] = value;
- }
+ });
const req: RequestLike = {
query,
diff --git a/tests/deno-http-server_test.ts b/tests/deno-http-server_test.ts
new file mode 100644
index 0000000..cc589c8
--- /dev/null
+++ b/tests/deno-http-server_test.ts
@@ -0,0 +1,94 @@
+import { assertEquals } from "std/assert/mod.ts";
+import { describe, it, beforeEach, afterEach } from "std/testing/bdd.ts";
+import { DenoHttpServer, ResponseBuilder } from "../src/lib/deno-http-server.ts";
+
+describe("DenoHttpServer", () => {
+ let server: DenoHttpServer;
+ let serverInstance: ReturnType;
+
+ beforeEach(() => {
+ server = new DenoHttpServer();
+ });
+
+ afterEach(async () => {
+ if (serverInstance) {
+ await server.close();
+ }
+ });
+
+ it("registers and handles GET routes", async () => {
+ let handlerCalled = false;
+ let reqPath = "";
+ let reqQuery: Record = {};
+
+ // Register a route handler
+ server.get("/test", (req, res) => {
+ handlerCalled = true;
+ reqPath = req.path;
+ reqQuery = req.query;
+ res.status(200).send("OK");
+ });
+
+ // Start the server on a random available port
+ const testPort = 9876;
+ serverInstance = server.listen(testPort, () => {
+ // Server started
+ });
+
+ // Send a request to the server
+ const response = await fetch(`http://localhost:${testPort}/test?param=value`);
+ const text = await response.text();
+
+ // Verify the response
+ assertEquals(response.status, 200);
+ assertEquals(text, "OK");
+ assertEquals(handlerCalled, true);
+ assertEquals(reqPath, "/test");
+ assertEquals(reqQuery.param, "value");
+ });
+
+ it("returns 404 for non-existent routes", async () => {
+ // Start the server on a random available port
+ const testPort = 9877;
+ serverInstance = server.listen(testPort);
+
+ // Send a request to a non-existent route
+ const response = await fetch(`http://localhost:${testPort}/non-existent`);
+
+ // Verify the response
+ assertEquals(response.status, 404);
+ });
+});
+
+describe("ResponseBuilder", () => {
+ it("builds response with status code and body", async () => {
+ const responseBuilder = new ResponseBuilder();
+
+ // Set status code and body
+ responseBuilder.status(404).send("Not Found");
+
+ // Get the response
+ const response = await responseBuilder.getResponse();
+
+ // Verify the response
+ assertEquals(response.status, 404);
+ assertEquals(await response.text(), "Not Found");
+ });
+
+ it("only sends response once", async () => {
+ const responseBuilder = new ResponseBuilder();
+
+ // Send first response
+ responseBuilder.status(200).send("First");
+
+ // Try to send another response
+ responseBuilder.status(404).send("Second");
+
+ // Get the response - should be the first one
+ const response = await responseBuilder.getResponse();
+
+ // Verify the response
+ assertEquals(response.status, 200);
+ assertEquals(await response.text(), "First");
+ });
+});
diff --git a/tests/deno-open_test.ts b/tests/deno-open_test.ts
new file mode 100644
index 0000000..59156e4
--- /dev/null
+++ b/tests/deno-open_test.ts
@@ -0,0 +1,113 @@
+import { assertEquals, assertRejects } from "std/assert/mod.ts";
+import { describe, it, beforeEach, afterEach } from "std/testing/bdd.ts";
+import { assertSpyCalls, spy, type Spy } from "std/testing/mock.ts";
+import open from "../src/lib/deno-open.ts";
+
+// Define the expected structure returned by the mocked Deno.Command
+interface MockCommandOutput {
+ spawn: () => { status: Promise<{ success: boolean; code: number }> };
+}
+
+describe("deno-open", () => {
+ let originalDenoCommand: typeof Deno.Command;
+ // Use a specific type for the spy
+ let commandSpy: Spy<
+ (command: string, options?: { args?: string[] }) => MockCommandOutput
+ >;
+
+ beforeEach(() => {
+ // Save original Deno.Command
+ originalDenoCommand = Deno.Command;
+ });
+
+ afterEach(() => {
+ // Restore original Deno.Command
+ (Deno.Command as unknown) = originalDenoCommand;
+ });
+
+ it("calls the correct command on macOS", async () => {
+ // Save original OS detection
+ const originalOs = Deno.build.os;
+
+ try {
+ // Mock OS detection - pretend we're on macOS
+ Object.defineProperty(Deno.build, "os", { value: "darwin", configurable: true });
+
+ // Mock Deno.Command implementation
+ const mockSpawn = { status: Promise.resolve({ success: true, code: 0 }) };
+ const mockCommandConstructor = () => ({ spawn: () => mockSpawn });
+ commandSpy = spy(mockCommandConstructor);
+ (Deno.Command as unknown) = commandSpy;
+
+ // Call open
+ const url = "https://example.com";
+ await open(url);
+
+ // Verify the spy was called with correct arguments
+ assertSpyCalls(commandSpy, 1);
+ assertEquals(commandSpy.calls[0].args[0], "open");
+ assertEquals((commandSpy.calls[0].args[1] as { args: string[] }).args[0], url);
+ } finally {
+ // Restore original OS detection
+ Object.defineProperty(Deno.build, "os", { value: originalOs, configurable: true });
+ }
+ });
+
+ it("calls the correct command on Windows", async () => {
+ // Save original OS detection
+ const originalOs = Deno.build.os;
+
+ try {
+ // Mock OS detection - pretend we're on Windows
+ Object.defineProperty(Deno.build, "os", { value: "windows", configurable: true });
+
+ // Mock Deno.Command implementation
+ const mockSpawn = { status: Promise.resolve({ success: true, code: 0 }) };
+ const mockCommandConstructor = () => ({ spawn: () => mockSpawn });
+ commandSpy = spy(mockCommandConstructor);
+ (Deno.Command as unknown) = commandSpy;
+
+ // Call open
+ const url = "https://example.com";
+ await open(url);
+
+ // Verify the spy was called with correct arguments
+ assertSpyCalls(commandSpy, 1);
+ assertEquals(commandSpy.calls[0].args[0], "cmd");
+ assertEquals((commandSpy.calls[0].args[1] as { args: string[] }).args[0], "/c");
+ assertEquals((commandSpy.calls[0].args[1] as { args: string[] }).args[1], "start");
+ assertEquals((commandSpy.calls[0].args[1] as { args: string[] }).args[2], "");
+ assertEquals((commandSpy.calls[0].args[1] as { args: string[] }).args[3], url);
+ } finally {
+ // Restore original OS detection
+ Object.defineProperty(Deno.build, "os", { value: originalOs, configurable: true });
+ }
+ });
+
+ it("throws error on command failure", async () => {
+ // Save original OS detection
+ const originalOs = Deno.build.os;
+
+ try {
+ // Mock OS detection
+ Object.defineProperty(Deno.build, "os", { value: "darwin", configurable: true });
+
+ // Mock Deno.Command to return failure
+ const mockSpawn = { status: Promise.resolve({ success: false, code: 1 }) };
+ const mockCommandConstructor = () => ({ spawn: () => mockSpawn });
+ commandSpy = spy(mockCommandConstructor);
+ (Deno.Command as unknown) = commandSpy;
+
+ // Call open and expect it to throw
+ await assertRejects(
+ () => open("https://example.com"),
+ Error,
+ "Failed to open"
+ );
+ assertSpyCalls(commandSpy, 1);
+ } finally {
+ // Restore original OS detection
+ Object.defineProperty(Deno.build, "os", { value: originalOs, configurable: true });
+ }
+ });
+});
diff --git a/tests/integration_test.ts b/tests/integration_test.ts
new file mode 100644
index 0000000..2e49b7d
--- /dev/null
+++ b/tests/integration_test.ts
@@ -0,0 +1,136 @@
+import { assertEquals, assertExists } from "std/assert/mod.ts";
+import { describe, it, beforeEach } from "std/testing/bdd.ts";
+import { mcpProxy } from "../src/lib/utils.ts";
+import type { Transport } from "npm:@modelcontextprotocol/sdk/shared/transport.js";
+
+// Mock Transport implementation for testing
+class MockTransport implements Transport {
+ public onmessage:
+ | ((message: unknown, extra?: { authInfo?: unknown }) => void)
+ | undefined;
+ public onclose: (() => void) | undefined;
+ public onerror: ((error: Error) => void) | undefined;
+ public closed = false;
+ public messages: unknown[] = [];
+ public errors: Error[] = [];
+
+ constructor(public name: string) { }
+
+ start(): Promise {
+ // Mock start method - does nothing
+ return Promise.resolve();
+ }
+
+ send(message: unknown): Promise {
+ this.messages.push(message);
+ return Promise.resolve();
+ }
+
+ close(): Promise {
+ this.closed = true;
+ if (this.onclose) {
+ this.onclose();
+ }
+ return Promise.resolve();
+ }
+
+ // Helper method to simulate receiving a message
+ simulateMessage(message: unknown): void {
+ if (this.onmessage) {
+ this.onmessage(message);
+ }
+ }
+
+ // Helper method to simulate a close event
+ simulateClose(): void {
+ if (this.onclose) {
+ this.onclose();
+ }
+ }
+
+ // Helper method to simulate an error
+ simulateError(error: Error): void {
+ this.errors.push(error);
+ if (this.onerror) {
+ this.onerror(error);
+ }
+ }
+}
+
+describe("MCP Proxy Integration", () => {
+ let clientTransport: MockTransport;
+ let serverTransport: MockTransport;
+
+ beforeEach(() => {
+ clientTransport = new MockTransport("client");
+ serverTransport = new MockTransport("server");
+ });
+
+ it("forwards messages from client to server", () => {
+ // Set up the proxy
+ mcpProxy({
+ transportToClient: clientTransport,
+ transportToServer: serverTransport,
+ });
+
+ // Verify event handlers are set up
+ assertExists(clientTransport.onmessage);
+ assertExists(clientTransport.onclose);
+ assertExists(clientTransport.onerror);
+ assertExists(serverTransport.onmessage);
+ assertExists(serverTransport.onclose);
+ assertExists(serverTransport.onerror);
+
+ // Simulate a message from client
+ const clientMessage = { id: 1, method: "test", params: {} };
+ clientTransport.simulateMessage(clientMessage);
+
+ // Check that the message was forwarded to server
+ assertEquals(serverTransport.messages.length, 1);
+ assertEquals(serverTransport.messages[0], clientMessage);
+ });
+
+ it("forwards messages from server to client", () => {
+ // Set up the proxy
+ mcpProxy({
+ transportToClient: clientTransport,
+ transportToServer: serverTransport,
+ });
+
+ // Simulate a message from server
+ const serverMessage = { id: 1, result: { value: "test" } };
+ serverTransport.simulateMessage(serverMessage);
+
+ // Check that the message was forwarded to client
+ assertEquals(clientTransport.messages.length, 1);
+ assertEquals(clientTransport.messages[0], serverMessage);
+ });
+
+ it("closes both transports when client closes", () => {
+ // Set up the proxy
+ mcpProxy({
+ transportToClient: clientTransport,
+ transportToServer: serverTransport,
+ });
+
+ // Simulate client closing
+ clientTransport.simulateClose();
+
+ // Check that server transport was closed
+ assertEquals(serverTransport.closed, true);
+ });
+
+ it("closes both transports when server closes", () => {
+ // Set up the proxy
+ mcpProxy({
+ transportToClient: clientTransport,
+ transportToServer: serverTransport,
+ });
+
+ // Simulate server closing
+ serverTransport.simulateClose();
+
+ // Check that client transport was closed
+ assertEquals(clientTransport.closed, true);
+ });
+});
diff --git a/tests/mcp-auth-config_test.ts b/tests/mcp-auth-config_test.ts
new file mode 100644
index 0000000..128d3a0
--- /dev/null
+++ b/tests/mcp-auth-config_test.ts
@@ -0,0 +1,67 @@
+import {
+ assertEquals,
+ assertMatch,
+ assertStringIncludes,
+} from "std/assert/mod.ts";
+import { describe, it, beforeEach, afterEach } from "std/testing/bdd.ts";
+import { assertSpyCalls, spy, stub } from "std/testing/mock.ts";
+import { FakeTime } from "std/testing/time.ts";
+import {
+ getConfigDir,
+ getConfigFilePath,
+} from "../src/lib/mcp-auth-config.ts";
+import { MCP_REMOTE_VERSION } from "../src/lib/utils.ts";
+import * as path from "node:path";
+import * as os from "node:os";
+
+describe("mcp-auth-config", () => {
+ describe("getConfigDir", () => {
+ const originalEnv = { ...Deno.env.toObject() };
+
+ afterEach(() => {
+ // Restore original environment
+ for (const key in Deno.env.toObject()) {
+ Deno.env.delete(key);
+ }
+ for (const [key, value] of Object.entries(originalEnv)) {
+ Deno.env.set(key, value);
+ }
+ });
+
+ it("uses MCP_REMOTE_CONFIG_DIR environment variable if set", () => {
+ const customDir = "/custom/config/dir";
+ Deno.env.set("MCP_REMOTE_CONFIG_DIR", customDir);
+
+ const configDir = getConfigDir();
+
+ assertStringIncludes(configDir, customDir);
+ assertStringIncludes(configDir, `mcp-remote-${MCP_REMOTE_VERSION}`);
+ });
+
+ it("falls back to ~/.mcp-auth if environment variable is not set", () => {
+ // Ensure the env var is not set
+ Deno.env.delete("MCP_REMOTE_CONFIG_DIR");
+
+ const homeDir = os.homedir();
+ const expectedBase = path.join(homeDir, ".mcp-auth");
+
+ const configDir = getConfigDir();
+
+ assertStringIncludes(configDir, expectedBase);
+ assertStringIncludes(configDir, `mcp-remote-${MCP_REMOTE_VERSION}`);
+ });
+ });
+
+ describe("getConfigFilePath", () => {
+ it("returns correct file path with server hash prefix", () => {
+ const serverUrlHash = "abc123";
+ const filename = "test.json";
+
+ const filePath = getConfigFilePath(serverUrlHash, filename);
+ const configDir = getConfigDir();
+
+ const expectedPath = path.join(configDir, `${serverUrlHash}_${filename}`);
+ assertEquals(filePath, expectedPath);
+ });
+ });
+});
diff --git a/tests/utils_test.ts b/tests/utils_test.ts
new file mode 100644
index 0000000..b843f43
--- /dev/null
+++ b/tests/utils_test.ts
@@ -0,0 +1,77 @@
+import { assertEquals, assertMatch } from "std/assert/mod.ts";
+import { getServerUrlHash, log, MCP_REMOTE_VERSION } from "../src/lib/utils.ts";
+import { afterEach, beforeEach, describe, it } from "std/testing/bdd.ts";
+import { assertSpyCalls, spy, type MethodSpy } from "std/testing/mock.ts";
+
+describe("utils", () => {
+ describe("getServerUrlHash", () => {
+ it("returns a hexadecimal hash of server URL", () => {
+ const serverUrl = "https://api.example.com";
+ const hash = getServerUrlHash(serverUrl);
+
+ // Hash should be 32 characters long (MD5)
+ assertEquals(hash.length, 32);
+ // Should only contain hexadecimal characters
+ assertMatch(hash, /^[0-9a-f]{32}$/);
+
+ // Test consistency - should return same hash for same URL
+ const hash2 = getServerUrlHash(serverUrl);
+ assertEquals(hash, hash2);
+
+ // Test different URLs produce different hashes
+ const differentUrl = "https://different.example.com";
+ const differentHash = getServerUrlHash(differentUrl);
+ assertEquals(differentHash.length, 32);
+ assertMatch(differentHash, /^[0-9a-f]{32}$/);
+
+ // Different URLs should produce different hashes
+ assertEquals(hash !== differentHash, true);
+ });
+ });
+
+ describe("log", () => {
+ let consoleErrorSpy: MethodSpy;
+
+ beforeEach(() => {
+ // Spy on console.error
+ consoleErrorSpy = spy(console, "error");
+ });
+
+ afterEach(() => {
+ // Restore original console.error
+ consoleErrorSpy.restore();
+ });
+
+ it("logs message with process ID", () => {
+ const message = "Test message";
+ log(message);
+
+ // Console.error should be called once
+ assertSpyCalls(consoleErrorSpy, 1);
+
+ // The log message should include the process ID and our message
+ const call = consoleErrorSpy.calls[0];
+ assertEquals(call.args.length, 2);
+ assertMatch(call.args[0] as string, /^\[\d+\] Test message$/);
+ });
+
+ it("logs additional parameters", () => {
+ const message = "Test message";
+ const additionalParam = { test: "value" };
+ log(message, additionalParam);
+
+ assertSpyCalls(consoleErrorSpy, 1);
+
+ const call = consoleErrorSpy.calls[0];
+ assertEquals(call.args.length, 3);
+ assertMatch(call.args[0] as string, /^\[\d+\] Test message$/);
+ assertEquals(call.args[1], additionalParam);
+ });
+ });
+
+ describe("MCP_REMOTE_VERSION", () => {
+ it("should be a valid semver version", () => {
+ assertMatch(MCP_REMOTE_VERSION, /^\d+\.\d+\.\d+$/);
+ });
+ });
+});