@zxkane/quip-mcp-server
Version:
MCP server for interacting with Quip spreadsheets
205 lines • 9.47 kB
JavaScript
;
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