UNPKG

@mbelinky/x-mcp-server

Version:

Enhanced MCP server for X with OAuth 2.0 support, media uploads, and comprehensive rate limiting.

56 lines (55 loc) 2.14 kB
import { XError } from '../types.js'; const V2_MEDIA_UPLOAD_URL = 'https://api.x.com/2/media/upload'; /** * Upload media using X API v2 endpoints * This implements the single-request upload for v2 */ export class V2MediaUploader { accessToken; constructor(accessToken) { this.accessToken = accessToken; } /** * Upload media file using v2 API * @param buffer - The media content as a Buffer * @param mimeType - The MIME type of the media * @returns The media ID string to use in tweets */ async uploadMedia(buffer, mimeType) { try { // v2 media upload uses a simple multipart upload const formData = new FormData(); // Create a Blob from the buffer with proper MIME type const blob = new Blob([buffer], { type: mimeType }); // Determine media category based on MIME type const mediaCategory = mimeType.startsWith('image/') ? 'tweet_image' : 'tweet_video'; // Add the media file formData.append('media', blob, 'media'); formData.append('media_category', mediaCategory); const response = await fetch(V2_MEDIA_UPLOAD_URL, { method: 'POST', headers: { 'Authorization': `Bearer ${this.accessToken}` }, body: formData }); if (!response.ok) { const errorText = await response.text(); throw new Error(`Upload failed: ${errorText}`); } const result = await response.json(); // v2 returns data.id or data.media_key if (result.data) { return result.data.id || result.data.media_key; } // Fallback for other response formats return result.media_id_string || result.media_id || result.id; } catch (error) { if (error instanceof Error) { throw new XError(`V2 media upload failed: ${error.message}`, 'media_upload_failed', 500); } throw error; } } }