Skip to main content
Glama
client.ts8.39 kB
import axios, { AxiosError } from 'axios'; import MediumAuth from './auth'; interface PublishArticleParams { title: string; content: string; tags?: string[]; publicationId?: string; publishStatus?: 'public' | 'draft' | 'unlisted'; notifyFollowers?: boolean; } interface UpdateArticleParams { articleId: string; title?: string; content?: string; tags?: string[]; publishStatus?: 'public' | 'draft' | 'unlisted'; } interface SearchArticlesParams { keywords?: string[]; publicationId?: string; tags?: string[]; } interface RetryConfig { maxRetries: number; baseDelay: number; maxDelay: number; } class MediumClient { private auth: MediumAuth; private baseUrl = 'https://api.medium.com/v1'; private retryConfig: RetryConfig = { maxRetries: 3, baseDelay: 1000, maxDelay: 10000 }; private rateLimitRemaining: number | null = null; private rateLimitReset: number | null = null; constructor(auth: MediumAuth) { this.auth = auth; } private async sleep(ms: number): Promise<void> { return new Promise(resolve => setTimeout(resolve, ms)); } private calculateBackoffDelay(attempt: number): number { const delay = this.retryConfig.baseDelay * Math.pow(2, attempt); return Math.min(delay, this.retryConfig.maxDelay); } private isRetryableError(error: AxiosError): boolean { if (!error.response) return true; // Network errors are retryable const status = error.response.status; // Retry on rate limits, server errors, and some client errors return status === 429 || status === 503 || status === 502 || status === 504; } private updateRateLimitInfo(headers: any): void { if (headers['x-ratelimit-remaining']) { this.rateLimitRemaining = parseInt(headers['x-ratelimit-remaining']); } if (headers['x-ratelimit-reset']) { this.rateLimitReset = parseInt(headers['x-ratelimit-reset']) * 1000; } } private async checkRateLimit(): Promise<void> { if (this.rateLimitRemaining !== null && this.rateLimitRemaining === 0) { if (this.rateLimitReset) { const now = Date.now(); const waitTime = this.rateLimitReset - now; if (waitTime > 0) { console.log(`⏳ Rate limit reached. Waiting ${Math.ceil(waitTime / 1000)}s...`); await this.sleep(waitTime); } } } } private async makeRequest( method: 'get' | 'post' | 'put' | 'delete', endpoint: string, data?: any, retryAttempt: number = 0 ): Promise<any> { try { // Check rate limit before making request await this.checkRateLimit(); const response = await axios({ method, url: `${this.baseUrl}${endpoint}`, headers: { 'Authorization': `Bearer ${this.auth.getAccessToken()}`, 'Content-Type': 'application/json', 'Accept': 'application/json' }, data, validateStatus: (status) => status < 500 // Don't throw on 4xx errors }); // Update rate limit info this.updateRateLimitInfo(response.headers); // Handle non-2xx responses if (response.status >= 400) { throw this.createDetailedError(response); } return response.data; } catch (error: any) { // Enhanced error logging const axiosError = error as AxiosError; if (axiosError.response) { console.error('Medium API Error:', { status: axiosError.response.status, statusText: axiosError.response.statusText, data: axiosError.response.data, endpoint }); } else { console.error('Network Error:', error.message); } // Retry logic with exponential backoff if (this.isRetryableError(axiosError) && retryAttempt < this.retryConfig.maxRetries) { const delay = this.calculateBackoffDelay(retryAttempt); console.log(`🔄 Retrying request (attempt ${retryAttempt + 1}/${this.retryConfig.maxRetries}) in ${delay}ms...`); await this.sleep(delay); return this.makeRequest(method, endpoint, data, retryAttempt + 1); } throw error; } } private createDetailedError(response: any): Error { const status = response.status; const data = response.data; let message = `Medium API Error (${status})`; if (data?.errors) { message += `: ${JSON.stringify(data.errors)}`; } else if (data?.message) { message += `: ${data.message}`; } else if (response.statusText) { message += `: ${response.statusText}`; } const error: any = new Error(message); error.status = status; error.data = data; return error; } async publishArticle(params: PublishArticleParams) { // First get user ID const user = await this.getUserProfile(); const userId = user.data?.id; if (!userId) { throw new Error('Failed to retrieve user ID'); } const payload: any = { title: params.title, contentFormat: 'markdown', content: params.content, publishStatus: params.publishStatus || 'draft' }; if (params.tags && params.tags.length > 0) { payload.tags = params.tags; } if (params.notifyFollowers !== undefined) { payload.notifyFollowers = params.notifyFollowers; } const endpoint = params.publicationId ? `/publications/${params.publicationId}/posts` : `/users/${userId}/posts`; return this.makeRequest('post', endpoint, payload); } async updateArticle(params: UpdateArticleParams) { const payload: any = {}; if (params.title) payload.title = params.title; if (params.content) { payload.content = params.content; payload.contentFormat = 'markdown'; } if (params.tags) payload.tags = params.tags; if (params.publishStatus) payload.publishStatus = params.publishStatus; return this.makeRequest('put', `/posts/${params.articleId}`, payload); } async deleteArticle(articleId: string) { return this.makeRequest('delete', `/posts/${articleId}`); } async getArticle(articleId: string) { return this.makeRequest('get', `/posts/${articleId}`); } async getUserPublications() { const user = await this.getUserProfile(); const userId = user.data?.id; if (!userId) { throw new Error('Failed to retrieve user ID'); } return this.makeRequest('get', `/users/${userId}/publications`); } async searchArticles(params: SearchArticlesParams) { const queryParams = new URLSearchParams(); if (params.keywords) { params.keywords.forEach(keyword => queryParams.append('q', keyword) ); } if (params.publicationId) { queryParams.append('publicationId', params.publicationId); } if (params.tags) { params.tags.forEach(tag => queryParams.append('tag', tag) ); } const query = queryParams.toString(); const endpoint = query ? `/articles?${query}` : '/articles'; return this.makeRequest('get', endpoint); } async getDrafts() { const user = await this.getUserProfile(); const userId = user.data?.id; if (!userId) { throw new Error('Failed to retrieve user ID'); } return this.makeRequest('get', `/users/${userId}/posts?status=draft`); } async getUserProfile() { return this.makeRequest('get', '/me'); } async createDraft(params: { title: string, content: string, tags?: string[] }) { const user = await this.getUserProfile(); const userId = user.data?.id; if (!userId) { throw new Error('Failed to retrieve user ID'); } return this.makeRequest('post', `/users/${userId}/posts`, { title: params.title, contentFormat: 'markdown', content: params.content, tags: params.tags, publishStatus: 'draft' }); } async getPublicationContributors(publicationId: string) { return this.makeRequest('get', `/publications/${publicationId}/contributors`); } async getPublicationPosts(publicationId: string) { return this.makeRequest('get', `/publications/${publicationId}/posts`); } // Utility methods getRateLimitInfo() { return { remaining: this.rateLimitRemaining, resetAt: this.rateLimitReset ? new Date(this.rateLimitReset) : null }; } } export default MediumClient;

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/aliiqbal208/medium-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server