ebay-mcp-remote-edition 2.0.14 → 2.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -88,7 +88,9 @@ export class CloudflareKVStore {
88
88
  await this.client.put(`/values/${encodeURIComponent(key)}`, JSON.stringify(value), { params });
89
89
  // Keep the in-memory cache consistent with what we wrote.
90
90
  // If the KV entry has its own TTL, honour it for the cache as well.
91
- const cacheTtl = expirationTtl ? Math.min(expirationTtl * 1_000, this.cacheTtlMs) : this.cacheTtlMs;
91
+ const cacheTtl = expirationTtl
92
+ ? Math.min(expirationTtl * 1_000, this.cacheTtlMs)
93
+ : this.cacheTtlMs;
92
94
  this.cache.set(key, { value, expiresAt: Date.now() + cacheTtl });
93
95
  }
94
96
  async delete(key) {
@@ -118,6 +118,37 @@ export class MultiUserAuthStore {
118
118
  await this.kv.put(`client:${clientId}`, record);
119
119
  return record;
120
120
  }
121
+ /**
122
+ * Upserts a client record using a **caller-supplied** `clientId`.
123
+ *
124
+ * Used by the `/authorize` endpoint to auto-register trusted desktop MCP
125
+ * clients (VS Code, Cursor, Windsurf, localhost loopback) that arrive at
126
+ * `/authorize` without a prior `/register` call (e.g. because the in-memory
127
+ * registration was lost between requests, or the client drives `/authorize`
128
+ * directly).
129
+ *
130
+ * An existing record for `clientId` is overwritten only if the supplied
131
+ * `redirectUri` is not already listed (additive merge otherwise).
132
+ */
133
+ async registerClientWithId(clientId, redirectUris, clientName) {
134
+ const existing = await this.kv.get(`client:${clientId}`);
135
+ const now = new Date().toISOString();
136
+ if (existing) {
137
+ // Merge any new redirect URIs into the existing record
138
+ const merged = Array.from(new Set([...existing.redirectUris, ...redirectUris]));
139
+ const updated = { ...existing, redirectUris: merged };
140
+ await this.kv.put(`client:${clientId}`, updated);
141
+ return updated;
142
+ }
143
+ const record = {
144
+ clientId,
145
+ redirectUris,
146
+ clientName,
147
+ createdAt: now,
148
+ };
149
+ await this.kv.put(`client:${clientId}`, record);
150
+ return record;
151
+ }
121
152
  async getClient(clientId) {
122
153
  return await this.kv.get(`client:${clientId}`);
123
154
  }
@@ -30,10 +30,10 @@ function getServerBaseUrl() {
30
30
  }
31
31
  function htmlEscape(value) {
32
32
  return value
33
- .replace(/&/g, '&')
34
- .replace(/</g, '&lt;')
35
- .replace(/>/g, '&gt;')
36
- .replace(/"/g, '&quot;')
33
+ .replace(/&/g, '&')
34
+ .replace(/</g, '<')
35
+ .replace(/>/g, '>')
36
+ .replace(/"/g, '"')
37
37
  .replace(/'/g, '&#39;');
38
38
  }
39
39
  function requireAdmin(req, res, next) {
@@ -61,6 +61,36 @@ function requireOauthStartKey(req, res, next) {
61
61
  }
62
62
  next();
63
63
  }
64
+ /**
65
+ * Returns true when a redirect URI belongs to a well-known desktop / IDE
66
+ * MCP client (VS Code, Cursor, Windsurf) or a localhost loopback.
67
+ *
68
+ * These clients drive the authorize flow directly and cannot always guarantee
69
+ * that their /register request was persisted before /authorize is called.
70
+ * We allow them to self-register on the fly so the flow doesn't hard-fail on
71
+ * a missing client_id when the user's eBay app credentials are already in env.
72
+ */
73
+ function isTrustedDesktopRedirectUri(redirectUri) {
74
+ try {
75
+ const u = new URL(redirectUri);
76
+ // Desktop IDE callback schemes
77
+ if (u.protocol === 'vscode:' ||
78
+ u.protocol === 'cursor:' ||
79
+ u.protocol === 'windsurf:' ||
80
+ u.protocol === 'claude:') {
81
+ return true;
82
+ }
83
+ // Localhost loopback (any port)
84
+ if ((u.protocol === 'http:' || u.protocol === 'https:') &&
85
+ (u.hostname === 'localhost' || u.hostname === '127.0.0.1' || u.hostname === '::1')) {
86
+ return true;
87
+ }
88
+ }
89
+ catch {
90
+ // Malformed URI – treat as untrusted
91
+ }
92
+ return false;
93
+ }
64
94
  async function createUserScopedApi(userId, environment) {
65
95
  const api = new EbaySellerApi(getEbayConfig(environment), { userId, environment });
66
96
  await api.initialize();
@@ -132,6 +162,10 @@ function createApp() {
132
162
  }
133
163
  const uris = redirectUris;
134
164
  const client = await authStore.registerClient(uris, clientName);
165
+ serverLogger.info('[register] MCP client registered', {
166
+ clientId: client.clientId,
167
+ redirectUris: client.redirectUris,
168
+ });
135
169
  res.status(201).json({
136
170
  client_id: client.clientId,
137
171
  redirect_uris: client.redirectUris,
@@ -151,22 +185,51 @@ function createApp() {
151
185
  const q = req.query;
152
186
  const { client_id: clientId, redirect_uri: redirectUri, response_type: responseType, state: mcpState, code_challenge: codeChallenge, code_challenge_method: codeChallengeMethod, } = q;
153
187
  const environment = q.env === 'sandbox' || q.env === 'production' ? q.env : getConfiguredEnvironment();
188
+ serverLogger.info('[authorize] Request received', {
189
+ clientId,
190
+ redirectUri,
191
+ responseType,
192
+ hasPkce: !!codeChallenge,
193
+ pkceMethod: codeChallengeMethod,
194
+ environment,
195
+ hasMcpState: !!mcpState,
196
+ });
154
197
  if (responseType !== 'code') {
198
+ serverLogger.warn('[authorize] Rejected: unsupported_response_type', { responseType });
155
199
  res.status(400).json({ error: 'unsupported_response_type' });
156
200
  return;
157
201
  }
158
202
  if (!clientId) {
203
+ serverLogger.warn('[authorize] Rejected: missing client_id');
159
204
  res
160
205
  .status(400)
161
206
  .json({ error: 'invalid_request', error_description: 'client_id is required' });
162
207
  return;
163
208
  }
164
- const client = await authStore.getClient(clientId);
209
+ // Look up the MCP client. If unknown, auto-register it for trusted desktop
210
+ // redirect URIs (VS Code, Cursor, Windsurf, localhost) so that the flow
211
+ // continues even when /register state was not persisted (e.g. memory backend)
212
+ // or when the IDE drives /authorize directly without a prior /register call.
213
+ let client = await authStore.getClient(clientId);
165
214
  if (!client) {
166
- res.status(400).json({ error: 'invalid_client', error_description: 'Unknown client_id' });
167
- return;
215
+ if (redirectUri && isTrustedDesktopRedirectUri(redirectUri)) {
216
+ serverLogger.info('[authorize] Auto-registering trusted desktop MCP client (client_id not in store)', { clientId, redirectUri });
217
+ client = await authStore.registerClientWithId(clientId, [redirectUri]);
218
+ }
219
+ else {
220
+ serverLogger.warn('[authorize] Rejected: unknown client_id (not a trusted desktop URI)', {
221
+ clientId,
222
+ redirectUri,
223
+ });
224
+ res.status(400).json({ error: 'invalid_client', error_description: 'Unknown client_id' });
225
+ return;
226
+ }
168
227
  }
169
228
  if (!redirectUri || !client.redirectUris.includes(redirectUri)) {
229
+ serverLogger.warn('[authorize] Rejected: redirect_uri mismatch', {
230
+ provided: redirectUri,
231
+ registered: client.redirectUris,
232
+ });
170
233
  res.status(400).json({
171
234
  error: 'invalid_request',
172
235
  error_description: 'redirect_uri not registered for this client',
@@ -174,6 +237,10 @@ function createApp() {
174
237
  return;
175
238
  }
176
239
  if (!codeChallenge || codeChallengeMethod !== 'S256') {
240
+ serverLogger.warn('[authorize] Rejected: missing or invalid PKCE', {
241
+ hasPkce: !!codeChallenge,
242
+ method: codeChallengeMethod,
243
+ });
177
244
  res.status(400).json({
178
245
  error: 'invalid_request',
179
246
  error_description: 'PKCE with S256 code_challenge is required',
@@ -182,6 +249,12 @@ function createApp() {
182
249
  }
183
250
  const ebayConfig = getEbayConfig(environment);
184
251
  if (!ebayConfig.clientId || !ebayConfig.clientSecret || !ebayConfig.redirectUri) {
252
+ serverLogger.error('[authorize] eBay app credentials missing in env', {
253
+ hasClientId: !!ebayConfig.clientId,
254
+ hasClientSecret: !!ebayConfig.clientSecret,
255
+ hasRedirectUri: !!ebayConfig.redirectUri,
256
+ environment,
257
+ });
185
258
  res.status(500).json({
186
259
  error: 'server_error',
187
260
  error_description: `Missing eBay configuration for ${environment}`,
@@ -196,9 +269,17 @@ function createApp() {
196
269
  mcpCodeChallengeMethod: codeChallengeMethod,
197
270
  });
198
271
  const oauthUrl = getOAuthAuthorizationUrl(ebayConfig.clientId, ebayConfig.redirectUri, environment, getHostedOauthScopes(environment), undefined, stateRecord.state);
272
+ serverLogger.info('[authorize] Redirecting to eBay OAuth', {
273
+ state: stateRecord.state,
274
+ environment,
275
+ });
199
276
  res.redirect(oauthUrl);
200
277
  }
201
278
  catch (error) {
279
+ serverLogger.error('[authorize] Unhandled error', {
280
+ error: error instanceof Error ? error.message : String(error),
281
+ stack: error instanceof Error ? error.stack : undefined,
282
+ });
202
283
  res.status(500).json({
203
284
  error: 'server_error',
204
285
  error_description: error instanceof Error ? error.message : String(error),
@@ -210,9 +291,14 @@ function createApp() {
210
291
  * Exchanges a short-lived MCP authorization code (+ PKCE verifier) for a session token.
211
292
  */
212
293
  app.post('/token', async (req, res) => {
294
+ serverLogger.info('[token] Request received', {
295
+ contentType: req.headers['content-type'],
296
+ hasBody: !!req.body,
297
+ });
213
298
  // Body may be form-encoded (RFC 6749 §4.1.3) or JSON — both are parsed by middleware.
214
299
  // Guard against unparsed bodies (missing Content-Type header etc.)
215
300
  if (!req.body || typeof req.body !== 'object') {
301
+ serverLogger.warn('[token] Rejected: missing or unparseable body');
216
302
  res.status(400).json({
217
303
  error: 'invalid_request',
218
304
  error_description: 'Request body is missing or unparseable. Use application/x-www-form-urlencoded or application/json.',
@@ -221,31 +307,58 @@ function createApp() {
221
307
  }
222
308
  const body = req.body;
223
309
  const { grant_type: grantType, code, redirect_uri: redirectUri, client_id: clientId, code_verifier: codeVerifier, } = body;
310
+ serverLogger.info('[token] Parsed fields', {
311
+ grantType,
312
+ hasCode: !!code,
313
+ redirectUri,
314
+ clientId,
315
+ hasCodeVerifier: !!codeVerifier,
316
+ });
224
317
  if (grantType !== 'authorization_code') {
318
+ serverLogger.warn('[token] Rejected: unsupported_grant_type', { grantType });
225
319
  res.status(400).json({ error: 'unsupported_grant_type' });
226
320
  return;
227
321
  }
228
322
  if (!code) {
323
+ serverLogger.warn('[token] Rejected: missing code');
229
324
  res.status(400).json({ error: 'invalid_request', error_description: 'code is required' });
230
325
  return;
231
326
  }
232
327
  const authCode = await authStore.consumeAuthCode(code);
233
328
  if (!authCode) {
329
+ serverLogger.warn('[token] Rejected: invalid or expired authorization code', {
330
+ codePrefix: code.substring(0, 8),
331
+ });
234
332
  res.status(400).json({
235
333
  error: 'invalid_grant',
236
334
  error_description: 'Invalid or expired authorization code',
237
335
  });
238
336
  return;
239
337
  }
338
+ serverLogger.info('[token] Auth code found', {
339
+ storedClientId: authCode.clientId,
340
+ providedClientId: clientId,
341
+ storedRedirectUri: authCode.redirectUri,
342
+ providedRedirectUri: redirectUri,
343
+ });
240
344
  if (authCode.clientId !== clientId) {
345
+ serverLogger.warn('[token] Rejected: client_id mismatch', {
346
+ stored: authCode.clientId,
347
+ provided: clientId,
348
+ });
241
349
  res.status(400).json({ error: 'invalid_client', error_description: 'client_id mismatch' });
242
350
  return;
243
351
  }
244
352
  if (authCode.redirectUri !== redirectUri) {
353
+ serverLogger.warn('[token] Rejected: redirect_uri mismatch', {
354
+ stored: authCode.redirectUri,
355
+ provided: redirectUri,
356
+ });
245
357
  res.status(400).json({ error: 'invalid_grant', error_description: 'redirect_uri mismatch' });
246
358
  return;
247
359
  }
248
360
  if (!codeVerifier) {
361
+ serverLogger.warn('[token] Rejected: missing code_verifier');
249
362
  res
250
363
  .status(400)
251
364
  .json({ error: 'invalid_request', error_description: 'code_verifier is required' });
@@ -254,12 +367,21 @@ function createApp() {
254
367
  // Verify PKCE S256: BASE64URL(SHA256(code_verifier)) must equal code_challenge
255
368
  const expectedChallenge = createHash('sha256').update(codeVerifier).digest('base64url');
256
369
  if (expectedChallenge !== authCode.codeChallenge) {
370
+ serverLogger.warn('[token] Rejected: PKCE verification failed', {
371
+ expected: authCode.codeChallenge,
372
+ computed: expectedChallenge,
373
+ });
257
374
  res
258
375
  .status(400)
259
376
  .json({ error: 'invalid_grant', error_description: 'PKCE verification failed' });
260
377
  return;
261
378
  }
262
379
  const session = await authStore.createSession(authCode.userId, authCode.environment);
380
+ serverLogger.info('[token] Session created, issuing access token', {
381
+ userId: authCode.userId,
382
+ environment: authCode.environment,
383
+ sessionTokenPrefix: session.sessionToken.substring(0, 8),
384
+ });
263
385
  res.json({
264
386
  access_token: session.sessionToken,
265
387
  token_type: 'bearer',
@@ -292,13 +414,23 @@ function createApp() {
292
414
  const envFromQuery = typeof req.query.env === 'string' ? req.query.env : undefined;
293
415
  const oauthError = typeof req.query.error === 'string' ? req.query.error : undefined;
294
416
  const errorDescription = typeof req.query.error_description === 'string' ? req.query.error_description : undefined;
417
+ serverLogger.info('[oauth/callback] Received', {
418
+ hasCode: !!code,
419
+ hasState: !!state,
420
+ oauthError,
421
+ });
295
422
  if (oauthError) {
423
+ serverLogger.warn('[oauth/callback] eBay returned OAuth error', {
424
+ oauthError,
425
+ errorDescription,
426
+ });
296
427
  res
297
428
  .status(400)
298
429
  .send(`<h1>OAuth failed</h1><p>${htmlEscape(errorDescription ?? oauthError)}</p>`);
299
430
  return;
300
431
  }
301
432
  if (!code) {
433
+ serverLogger.warn('[oauth/callback] Missing authorization code');
302
434
  res.status(400).send('<h1>Missing authorization code</h1>');
303
435
  return;
304
436
  }
@@ -307,10 +439,17 @@ function createApp() {
307
439
  if (state) {
308
440
  stateRecord = await authStore.consumeOAuthState(state);
309
441
  if (!stateRecord) {
442
+ serverLogger.warn('[oauth/callback] OAuth state not found or expired', { state });
310
443
  res.status(400).send('<h1>Invalid or expired OAuth state</h1>');
311
444
  return;
312
445
  }
313
446
  environment = stateRecord.environment;
447
+ serverLogger.info('[oauth/callback] State resolved', {
448
+ environment,
449
+ isMcpFlow: !!(stateRecord.mcpClientId && stateRecord.mcpRedirectUri),
450
+ mcpClientId: stateRecord.mcpClientId,
451
+ mcpRedirectUri: stateRecord.mcpRedirectUri,
452
+ });
314
453
  }
315
454
  else {
316
455
  environment =
@@ -322,7 +461,12 @@ function createApp() {
322
461
  const userId = randomUUID();
323
462
  const api = await createUserScopedApi(userId, environment);
324
463
  const oauthClient = api.getAuthClient().getOAuthClient();
464
+ serverLogger.info('[oauth/callback] Exchanging code for eBay tokens', { userId });
325
465
  const tokenData = await oauthClient.exchangeCodeForToken(code);
466
+ serverLogger.info('[oauth/callback] eBay token exchange successful', {
467
+ userId,
468
+ hasScope: !!tokenData.scope,
469
+ });
326
470
  // ── MCP OAuth flow: redirect back to the registered MCP client ─────────
327
471
  if (stateRecord?.mcpClientId && stateRecord.mcpRedirectUri && stateRecord.mcpCodeChallenge) {
328
472
  const authCodeRecord = await authStore.createAuthCode(stateRecord.mcpClientId, stateRecord.mcpRedirectUri, stateRecord.mcpCodeChallenge, stateRecord.mcpCodeChallengeMethod ?? 'S256', userId, environment);
@@ -331,15 +475,18 @@ function createApp() {
331
475
  if (stateRecord.mcpState) {
332
476
  redirectUrl.searchParams.set('state', stateRecord.mcpState);
333
477
  }
334
- serverLogger.info('MCP OAuth flow complete, redirecting to client', {
478
+ serverLogger.info('[oauth/callback] MCP OAuth flow complete, redirecting to client', {
335
479
  clientId: stateRecord.mcpClientId,
336
480
  redirectUri: stateRecord.mcpRedirectUri,
337
481
  userId,
482
+ authCodePrefix: authCodeRecord.code.substring(0, 8),
483
+ finalRedirectUrl: redirectUrl.toString().substring(0, 120),
338
484
  });
339
485
  res.redirect(redirectUrl.toString());
340
486
  return;
341
487
  }
342
488
  // ── End MCP OAuth flow ─────────────────────────────────────────────────
489
+ serverLogger.info('[oauth/callback] Non-MCP flow: creating hosted session', { userId });
343
490
  const session = await authStore.createSession(userId, environment);
344
491
  res.status(200).send(`<!doctype html>
345
492
  <html>
@@ -403,6 +550,10 @@ function createApp() {
403
550
  </html>`);
404
551
  }
405
552
  catch (error) {
553
+ serverLogger.error('[oauth/callback] Unhandled error', {
554
+ error: error instanceof Error ? error.message : String(error),
555
+ stack: error instanceof Error ? error.stack : undefined,
556
+ });
406
557
  res
407
558
  .status(500)
408
559
  .send(`<h1>OAuth callback failed</h1><pre>${htmlEscape(error instanceof Error ? error.message : String(error))}</pre>`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ebay-mcp-remote-edition",
3
- "version": "2.0.14",
3
+ "version": "2.0.16",
4
4
  "description": "Remote + Local MCP server for eBay APIs - provides access to eBay developer functionality through MCP (Model Context Protocol)",
5
5
  "type": "module",
6
6
  "main": "build/index.js",