Skip to main content
Glama

Smartsheet MCP Server

mcp-mock.ts19.9 kB
/** * Mock infrastructure for MCP (Model Context Protocol) testing */ import { jest } from '@jest/globals'; import { CallToolRequest, CallToolResult, Tool, ListToolsResult, ErrorCode, McpError } from '@modelcontextprotocol/sdk/types'; export interface MockMcpServerOptions { pythonPath?: string; apiKey?: string; enableLogging?: boolean; } export class MockMcpServer { private tools: Tool[] = []; private pythonPath: string; private apiKey: string; private enableLogging: boolean; private shouldSimulateFailure: boolean = false; private failureScenarios: Map<string, any> = new Map(); constructor(options: MockMcpServerOptions = {}) { this.pythonPath = options.pythonPath || '/usr/bin/python3'; this.apiKey = options.apiKey || 'test-api-key'; this.enableLogging = options.enableLogging || false; } /** * Register a tool for testing */ registerTool(tool: Tool): void { this.tools.push(tool); } /** * Configure failure scenarios for testing */ setFailureScenario(toolName: string, scenario: 'error' | 'invalid_response' | 'timeout' | 'success_with_error'): void { this.failureScenarios.set(toolName, scenario); } /** * Clear all failure scenarios */ clearFailureScenarios(): void { this.failureScenarios.clear(); this.shouldSimulateFailure = false; } /** * Access to internal properties for testing */ get pythonPath(): string { return this.pythonPath; } get apiKey(): string { return this.apiKey; } set tools(tools: Tool[]) { this.tools = tools; } /** * Mock list_tools handler */ async listTools(): Promise<ListToolsResult> { return { tools: this.tools }; } /** * Mock call_tool handler with validation */ async callTool(request: CallToolRequest): Promise<CallToolResult> { const { name, arguments: args } = request.params; // Find the tool const tool = this.tools.find(t => t.name === name); if (!tool) { throw new McpError( ErrorCode.InvalidRequest, `Unknown tool: ${name}` ); } // Validate arguments against schema this.validateArguments(args, tool.inputSchema); // Route to appropriate mock handler return this.routeToolCall(name, args); } /** * Validate arguments against JSON schema */ private validateArguments(args: any, schema: any): void { if (!args || typeof args !== 'object') { throw new McpError( ErrorCode.InvalidParams, 'Invalid arguments: must be an object' ); } // Basic validation for required fields if (schema.required) { for (const field of schema.required) { if (!(field in args)) { throw new McpError( ErrorCode.InvalidParams, `Missing required parameter: ${field}` ); } } } } /** * Route tool calls to mock implementations */ private async routeToolCall(toolName: string, args: any): Promise<CallToolResult> { // Check for failure scenarios first const failureScenario = this.failureScenarios.get(toolName); if (failureScenario) { return this.handleFailureScenario(failureScenario, toolName, args); } // Simulate calling the Python CLI if mockExec is available const mockData = await this.simulatePythonCliCall(toolName, args); // Simulate some processing time await new Promise(resolve => setTimeout(resolve, 10)); // Handle special case for raw responses (like invalid JSON) if (mockData && mockData._rawResponse) { return { content: [ { type: 'text', text: mockData._rawResponse } ] }; } return { content: [ { type: 'text', text: JSON.stringify(mockData, null, 2) } ] }; } /** * Handle failure scenarios for testing */ private async handleFailureScenario(scenario: string, toolName: string, args: any): Promise<CallToolResult> { switch (scenario) { case 'error': throw new McpError( ErrorCode.InternalError, `Simulated error for ${toolName}` ); case 'invalid_response': return { content: [ { type: 'text', text: 'Invalid JSON{' } ] }; case 'timeout': throw new McpError( ErrorCode.InternalError, 'Command timeout' ); case 'success_with_error': return { content: [ { type: 'text', text: JSON.stringify({ success: false, operation: toolName, error: `Simulated API error for ${toolName}`, details: args }, null, 2) } ] }; default: return this.routeToolCall(toolName, args); } } /** * Simulate Python CLI execution if mockExec is available in global scope */ private async simulatePythonCliCall(toolName: string, args: any): Promise<any> { // Check if we're in a Jest environment with mocked child_process if (typeof jest !== 'undefined') { try { const { exec } = jest.requireMock('child_process'); if (exec && typeof exec === 'function') { // Build the CLI command that would be executed const command = this.buildCliCommand(toolName, args); const options = { timeout: 300000, maxBuffer: 10 * 1024 * 1024 }; // Create a promise to handle the mock exec call return new Promise((resolve, reject) => { const callback = (error: any, result: any, stderr?: any) => { if (error) { reject(new McpError(ErrorCode.InternalError, error.message)); } else if (stderr) { // Try to parse stderr as JSON error response try { const errorData = JSON.parse(stderr); if (!errorData.success) { resolve(errorData); } else { resolve(this.getMockData(toolName, args)); } } catch { reject(new McpError(ErrorCode.InternalError, stderr)); } } else if (result && result.stdout) { try { const parsedResult = JSON.parse(result.stdout); resolve(parsedResult); } catch { // If stdout is not valid JSON, return as is for malformed JSON tests if (result.stdout === 'Invalid JSON{') { // Return invalid JSON directly without throwing error resolve({ _rawResponse: result.stdout }); } else { resolve(this.getMockData(toolName, args)); } } } else if (typeof result === 'object' && result !== null) { // Direct result object resolve(result); } else { resolve(this.getMockData(toolName, args)); } }; // Simulate the exec call try { exec(command, options, callback); } catch (execError) { reject(new McpError(ErrorCode.InternalError, execError.message)); } }); } } catch (error) { // Fall back to mock data if Jest mocking isn't set up } } // Fall back to mock data return this.getMockData(toolName, args); } /** * Build CLI command string for simulation */ private buildCliCommand(toolName: string, args: any): string { const operation = this.getOperationFromTool(toolName); let command = `${this.pythonPath} -m smartsheet_ops.cli --operation ${operation}`; if (args.sheet_id) { command += ` --sheet-id ${args.sheet_id}`; } if (Object.keys(args).length > 1 || !args.sheet_id) { const dataArgs = { ...args }; if (args.sheet_id) delete dataArgs.sheet_id; if (Object.keys(dataArgs).length > 0) { command += ` --data '${JSON.stringify(dataArgs)}'`; } } return command; } /** * Map tool names to CLI operations */ private getOperationFromTool(toolName: string): string { const operationMap: { [key: string]: string } = { 'get_column_map': 'get_column_map', 'get_sheet_info': 'get_column_map', 'smartsheet_write': 'add_rows', 'smartsheet_search': 'search_sheet', 'smartsheet_get_sheet_cross_references': 'get_sheet_cross_references', 'smartsheet_create_discussion': 'create_discussion', 'smartsheet_upload_attachment': 'upload_attachment', 'list_workspaces': 'list_workspaces' }; return operationMap[toolName] || toolName; } /** * Get mock data based on tool name and arguments */ private getMockData(toolName: string, args: any): any { switch (toolName) { case 'get_column_map': case 'get_sheet_info': return this.getMockSheetInfo(args.sheet_id); case 'smartsheet_write': return this.getMockWriteResult(args); case 'smartsheet_search': return this.getMockSearchResult(args); case 'smartsheet_get_sheet_cross_references': return this.getMockCrossReferences(args); case 'smartsheet_create_discussion': return this.getMockDiscussion(args); case 'smartsheet_upload_attachment': return this.getMockAttachment(args); case 'list_workspaces': return this.getMockWorkspaces(); default: return { success: true, operation: toolName, message: `Mock result for ${toolName}`, data: args }; } } private getMockSheetInfo(sheetId: string) { return { success: true, sheet_id: sheetId, sheet_name: 'Test Sheet', column_map: { 'Task Name': '7777777777777777', 'Status': '8888888888888888', 'Due Date': '9999999999999999' }, sample_data: [ { 'Task Name': 'Test Task 1', 'Status': 'In Progress', 'Due Date': '2024-01-15' }, { 'Task Name': 'Test Task 2', 'Status': 'Complete', 'Due Date': '2024-01-10' } ], metadata: { total_columns: 3, total_rows: 2, system_columns: 0, formula_columns: 0 } }; } private getMockWriteResult(args: any) { return { success: true, rows_added: args.row_data?.length || 0, message: 'Rows added successfully', row_ids: ['5555555555555555', '6666666666666666'] }; } private getMockSearchResult(args: any) { return { success: true, pattern: args.pattern, matches_found: 2, matched_row_ids: ['5555555555555555', '6666666666666666'], search_details: [ { row_id: '5555555555555555', column_matches: ['Task Name'], match_text: args.pattern } ] }; } private getMockCrossReferences(args: any) { return { success: true, sheet_id: args.sheet_id, sheet_name: 'Test Sheet', total_references: 1, cross_references: [ { row_id: '5555555555555555', column_id: '7777777777777777', column_title: 'Referenced Value', reference: '[Target Sheet]Column1', referenced_sheet_name: 'Target Sheet', formula: '=INDEX({[Target Sheet]Column1:Column1}, MATCH([ID]@row, {[Target Sheet]ID:ID}, 0))', cell_value: 'Result Value' } ], include_details: args.include_details || true }; } private getMockDiscussion(args: any) { return { success: true, discussion_id: '1111111111111111', title: args.title || 'Test Discussion', comment_count: 1, discussion_type: args.discussion_type, target_id: args.target_id, created_by: 'test@example.com', created_at: new Date().toISOString() }; } private getMockAttachment(args: any) { return { success: true, attachment_id: '3333333333333333', file_name: args.file_name || 'test_file.pdf', file_size: 1024, attachment_type: args.attachment_type, target_id: args.target_id, upload_url: 'https://smartsheet.com/attachments/3333333333333333' }; } private getMockWorkspaces() { return { success: true, total_workspaces: 2, workspaces: [ { workspace_id: '9876543210987654', name: 'Test Workspace', permalink: 'https://app.smartsheet.com/workspaces/9876543210987654' }, { workspace_id: '8876543210987654', name: 'Healthcare Workspace', permalink: 'https://app.smartsheet.com/workspaces/8876543210987654' } ] }; } } /** * Mock exec function for child_process */ export function createMockExec(defaultResponse?: any) { return jest.fn().mockImplementation((command: string, options: any, callback: any) => { // Parse command to determine what operation is being called const operationMatch = command.match(/--operation\s+(\w+)/); const operation = operationMatch ? operationMatch[1] : 'unknown'; // Create mock response based on operation let mockResponse = defaultResponse || { success: true, operation: operation, timestamp: new Date().toISOString(), test_mode: true }; // If defaultResponse was provided, use it but ensure it has the right operation if (defaultResponse && typeof defaultResponse === 'object') { mockResponse = { ...defaultResponse, operation: operation }; } // Simulate async behavior process.nextTick(() => { if (callback) { callback(null, { stdout: JSON.stringify(mockResponse, null, 2), stderr: '' }); } }); }); } /** * Create a mock exec function that can simulate different scenarios */ export function createConfigurableMockExec() { const mockExec = jest.fn(); // Helper methods to configure behavior mockExec.mockSuccess = (response: any) => { mockExec.mockImplementation((command: string, options: any, callback: any) => { process.nextTick(() => { callback(null, { stdout: JSON.stringify(response, null, 2), stderr: '' }); }); }); }; mockExec.mockError = (error: string | Error) => { mockExec.mockImplementation((command: string, options: any, callback: any) => { process.nextTick(() => { callback(error instanceof Error ? error : new Error(error), null); }); }); }; mockExec.mockStderrError = (errorMessage: string) => { mockExec.mockImplementation((command: string, options: any, callback: any) => { const errorResponse = { success: false, error: errorMessage }; process.nextTick(() => { callback(null, { stdout: JSON.stringify(errorResponse, null, 2), stderr: errorMessage }); }); }); }; mockExec.mockInvalidJson = () => { mockExec.mockImplementation((command: string, options: any, callback: any) => { process.nextTick(() => { callback(null, { stdout: 'Invalid JSON{', stderr: '' }); }); }); }; mockExec.mockTimeout = () => { mockExec.mockImplementation((command: string, options: any, callback: any) => { const error = new Error('Command timeout') as any; error.killed = true; error.signal = 'SIGTERM'; process.nextTick(() => { callback(error, null); }); }); }; return mockExec; } /** * Create a complete mock Smartsheet server for testing */ export function createMockSmartsheetServer(options: MockMcpServerOptions = {}): MockMcpServer { const server = new MockMcpServer(options); // Don't pre-register tools - let tests register what they need // This provides more flexibility and cleaner test isolation return server; } /** * Create a mock server with common Smartsheet tools pre-registered for integration tests */ export function createMockSmartsheetServerWithTools(options: MockMcpServerOptions = {}): MockMcpServer { const server = new MockMcpServer(options); // Register common tools for integration testing const tools: Tool[] = [ { name: 'get_column_map', description: 'Get column mapping and sample data from a Smartsheet', inputSchema: { type: 'object', properties: { sheet_id: { type: 'string', description: 'Smartsheet sheet ID' } }, required: ['sheet_id'] } }, { name: 'smartsheet_get_sheet_cross_references', description: 'Get all cross-sheet references in a sheet', inputSchema: { type: 'object', properties: { sheet_id: { type: 'string', description: 'Smartsheet sheet ID to analyze' }, include_details: { type: 'boolean', description: 'Include detailed formula analysis', default: true } }, required: ['sheet_id'] } }, { name: 'smartsheet_write', description: 'Write data to Smartsheet', inputSchema: { type: 'object', properties: { sheet_id: { type: 'string', description: 'Smartsheet sheet ID' }, row_data: { type: 'array', description: 'Array of row data objects' }, column_map: { type: 'object', description: 'Column mapping object' } }, required: ['sheet_id', 'row_data', 'column_map'] } }, { name: 'smartsheet_create_discussion', description: 'Create a discussion on a sheet or row', inputSchema: { type: 'object', properties: { sheet_id: { type: 'string', description: 'Smartsheet sheet ID' }, discussion_type: { type: 'string', enum: ['sheet', 'row'], description: 'Type of discussion' }, comment_text: { type: 'string', description: 'Discussion comment text' }, title: { type: 'string', description: 'Discussion title' }, target_id: { type: 'string', description: 'Target row ID (for row discussions)' } }, required: ['sheet_id', 'discussion_type', 'comment_text'] } }, { name: 'smartsheet_upload_attachment', description: 'Upload attachment to sheet, row, or comment', inputSchema: { type: 'object', properties: { sheet_id: { type: 'string', description: 'Smartsheet sheet ID' }, file_path: { type: 'string', description: 'Path to file to upload' }, attachment_type: { type: 'string', enum: ['sheet', 'row', 'comment'], description: 'Type of attachment' }, target_id: { type: 'string', description: 'Target ID (row or comment)' }, file_name: { type: 'string', description: 'Custom file name' } }, required: ['sheet_id', 'file_path', 'attachment_type'] } }, { name: 'list_workspaces', description: 'List all available workspaces', inputSchema: { type: 'object', properties: {}, required: [] } } ]; tools.forEach(tool => server.registerTool(tool)); return server; } /** * Helper to create mock CLI responses */ export function createMockCliResponse(operation: string, success: boolean = true, data: any = {}) { return JSON.stringify({ success, operation, timestamp: new Date().toISOString(), ...data }, null, 2); } export default { MockMcpServer, createMockExec, createMockSmartsheetServer, createMockCliResponse };

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/terilios/smartsheet-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server