Skip to content

Commit 81cae24

Browse files
committed
Functional Mercado Libre integration with Cursor + Mercado Libre MCP Server
1 parent ddb7b05 commit 81cae24

File tree

10 files changed

+6830
-3
lines changed

10 files changed

+6830
-3
lines changed

NOTICE.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,30 @@ License: WTFPL OR ISC
5151

5252
---
5353

54+
### [axios 1.9.0](https://github.com/axios/axios)
55+
Copyright (c) 2014-present Matt Zabriskie
56+
License: MIT
57+
58+
---
59+
60+
### [form-data 4.0.2](https://github.com/form-data/form-data)
61+
Copyright (c) 2010-2019 Felix Geisendörfer, Jonathan Ong, and contributors
62+
License: MIT
63+
64+
---
65+
66+
### [node-fetch 2.7.0](https://github.com/node-fetch/node-fetch)
67+
Copyright (c) 2016 David Frank
68+
License: MIT
69+
70+
---
71+
72+
### [sharp 0.34.2](https://github.com/lovell/sharp)
73+
Copyright (c) 2013-2024 Lovell Fuller and contributors
74+
License: Apache-2.0
75+
76+
---
77+
5478
## License Information for Included Software
5579

5680
- **Apache-2.0**

api/ml-auth.js

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
const axios = require('axios');
2+
const fs = require('fs');
3+
const path = require('path');
4+
const crypto = require('crypto');
5+
6+
class MLAuthService {
7+
constructor() {
8+
this.appId = process.env.ML_APP_ID;
9+
this.secretKey = process.env.ML_SECRET_KEY;
10+
this.redirectUri = process.env.ML_REDIRECT_URI;
11+
this.siteId = 'MCO'; // Colombia
12+
this.baseUrl = 'https://api.mercadolibre.com';
13+
this.authUrl = 'https://auth.mercadolibre.com.co';
14+
15+
this.tokenFile = path.join(__dirname, 'ml-tokens.json');
16+
this.tokens = this.loadTokens();
17+
}
18+
19+
// Generate authorization URL
20+
getAuthorizationUrl() {
21+
const state = crypto.randomBytes(16).toString('hex');
22+
23+
const authUrl = `${this.authUrl}/authorization?response_type=code&client_id=${this.appId}&redirect_uri=${encodeURIComponent(this.redirectUri)}&state=${state}`;
24+
25+
// Store state temporarily
26+
this.tempAuthData = { state };
27+
28+
return { authUrl, state };
29+
}
30+
31+
// Exchange authorization code for tokens
32+
async exchangeCodeForTokens(code, state) {
33+
try {
34+
// Validate state
35+
if (!this.tempAuthData || this.tempAuthData.state !== state) {
36+
throw new Error('Invalid state parameter');
37+
}
38+
39+
const tokenData = new URLSearchParams({
40+
grant_type: 'authorization_code',
41+
client_id: this.appId,
42+
client_secret: this.secretKey,
43+
code: code,
44+
redirect_uri: this.redirectUri
45+
});
46+
47+
const response = await axios.post(`${this.baseUrl}/oauth/token`, tokenData, {
48+
headers: {
49+
'Content-Type': 'application/x-www-form-urlencoded'
50+
}
51+
});
52+
53+
const tokens = response.data;
54+
tokens.expires_at = Date.now() + (tokens.expires_in * 1000);
55+
56+
this.tokens = tokens;
57+
this.saveTokens();
58+
59+
// Clear temporary auth data
60+
this.tempAuthData = null;
61+
62+
return tokens;
63+
} catch (error) {
64+
console.error('Error exchanging code for tokens:', error.response?.data || error.message);
65+
throw error;
66+
}
67+
}
68+
69+
// Refresh access token
70+
async refreshToken() {
71+
try {
72+
if (!this.tokens.refresh_token) {
73+
throw new Error('No refresh token available');
74+
}
75+
76+
const tokenData = new URLSearchParams({
77+
grant_type: 'refresh_token',
78+
client_id: this.appId,
79+
client_secret: this.secretKey,
80+
refresh_token: this.tokens.refresh_token
81+
});
82+
83+
const response = await axios.post(`${this.baseUrl}/oauth/token`, tokenData, {
84+
headers: {
85+
'Content-Type': 'application/x-www-form-urlencoded'
86+
}
87+
});
88+
89+
const newTokens = response.data;
90+
newTokens.expires_at = Date.now() + (newTokens.expires_in * 1000);
91+
92+
this.tokens = newTokens;
93+
this.saveTokens();
94+
95+
return newTokens;
96+
} catch (error) {
97+
console.error('Error refreshing token:', error.response?.data || error.message);
98+
throw error;
99+
}
100+
}
101+
102+
// Check if token is valid and refresh if needed
103+
async ensureValidToken() {
104+
if (!this.tokens.access_token) {
105+
throw new Error('No access token available. Please authenticate first.');
106+
}
107+
108+
// Check if token expires in the next 5 minutes
109+
const expiresIn = this.tokens.expires_at - Date.now();
110+
if (expiresIn < 5 * 60 * 1000) { // 5 minutes
111+
console.log('Token expiring soon, refreshing...');
112+
await this.refreshToken();
113+
}
114+
115+
return this.tokens.access_token;
116+
}
117+
118+
// Make authenticated API request
119+
async apiRequest(method, endpoint, data = null, headers = {}) {
120+
try {
121+
const accessToken = await this.ensureValidToken();
122+
123+
const config = {
124+
method,
125+
url: `${this.baseUrl}${endpoint}`,
126+
headers: {
127+
'Authorization': `Bearer ${accessToken}`,
128+
'Content-Type': 'application/json',
129+
...headers
130+
}
131+
};
132+
133+
if (data) {
134+
config.data = data;
135+
}
136+
137+
const response = await axios(config);
138+
return response.data;
139+
} catch (error) {
140+
if (error.response?.status === 401) {
141+
// Token might be invalid, try refreshing
142+
try {
143+
await this.refreshToken();
144+
const accessToken = await this.ensureValidToken();
145+
146+
const config = {
147+
method,
148+
url: `${this.baseUrl}${endpoint}`,
149+
headers: {
150+
'Authorization': `Bearer ${accessToken}`,
151+
'Content-Type': 'application/json',
152+
...headers
153+
}
154+
};
155+
156+
if (data) {
157+
config.data = data;
158+
}
159+
160+
const response = await axios(config);
161+
return response.data;
162+
} catch (refreshError) {
163+
console.error('Failed to refresh token:', refreshError.response?.data || refreshError.message);
164+
throw new Error('Authentication failed. Please re-authenticate.');
165+
}
166+
}
167+
168+
console.error('API request failed:', error.response?.data || error.message);
169+
throw error;
170+
}
171+
}
172+
173+
// Get user information
174+
async getUserInfo() {
175+
return await this.apiRequest('GET', '/users/me');
176+
}
177+
178+
// Get user's marketplace information
179+
async getMarketplaceInfo() {
180+
const userInfo = await this.getUserInfo();
181+
return await this.apiRequest('GET', `/marketplace/users/${userInfo.id}`);
182+
}
183+
184+
// Check authentication status
185+
isAuthenticated() {
186+
return !!(this.tokens.access_token && this.tokens.expires_at > Date.now());
187+
}
188+
189+
// Get current tokens (without sensitive data)
190+
getTokenInfo() {
191+
if (!this.tokens.access_token) {
192+
return { authenticated: false };
193+
}
194+
195+
return {
196+
authenticated: true,
197+
expires_at: this.tokens.expires_at,
198+
expires_in: Math.max(0, Math.floor((this.tokens.expires_at - Date.now()) / 1000)),
199+
user_id: this.tokens.user_id,
200+
scope: this.tokens.scope
201+
};
202+
}
203+
204+
// Load tokens from file
205+
loadTokens() {
206+
try {
207+
if (fs.existsSync(this.tokenFile)) {
208+
const data = fs.readFileSync(this.tokenFile, 'utf8');
209+
return JSON.parse(data);
210+
}
211+
} catch (error) {
212+
console.error('Error loading tokens:', error.message);
213+
}
214+
return {};
215+
}
216+
217+
// Save tokens to file
218+
saveTokens() {
219+
try {
220+
fs.writeFileSync(this.tokenFile, JSON.stringify(this.tokens, null, 2));
221+
} catch (error) {
222+
console.error('Error saving tokens:', error.message);
223+
}
224+
}
225+
226+
// Clear tokens (logout)
227+
clearTokens() {
228+
this.tokens = {};
229+
try {
230+
if (fs.existsSync(this.tokenFile)) {
231+
fs.unlinkSync(this.tokenFile);
232+
}
233+
} catch (error) {
234+
console.error('Error clearing tokens:', error.message);
235+
}
236+
}
237+
}
238+
239+
module.exports = MLAuthService;

0 commit comments

Comments
 (0)