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
14 changes: 0 additions & 14 deletions jest.config.js

This file was deleted.

4,359 changes: 1,313 additions & 3,046 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 3 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@
"lint": "eslint src/ && prettier --check .",
"lint:fix": "eslint src/ --fix && prettier --write .",
"check": "npm run typecheck && npm run lint",
"test": "jest",
"test": "vitest run",
"test:watch": "vitest",
"start": "npm run server",
"server": "tsx watch --clear-screen=false scripts/cli.ts server",
"client": "tsx scripts/cli.ts client"
Expand Down Expand Up @@ -102,27 +103,24 @@
"devDependencies": {
"@cfworker/json-schema": "^4.1.1",
"@eslint/js": "^9.8.0",
"@jest-mock/express": "^3.0.0",
"@types/content-type": "^1.1.8",
"@types/cors": "^2.8.17",
"@types/cross-spawn": "^6.0.6",
"@types/eslint__js": "^8.42.3",
"@types/eventsource": "^1.1.15",
"@types/express": "^5.0.0",
"@types/jest": "^29.5.12",
"@types/node": "^22.0.2",
"@types/supertest": "^6.0.2",
"@types/ws": "^8.5.12",
"@typescript/native-preview": "^7.0.0-dev.20251103.1",
"eslint": "^9.8.0",
"eslint-config-prettier": "^10.1.8",
"jest": "^29.7.0",
"prettier": "3.6.2",
"supertest": "^7.0.0",
"ts-jest": "^29.2.4",
"tsx": "^4.16.5",
"typescript": "^5.5.4",
"typescript-eslint": "^8.0.0",
"vitest": "^4.0.8",
"ws": "^8.18.0"
},
"resolutions": {
Expand Down
155 changes: 83 additions & 72 deletions src/client/auth.test.ts

Large diffs are not rendered by default.

41 changes: 21 additions & 20 deletions src/client/cross-spawn.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,34 @@ import { StdioClientTransport, getDefaultEnvironment } from './stdio.js';
import spawn from 'cross-spawn';
import { JSONRPCMessage } from '../types.js';
import { ChildProcess } from 'node:child_process';
import { Mock, MockedFunction } from 'vitest';

// mock cross-spawn
jest.mock('cross-spawn');
const mockSpawn = spawn as jest.MockedFunction<typeof spawn>;
vi.mock('cross-spawn');
const mockSpawn = spawn as unknown as MockedFunction<typeof spawn>;

describe('StdioClientTransport using cross-spawn', () => {
beforeEach(() => {
// mock cross-spawn's return value
mockSpawn.mockImplementation(() => {
const mockProcess: {
on: jest.Mock;
stdin?: { on: jest.Mock; write: jest.Mock };
stdout?: { on: jest.Mock };
on: Mock;
stdin?: { on: Mock; write: Mock };
stdout?: { on: Mock };
stderr?: null;
} = {
on: jest.fn((event: string, callback: () => void) => {
on: vi.fn((event: string, callback: () => void) => {
if (event === 'spawn') {
callback();
}
return mockProcess;
}),
stdin: {
on: jest.fn(),
write: jest.fn().mockReturnValue(true)
on: vi.fn(),
write: vi.fn().mockReturnValue(true)
},
stdout: {
on: jest.fn()
on: vi.fn()
},
stderr: null
};
Expand All @@ -37,7 +38,7 @@ describe('StdioClientTransport using cross-spawn', () => {
});

afterEach(() => {
jest.clearAllMocks();
vi.clearAllMocks();
});

test('should call cross-spawn correctly', async () => {
Expand Down Expand Up @@ -105,30 +106,30 @@ describe('StdioClientTransport using cross-spawn', () => {

// get the mock process object
const mockProcess: {
on: jest.Mock;
on: Mock;
stdin: {
on: jest.Mock;
write: jest.Mock;
once: jest.Mock;
on: Mock;
write: Mock;
once: Mock;
};
stdout: {
on: jest.Mock;
on: Mock;
};
stderr: null;
} = {
on: jest.fn((event: string, callback: () => void) => {
on: vi.fn((event: string, callback: () => void) => {
if (event === 'spawn') {
callback();
}
return mockProcess;
}),
stdin: {
on: jest.fn(),
write: jest.fn().mockReturnValue(true),
once: jest.fn()
on: vi.fn(),
write: vi.fn().mockReturnValue(true),
once: vi.fn()
},
stdout: {
on: jest.fn()
on: vi.fn()
},
stderr: null
};
Expand Down
18 changes: 9 additions & 9 deletions src/client/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ import { InMemoryTransport } from '../inMemory.js';
*/
test('should initialize with matching protocol version', async () => {
const clientTransport: Transport = {
start: jest.fn().mockResolvedValue(undefined),
close: jest.fn().mockResolvedValue(undefined),
send: jest.fn().mockImplementation(message => {
start: vi.fn().mockResolvedValue(undefined),
close: vi.fn().mockResolvedValue(undefined),
send: vi.fn().mockImplementation(message => {
if (message.method === 'initialize') {
clientTransport.onmessage?.({
jsonrpc: '2.0',
Expand Down Expand Up @@ -86,9 +86,9 @@ test('should initialize with matching protocol version', async () => {
test('should initialize with supported older protocol version', async () => {
const OLD_VERSION = SUPPORTED_PROTOCOL_VERSIONS[1];
const clientTransport: Transport = {
start: jest.fn().mockResolvedValue(undefined),
close: jest.fn().mockResolvedValue(undefined),
send: jest.fn().mockImplementation(message => {
start: vi.fn().mockResolvedValue(undefined),
close: vi.fn().mockResolvedValue(undefined),
send: vi.fn().mockImplementation(message => {
if (message.method === 'initialize') {
clientTransport.onmessage?.({
jsonrpc: '2.0',
Expand Down Expand Up @@ -136,9 +136,9 @@ test('should initialize with supported older protocol version', async () => {
*/
test('should reject unsupported protocol version', async () => {
const clientTransport: Transport = {
start: jest.fn().mockResolvedValue(undefined),
close: jest.fn().mockResolvedValue(undefined),
send: jest.fn().mockImplementation(message => {
start: vi.fn().mockResolvedValue(undefined),
close: vi.fn().mockResolvedValue(undefined),
send: vi.fn().mockImplementation(message => {
if (message.method === 'initialize') {
clientTransport.onmessage?.({
jsonrpc: '2.0',
Expand Down
95 changes: 48 additions & 47 deletions src/client/middleware.test.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,28 @@
import { withOAuth, withLogging, applyMiddlewares, createMiddleware } from './middleware.js';
import { OAuthClientProvider } from './auth.js';
import { FetchLike } from '../shared/transport.js';
import { MockInstance, Mocked, MockedFunction } from 'vitest';

jest.mock('../client/auth.js', () => {
const actual = jest.requireActual('../client/auth.js');
vi.mock('../client/auth.js', async () => {
const actual = await vi.importActual<typeof import('../client/auth.js')>('../client/auth.js');
return {
...actual,
auth: jest.fn(),
extractWWWAuthenticateParams: jest.fn()
auth: vi.fn(),
extractWWWAuthenticateParams: vi.fn()
};
});

import { auth, extractWWWAuthenticateParams } from './auth.js';

const mockAuth = auth as jest.MockedFunction<typeof auth>;
const mockExtractWWWAuthenticateParams = extractWWWAuthenticateParams as jest.MockedFunction<typeof extractWWWAuthenticateParams>;
const mockAuth = auth as MockedFunction<typeof auth>;
const mockExtractWWWAuthenticateParams = extractWWWAuthenticateParams as MockedFunction<typeof extractWWWAuthenticateParams>;

describe('withOAuth', () => {
let mockProvider: jest.Mocked<OAuthClientProvider>;
let mockFetch: jest.MockedFunction<FetchLike>;
let mockProvider: Mocked<OAuthClientProvider>;
let mockFetch: MockedFunction<FetchLike>;

beforeEach(() => {
jest.clearAllMocks();
vi.clearAllMocks();

mockProvider = {
get redirectUrl() {
Expand All @@ -30,16 +31,16 @@ describe('withOAuth', () => {
get clientMetadata() {
return { redirect_uris: ['http://localhost/callback'] };
},
tokens: jest.fn(),
saveTokens: jest.fn(),
clientInformation: jest.fn(),
redirectToAuthorization: jest.fn(),
saveCodeVerifier: jest.fn(),
codeVerifier: jest.fn(),
invalidateCredentials: jest.fn()
tokens: vi.fn(),
saveTokens: vi.fn(),
clientInformation: vi.fn(),
redirectToAuthorization: vi.fn(),
saveCodeVerifier: vi.fn(),
codeVerifier: vi.fn(),
invalidateCredentials: vi.fn()
};

mockFetch = jest.fn();
mockFetch = vi.fn();
});

it('should add Authorization header when tokens are available (with explicit baseUrl)', async () => {
Expand Down Expand Up @@ -371,8 +372,8 @@ describe('withOAuth', () => {
});

describe('withLogging', () => {
let mockFetch: jest.MockedFunction<FetchLike>;
let mockLogger: jest.MockedFunction<
let mockFetch: MockedFunction<FetchLike>;
let mockLogger: MockedFunction<
(input: {
method: string;
url: string | URL;
Expand All @@ -384,17 +385,17 @@ describe('withLogging', () => {
error?: Error;
}) => void
>;
let consoleErrorSpy: jest.SpyInstance;
let consoleLogSpy: jest.SpyInstance;
let consoleErrorSpy: MockInstance;
let consoleLogSpy: MockInstance;

beforeEach(() => {
jest.clearAllMocks();
vi.clearAllMocks();

consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});

mockFetch = jest.fn();
mockLogger = jest.fn();
mockFetch = vi.fn();
mockLogger = vi.fn();
});

afterEach(() => {
Expand Down Expand Up @@ -614,11 +615,11 @@ describe('withLogging', () => {
});

describe('applyMiddleware', () => {
let mockFetch: jest.MockedFunction<FetchLike>;
let mockFetch: MockedFunction<FetchLike>;

beforeEach(() => {
jest.clearAllMocks();
mockFetch = jest.fn();
vi.clearAllMocks();
mockFetch = vi.fn();
});

it('should compose no middleware correctly', () => {
Expand Down Expand Up @@ -703,7 +704,7 @@ describe('applyMiddleware', () => {
};

// Use custom logger to avoid console output
const mockLogger = jest.fn();
const mockLogger = vi.fn();
const composedFetch = applyMiddlewares(oauthMiddleware, withLogging({ logger: mockLogger, statusLevel: 0 }))(mockFetch);

await composedFetch('https://api.example.com/data');
Expand Down Expand Up @@ -743,11 +744,11 @@ describe('applyMiddleware', () => {
});

describe('Integration Tests', () => {
let mockProvider: jest.Mocked<OAuthClientProvider>;
let mockFetch: jest.MockedFunction<FetchLike>;
let mockProvider: Mocked<OAuthClientProvider>;
let mockFetch: MockedFunction<FetchLike>;

beforeEach(() => {
jest.clearAllMocks();
vi.clearAllMocks();

mockProvider = {
get redirectUrl() {
Expand All @@ -756,16 +757,16 @@ describe('Integration Tests', () => {
get clientMetadata() {
return { redirect_uris: ['http://localhost/callback'] };
},
tokens: jest.fn(),
saveTokens: jest.fn(),
clientInformation: jest.fn(),
redirectToAuthorization: jest.fn(),
saveCodeVerifier: jest.fn(),
codeVerifier: jest.fn(),
invalidateCredentials: jest.fn()
tokens: vi.fn(),
saveTokens: vi.fn(),
clientInformation: vi.fn(),
redirectToAuthorization: vi.fn(),
saveCodeVerifier: vi.fn(),
codeVerifier: vi.fn(),
invalidateCredentials: vi.fn()
};

mockFetch = jest.fn();
mockFetch = vi.fn();
});

it('should work with SSE transport pattern', async () => {
Expand All @@ -783,7 +784,7 @@ describe('Integration Tests', () => {
mockFetch.mockResolvedValue(response);

// Use custom logger to avoid console output
const mockLogger = jest.fn();
const mockLogger = vi.fn();
const enhancedFetch = applyMiddlewares(
withOAuth(mockProvider as OAuthClientProvider, 'https://mcp-server.example.com'),
withLogging({ logger: mockLogger, statusLevel: 400 }) // Only log errors
Expand Down Expand Up @@ -830,7 +831,7 @@ describe('Integration Tests', () => {
mockFetch.mockResolvedValue(response);

// Use custom logger to avoid console output
const mockLogger = jest.fn();
const mockLogger = vi.fn();
const enhancedFetch = applyMiddlewares(
withOAuth(mockProvider as OAuthClientProvider, 'https://streamable-server.example.com'),
withLogging({
Expand Down Expand Up @@ -891,7 +892,7 @@ describe('Integration Tests', () => {
mockAuth.mockResolvedValue('AUTHORIZED');

// Use custom logger to avoid console output
const mockLogger = jest.fn();
const mockLogger = vi.fn();
const enhancedFetch = applyMiddlewares(
withOAuth(mockProvider as OAuthClientProvider, 'https://mcp-server.example.com'),
withLogging({ logger: mockLogger, statusLevel: 0 })
Expand All @@ -914,11 +915,11 @@ describe('Integration Tests', () => {
});

describe('createMiddleware', () => {
let mockFetch: jest.MockedFunction<FetchLike>;
let mockFetch: MockedFunction<FetchLike>;

beforeEach(() => {
jest.clearAllMocks();
mockFetch = jest.fn();
vi.clearAllMocks();
mockFetch = vi.fn();
});

it('should create middleware with cleaner syntax', async () => {
Expand Down
Loading
Loading