UNPKG

@takashito/linode-mcp-server

Version:

MCP server for Linode API

783 lines (777 loc) 31.7 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; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.registerObjectStorageTools = registerObjectStorageTools; const schemas = __importStar(require("./schemas")); const fs = __importStar(require("fs")); const axios_1 = __importDefault(require("axios")); const path = __importStar(require("path")); const os = __importStar(require("os")); const url_1 = require("url"); const mime = __importStar(require("mime-types")); function registerObjectStorageTools(server, client) { // Clusters server.tool('list_object_storage_clusters', 'Get a list of all Object Storage clusters', schemas.listClustersSchema.shape, async (_, extra) => { const clusters = await client.objectStorage.getClusters(); return { content: [ { type: 'text', text: formatObjectStorageClusters(clusters) }, ], }; }); // Endpoints server.tool('list_object_storage_endpoints', 'Get a list of all Object Storage endpoints with their types', schemas.listEndpointsSchema.shape, async (params, extra) => { const result = await client.objectStorage.getEndpoints(params); return { content: [ { type: 'text', text: formatObjectStorageEndpoints(result.data) }, ], }; }); // Buckets server.tool('list_object_storage_buckets', 'Get a list of all Object Storage buckets', schemas.listBucketsSchema.shape, async (params, extra) => { const result = await client.objectStorage.getBuckets(params); return { content: [ { type: 'text', text: formatObjectStorageBuckets(result.data) }, ], }; }); server.tool('get_object_storage_bucket', 'Get details for a specific Object Storage bucket', schemas.getBucketSchema.shape, async (params, extra) => { try { // The client implementation expects region (cluster in client code) and bucket name const result = await client.objectStorage.getBucket(params.region, params.bucket // Use bucket parameter from the schema for lookup ); return { content: [ { type: 'text', text: formatObjectStorageBucket(result) }, ], }; } catch (error) { // If the error has a response, log that too if (error?.response && error.response.data) { console.error("Error response data:", JSON.stringify(error.response.data)); } throw error; // Rethrow to maintain original behavior } }); server.tool('create_object_storage_bucket', 'Create a new Object Storage bucket', schemas.createBucketSchema.shape, async (params, extra) => { try { // Ensure parameters match the client's expectations // The Linode API expects 'label' for the bucket name and 'region' for the location const createParams = { label: params.label, cluster: params.region, // Use region parameter but map to cluster for client endpoint_type: params.endpoint_type, acl: params.acl, cors_enabled: params.cors_enabled }; const result = await client.objectStorage.createBucket(createParams); return { content: [ { type: 'text', text: formatObjectStorageBucket(result) }, ], }; } catch (error) { console.error("Bucket creation error:", error?.message || error); // If the error has a response, log that too if (error?.response && error.response.data) { console.error("Error response data:", JSON.stringify(error.response.data)); } throw error; // Rethrow to maintain original behavior } }); server.tool('delete_object_storage_bucket', 'Delete an Object Storage bucket', schemas.deleteBucketSchema.shape, async (params, extra) => { await client.objectStorage.deleteBucket(params.region, params.bucket); return { content: [ { type: 'text', text: `Bucket '${params.bucket}' in region '${params.region}' has been deleted.` }, ], }; }); server.tool('get_object_storage_bucket_access', 'Get access configuration for an Object Storage bucket', schemas.getBucketAccessSchema.shape, async (params, extra) => { const result = await client.objectStorage.getBucketAccess(params.region, params.bucket); return { content: [ { type: 'text', text: `Bucket Access for '${params.bucket}': ACL: ${result.acl} CORS Enabled: ${result.cors_enabled ? 'Yes' : 'No'}` }, ], }; }); server.tool('update_object_storage_bucket_access', 'Update access configuration for an Object Storage bucket', schemas.updateBucketAccessSchema.shape, async (params, extra) => { const { region, bucket, ...data } = params; const result = await client.objectStorage.updateBucketAccess(region, bucket, data); return { content: [ { type: 'text', text: `Updated Bucket Access for '${bucket}': ACL: ${result.acl} CORS Enabled: ${result.cors_enabled ? 'Yes' : 'No'}` }, ], }; }); // Objects server.tool('list_object_storage_objects', 'List objects in an Object Storage bucket', schemas.listObjectsSchema.shape, async (params, extra) => { const { region, bucket, ...paginationParams } = params; const result = await client.objectStorage.getObjects(region, bucket, paginationParams); return { content: [ { type: 'text', text: formatObjectStorageObjects(result.data) }, ], }; }); // SSL/TLS certificates server.tool('get_object_storage_bucket_certificate', 'Get SSL/TLS certificate for an Object Storage bucket', schemas.getBucketCertificateSchema.shape, async (params, extra) => { try { const result = await client.objectStorage.getBucketCertificate(params.region, params.bucket); return { content: [ { type: 'text', text: `Certificate for '${params.bucket}': Expires: ${new Date(result.expiry).toLocaleString()}` }, ], }; } catch (error) { if (error.response && error.response.status === 404) { return { content: [ { type: 'text', text: `No certificate found for bucket '${params.bucket}' in region '${params.region}'.` }, ], }; } throw error; } }); server.tool('upload_object_storage_bucket_certificate', 'Upload SSL/TLS certificate for an Object Storage bucket', schemas.uploadBucketCertificateSchema.shape, async (params, extra) => { const { region, bucket, certificate, private_key } = params; const result = await client.objectStorage.uploadBucketCertificate(region, bucket, { certificate, private_key, }); return { content: [ { type: 'text', text: `Certificate uploaded for bucket '${bucket}' in region '${region}'. Expires: ${new Date(result.expiry).toLocaleString()}` }, ], }; }); server.tool('delete_object_storage_bucket_certificate', 'Delete SSL/TLS certificate for an Object Storage bucket', schemas.deleteBucketCertificateSchema.shape, async (params, extra) => { await client.objectStorage.deleteBucketCertificate(params.region, params.bucket); return { content: [ { type: 'text', text: `Certificate for bucket '${params.bucket}' in region '${params.region}' has been deleted.` }, ], }; }); // Access keys server.tool('list_object_storage_keys', 'Get a list of all Object Storage keys', schemas.listKeysSchema.shape, async (params, extra) => { const result = await client.objectStorage.getKeys(params); return { content: [ { type: 'text', text: formatObjectStorageKeys(result.data) }, ], }; }); server.tool('get_object_storage_key', 'Get details for a specific Object Storage key', schemas.getKeySchema.shape, async (params, extra) => { const result = await client.objectStorage.getKey(params.id); return { content: [ { type: 'text', text: formatObjectStorageKey(result) }, ], }; }); server.tool('create_object_storage_key', 'Create a new Object Storage key', schemas.createKeySchema.shape, async (params, extra) => { const result = await client.objectStorage.createKey(params); return { content: [ { type: 'text', text: formatObjectStorageKey(result) }, ], }; }); server.tool('update_object_storage_key', 'Update an Object Storage key', schemas.updateKeySchema.shape, async (params, extra) => { const { id, ...updateData } = params; const result = await client.objectStorage.updateKey(id, updateData); return { content: [ { type: 'text', text: formatObjectStorageKey(result) }, ], }; }); server.tool('delete_object_storage_key', 'Delete an Object Storage key', schemas.deleteKeySchema.shape, async (params, extra) => { await client.objectStorage.deleteKey(params.id); return { content: [ { type: 'text', text: `Object Storage key with ID ${params.id} has been deleted.` }, ], }; }); // Default access server.tool('get_object_storage_default_bucket_access', 'Get default bucket access configuration', schemas.getDefaultBucketAccessSchema.shape, async (_, extra) => { try { const result = await client.objectStorage.getDefaultBucketAccess(); return { content: [ { type: 'text', text: `Default Bucket Access: Cluster: ${result.cluster} Bucket: ${result.bucket_name} Region: ${result.region} ACL: ${result.acl} CORS Enabled: ${result.cors_enabled ? 'Yes' : 'No'}` }, ], }; } catch (error) { if (error.response && error.response.status === 404) { return { content: [ { type: 'text', text: 'No default bucket access configuration found.' }, ], }; } throw error; } }); server.tool('update_object_storage_default_bucket_access', 'Update default bucket access configuration', schemas.updateDefaultBucketAccessSchema.shape, async (params, extra) => { const result = await client.objectStorage.updateDefaultBucketAccess(params); return { content: [ { type: 'text', text: `Updated Default Bucket Access: Cluster: ${result.cluster} Bucket: ${result.bucket_name} Region: ${result.region} ACL: ${result.acl} CORS Enabled: ${result.cors_enabled ? 'Yes' : 'No'}` }, ], }; }); // Object ACL Management server.tool('update_object_acl', 'Update access control level (ACL) for an object in a bucket', schemas.updateObjectACLSchema.shape, async (params, extra) => { const { region, bucket, name, acl } = params; await client.objectStorage.updateObjectACL(region, bucket, name, { acl }); return { content: [ { type: 'text', text: `ACL for object '${name}' in bucket '${bucket}' has been updated to '${acl}'.` }, ], }; }); // Object URL Generation server.tool('generate_object_url', 'Generate a pre-signed URL for an object in a bucket', schemas.getObjectURLSchema.shape, async (params, extra) => { const { region, bucket, name, ...urlParams } = params; // Ensure we're using the right parameter names for the API const apiParams = { name, ...urlParams }; const result = await client.objectStorage.getObjectURL(region, bucket, apiParams); return { content: [ { type: 'text', text: `Pre-signed URL for object '${name}' in bucket '${bucket}': ${result.url} Note: This URL is temporary and will expire after ${params.expires_in || 3600} seconds.` }, ], }; }); // Upload Object server.tool('upload_object', 'Upload and create an new object to an Object Storage bucket', schemas.uploadObjectSchema.shape, async (params, extra) => { try { const { region, bucket, object_path, source, content_type, acl, expires_in } = params; // Determine content type from file extension or parameter const determinedContentType = content_type || getContentType(object_path); // Get pre-signed URL for PUT operation const urlResult = await client.objectStorage.getObjectURL(region, bucket, { name: object_path, method: 'PUT', expires_in: expires_in || 3600, content_type: determinedContentType }); // Get data from source based on type let data; let dataSource; if (isURL(source)) { data = await fetchFromURL(source); dataSource = 'URL'; } else if (isFilePath(source)) { data = await readFromFile(source); dataSource = 'File'; } else { // Treat as raw string content data = Buffer.from(source); dataSource = 'String content'; } // Upload to pre-signed URL await axios_1.default.put(urlResult.url, data, { headers: { 'Content-Type': determinedContentType } }); // Set ACL if provided if (acl) { await client.objectStorage.updateObjectACL(region, bucket, object_path, { acl }); } return { content: [ { type: 'text', text: `Successfully uploaded object '${object_path}' to bucket '${bucket}'. Source: ${dataSource} Size: ${formatBytes(data.length)} Content Type: ${determinedContentType} ACL: ${acl || 'Default bucket ACL'}` }, ], }; } catch (error) { console.error("Error uploading object:", error); throw new Error(`Failed to upload object: ${error.message}`); } }); // Download Object server.tool('download_object', 'Download an object from a bucket and save it to a local file', schemas.downloadObjectSchema.shape, async (params, extra) => { try { const { region, bucket, object_path, destination, expires_in } = params; // Get pre-signed URL for GET operation const result = await client.objectStorage.getObjectURL(region, bucket, { name: object_path, method: 'GET', expires_in: expires_in || 3600 }); // If destination is provided, download the file if (destination) { // Download the file to the specified location const downloadResult = await downloadToFile(result.url, destination); return { content: [ { type: 'text', text: `Successfully downloaded '${object_path}' from bucket '${bucket}'. Saved to: ${downloadResult.filePath} Size: ${formatBytes(downloadResult.size)} Content Type: ${getContentType(object_path)}` }, ], }; } // If no destination provided, attempt to save to Downloads folder or user's home directory try { // Get the appropriate download directory (Downloads folder or home directory) const downloadDir = getDownloadDirectory(); const localFilename = path.basename(object_path); const localPath = path.join(downloadDir, localFilename); // Download the file const downloadResult = await downloadToFile(result.url, localPath); return { content: [ { type: 'text', text: `Successfully downloaded '${object_path}' from bucket '${bucket}'. Saved to: ${downloadResult.filePath} Size: ${formatBytes(downloadResult.size)} Content Type: ${getContentType(object_path)}` }, ], }; } catch (downloadError) { // If download fails, provide the URL so the user can download manually console.error("Error downloading file:", downloadError); return { content: [ { type: 'text', text: `Unable to automatically download the file: ${downloadError.message} Pre-signed URL for downloading '${object_path}' from bucket '${bucket}': ${result.url} Note: This URL is temporary and will expire after ${expires_in || 3600} seconds. You can directly open this URL in a browser or use it with tools like curl or wget: curl -o "${path.basename(object_path)}" "${result.url}"` }, ], }; } } catch (error) { console.error("Error with download operation:", error); throw new Error(`Failed to download object: ${error.message}`); } }); // Delete Object server.tool('delete_object', 'Delete an object from an Object Storage bucket', schemas.deleteObjectSchema.shape, async (params, extra) => { try { const { region, bucket, object_path, expires_in } = params; // Get pre-signed URL for DELETE operation const result = await client.objectStorage.getObjectURL(region, bucket, { name: object_path, method: 'DELETE', expires_in: expires_in || 3600 }); // Execute the DELETE request await axios_1.default.delete(result.url); return { content: [ { type: 'text', text: `Successfully deleted object '${object_path}' from bucket '${bucket}'.` }, ], }; } catch (error) { console.error("Error deleting object:", error); throw new Error(`Failed to delete object: ${error.message}`); } }); // Transfer Statistics server.tool('get_object_storage_transfer', 'Get Object Storage transfer statistics', schemas.getTransferStatsSchema.shape, async (_, extra) => { const result = await client.objectStorage.getTransferStats(); return { content: [ { type: 'text', text: formatTransferStats(result) }, ], }; }); // Object Storage Types server.tool('list_object_storage_types', 'Get a list of all available Object Storage types and prices, including any region-specific rates.', schemas.listObjectStorageTypesSchema.shape, async (_, extra) => { const result = await client.objectStorage.getTypes(); return { content: [ { type: 'text', text: formatObjectStorageTypes(result) }, ], }; }); // Service server.tool('cancel_object_storage', 'Cancel Object Storage service', schemas.cancelObjectStorageSchema.shape, async (_, extra) => { await client.objectStorage.cancelObjectStorage(); return { content: [ { type: 'text', text: 'Object Storage service has been cancelled.' }, ], }; }); } /** * Formats transfer statistics for display */ function formatTransferStats(stats) { const overall = [ 'Overall Transfer Statistics:', `Used: ${formatBytes(stats.used)}`, `Billable: ${formatBytes(stats.billable)}`, `Quota: ${formatBytes(stats.quota)}`, `Utilization: ${((stats.used / stats.quota) * 100).toFixed(2)}%`, '', 'Transfer by Region:' ]; if (stats.regions && stats.regions.length > 0) { stats.regions.forEach(region => { overall.push(` Region: ${region.region}`); overall.push(` Used: ${formatBytes(region.used)}`); overall.push(` Billable: ${formatBytes(region.billable)}`); overall.push(` Quota: ${formatBytes(region.quota)}`); overall.push(` Utilization: ${((region.used / region.quota) * 100).toFixed(2)}%`); }); } else { overall.push(' No region-specific data available.'); } return overall.join('\n'); } /** * Formats Object Storage types for display */ function formatObjectStorageTypes(types) { if (types.length === 0) { return 'No Object Storage types found.'; } const headers = [ 'ID | Label | Storage Price | Transfer Price', '----|-------|--------------|---------------' ]; const rows = types.map(type => { return `${type.id} | ${type.label} | $${type.storage_price.toFixed(4)}/GB | $${type.transfer_price.toFixed(4)}/GB`; }); return headers.concat(rows).join('\n'); } /** * Formats an Object Storage cluster for display */ function formatObjectStorageCluster(cluster) { return `${cluster.id} (${cluster.region}, Status: ${cluster.status})`; } /** * Formats a list of Object Storage clusters for display */ function formatObjectStorageClusters(clusters) { if (clusters.length === 0) { return 'No Object Storage clusters found.'; } return clusters.map(formatObjectStorageCluster).join('\n'); } /** * Formats an Object Storage bucket for display */ function formatObjectStorageBucket(bucket) { const details = [ `Bucket: ${bucket.label}`, `Cluster: ${bucket.cluster}`, `Region: ${bucket.region}`, `Created: ${new Date(bucket.created).toLocaleString()}`, `Size: ${formatBytes(bucket.size)}`, `Objects: ${bucket.objects}`, `Access Control: ${bucket.acl}`, `CORS Enabled: ${bucket.cors_enabled ? 'Yes' : 'No'}`, `Hostname: ${bucket.hostname}` ]; return details.join('\n'); } /** * Formats a list of Object Storage buckets for display */ function formatObjectStorageBuckets(buckets) { if (buckets.length === 0) { return 'No Object Storage buckets found.'; } const rows = buckets.map(bucket => { return `${bucket.label} (Cluster: ${bucket.cluster}, Region: ${bucket.region}, Objects: ${bucket.objects}, Size: ${formatBytes(bucket.size)})`; }); return rows.join('\n'); } /** * Formats an Object Storage key for display */ function formatObjectStorageKey(key) { const details = [ `ID: ${key.id}`, `Label: ${key.label}`, `Access Key: ${key.access_key}`, `Secret Key: ${key.secret_key ? '********' : 'Not available'}`, `Limited: ${key.limited ? 'Yes' : 'No'}`, `Created: ${new Date(key.created).toLocaleString()}` ]; if (key.bucket_access && key.bucket_access.length > 0) { details.push('Bucket Access:'); key.bucket_access.forEach(access => { details.push(` - ${access.cluster}/${access.bucket_name} (${access.permissions})`); }); } else { details.push('Bucket Access: Full access to all buckets'); } return details.join('\n'); } /** * Formats a list of Object Storage keys for display */ function formatObjectStorageKeys(keys) { if (keys.length === 0) { return 'No Object Storage keys found.'; } const rows = keys.map(key => { const accessType = key.limited ? 'Limited' : 'Full'; return `${key.label} (ID: ${key.id}, Access: ${key.access_key}, Type: ${accessType})`; }); return rows.join('\n'); } /** * Formats an Object Storage object for display */ function formatObjectStorageObject(obj) { return `${obj.name} (Size: ${formatBytes(obj.size)}, Modified: ${new Date(obj.last_modified).toLocaleString()})`; } /** * Formats a list of Object Storage objects for display */ function formatObjectStorageObjects(objects) { if (objects.length === 0) { return 'No objects found in bucket.'; } return objects.map(formatObjectStorageObject).join('\n'); } /** * Formats an endpoint for display */ function formatEndpoint(endpoint) { const s3Endpoint = endpoint.s3_endpoint || 'Not available'; return `Region: ${endpoint.region}, Type: ${endpoint.endpoint_type}, Endpoint: ${s3Endpoint}`; } /** * Formats a list of Object Storage endpoints for display */ function formatObjectStorageEndpoints(endpoints) { if (endpoints.length === 0) { return 'No Object Storage endpoints found.'; } return endpoints.map(formatEndpoint).join('\n'); } /** * Formats bytes into a human-readable string */ function formatBytes(bytes, decimals = 2) { if (bytes === 0) return '0 Bytes'; const k = 1024; const dm = decimals < 0 ? 0 : decimals; const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; } /** * Determines if a string is a URL */ function isURL(str) { try { new url_1.URL(str); return str.startsWith('http://') || str.startsWith('https://'); } catch { return false; } } /** * Determines if a string is likely a file path */ function isFilePath(str) { // Check for Unix-like paths const unixPathPattern = str.startsWith('/') || str.includes('./') || str.includes('../'); // Check for Windows paths (drive letter or UNC path) const windowsDrivePattern = /^[a-zA-Z]:[/\\]/.test(str); const windowsUNCPattern = /^\\\\/.test(str); return unixPathPattern || windowsDrivePattern || windowsUNCPattern; } /** * Gets data from a URL */ async function fetchFromURL(url) { try { const response = await axios_1.default.get(url, { responseType: 'arraybuffer' }); return Buffer.from(response.data); } catch (error) { throw new Error(`Failed to fetch from URL: ${error.message}`); } } /** * Reads data from a file */ async function readFromFile(filePath) { try { return await fs.promises.readFile(filePath); } catch (error) { throw new Error(`Failed to read file: ${error.message}`); } } /** * Downloads data from a URL and saves it to a file */ async function downloadToFile(url, destinationPath) { try { // Get the data from the URL const response = await axios_1.default.get(url, { responseType: 'arraybuffer' }); // Make sure the directory exists const dirname = path.dirname(destinationPath); await fs.promises.mkdir(dirname, { recursive: true }); // Write the file await fs.promises.writeFile(destinationPath, response.data); return { filePath: destinationPath, size: response.data.length }; } catch (error) { throw new Error(`Failed to download file: ${error.message}`); } } /** * Gets content type based on file path or URL */ function getContentType(source) { if (source.includes('.')) { const extension = source.split('.').pop()?.toLowerCase(); if (extension) { const mimeType = mime.lookup(extension); if (mimeType) { return mimeType; } } } return 'application/octet-stream'; // Default if no extension or unknown type } /** * Gets the most appropriate download directory path based on OS */ function getDownloadDirectory() { const homeDir = os.homedir(); // Try standard download directories based on OS if (process.platform === 'win32') { // Windows: First try Downloads folder in user's home directory const windowsDownloads = path.join(homeDir, 'Downloads'); if (fs.existsSync(windowsDownloads)) { return windowsDownloads; } } else if (process.platform === 'darwin') { // macOS: Check standard Downloads directory const macDownloads = path.join(homeDir, 'Downloads'); if (fs.existsSync(macDownloads)) { return macDownloads; } } else { // Linux/Unix: Check standard XDG downloads directory or fallback // Try XDG user directories first if defined try { // Check if XDG_DOWNLOAD_DIR is defined in user-dirs.dirs const xdgConfigPath = path.join(homeDir, '.config', 'user-dirs.dirs'); if (fs.existsSync(xdgConfigPath)) { const content = fs.readFileSync(xdgConfigPath, 'utf-8'); const match = content.match(/XDG_DOWNLOAD_DIR="([^"]+)"/); if (match && match[1]) { let downloadDir = match[1].replace('$HOME', homeDir); // Handle tilde expansion if (downloadDir.startsWith('~/')) { downloadDir = path.join(homeDir, downloadDir.substring(2)); } if (fs.existsSync(downloadDir)) { return downloadDir; } } } } catch (error) { // Ignore errors reading XDG config and fall back } // Standard Linux fallback to ~/Downloads if it exists const linuxDownloads = path.join(homeDir, 'Downloads'); if (fs.existsSync(linuxDownloads)) { return linuxDownloads; } } // Default fallback to user's home directory if no Downloads folder exists return homeDir; } //# sourceMappingURL=tools.js.map