amazon-mcp-server
Version:
Model Context Protocol server for Amazon Seller API
223 lines (197 loc) • 6.8 kB
text/typescript
import axios from 'axios';
/**
* Manages authentication with the Amazon Seller API
* Handles token refresh and provides authenticated request methods
*/
export class AmazonAuthManager {
private clientId: string;
private clientSecret: string;
private refreshToken: string;
private accessToken: string | null = null;
private tokenExpiration: Date | null = null;
private tokenRefreshPromise: Promise<string> | null = null;
private refreshIntervalId: NodeJS.Timeout | null = null;
/**
* Creates a new instance of the Amazon Auth Manager
* @param clientId Amazon client ID
* @param clientSecret Amazon client secret
* @param refreshToken Amazon refresh token
*/
constructor(clientId: string, clientSecret: string, refreshToken: string) {
this.clientId = clientId;
this.clientSecret = clientSecret;
this.refreshToken = refreshToken;
// Start the token refresh cycle
this.startTokenRefreshCycle();
}
/**
* Gets a valid access token, refreshing if necessary
* @returns Promise resolving to a valid access token
*/
public async getAccessToken(): Promise<string> {
// If we already have a refresh promise in progress, return that
if (this.tokenRefreshPromise) {
return this.tokenRefreshPromise;
}
// If the token exists and isn't expired (with a 5-minute margin)
if (this.accessToken && this.tokenExpiration &&
this.tokenExpiration.getTime() > Date.now() + 300000) {
return this.accessToken;
}
// Otherwise, refresh the token
this.tokenRefreshPromise = this.refreshAccessToken();
try {
return await this.tokenRefreshPromise;
} finally {
// Reset the promise once completed (success or failure)
this.tokenRefreshPromise = null;
}
}
/**
* Makes an authorized request to the Amazon Seller API
* @param method HTTP method
* @param endpoint API endpoint
* @param params Query parameters
* @param data Request body
* @returns Promise resolving to the API response
*/
public async makeAuthorizedRequest(
method: string,
endpoint: string,
params?: any,
data?: any
): Promise<any> {
try {
const accessToken = await this.getAccessToken();
const response = await axios({
method,
url: endpoint,
params,
data,
headers: {
'x-amz-access-token': accessToken,
'Content-Type': 'application/json'
}
});
return response.data;
} catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 401) {
// Invalid token, force a refresh and retry once
this.tokenRefreshPromise = null;
this.accessToken = null;
const newAccessToken = await this.getAccessToken();
return axios({
method,
url: endpoint,
params,
data,
headers: {
'x-amz-access-token': newAccessToken,
'Content-Type': 'application/json'
}
}).then(response => response.data);
}
throw error;
}
}
/**
* Starts the automatic token refresh cycle
* Refreshes immediately and then every 55 minutes
*/
private startTokenRefreshCycle(): void {
// Refresh immediately on startup
this.getAccessToken().catch(err => {
console.error('Initial token refresh failed:', err);
});
// Schedule a refresh every 55 minutes (3300000 ms)
// This ensures we always have a valid token
this.refreshIntervalId = setInterval(() => {
this.tokenRefreshPromise = null; // Force a new refresh
this.getAccessToken().catch(err => {
console.error('Scheduled token refresh failed:', err);
});
}, 3300000);
}
/**
* Refreshes the access token using the refresh token
* @returns Promise resolving to the new access token
*/
private async refreshAccessToken(): Promise<string> {
try {
const response = await axios.post('https://api.amazon.com/auth/o2/token', {
grant_type: 'refresh_token',
refresh_token: this.refreshToken,
client_id: this.clientId,
client_secret: this.clientSecret
});
this.accessToken = response.data.access_token;
// Amazon tokens expire after 1 hour, we schedule renewal after 55 minutes
const expiresInMs = (response.data.expires_in || 3600) * 1000;
this.tokenExpiration = new Date(Date.now() + expiresInMs);
if (!this.accessToken) {
throw new Error('No access token received from Amazon API');
}
return this.accessToken;
} catch (error) {
console.error('Failed to refresh Amazon access token:', error);
throw new Error('Authentication failed with Amazon API');
}
}
/**
* Handles authentication errors with appropriate recovery strategies
* @param error The error to handle
*/
private async handleAuthError(error: any): Promise<void> {
if (axios.isAxiosError(error)) {
if (error.response?.status === 401) {
// Expired token, force a refresh
this.tokenRefreshPromise = null;
this.accessToken = null;
await this.getAccessToken();
} else if (error.response?.status === 403) {
// Permission issue, log error but don't retry
console.error('Permission error with Amazon API:', error.response.data);
throw new Error('Insufficient permissions for Amazon API');
} else if (!error.response) {
// Network error, implement exponential backoff retry
await this.retryWithExponentialBackoff(() => this.refreshAccessToken());
}
}
}
/**
* Retries a function with exponential backoff
* @param fn Function to retry
* @param maxRetries Maximum number of retries
* @param initialDelay Initial delay in milliseconds
* @returns Promise resolving to the function result
*/
private async retryWithExponentialBackoff(
fn: () => Promise<any>,
maxRetries = 5,
initialDelay = 1000
): Promise<any> {
let retries = 0;
let delay = initialDelay;
while (retries < maxRetries) {
try {
return await fn();
} catch (error) {
retries++;
if (retries >= maxRetries) throw error;
// Wait with exponential backoff
await new Promise(resolve => setTimeout(resolve, delay));
delay *= 2; // Double the delay each retry
}
}
}
/**
* Stops the token refresh cycle
* Call this when shutting down the server
*/
public stopTokenRefreshCycle(): void {
if (this.refreshIntervalId) {
clearInterval(this.refreshIntervalId);
this.refreshIntervalId = null;
}
}
}