@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
JavaScript
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;
}
}