UNPKG

@zxkane/quip-mcp-server

Version:

MCP server for interacting with Quip spreadsheets

205 lines 9.47 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.getQuipTools = getQuipTools; exports.handleQuipReadSpreadsheet = handleQuipReadSpreadsheet; /** * Tool definitions and handlers for the Quip MCP Server */ const fs = __importStar(require("fs-extra")); const os = __importStar(require("os")); const path = __importStar(require("path")); const logger_1 = require("./logger"); const errors_1 = require("./errors"); const quipClient_1 = require("./quipClient"); const mockClient_1 = require("./mockClient"); const storage_1 = require("./storage"); /** * Get the list of Quip tools available in this MCP server * * @returns List of Tool objects */ function getQuipTools() { return [ { name: "quip_read_spreadsheet", description: "Read the content of a Quip spreadsheet by its thread ID. Returns a JSON object containing truncated CSV content (limited to 10KB) and metadata. For large spreadsheets, the returned content may be truncated. To access the complete CSV data, use the resource interface with URI format: 'quip://{threadId}?sheet={sheetName}' for local storage, 'file:///<storage path>/{threadId}-{sheetName}.csv' for file protocol, 's3://{bucket}/{prefix}{threadId}-{sheetName}.csv' for S3 storage identification, or HTTPS URLs (presigned S3 URLs) for direct access to S3 resources when S3 storage is used. The returned data structure includes: { 'csv_content': string (possibly truncated CSV data), 'metadata': { 'rows': number, 'columns': number, 'is_truncated': boolean, 'resource_uri': string } }", inputSchema: { type: "object", properties: { threadId: { type: "string", description: "The Quip document thread ID" }, sheetName: { type: "string", description: "Optional sheet or tab name to read from" } }, required: ["threadId"] } } ]; } /** * Handle the quip_read_spreadsheet tool * * @param args Tool arguments * @param storage Storage interface * @returns Promise resolving to array of content objects */ async function handleQuipReadSpreadsheet(args, storage, useMock = false) { const threadId = args.threadId; const sheetName = args.sheetName; if (!threadId) { throw new errors_1.InvalidParamsError("threadId is required"); } logger_1.logger.info(`Reading spreadsheet from thread ${threadId}`, { sheet: sheetName || 'default', mock: useMock }); // Initialize client based on mode let client; if (useMock) { // Use mock client logger_1.logger.info('Using mock Quip client'); client = new mockClient_1.MockQuipClient(); } else { // Get Quip token from environment const quipToken = process.env.QUIP_TOKEN; const quipBaseUrl = process.env.QUIP_BASE_URL || "https://platform.quip.com"; if (!quipToken) { logger_1.logger.error('QUIP_TOKEN environment variable is not set'); throw new errors_1.QuipApiError("QUIP_TOKEN environment variable is not set"); } // Initialize real Quip client client = new quipClient_1.QuipClient(quipToken, quipBaseUrl); } // Variable to store CSV data let csvData = null; try { // Check if the thread is a spreadsheet if (!await client.isSpreadsheet(threadId)) { logger_1.logger.error(`Thread ${threadId} is not a spreadsheet or does not exist`); throw new errors_1.QuipApiError(`Thread ${threadId} is not a spreadsheet or does not exist`); } // Try primary export method first let errorMessage = null; try { // Export thread to XLSX first logger_1.logger.info(`Attempting primary export method: XLSX for thread ${threadId}`); const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'quip-mcp-')); const xlsxPath = path.join(tempDir, `${threadId}.xlsx`); await client.exportThreadToXLSX(threadId, xlsxPath); // Convert XLSX to CSV logger_1.logger.info(`Converting sheet '${sheetName || 'default'}' from XLSX to CSV`); csvData = (0, quipClient_1.convertXLSXToCSV)(xlsxPath, sheetName); // Clean up temporary XLSX file await fs.remove(xlsxPath); await fs.remove(tempDir); } catch (error) { errorMessage = error instanceof Error ? error.message : String(error); logger_1.logger.warn(`Primary export method failed: ${errorMessage}`); logger_1.logger.info("Attempting fallback export method"); try { // Try fallback method csvData = await client.exportThreadToCSVFallback(threadId, sheetName); logger_1.logger.info("Successfully exported using fallback method"); } catch (fallbackError) { const fallbackErrorMsg = fallbackError instanceof Error ? fallbackError.message : String(fallbackError); logger_1.logger.error(`Fallback export method also failed: ${fallbackErrorMsg}`); throw new errors_1.QuipApiError(`Failed to export spreadsheet. Primary error: ${errorMessage}, Fallback error: ${fallbackErrorMsg}`); } } if (!csvData) { throw new errors_1.QuipApiError("Failed to export data: no CSV content generated"); } } catch (error) { if (error instanceof errors_1.QuipApiError) { throw error; } const errorMessage = error instanceof Error ? error.message : String(error); logger_1.logger.error(`Error handling quip_read_spreadsheet: ${errorMessage}`); throw new errors_1.QuipApiError(`Error handling quip_read_spreadsheet: ${errorMessage}`); } // At this point, csvData should be populated if (!csvData) { throw new errors_1.QuipApiError("Unexpected error: CSV data is null after successful export"); } // Save the full CSV content to storage const storageId = await storage.saveCSV(threadId, csvData, sheetName); logger_1.logger.info(`Saved spreadsheet data to storage with ID: ${storageId}`, { sheet: sheetName || 'default' }); // Get metadata const metadata = await storage.getMetadata(threadId, sheetName); // Truncate CSV content if it's too large (> 10KB) const MAX_SIZE = 10 * 1024; // 10KB const [truncatedCsv, isTruncated] = (0, storage_1.truncateCSVContent)(csvData, MAX_SIZE); // Update metadata with truncation info metadata.is_truncated = isTruncated; if (metadata.resource_uri.startsWith('s3+https://') && typeof storage.generatePresignedUrl === 'function') { logger_1.logger.debug(`Converting s3+https:// URI to presigned URL for thread_id: ${threadId}, sheet_name: ${sheetName || 'default'}`); try { // Generate a presigned URL const presignedUrl = await storage.generatePresignedUrl(threadId, sheetName); // Update the URI to use the presigned URL metadata.resource_uri = presignedUrl; logger_1.logger.info(`Generated presigned URL: ${presignedUrl}`); } catch (error) { logger_1.logger.error(`Failed to generate presigned URL: ${error instanceof Error ? error.message : String(error)}`); // Continue with the original URI } } // Create response with CSV content and metadata const responseData = { csv_content: truncatedCsv, metadata: metadata }; logger_1.logger.info(`Returning spreadsheet data for thread ${threadId}`, { sheet: sheetName || 'default', rows: metadata.total_rows, truncated: isTruncated }); // Convert to JSON and return return [{ type: "text", text: JSON.stringify(responseData) }]; } //# sourceMappingURL=tools.js.map