UNPKG

@smartsheet/smar-mcp

Version:

A Model Context Protocol (MCP) server for interacting with the Smartsheet API. This server provides tools for searching, retrieving, and updating Smartsheet sheets through the MCP protocol.

123 lines (122 loc) 4.86 kB
import axios from 'axios'; import { SmartsheetDiscussionAPI } from './smartsheet-discussion-api.js'; import { SmartsheetFolderAPI } from './smartsheet-folder-api.js'; import { SmartsheetSearchAPI } from './smartsheet-search-api.js'; import { SmartsheetSheetAPI } from './smartsheet-sheet-api.js'; import { SmartsheetWorkspaceAPI } from './smartsheet-workspace-api.js'; import { SmartsheetUserAPI } from './smartsheet-user-api.js'; import packageJson from '../../package.json' with { type: 'json' }; /** * Direct Smartsheet API client that doesn't rely on the SDK */ export class SmartsheetAPI { baseUrl; accessToken; sheets; workspaces; folders; users; search; discussions; /** * Creates a new SmartsheetAPI instance * @param accessToken Smartsheet API access token * @param baseUrl Smartsheet API base URL */ constructor(accessToken, baseUrl) { this.baseUrl = baseUrl || ''; this.accessToken = accessToken || ''; this.sheets = new SmartsheetSheetAPI(this); this.workspaces = new SmartsheetWorkspaceAPI(this); this.folders = new SmartsheetFolderAPI(this); this.users = new SmartsheetUserAPI(this); this.search = new SmartsheetSearchAPI(this); this.discussions = new SmartsheetDiscussionAPI(this); if (this.accessToken === '') { throw new Error('SMARTSHEET_API_KEY environment variable is not set'); } if (this.baseUrl === '') { throw new Error('SMARTSHEET_ENDPOINT environment variable is not set'); } if (!this.baseUrl.startsWith('https://')) { throw new Error('SMARTSHEET_ENDPOINT must use HTTPS'); } } /** * Makes a request to the Smartsheet API with retry logic * @param method HTTP method * @param endpoint API endpoint * @param data Request body data * @param queryParams Query parameters * @returns API response */ async request(method, endpoint, data, queryParams) { const maxRetries = 3; let retries = 0; while (retries <= maxRetries) { try { const url = new URL(`${this.baseUrl}${endpoint}`); // Add query parameters if provided if (queryParams) { Object.entries(queryParams).forEach(([key, value]) => { if (value !== undefined && value !== null) { url.searchParams.append(key, String(value)); } }); } console.info(`API Request: ${method} ${endpoint}`); const response = await axios({ method, url: url.toString(), data, headers: { 'Authorization': `Bearer ${this.accessToken}`, 'Content-Type': 'application/json', 'User-Agent': `smar-mcp/${packageJson.version}`, } }); return response.data; } catch (error) { // Check if rate limited if (error.response?.status === 429 && retries < maxRetries) { const retryAfter = error.response.headers['retry-after'] || 1; const delay = Math.min(Math.max(parseInt(retryAfter, 10) * 1000, Math.pow(2, retries) * 1000 + Math.random() * 1000), 60000); console.error(`[Rate Limit] Retrying in ${delay}ms...`); await new Promise(resolve => setTimeout(resolve, delay)); retries++; } else { console.error(`API Error: ${error.message}`, this.sanitizeError(error)); throw this.formatError(error); } } } throw new Error('Maximum retries exceeded'); } /** * Sanitizes an error object for safe logging by stripping sensitive fields * such as Authorization headers and tokens. */ sanitizeError(error) { return { message: error.message, status: error.response?.status, data: error.response?.data, }; } /** * Formats an error for consistent error handling * @param error Error to format * @returns Formatted error */ formatError(error) { const errorMessage = error.response?.data?.message || error.message; const formattedError = new Error(errorMessage); // Add additional properties formattedError.statusCode = error.response?.status; formattedError.errorCode = error.response?.data?.errorCode; formattedError.detail = error.response?.data?.detail; return formattedError; } }