threads-mcp-server
Version:
Professional Threads MCP Server - Fixed API issues, enhanced setup validation, and enterprise features
168 lines • 6.93 kB
JavaScript
import axios from 'axios';
export class ThreadsAPIClient {
client;
accessToken;
baseURL = 'https://graph.threads.net/v1.0';
requiredScopes = ['threads_basic', 'threads_content_publish', 'threads_manage_insights', 'threads_read_replies'];
constructor(accessToken) {
this.accessToken = accessToken;
this.client = axios.create({
baseURL: this.baseURL,
timeout: 30000,
headers: {
'Content-Type': 'application/json',
},
});
this.client.interceptors.request.use((config) => {
config.params = {
...config.params,
access_token: this.accessToken,
};
return config;
});
// Remove the response interceptor to handle errors in get method
// this.client.interceptors.response.use(
// (response) => response,
// (error: AxiosError<ThreadsAPIError>) => {
// if (error.response?.data?.error) {
// const apiError = error.response.data.error;
// throw new Error(`Threads API Error: ${apiError.message} (Code: ${apiError.code})`);
// }
// throw error;
// }
// );
}
async get(endpoint, params, retries = 3) {
for (let attempt = 1; attempt <= retries; attempt++) {
try {
const response = await this.client.get(endpoint, { params });
return response.data;
}
catch (error) {
const apiError = error.response?.data?.error;
const isTransientError = apiError?.is_transient === true;
const isCode2Error = apiError?.code === 2;
const isLastAttempt = attempt === retries;
if ((isTransientError || isCode2Error) && !isLastAttempt) {
console.error(`Attempt ${attempt} failed with error code ${apiError?.code}, retrying in ${attempt * 1000}ms...`);
await this.sleep(attempt * 1000);
continue;
}
// Use enhanced error handling
throw this.handleAPIError(error);
}
}
throw new Error('Max retries exceeded');
}
async post(endpoint, data) {
try {
const response = await this.client.post(endpoint, data);
return response.data;
}
catch (error) {
throw this.handleAPIError(error);
}
}
async delete(endpoint, retries = 3) {
for (let attempt = 1; attempt <= retries; attempt++) {
try {
const response = await this.client.delete(endpoint);
return response.data;
}
catch (error) {
const apiError = error.response?.data?.error;
const isTransientError = apiError?.is_transient === true;
const isCode2Error = apiError?.code === 2;
const isLastAttempt = attempt === retries;
if ((isTransientError || isCode2Error) && !isLastAttempt) {
console.error(`Delete attempt ${attempt} failed with error code ${apiError?.code}, retrying in ${attempt * 1000}ms...`);
await this.sleep(attempt * 1000);
continue;
}
throw this.handleAPIError(error);
}
}
throw new Error('Max retries exceeded');
}
async paginate(endpoint, params, maxPages = 10) {
const results = [];
let nextUrl = undefined;
let pages = 0;
do {
const response = await this.get(nextUrl || endpoint, nextUrl ? {} : params);
results.push(...response.data);
nextUrl = response.paging?.next;
pages++;
} while (nextUrl && pages < maxPages);
return results;
}
updateAccessToken(token) {
this.accessToken = token;
}
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// NEW: Validate access token and scopes
async validateToken() {
try {
// Use the debug_token endpoint to check token validity and scopes
const response = await this.client.get('/debug_token', {
params: {
input_token: this.accessToken,
access_token: this.accessToken
}
});
return {
valid: true,
scopes: response.data?.data?.scopes || [],
};
}
catch (error) {
const errorMessage = error.response?.data?.error?.message || error.message;
return {
valid: false,
error: `Token validation failed: ${errorMessage}`
};
}
}
// NEW: Check if required scopes are available
async checkScopes(requiredScopes = this.requiredScopes) {
const tokenInfo = await this.validateToken();
if (!tokenInfo.valid || !tokenInfo.scopes) {
return { hasRequired: false, missing: requiredScopes };
}
const missing = requiredScopes.filter(scope => !tokenInfo.scopes.includes(scope));
return {
hasRequired: missing.length === 0,
missing
};
}
// NEW: Enhanced error handling with helpful messages
handleAPIError(error) {
const apiError = error.response?.data?.error;
if (!apiError) {
return error;
}
const { code, message, error_subcode, fbtrace_id } = apiError;
// Provide helpful error messages for common issues
if (code === 190) {
return new Error(`Authentication failed: ${message}. Please check your access token and ensure it has proper scopes: ${this.requiredScopes.join(', ')}`);
}
if (code === 200 && error_subcode === 1360028) {
return new Error(`Business account required: ${message}. Convert your Instagram account to a business account and complete Meta Business verification.`);
}
if (code === 100) {
return new Error(`Invalid parameter: ${message}. Check your request format and ensure media URLs are publicly accessible.`);
}
if (code === 10 && message.includes('scope')) {
return new Error(`Permission denied: ${message}. Your access token is missing required scopes. Required: ${this.requiredScopes.join(', ')}`);
}
if (code === 4) {
return new Error(`Rate limit exceeded: ${message}. Please wait before making more requests.`);
}
// Generic error with trace ID for debugging
const traceInfo = fbtrace_id ? ` (Trace ID: ${fbtrace_id})` : '';
return new Error(`Threads API Error (${code}): ${message}${traceInfo}`);
}
}
//# sourceMappingURL=client.js.map