ebay-mcp-remote-edition 3.1.2 → 3.2.0
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.
- package/LICENSE +1 -1
- package/README.md +2 -2
- package/build/auth/multi-user-store.js +8 -2
- package/build/auth/oauth.js +31 -5
- package/build/config/environment.js +10 -0
- package/build/scripts/{run-with-local-env.js → env-check.js} +1 -1
- package/build/server-http.js +269 -90
- package/build/tools/chat-tools.js +50 -0
- package/build/tools/index.js +45 -4
- package/build/utils/version.js +1 -1
- package/build/validation/providers/chart.js +3 -0
- package/build/validation/providers/ebay-sold.js +171 -0
- package/build/validation/providers/ebay.js +112 -0
- package/build/validation/providers/social.js +7 -0
- package/build/validation/recommendation.js +84 -0
- package/build/validation/run-validation.js +140 -0
- package/build/validation/schemas.js +96 -0
- package/build/validation/types.js +1 -0
- package/package.json +10 -10
package/LICENSE
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c)
|
|
3
|
+
Copyright (c) 2026 Kenyatta Naji Johnson-Adams
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
package/README.md
CHANGED
|
@@ -154,10 +154,10 @@ For hosted deployments, register your server's public HTTPS URL instead (e.g. `h
|
|
|
154
154
|
|
|
155
155
|
### Install
|
|
156
156
|
|
|
157
|
-
**Option A —
|
|
157
|
+
**Option A — pnpm global install (no build step):**
|
|
158
158
|
|
|
159
159
|
```bash
|
|
160
|
-
|
|
160
|
+
pnpm install -g ebay-mcp-remote-edition
|
|
161
161
|
```
|
|
162
162
|
|
|
163
163
|
**Option B — clone and build (for contributors or self-hosting):**
|
|
@@ -44,7 +44,7 @@ export class MultiUserAuthStore {
|
|
|
44
44
|
* `lastUsedAt` once per TOUCH_THROTTLE_MS (default: 1 hour).
|
|
45
45
|
*/
|
|
46
46
|
sessionTouchCache = new Map();
|
|
47
|
-
static
|
|
47
|
+
static touchThrottleMs = 60 * 60 * 1_000; // 1 hour
|
|
48
48
|
stateKey(state) {
|
|
49
49
|
return `oauth_state:${state}`;
|
|
50
50
|
}
|
|
@@ -67,6 +67,12 @@ export class MultiUserAuthStore {
|
|
|
67
67
|
await this.kv.put(this.stateKey(state), record, OAUTH_STATE_TTL_S);
|
|
68
68
|
return record;
|
|
69
69
|
}
|
|
70
|
+
async getOAuthState(state) {
|
|
71
|
+
return await this.kv.get(this.stateKey(state));
|
|
72
|
+
}
|
|
73
|
+
async deleteOAuthState(state) {
|
|
74
|
+
await this.kv.delete(this.stateKey(state));
|
|
75
|
+
}
|
|
70
76
|
async consumeOAuthState(state) {
|
|
71
77
|
const key = this.stateKey(state);
|
|
72
78
|
const record = await this.kv.get(key);
|
|
@@ -122,7 +128,7 @@ export class MultiUserAuthStore {
|
|
|
122
128
|
// Skip the KV write entirely if we touched this session recently.
|
|
123
129
|
// The in-memory cache in CloudflareKVStore already keeps reads free,
|
|
124
130
|
// so the only cost we're avoiding here is the unnecessary KV PUT.
|
|
125
|
-
if (lastTouched !== undefined && now - lastTouched < MultiUserAuthStore.
|
|
131
|
+
if (lastTouched !== undefined && now - lastTouched < MultiUserAuthStore.touchThrottleMs) {
|
|
126
132
|
return;
|
|
127
133
|
}
|
|
128
134
|
const record = await this.getSession(sessionToken);
|
package/build/auth/oauth.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import axios from 'axios';
|
|
2
|
-
import {
|
|
2
|
+
import { getOAuthTokenBaseUrl } from '../config/environment.js';
|
|
3
3
|
import { MultiUserAuthStore } from '../auth/multi-user-store.js';
|
|
4
4
|
export class EbayOAuthClient {
|
|
5
5
|
config;
|
|
@@ -8,15 +8,20 @@ export class EbayOAuthClient {
|
|
|
8
8
|
appAccessTokenExpiry = 0;
|
|
9
9
|
userTokens = null;
|
|
10
10
|
authStore = new MultiUserAuthStore();
|
|
11
|
+
authSource = null;
|
|
11
12
|
constructor(config, context) {
|
|
12
13
|
this.config = config;
|
|
13
14
|
this.context = context;
|
|
14
15
|
}
|
|
16
|
+
getTokenEndpoint() {
|
|
17
|
+
return `${getOAuthTokenBaseUrl(this.config.environment)}/identity/v1/oauth2/token`;
|
|
18
|
+
}
|
|
15
19
|
async initialize() {
|
|
16
20
|
if (this.context?.userId && this.context.environment) {
|
|
17
21
|
const stored = await this.authStore.getUserTokens(this.context.userId, this.context.environment);
|
|
18
22
|
if (stored?.tokenData) {
|
|
19
23
|
this.userTokens = stored.tokenData;
|
|
24
|
+
this.authSource = 'stored_user_tokens';
|
|
20
25
|
return;
|
|
21
26
|
}
|
|
22
27
|
}
|
|
@@ -24,7 +29,7 @@ export class EbayOAuthClient {
|
|
|
24
29
|
const envRefreshToken = process.env.EBAY_USER_REFRESH_TOKEN;
|
|
25
30
|
if (envRefreshToken) {
|
|
26
31
|
try {
|
|
27
|
-
const authUrl =
|
|
32
|
+
const authUrl = this.getTokenEndpoint();
|
|
28
33
|
const credentials = Buffer.from(`${this.config.clientId}:${this.config.clientSecret}`).toString('base64');
|
|
29
34
|
const response = await axios.post(authUrl, new URLSearchParams({
|
|
30
35
|
grant_type: 'refresh_token',
|
|
@@ -50,6 +55,7 @@ export class EbayOAuthClient {
|
|
|
50
55
|
: now + 18 * 30 * 24 * 60 * 60 * 1000,
|
|
51
56
|
scope: tokenData.scope,
|
|
52
57
|
};
|
|
58
|
+
this.authSource = 'env_refresh_token_fallback';
|
|
53
59
|
}
|
|
54
60
|
catch {
|
|
55
61
|
// If refresh fails, leave userTokens as null
|
|
@@ -99,13 +105,14 @@ export class EbayOAuthClient {
|
|
|
99
105
|
userAccessTokenExpiry: accessTokenExpiry ?? now + 7200 * 1000,
|
|
100
106
|
userRefreshTokenExpiry: refreshTokenExpiry ?? now + 18 * 30 * 24 * 60 * 60 * 1000,
|
|
101
107
|
};
|
|
108
|
+
this.authSource = 'manual_set_user_tokens';
|
|
102
109
|
await this.persistUserTokens();
|
|
103
110
|
}
|
|
104
111
|
async getOrRefreshAppAccessToken() {
|
|
105
112
|
if (this.appAccessToken && Date.now() < this.appAccessTokenExpiry) {
|
|
106
113
|
return this.appAccessToken;
|
|
107
114
|
}
|
|
108
|
-
const authUrl =
|
|
115
|
+
const authUrl = this.getTokenEndpoint();
|
|
109
116
|
const credentials = Buffer.from(`${this.config.clientId}:${this.config.clientSecret}`).toString('base64');
|
|
110
117
|
const response = await axios.post(authUrl, new URLSearchParams({
|
|
111
118
|
grant_type: 'client_credentials',
|
|
@@ -124,7 +131,7 @@ export class EbayOAuthClient {
|
|
|
124
131
|
if (!this.config.redirectUri) {
|
|
125
132
|
throw new Error('Redirect URI is required for authorization code exchange');
|
|
126
133
|
}
|
|
127
|
-
const tokenUrl =
|
|
134
|
+
const tokenUrl = this.getTokenEndpoint();
|
|
128
135
|
const credentials = Buffer.from(`${this.config.clientId}:${this.config.clientSecret}`).toString('base64');
|
|
129
136
|
try {
|
|
130
137
|
const response = await axios.post(tokenUrl, new URLSearchParams({
|
|
@@ -150,6 +157,7 @@ export class EbayOAuthClient {
|
|
|
150
157
|
userRefreshTokenExpiry: now + tokenData.refresh_token_expires_in * 1000,
|
|
151
158
|
scope: tokenData.scope,
|
|
152
159
|
};
|
|
160
|
+
this.authSource = 'authorization_code_exchange';
|
|
153
161
|
await this.persistUserTokens();
|
|
154
162
|
return tokenData;
|
|
155
163
|
}
|
|
@@ -170,7 +178,7 @@ export class EbayOAuthClient {
|
|
|
170
178
|
if (!this.userTokens) {
|
|
171
179
|
throw new Error('No user tokens available to refresh');
|
|
172
180
|
}
|
|
173
|
-
const authUrl =
|
|
181
|
+
const authUrl = this.getTokenEndpoint();
|
|
174
182
|
const credentials = Buffer.from(`${this.config.clientId}:${this.config.clientSecret}`).toString('base64');
|
|
175
183
|
const response = await axios.post(authUrl, new URLSearchParams({
|
|
176
184
|
grant_type: 'refresh_token',
|
|
@@ -198,6 +206,24 @@ export class EbayOAuthClient {
|
|
|
198
206
|
};
|
|
199
207
|
await this.persistUserTokens();
|
|
200
208
|
}
|
|
209
|
+
getAuthDebugInfo() {
|
|
210
|
+
return {
|
|
211
|
+
tokenEndpoint: this.getTokenEndpoint(),
|
|
212
|
+
environment: this.config.environment,
|
|
213
|
+
hasClientId: this.config.clientId.trim().length > 0,
|
|
214
|
+
hasClientSecret: this.config.clientSecret.trim().length > 0,
|
|
215
|
+
hasRefreshToken: !!this.userTokens?.userRefreshToken,
|
|
216
|
+
hasAccessToken: !!this.userTokens?.userAccessToken,
|
|
217
|
+
hasRedirectUri: !!this.config.redirectUri,
|
|
218
|
+
...(this.userTokens?.userRefreshTokenExpiry
|
|
219
|
+
? { refreshTokenExpiry: this.userTokens.userRefreshTokenExpiry }
|
|
220
|
+
: {}),
|
|
221
|
+
...(this.userTokens?.userAccessTokenExpiry
|
|
222
|
+
? { accessTokenExpiry: this.userTokens.userAccessTokenExpiry }
|
|
223
|
+
: {}),
|
|
224
|
+
...(this.authSource ? { source: this.authSource } : {}),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
201
227
|
isAuthenticated() {
|
|
202
228
|
if (this.userTokens && !this.isUserAccessTokenExpired(this.userTokens)) {
|
|
203
229
|
return true;
|
|
@@ -289,9 +289,19 @@ export function getEbayConfig(environmentOverride) {
|
|
|
289
289
|
appAccessToken: process.env.EBAY_APP_ACCESS_TOKEN ?? '',
|
|
290
290
|
};
|
|
291
291
|
}
|
|
292
|
+
export function getValidationRunnerUserId(environment) {
|
|
293
|
+
const envSpecific = environment === 'production'
|
|
294
|
+
? process.env.VALIDATION_RUNNER_USER_ID_PRODUCTION
|
|
295
|
+
: process.env.VALIDATION_RUNNER_USER_ID_SANDBOX;
|
|
296
|
+
const resolved = (envSpecific ?? process.env.VALIDATION_RUNNER_USER_ID ?? '').trim();
|
|
297
|
+
return resolved || null;
|
|
298
|
+
}
|
|
292
299
|
export function getBaseUrl(environment) {
|
|
293
300
|
return environment === 'production' ? 'https://api.ebay.com' : 'https://api.sandbox.ebay.com';
|
|
294
301
|
}
|
|
302
|
+
export function getOAuthTokenBaseUrl(environment) {
|
|
303
|
+
return environment === 'production' ? 'https://api.ebay.com' : 'https://api.sandbox.ebay.com';
|
|
304
|
+
}
|
|
295
305
|
export function getIdentityBaseUrl(environment) {
|
|
296
306
|
return environment === 'production' ? 'https://apiz.ebay.com' : 'https://apiz.sandbox.ebay.com';
|
|
297
307
|
}
|
|
@@ -11,7 +11,7 @@ function isHostedEnvironment() {
|
|
|
11
11
|
function main() {
|
|
12
12
|
const args = process.argv.slice(2);
|
|
13
13
|
if (args.length === 0) {
|
|
14
|
-
throw new Error('Usage: tsx src/scripts/
|
|
14
|
+
throw new Error('Usage: tsx src/scripts/env-check.ts <command> [args...]');
|
|
15
15
|
}
|
|
16
16
|
const hosted = isHostedEnvironment();
|
|
17
17
|
const [command, ...commandArgs] = args;
|
package/build/server-http.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
2
|
import helmet from 'helmet';
|
|
3
3
|
import cors from 'cors';
|
|
4
|
+
import axios from 'axios';
|
|
4
5
|
import { createServer as createHttpsServer } from 'https';
|
|
5
6
|
import { readFileSync } from 'fs';
|
|
6
7
|
import { dirname, join, resolve } from 'path';
|
|
@@ -10,8 +11,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
10
11
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
11
12
|
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
|
|
12
13
|
import { EbaySellerApi } from './api/index.js';
|
|
13
|
-
import { getConfiguredEnvironment, getHostedOauthScopes, getEbayConfig, getOAuthAuthorizationUrl, validateCredentialsForEnvironment, ruNameToEnvironment, } from './config/environment.js';
|
|
14
|
-
import { getToolDefinitions, executeTool } from './tools/index.js';
|
|
14
|
+
import { getBaseUrl, getConfiguredEnvironment, getHostedOauthScopes, getEbayConfig, getOAuthAuthorizationUrl, getValidationRunnerUserId, validateCredentialsForEnvironment, ruNameToEnvironment, } from './config/environment.js';
|
|
15
15
|
import { getVersion } from './utils/version.js';
|
|
16
16
|
import { serverLogger } from './utils/logger.js';
|
|
17
17
|
import { MultiUserAuthStore } from './auth/multi-user-store.js';
|
|
@@ -38,6 +38,48 @@ function htmlEscape(value) {
|
|
|
38
38
|
.replace(/"/g, '"')
|
|
39
39
|
.replace(/'/g, ''');
|
|
40
40
|
}
|
|
41
|
+
function getValidationIdFromBody(body) {
|
|
42
|
+
if (typeof body === 'object' &&
|
|
43
|
+
body !== null &&
|
|
44
|
+
'validationId' in body &&
|
|
45
|
+
typeof body.validationId === 'string') {
|
|
46
|
+
return body.validationId;
|
|
47
|
+
}
|
|
48
|
+
return '';
|
|
49
|
+
}
|
|
50
|
+
function getRetryTimestampFromBody(body) {
|
|
51
|
+
if (typeof body === 'object' &&
|
|
52
|
+
body !== null &&
|
|
53
|
+
'timestamp' in body &&
|
|
54
|
+
typeof body.timestamp === 'string') {
|
|
55
|
+
const parsed = new Date(body.timestamp);
|
|
56
|
+
if (Number.isFinite(parsed.getTime())) {
|
|
57
|
+
return new Date(parsed.getTime() + 30 * 60 * 1000).toISOString();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return new Date(Date.now() + 30 * 60 * 1000).toISOString();
|
|
61
|
+
}
|
|
62
|
+
function getAxiosFailureDebug(error) {
|
|
63
|
+
if (!axios.isAxiosError(error)) {
|
|
64
|
+
return {
|
|
65
|
+
responseStatus: null,
|
|
66
|
+
responseBodyExcerpt: null,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
const responseStatus = error.response?.status ?? null;
|
|
70
|
+
const rawBody = error.response?.data;
|
|
71
|
+
if (rawBody === undefined) {
|
|
72
|
+
return {
|
|
73
|
+
responseStatus,
|
|
74
|
+
responseBodyExcerpt: null,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const bodyText = typeof rawBody === 'string' ? rawBody : JSON.stringify(rawBody, null, 2);
|
|
78
|
+
return {
|
|
79
|
+
responseStatus,
|
|
80
|
+
responseBodyExcerpt: bodyText.slice(0, 500),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
41
83
|
function requireAdmin(req, res, next) {
|
|
42
84
|
if (!CONFIG.adminApiKey) {
|
|
43
85
|
res.status(500).json({ error: 'ADMIN_API_KEY is not configured' });
|
|
@@ -107,6 +149,7 @@ function createApp() {
|
|
|
107
149
|
app.use(express.urlencoded({ extended: false }));
|
|
108
150
|
app.use(helmet({ xPoweredBy: false }));
|
|
109
151
|
app.use('/icons', express.static(join(projectRoot, 'public', 'icons')));
|
|
152
|
+
app.use('/callback-copy.js', express.static(join(projectRoot, 'public', 'callback-copy.js')));
|
|
110
153
|
app.use((req, res, next) => {
|
|
111
154
|
const start = Date.now();
|
|
112
155
|
res.on('finish', () => {
|
|
@@ -317,9 +360,9 @@ function mountEnvRouter(hardcodedEnv, serverUrl, iconBaseUrl) {
|
|
|
317
360
|
}
|
|
318
361
|
}
|
|
319
362
|
// Step 2: per-env RuName detection.
|
|
320
|
-
const sandboxRuName = process.env.EBAY_SANDBOX_RUNAME
|
|
321
|
-
const productionRuName = process.env.EBAY_PRODUCTION_RUNAME
|
|
322
|
-
const genericRuName = process.env.EBAY_RUNAME
|
|
363
|
+
const sandboxRuName = process.env.EBAY_SANDBOX_RUNAME ?? process.env.EBAY_SANDBOX_REDIRECT_URI;
|
|
364
|
+
const productionRuName = process.env.EBAY_PRODUCTION_RUNAME ?? process.env.EBAY_PRODUCTION_REDIRECT_URI;
|
|
365
|
+
const genericRuName = process.env.EBAY_RUNAME ?? process.env.EBAY_REDIRECT_URI;
|
|
323
366
|
const sandboxDetected = ruNameToEnvironment(sandboxRuName);
|
|
324
367
|
const productionDetected = ruNameToEnvironment(productionRuName);
|
|
325
368
|
if (sandboxDetected && !productionDetected)
|
|
@@ -333,6 +376,113 @@ function mountEnvRouter(hardcodedEnv, serverUrl, iconBaseUrl) {
|
|
|
333
376
|
// Step 4: final fallback.
|
|
334
377
|
return getConfiguredEnvironment();
|
|
335
378
|
}
|
|
379
|
+
router.post('/validation/run', requireAdmin, async (req, res) => {
|
|
380
|
+
const environment = resolveEnv(req);
|
|
381
|
+
const validationRunnerUserId = getValidationRunnerUserId(environment);
|
|
382
|
+
if (!validationRunnerUserId) {
|
|
383
|
+
res.status(500).json({
|
|
384
|
+
status: 'error',
|
|
385
|
+
validationId: getValidationIdFromBody(req.body),
|
|
386
|
+
errorCode: 'VALIDATION_USER_NOT_CONFIGURED',
|
|
387
|
+
message: `No validation runner user is configured for ${environment}`,
|
|
388
|
+
retryable: false,
|
|
389
|
+
nextCheckAt: null,
|
|
390
|
+
});
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
const storedTokens = await authStore.getUserTokens(validationRunnerUserId, environment);
|
|
394
|
+
if (!storedTokens?.tokenData) {
|
|
395
|
+
res.status(500).json({
|
|
396
|
+
status: 'error',
|
|
397
|
+
validationId: getValidationIdFromBody(req.body),
|
|
398
|
+
errorCode: 'VALIDATION_USER_TOKENS_MISSING',
|
|
399
|
+
message: `Stored refresh-token-backed credentials were not found for validation user ${validationRunnerUserId} in ${environment}`,
|
|
400
|
+
retryable: false,
|
|
401
|
+
nextCheckAt: null,
|
|
402
|
+
});
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
try {
|
|
406
|
+
const api = await createUserScopedApi(validationRunnerUserId, environment);
|
|
407
|
+
const { runValidation } = await import('./validation/run-validation.js');
|
|
408
|
+
const result = await runValidation(api, req.body);
|
|
409
|
+
if (result.status === 'error') {
|
|
410
|
+
res.status(500).json(result);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
res.json(result);
|
|
414
|
+
}
|
|
415
|
+
catch (error) {
|
|
416
|
+
res.status(500).json({
|
|
417
|
+
status: 'error',
|
|
418
|
+
validationId: getValidationIdFromBody(req.body),
|
|
419
|
+
errorCode: 'VALIDATION_ROUTE_ERROR',
|
|
420
|
+
message: error instanceof Error ? error.message : String(error),
|
|
421
|
+
retryable: true,
|
|
422
|
+
nextCheckAt: getRetryTimestampFromBody(req.body),
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
router.get('/validation/health', requireAdmin, async (req, res) => {
|
|
427
|
+
const environment = resolveEnv(req);
|
|
428
|
+
const validationRunnerUserId = getValidationRunnerUserId(environment);
|
|
429
|
+
const storedTokens = validationRunnerUserId
|
|
430
|
+
? await authStore.getUserTokens(validationRunnerUserId, environment)
|
|
431
|
+
: null;
|
|
432
|
+
let authenticated = false;
|
|
433
|
+
let authError = null;
|
|
434
|
+
let tokenStatus = null;
|
|
435
|
+
let authDebug = null;
|
|
436
|
+
if (validationRunnerUserId && storedTokens?.tokenData) {
|
|
437
|
+
try {
|
|
438
|
+
const api = await createUserScopedApi(validationRunnerUserId, environment);
|
|
439
|
+
const oauthClient = api.getAuthClient().getOAuthClient();
|
|
440
|
+
const config = api.getAuthClient().getConfig();
|
|
441
|
+
authDebug = {
|
|
442
|
+
...oauthClient.getAuthDebugInfo(),
|
|
443
|
+
configuredMarketplaceId: config.marketplaceId ?? '',
|
|
444
|
+
configuredContentLanguage: config.contentLanguage ?? '',
|
|
445
|
+
};
|
|
446
|
+
await api.getAuthClient().getOAuthClient().getAccessToken();
|
|
447
|
+
authenticated = true;
|
|
448
|
+
tokenStatus = api.getTokenInfo();
|
|
449
|
+
}
|
|
450
|
+
catch (error) {
|
|
451
|
+
authError = error instanceof Error ? error.message : String(error);
|
|
452
|
+
const failureDebug = getAxiosFailureDebug(error);
|
|
453
|
+
authDebug = authDebug
|
|
454
|
+
? {
|
|
455
|
+
...authDebug,
|
|
456
|
+
responseStatus: failureDebug.responseStatus,
|
|
457
|
+
responseBodyExcerpt: failureDebug.responseBodyExcerpt,
|
|
458
|
+
}
|
|
459
|
+
: null;
|
|
460
|
+
serverLogger.error('Validation health auth check failed', {
|
|
461
|
+
environment,
|
|
462
|
+
validationRunnerUserId,
|
|
463
|
+
tokenEndpoint: authDebug?.tokenEndpoint ?? null,
|
|
464
|
+
responseStatus: failureDebug.responseStatus,
|
|
465
|
+
responseBodyExcerpt: failureDebug.responseBodyExcerpt,
|
|
466
|
+
authError,
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
res.json({
|
|
471
|
+
status: authenticated ? 'ok' : 'degraded',
|
|
472
|
+
environment,
|
|
473
|
+
validationRunnerUserId,
|
|
474
|
+
hasStoredTokens: !!storedTokens?.tokenData,
|
|
475
|
+
authenticated,
|
|
476
|
+
tokenStatus,
|
|
477
|
+
authDebug,
|
|
478
|
+
providers: {
|
|
479
|
+
ebay: { available: true, implemented: true, confidence: 'medium' },
|
|
480
|
+
social: { available: false, implemented: false, confidence: 'low' },
|
|
481
|
+
chart: { available: false, implemented: false, confidence: 'low' },
|
|
482
|
+
},
|
|
483
|
+
...(authError ? { authError } : {}),
|
|
484
|
+
});
|
|
485
|
+
});
|
|
336
486
|
// ── RFC 8414 – Authorization Server Metadata ──────────────────────────
|
|
337
487
|
// For env-scoped routers: endpoints are relative to the env base URL.
|
|
338
488
|
// For the ROOT router: endpoints are relative to the DEFAULT environment's
|
|
@@ -671,6 +821,19 @@ function mountEnvRouter(hardcodedEnv, serverUrl, iconBaseUrl) {
|
|
|
671
821
|
next();
|
|
672
822
|
};
|
|
673
823
|
async function createMcpServer(userId, environment) {
|
|
824
|
+
let getToolDefinitions;
|
|
825
|
+
let executeTool;
|
|
826
|
+
try {
|
|
827
|
+
({ getToolDefinitions, executeTool } = await import('./tools/index.js'));
|
|
828
|
+
}
|
|
829
|
+
catch (error) {
|
|
830
|
+
serverLogger.error('Failed to import MCP tool registry', {
|
|
831
|
+
userId,
|
|
832
|
+
environment,
|
|
833
|
+
error: error instanceof Error ? error.message : String(error),
|
|
834
|
+
});
|
|
835
|
+
throw error;
|
|
836
|
+
}
|
|
674
837
|
const api = await createUserScopedApi(userId, environment);
|
|
675
838
|
const server = new McpServer({
|
|
676
839
|
name: 'ebay-mcp-remote-edition',
|
|
@@ -685,23 +848,36 @@ function mountEnvRouter(hardcodedEnv, serverUrl, iconBaseUrl) {
|
|
|
685
848
|
});
|
|
686
849
|
const tools = getToolDefinitions();
|
|
687
850
|
for (const toolDef of tools) {
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
851
|
+
try {
|
|
852
|
+
server.registerTool(toolDef.name, { description: toolDef.description, inputSchema: toolDef.inputSchema }, async (args) => {
|
|
853
|
+
try {
|
|
854
|
+
const result = await executeTool(api, toolDef.name, args);
|
|
855
|
+
return {
|
|
856
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
catch (error) {
|
|
860
|
+
return {
|
|
861
|
+
content: [
|
|
862
|
+
{
|
|
863
|
+
type: 'text',
|
|
864
|
+
text: JSON.stringify({ error: error instanceof Error ? error.message : String(error) }, null, 2),
|
|
865
|
+
},
|
|
866
|
+
],
|
|
867
|
+
isError: true,
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
catch (error) {
|
|
873
|
+
serverLogger.error('Failed to register MCP tool', {
|
|
874
|
+
toolName: toolDef.name,
|
|
875
|
+
userId,
|
|
876
|
+
environment,
|
|
877
|
+
error: error instanceof Error ? error.message : String(error),
|
|
878
|
+
});
|
|
879
|
+
throw error;
|
|
880
|
+
}
|
|
705
881
|
}
|
|
706
882
|
return server;
|
|
707
883
|
}
|
|
@@ -717,26 +893,51 @@ function mountEnvRouter(hardcodedEnv, serverUrl, iconBaseUrl) {
|
|
|
717
893
|
transport = transports.get(sessionId);
|
|
718
894
|
}
|
|
719
895
|
else if (!sessionId && isInitializeRequest(req.body)) {
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
896
|
+
try {
|
|
897
|
+
transport = new StreamableHTTPServerTransport({
|
|
898
|
+
sessionIdGenerator: () => randomUUID(),
|
|
899
|
+
onsessioninitialized: (newSessionId) => {
|
|
900
|
+
transports.set(newSessionId, transport);
|
|
901
|
+
serverLogger.info('New MCP session initialized', {
|
|
902
|
+
sessionId: newSessionId,
|
|
903
|
+
userId: userContext.userId,
|
|
904
|
+
environment: userContext.environment,
|
|
905
|
+
});
|
|
906
|
+
},
|
|
907
|
+
});
|
|
908
|
+
transport.onclose = () => {
|
|
909
|
+
if (transport.sessionId) {
|
|
910
|
+
transports.delete(transport.sessionId);
|
|
911
|
+
}
|
|
912
|
+
};
|
|
913
|
+
const server = await createMcpServer(userContext.userId, userContext.environment);
|
|
914
|
+
await server.connect(transport);
|
|
915
|
+
}
|
|
916
|
+
catch (error) {
|
|
917
|
+
serverLogger.error('Failed to initialize MCP session', {
|
|
918
|
+
userId: userContext.userId,
|
|
919
|
+
environment: userContext.environment,
|
|
920
|
+
error: error instanceof Error ? error.message : String(error),
|
|
921
|
+
});
|
|
922
|
+
res.status(500).json({
|
|
923
|
+
jsonrpc: '2.0',
|
|
924
|
+
error: {
|
|
925
|
+
code: -32603,
|
|
926
|
+
message: error instanceof Error ? error.message : 'Failed to initialize MCP session',
|
|
927
|
+
},
|
|
928
|
+
id: null,
|
|
929
|
+
});
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
738
932
|
}
|
|
739
933
|
else {
|
|
934
|
+
serverLogger.warn('Rejected MCP request without valid transport session', {
|
|
935
|
+
hasSessionId: !!sessionId,
|
|
936
|
+
sessionId,
|
|
937
|
+
isInitialize: isInitializeRequest(req.body),
|
|
938
|
+
userId: userContext.userId,
|
|
939
|
+
environment: userContext.environment,
|
|
940
|
+
});
|
|
740
941
|
res.status(400).json({
|
|
741
942
|
jsonrpc: '2.0',
|
|
742
943
|
error: { code: -32000, message: 'Bad Request: No valid session ID provided' },
|
|
@@ -769,7 +970,6 @@ async function handleOAuthCallback(req, res, serverUrl) {
|
|
|
769
970
|
try {
|
|
770
971
|
const code = typeof req.query.code === 'string' ? req.query.code : undefined;
|
|
771
972
|
const state = typeof req.query.state === 'string' ? req.query.state : undefined;
|
|
772
|
-
const envFromQuery = typeof req.query.env === 'string' ? req.query.env : undefined;
|
|
773
973
|
const oauthError = typeof req.query.error === 'string' ? req.query.error : undefined;
|
|
774
974
|
const errorDescription = typeof req.query.error_description === 'string' ? req.query.error_description : undefined;
|
|
775
975
|
serverLogger.info('[oauth/callback] Received', {
|
|
@@ -787,33 +987,37 @@ async function handleOAuthCallback(req, res, serverUrl) {
|
|
|
787
987
|
res.status(400).send('<h1>Missing authorization code</h1>');
|
|
788
988
|
return;
|
|
789
989
|
}
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
res.status(400).send('<h1>Invalid or expired OAuth state</h1>');
|
|
797
|
-
return;
|
|
798
|
-
}
|
|
799
|
-
environment = stateRecord.environment;
|
|
800
|
-
serverLogger.info('[oauth/callback] State resolved', {
|
|
801
|
-
environment,
|
|
802
|
-
isMcpFlow: !!(stateRecord.mcpClientId && stateRecord.mcpRedirectUri),
|
|
803
|
-
});
|
|
990
|
+
if (!state) {
|
|
991
|
+
serverLogger.warn('[oauth/callback] Missing OAuth state in callback');
|
|
992
|
+
res
|
|
993
|
+
.status(400)
|
|
994
|
+
.send("<h1>Missing OAuth state</h1><p>Restart the OAuth flow from this server's /oauth/start or /authorize endpoint.</p>");
|
|
995
|
+
return;
|
|
804
996
|
}
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
serverLogger.warn('OAuth callback received without state; falling back to configured/query environment', { environment });
|
|
997
|
+
const stateRecord = await authStore.getOAuthState(state);
|
|
998
|
+
if (!stateRecord) {
|
|
999
|
+
serverLogger.warn('[oauth/callback] OAuth state not found or expired', { state });
|
|
1000
|
+
res.status(400).send('<h1>Invalid or expired OAuth state</h1>');
|
|
1001
|
+
return;
|
|
811
1002
|
}
|
|
1003
|
+
const environment = stateRecord.environment;
|
|
1004
|
+
serverLogger.info('[oauth/callback] State resolved', {
|
|
1005
|
+
environment,
|
|
1006
|
+
isMcpFlow: !!(stateRecord.mcpClientId && stateRecord.mcpRedirectUri),
|
|
1007
|
+
});
|
|
1008
|
+
const ebayConfig = getEbayConfig(environment);
|
|
1009
|
+
serverLogger.info('[oauth/callback] Prepared eBay token exchange', {
|
|
1010
|
+
environment,
|
|
1011
|
+
tokenBaseUrl: getBaseUrl(environment),
|
|
1012
|
+
clientIdPrefix: ebayConfig.clientId ? `${ebayConfig.clientId.slice(0, 12)}...` : '(missing)',
|
|
1013
|
+
ruName: ebayConfig.redirectUri ?? '(missing)',
|
|
1014
|
+
});
|
|
812
1015
|
const userId = randomUUID();
|
|
813
1016
|
const api = await createUserScopedApi(userId, environment);
|
|
814
1017
|
const oauthClient = api.getAuthClient().getOAuthClient();
|
|
815
1018
|
serverLogger.info('[oauth/callback] Exchanging code for eBay tokens', { userId });
|
|
816
1019
|
const tokenData = await oauthClient.exchangeCodeForToken(code);
|
|
1020
|
+
await authStore.deleteOAuthState(state);
|
|
817
1021
|
serverLogger.info('[oauth/callback] eBay token exchange successful', {
|
|
818
1022
|
userId,
|
|
819
1023
|
hasScope: !!tokenData.scope,
|
|
@@ -901,32 +1105,7 @@ async function handleOAuthCallback(req, res, serverUrl) {
|
|
|
901
1105
|
.muted { color: #6b7280; }
|
|
902
1106
|
a { color: #2563eb; }
|
|
903
1107
|
</style>
|
|
904
|
-
<script>
|
|
905
|
-
async function copyText(elementId, statusId) {
|
|
906
|
-
const token = document.getElementById(elementId).innerText;
|
|
907
|
-
const status = document.getElementById(statusId);
|
|
908
|
-
try {
|
|
909
|
-
if (navigator.clipboard && window.isSecureContext) {
|
|
910
|
-
await navigator.clipboard.writeText(token);
|
|
911
|
-
} else {
|
|
912
|
-
const temp = document.createElement('textarea');
|
|
913
|
-
temp.value = token;
|
|
914
|
-
temp.setAttribute('readonly', '');
|
|
915
|
-
temp.style.position = 'absolute';
|
|
916
|
-
temp.style.left = '-9999px';
|
|
917
|
-
document.body.appendChild(temp);
|
|
918
|
-
temp.select();
|
|
919
|
-
document.execCommand('copy');
|
|
920
|
-
document.body.removeChild(temp);
|
|
921
|
-
}
|
|
922
|
-
status.textContent = 'Copied!';
|
|
923
|
-
setTimeout(() => { status.textContent = ''; }, 1800);
|
|
924
|
-
} catch (err) {
|
|
925
|
-
status.textContent = 'Copy failed — select manually';
|
|
926
|
-
setTimeout(() => { status.textContent = ''; }, 2500);
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
|
-
</script>
|
|
1108
|
+
<script src="/callback-copy.js" defer></script>
|
|
930
1109
|
</head>
|
|
931
1110
|
<body>
|
|
932
1111
|
<h1>eBay account connected ✓</h1>
|
|
@@ -936,21 +1115,21 @@ async function handleOAuthCallback(req, res, serverUrl) {
|
|
|
936
1115
|
<h2>① Session token — paste as Bearer token in your MCP client</h2>
|
|
937
1116
|
<div class="card">
|
|
938
1117
|
<pre id="session-token">${htmlEscape(session.sessionToken)}</pre>
|
|
939
|
-
<button class="copy-btn"
|
|
1118
|
+
<button class="copy-btn" data-copy-source="session-token" data-copy-status="st-status">Copy</button><span id="st-status" class="copy-status"></span>
|
|
940
1119
|
<p class="muted">Set as <code>EBAY_SESSION_TOKEN</code> in your server env to survive restarts.</p>
|
|
941
1120
|
</div>
|
|
942
1121
|
|
|
943
1122
|
<h2>② eBay user access token</h2>
|
|
944
1123
|
<div class="card">
|
|
945
1124
|
<pre id="access-token">${htmlEscape(tokenData.access_token)}</pre>
|
|
946
|
-
<button class="copy-btn"
|
|
1125
|
+
<button class="copy-btn" data-copy-source="access-token" data-copy-status="at-status">Copy</button><span id="at-status" class="copy-status"></span>
|
|
947
1126
|
<p class="muted">Set as <code>EBAY_USER_ACCESS_TOKEN</code> in your server env (optional — auto-refreshed from refresh token).</p>
|
|
948
1127
|
</div>
|
|
949
1128
|
|
|
950
1129
|
<h2>③ eBay user refresh token</h2>
|
|
951
1130
|
<div class="card">
|
|
952
1131
|
<pre id="refresh-token">${htmlEscape(tokenData.refresh_token ?? '')}</pre>
|
|
953
|
-
<button class="copy-btn"
|
|
1132
|
+
<button class="copy-btn" data-copy-source="refresh-token" data-copy-status="rt-status">Copy</button><span id="rt-status" class="copy-status"></span>
|
|
954
1133
|
<p class="muted">Set as <code>EBAY_USER_REFRESH_TOKEN</code> in your server env. The server uses this to keep the access token fresh automatically.</p>
|
|
955
1134
|
</div>
|
|
956
1135
|
|