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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "mcp-gsheets-server",
"version": "1.1.0",
"version": "1.1.1",
"description": "MCP server for Google Sheets integration",
"main": "dist/index.js",
"bin": {
Expand Down
9 changes: 8 additions & 1 deletion src/services/auth.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { google } from 'googleapis';

type OAuthTokens = {
access_token?: string | null;
refresh_token?: string | null;
expiry_date?: number | null;
[key: string]: any;

Check warning on line 7 in src/services/auth.ts

View workflow job for this annotation

GitHub Actions / test (20.x)

Unexpected any. Specify a different type

Check warning on line 7 in src/services/auth.ts

View workflow job for this annotation

GitHub Actions / test (22.x)

Unexpected any. Specify a different type
};
import { config } from '../config/index.js';
import { logger } from '../utils/logger.js';
import { BrowserLauncherService } from '../utils/browser-launcher.js';
Expand Down Expand Up @@ -46,7 +53,7 @@
}
}

private async saveTokens(tokens: any): Promise<void> {
private async saveTokens(tokens: OAuthTokens): Promise<void> {
try {
await fs.writeFile(TOKENS_FILE, JSON.stringify(tokens, null, 2));
logger.info('OAuth tokens saved to disk');
Expand Down
11 changes: 9 additions & 2 deletions src/services/oauth-server.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import http from 'http';
import url from 'url';
import { google } from 'googleapis';

type OAuthTokens = {
access_token?: string | null;
refresh_token?: string | null;
expiry_date?: number | null;
[key: string]: any;

Check warning on line 9 in src/services/oauth-server.ts

View workflow job for this annotation

GitHub Actions / test (20.x)

Unexpected any. Specify a different type

Check warning on line 9 in src/services/oauth-server.ts

View workflow job for this annotation

GitHub Actions / test (22.x)

Unexpected any. Specify a different type
};
import { config } from '../config/index.js';
import { logger } from '../utils/logger.js';

Expand Down Expand Up @@ -33,7 +40,7 @@
return authUrl;
}

async startAuthFlow(): Promise<{ authUrl: string; tokens: any }> {
async startAuthFlow(): Promise<{ authUrl: string; tokens: OAuthTokens }> {
const authUrl = this.generateAuthUrl();

return new Promise((resolve, reject) => {
Expand Down Expand Up @@ -94,7 +101,7 @@
});
}

setTokens(tokens: any): void {
setTokens(tokens: OAuthTokens): void {
this.oauth2Client.setCredentials(tokens);
logger.info('OAuth tokens set successfully');
}
Expand Down
129 changes: 129 additions & 0 deletions tests/unit/controllers/mcp-server.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
import { McpSheetsServer } from '../../../src/controllers/mcp-server.js';
import { GoogleSheetsService } from '../../../src/services/sheets.js';

// Mock the MCP SDK
jest.mock('@modelcontextprotocol/sdk/server/index.js', () => ({
Server: jest.fn().mockImplementation(() => ({
setRequestHandler: jest.fn(),
connect: jest.fn(),
close: jest.fn(),
_requestHandlers: new Map(),
})),
}));

jest.mock('@modelcontextprotocol/sdk/types.js', () => ({
ErrorCode: {
MethodNotFound: -32601,
InvalidRequest: -32600,
InvalidParams: -32602,
InternalError: -32603,
},
McpError: jest.fn().mockImplementation(function(code: number, message: string) {
const error = new Error(message);
(error as any).code = code;
return error;
}),
ListToolsRequestSchema: 'tools/list',
CallToolRequestSchema: 'tools/call',
}));

// Mock the GoogleSheetsService
jest.mock('../../../src/services/sheets.js');
jest.mock('../../../src/utils/logger.js', () => ({
logger: {
info: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
debug: jest.fn(),
},
}));

jest.mock('../../../src/config/index.js', () => ({
config: {
mcpServerName: 'test-server',
mcpServerVersion: '1.0.0',
},
}));

const MockedGoogleSheetsService = GoogleSheetsService as jest.MockedClass<typeof GoogleSheetsService>;

describe('McpSheetsServer', () => {
let mcpServer: McpSheetsServer;
let mockSheetsService: jest.Mocked<GoogleSheetsService>;
let mockServer: any;

beforeEach(() => {
jest.clearAllMocks();
mockSheetsService = {
findSheets: jest.fn(),
getSheetData: jest.fn(),
createSheet: jest.fn(),
updateSheet: jest.fn(),
deleteSheet: jest.fn(),
} as any;

MockedGoogleSheetsService.mockImplementation(() => mockSheetsService);

mockServer = {
setRequestHandler: jest.fn(),
connect: jest.fn(),
close: jest.fn(),
_requestHandlers: new Map(),
};

// Update the Server mock to return our mockServer
const { Server } = jest.requireMock('@modelcontextprotocol/sdk/server/index.js');
Server.mockImplementation(() => mockServer);

mcpServer = new McpSheetsServer();
});

describe('constructor', () => {
it('should initialize server and setup handlers', () => {
expect(mockServer.setRequestHandler).toHaveBeenCalledTimes(2);
expect(MockedGoogleSheetsService).toHaveBeenCalledTimes(1);
});
});

describe('getServer', () => {
it('should return the server instance', () => {
const server = mcpServer.getServer();
expect(server).toBe(mockServer);
});
});

describe('tool configuration', () => {
it('should setup handlers for list and call tools', () => {
// Verify that setRequestHandler was called with correct schemas
const setRequestHandlerCalls = mockServer.setRequestHandler.mock.calls;

expect(setRequestHandlerCalls).toHaveLength(2);
expect(setRequestHandlerCalls[0][0]).toBe('tools/list');
expect(setRequestHandlerCalls[1][0]).toBe('tools/call');
});

it('should provide proper tool definitions', async () => {
// Get the list tools handler
const listToolsHandler = mockServer.setRequestHandler.mock.calls[0][1];
const result = await listToolsHandler({});

expect(result.tools).toHaveLength(5);
expect(result.tools.map((t: any) => t.name)).toEqual([
'find_sheets',
'get_sheet_data',
'create_sheet',
'update_sheet',
'delete_sheet'
]);

// Verify schemas have required properties
const findSheetsSchema = result.tools.find((t: any) => t.name === 'find_sheets');
expect(findSheetsSchema.inputSchema.properties).toHaveProperty('query');
expect(findSheetsSchema.inputSchema.properties).toHaveProperty('maxResults');

const getSheetDataSchema = result.tools.find((t: any) => t.name === 'get_sheet_data');
expect(getSheetDataSchema.inputSchema.required).toContain('sheetId');
});
});
});