ebay-mcp-remote-edition 3.1.2 → 3.3.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 +242 -3
- package/build/api/analytics-and-report/analytics.js +4 -4
- package/build/api/client-trading.js +2 -2
- package/build/api/communication/feedback.js +5 -5
- package/build/api/communication/message.js +5 -5
- package/build/api/communication/negotiation.js +3 -3
- package/build/api/communication/notification.js +21 -21
- package/build/api/listing-management/inventory.js +36 -36
- package/build/api/listing-metadata/metadata.js +24 -24
- package/build/auth/multi-user-store.js +8 -2
- package/build/auth/oauth.js +33 -7
- package/build/auth/token-verifier.js +3 -3
- 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 +286 -90
- package/build/tools/chat-tools.js +50 -0
- package/build/tools/index.js +50 -9
- package/build/utils/version.js +1 -1
- package/build/validation/providers/chart.js +3 -0
- package/build/validation/providers/ebay-sold.js +216 -0
- package/build/validation/providers/ebay.js +170 -0
- package/build/validation/providers/query-utils.js +454 -0
- package/build/validation/providers/research.js +14 -0
- package/build/validation/providers/social.js +683 -0
- package/build/validation/providers/terapeak.js +30 -0
- package/build/validation/recommendation.js +100 -0
- package/build/validation/run-validation.js +374 -0
- package/build/validation/schemas.js +105 -0
- package/build/validation/types.js +1 -0
- package/package.json +29 -27
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,130 @@ 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
|
+
const socialConfig = {
|
|
433
|
+
hasTwitterBearerToken: Boolean(process.env.TWITTER_BEARER_TOKEN?.trim()),
|
|
434
|
+
hasYoutubeApiKey: Boolean(process.env.YOUTUBE_API_KEY?.trim()),
|
|
435
|
+
hasRedditUserAgent: Boolean(process.env.REDDIT_USER_AGENT?.trim()),
|
|
436
|
+
};
|
|
437
|
+
let authenticated = false;
|
|
438
|
+
let authError = null;
|
|
439
|
+
let tokenStatus = null;
|
|
440
|
+
let authDebug = null;
|
|
441
|
+
if (validationRunnerUserId && storedTokens?.tokenData) {
|
|
442
|
+
try {
|
|
443
|
+
const api = await createUserScopedApi(validationRunnerUserId, environment);
|
|
444
|
+
const oauthClient = api.getAuthClient().getOAuthClient();
|
|
445
|
+
const config = api.getAuthClient().getConfig();
|
|
446
|
+
authDebug = {
|
|
447
|
+
...oauthClient.getAuthDebugInfo(),
|
|
448
|
+
configuredMarketplaceId: config.marketplaceId ?? '',
|
|
449
|
+
configuredContentLanguage: config.contentLanguage ?? '',
|
|
450
|
+
};
|
|
451
|
+
await api.getAuthClient().getOAuthClient().getAccessToken();
|
|
452
|
+
authenticated = true;
|
|
453
|
+
tokenStatus = api.getTokenInfo();
|
|
454
|
+
}
|
|
455
|
+
catch (error) {
|
|
456
|
+
authError = error instanceof Error ? error.message : String(error);
|
|
457
|
+
const failureDebug = getAxiosFailureDebug(error);
|
|
458
|
+
authDebug = authDebug
|
|
459
|
+
? {
|
|
460
|
+
...authDebug,
|
|
461
|
+
responseStatus: failureDebug.responseStatus,
|
|
462
|
+
responseBodyExcerpt: failureDebug.responseBodyExcerpt,
|
|
463
|
+
}
|
|
464
|
+
: null;
|
|
465
|
+
serverLogger.error('Validation health auth check failed', {
|
|
466
|
+
environment,
|
|
467
|
+
validationRunnerUserId,
|
|
468
|
+
tokenEndpoint: authDebug?.tokenEndpoint ?? null,
|
|
469
|
+
responseStatus: failureDebug.responseStatus,
|
|
470
|
+
responseBodyExcerpt: failureDebug.responseBodyExcerpt,
|
|
471
|
+
authError,
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
const healthResponse = {
|
|
476
|
+
status: authenticated ? 'ok' : 'degraded',
|
|
477
|
+
environment,
|
|
478
|
+
validationRunnerUserId,
|
|
479
|
+
hasStoredTokens: !!storedTokens?.tokenData,
|
|
480
|
+
authenticated,
|
|
481
|
+
tokenStatus,
|
|
482
|
+
authDebug,
|
|
483
|
+
providers: {
|
|
484
|
+
ebay: { available: true, implemented: true, confidence: 'medium' },
|
|
485
|
+
social: { available: false, implemented: false, confidence: 'low' },
|
|
486
|
+
chart: { available: false, implemented: false, confidence: 'low' },
|
|
487
|
+
socialConfig,
|
|
488
|
+
},
|
|
489
|
+
...(authError ? { authError } : {}),
|
|
490
|
+
};
|
|
491
|
+
serverLogger.info('Validation health response emitted', {
|
|
492
|
+
environment,
|
|
493
|
+
path: req.originalUrl,
|
|
494
|
+
status: healthResponse.status,
|
|
495
|
+
version: getVersion(),
|
|
496
|
+
hasSocialConfigAtRoot: Object.prototype.hasOwnProperty.call(healthResponse, 'socialConfig'),
|
|
497
|
+
hasSocialConfigUnderProviders: Object.prototype.hasOwnProperty.call(healthResponse.providers, 'socialConfig'),
|
|
498
|
+
providerKeys: Object.keys(healthResponse.providers),
|
|
499
|
+
socialConfig,
|
|
500
|
+
});
|
|
501
|
+
res.json(healthResponse);
|
|
502
|
+
});
|
|
336
503
|
// ── RFC 8414 – Authorization Server Metadata ──────────────────────────
|
|
337
504
|
// For env-scoped routers: endpoints are relative to the env base URL.
|
|
338
505
|
// For the ROOT router: endpoints are relative to the DEFAULT environment's
|
|
@@ -671,6 +838,19 @@ function mountEnvRouter(hardcodedEnv, serverUrl, iconBaseUrl) {
|
|
|
671
838
|
next();
|
|
672
839
|
};
|
|
673
840
|
async function createMcpServer(userId, environment) {
|
|
841
|
+
let getToolDefinitions;
|
|
842
|
+
let executeTool;
|
|
843
|
+
try {
|
|
844
|
+
({ getToolDefinitions, executeTool } = await import('./tools/index.js'));
|
|
845
|
+
}
|
|
846
|
+
catch (error) {
|
|
847
|
+
serverLogger.error('Failed to import MCP tool registry', {
|
|
848
|
+
userId,
|
|
849
|
+
environment,
|
|
850
|
+
error: error instanceof Error ? error.message : String(error),
|
|
851
|
+
});
|
|
852
|
+
throw error;
|
|
853
|
+
}
|
|
674
854
|
const api = await createUserScopedApi(userId, environment);
|
|
675
855
|
const server = new McpServer({
|
|
676
856
|
name: 'ebay-mcp-remote-edition',
|
|
@@ -685,23 +865,36 @@ function mountEnvRouter(hardcodedEnv, serverUrl, iconBaseUrl) {
|
|
|
685
865
|
});
|
|
686
866
|
const tools = getToolDefinitions();
|
|
687
867
|
for (const toolDef of tools) {
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
868
|
+
try {
|
|
869
|
+
server.registerTool(toolDef.name, { description: toolDef.description, inputSchema: toolDef.inputSchema }, async (args) => {
|
|
870
|
+
try {
|
|
871
|
+
const result = await executeTool(api, toolDef.name, args);
|
|
872
|
+
return {
|
|
873
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
catch (error) {
|
|
877
|
+
return {
|
|
878
|
+
content: [
|
|
879
|
+
{
|
|
880
|
+
type: 'text',
|
|
881
|
+
text: JSON.stringify({ error: error instanceof Error ? error.message : String(error) }, null, 2),
|
|
882
|
+
},
|
|
883
|
+
],
|
|
884
|
+
isError: true,
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
catch (error) {
|
|
890
|
+
serverLogger.error('Failed to register MCP tool', {
|
|
891
|
+
toolName: toolDef.name,
|
|
892
|
+
userId,
|
|
893
|
+
environment,
|
|
894
|
+
error: error instanceof Error ? error.message : String(error),
|
|
895
|
+
});
|
|
896
|
+
throw error;
|
|
897
|
+
}
|
|
705
898
|
}
|
|
706
899
|
return server;
|
|
707
900
|
}
|
|
@@ -717,26 +910,51 @@ function mountEnvRouter(hardcodedEnv, serverUrl, iconBaseUrl) {
|
|
|
717
910
|
transport = transports.get(sessionId);
|
|
718
911
|
}
|
|
719
912
|
else if (!sessionId && isInitializeRequest(req.body)) {
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
913
|
+
try {
|
|
914
|
+
transport = new StreamableHTTPServerTransport({
|
|
915
|
+
sessionIdGenerator: () => randomUUID(),
|
|
916
|
+
onsessioninitialized: (newSessionId) => {
|
|
917
|
+
transports.set(newSessionId, transport);
|
|
918
|
+
serverLogger.info('New MCP session initialized', {
|
|
919
|
+
sessionId: newSessionId,
|
|
920
|
+
userId: userContext.userId,
|
|
921
|
+
environment: userContext.environment,
|
|
922
|
+
});
|
|
923
|
+
},
|
|
924
|
+
});
|
|
925
|
+
transport.onclose = () => {
|
|
926
|
+
if (transport.sessionId) {
|
|
927
|
+
transports.delete(transport.sessionId);
|
|
928
|
+
}
|
|
929
|
+
};
|
|
930
|
+
const server = await createMcpServer(userContext.userId, userContext.environment);
|
|
931
|
+
await server.connect(transport);
|
|
932
|
+
}
|
|
933
|
+
catch (error) {
|
|
934
|
+
serverLogger.error('Failed to initialize MCP session', {
|
|
935
|
+
userId: userContext.userId,
|
|
936
|
+
environment: userContext.environment,
|
|
937
|
+
error: error instanceof Error ? error.message : String(error),
|
|
938
|
+
});
|
|
939
|
+
res.status(500).json({
|
|
940
|
+
jsonrpc: '2.0',
|
|
941
|
+
error: {
|
|
942
|
+
code: -32603,
|
|
943
|
+
message: error instanceof Error ? error.message : 'Failed to initialize MCP session',
|
|
944
|
+
},
|
|
945
|
+
id: null,
|
|
946
|
+
});
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
738
949
|
}
|
|
739
950
|
else {
|
|
951
|
+
serverLogger.warn('Rejected MCP request without valid transport session', {
|
|
952
|
+
hasSessionId: !!sessionId,
|
|
953
|
+
sessionId,
|
|
954
|
+
isInitialize: isInitializeRequest(req.body),
|
|
955
|
+
userId: userContext.userId,
|
|
956
|
+
environment: userContext.environment,
|
|
957
|
+
});
|
|
740
958
|
res.status(400).json({
|
|
741
959
|
jsonrpc: '2.0',
|
|
742
960
|
error: { code: -32000, message: 'Bad Request: No valid session ID provided' },
|
|
@@ -769,7 +987,6 @@ async function handleOAuthCallback(req, res, serverUrl) {
|
|
|
769
987
|
try {
|
|
770
988
|
const code = typeof req.query.code === 'string' ? req.query.code : undefined;
|
|
771
989
|
const state = typeof req.query.state === 'string' ? req.query.state : undefined;
|
|
772
|
-
const envFromQuery = typeof req.query.env === 'string' ? req.query.env : undefined;
|
|
773
990
|
const oauthError = typeof req.query.error === 'string' ? req.query.error : undefined;
|
|
774
991
|
const errorDescription = typeof req.query.error_description === 'string' ? req.query.error_description : undefined;
|
|
775
992
|
serverLogger.info('[oauth/callback] Received', {
|
|
@@ -787,33 +1004,37 @@ async function handleOAuthCallback(req, res, serverUrl) {
|
|
|
787
1004
|
res.status(400).send('<h1>Missing authorization code</h1>');
|
|
788
1005
|
return;
|
|
789
1006
|
}
|
|
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
|
-
});
|
|
1007
|
+
if (!state) {
|
|
1008
|
+
serverLogger.warn('[oauth/callback] Missing OAuth state in callback');
|
|
1009
|
+
res
|
|
1010
|
+
.status(400)
|
|
1011
|
+
.send("<h1>Missing OAuth state</h1><p>Restart the OAuth flow from this server's /oauth/start or /authorize endpoint.</p>");
|
|
1012
|
+
return;
|
|
804
1013
|
}
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
serverLogger.warn('OAuth callback received without state; falling back to configured/query environment', { environment });
|
|
1014
|
+
const stateRecord = await authStore.getOAuthState(state);
|
|
1015
|
+
if (!stateRecord) {
|
|
1016
|
+
serverLogger.warn('[oauth/callback] OAuth state not found or expired', { state });
|
|
1017
|
+
res.status(400).send('<h1>Invalid or expired OAuth state</h1>');
|
|
1018
|
+
return;
|
|
811
1019
|
}
|
|
1020
|
+
const environment = stateRecord.environment;
|
|
1021
|
+
serverLogger.info('[oauth/callback] State resolved', {
|
|
1022
|
+
environment,
|
|
1023
|
+
isMcpFlow: !!(stateRecord.mcpClientId && stateRecord.mcpRedirectUri),
|
|
1024
|
+
});
|
|
1025
|
+
const ebayConfig = getEbayConfig(environment);
|
|
1026
|
+
serverLogger.info('[oauth/callback] Prepared eBay token exchange', {
|
|
1027
|
+
environment,
|
|
1028
|
+
tokenBaseUrl: getBaseUrl(environment),
|
|
1029
|
+
clientIdPrefix: ebayConfig.clientId ? `${ebayConfig.clientId.slice(0, 12)}...` : '(missing)',
|
|
1030
|
+
ruName: ebayConfig.redirectUri ?? '(missing)',
|
|
1031
|
+
});
|
|
812
1032
|
const userId = randomUUID();
|
|
813
1033
|
const api = await createUserScopedApi(userId, environment);
|
|
814
1034
|
const oauthClient = api.getAuthClient().getOAuthClient();
|
|
815
1035
|
serverLogger.info('[oauth/callback] Exchanging code for eBay tokens', { userId });
|
|
816
1036
|
const tokenData = await oauthClient.exchangeCodeForToken(code);
|
|
1037
|
+
await authStore.deleteOAuthState(state);
|
|
817
1038
|
serverLogger.info('[oauth/callback] eBay token exchange successful', {
|
|
818
1039
|
userId,
|
|
819
1040
|
hasScope: !!tokenData.scope,
|
|
@@ -901,32 +1122,7 @@ async function handleOAuthCallback(req, res, serverUrl) {
|
|
|
901
1122
|
.muted { color: #6b7280; }
|
|
902
1123
|
a { color: #2563eb; }
|
|
903
1124
|
</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>
|
|
1125
|
+
<script src="/callback-copy.js" defer></script>
|
|
930
1126
|
</head>
|
|
931
1127
|
<body>
|
|
932
1128
|
<h1>eBay account connected ✓</h1>
|
|
@@ -936,21 +1132,21 @@ async function handleOAuthCallback(req, res, serverUrl) {
|
|
|
936
1132
|
<h2>① Session token — paste as Bearer token in your MCP client</h2>
|
|
937
1133
|
<div class="card">
|
|
938
1134
|
<pre id="session-token">${htmlEscape(session.sessionToken)}</pre>
|
|
939
|
-
<button class="copy-btn"
|
|
1135
|
+
<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
1136
|
<p class="muted">Set as <code>EBAY_SESSION_TOKEN</code> in your server env to survive restarts.</p>
|
|
941
1137
|
</div>
|
|
942
1138
|
|
|
943
1139
|
<h2>② eBay user access token</h2>
|
|
944
1140
|
<div class="card">
|
|
945
1141
|
<pre id="access-token">${htmlEscape(tokenData.access_token)}</pre>
|
|
946
|
-
<button class="copy-btn"
|
|
1142
|
+
<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
1143
|
<p class="muted">Set as <code>EBAY_USER_ACCESS_TOKEN</code> in your server env (optional — auto-refreshed from refresh token).</p>
|
|
948
1144
|
</div>
|
|
949
1145
|
|
|
950
1146
|
<h2>③ eBay user refresh token</h2>
|
|
951
1147
|
<div class="card">
|
|
952
1148
|
<pre id="refresh-token">${htmlEscape(tokenData.refresh_token ?? '')}</pre>
|
|
953
|
-
<button class="copy-btn"
|
|
1149
|
+
<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
1150
|
<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
1151
|
</div>
|
|
956
1152
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const chatGptTools = [
|
|
3
|
+
{
|
|
4
|
+
name: 'search',
|
|
5
|
+
description: 'Search for eBay inventory items',
|
|
6
|
+
inputSchema: {
|
|
7
|
+
query: z.string().describe('Search query'),
|
|
8
|
+
limit: z.number().optional().describe('Maximum number of results'),
|
|
9
|
+
},
|
|
10
|
+
title: 'Search',
|
|
11
|
+
outputSchema: {
|
|
12
|
+
type: 'object',
|
|
13
|
+
properties: {
|
|
14
|
+
success: { type: 'boolean' },
|
|
15
|
+
data: { type: 'object' },
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
annotations: {
|
|
19
|
+
title: 'Search',
|
|
20
|
+
readOnlyHint: true,
|
|
21
|
+
},
|
|
22
|
+
_meta: {
|
|
23
|
+
category: 'chat',
|
|
24
|
+
version: '1.0.0',
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: 'fetch',
|
|
29
|
+
description: 'Fetch a specific eBay inventory item by SKU',
|
|
30
|
+
inputSchema: {
|
|
31
|
+
id: z.string().describe('Item SKU'),
|
|
32
|
+
},
|
|
33
|
+
title: 'Fetch',
|
|
34
|
+
outputSchema: {
|
|
35
|
+
type: 'object',
|
|
36
|
+
properties: {
|
|
37
|
+
success: { type: 'boolean' },
|
|
38
|
+
data: { type: 'object' },
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
annotations: {
|
|
42
|
+
title: 'Fetch',
|
|
43
|
+
readOnlyHint: true,
|
|
44
|
+
},
|
|
45
|
+
_meta: {
|
|
46
|
+
category: 'chat',
|
|
47
|
+
version: '1.0.0',
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
];
|
package/build/tools/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getOAuthAuthorizationUrl, validateScopes } from '../config/environment.js';
|
|
2
2
|
import { accountTools, analyticsTools, communicationTools, developerTools, fulfillmentTools, inventoryTools, marketingTools, metadataTools, otherApiTools, taxonomyTools, tradingTools, tokenManagementTools, } from '../tools/definitions/index.js';
|
|
3
|
-
import { chatGptTools } from '../tools/
|
|
3
|
+
import { chatGptTools } from '../tools/chat-tools.js';
|
|
4
4
|
import { getApiStatusFeed } from '../utils/api-status-feed.js';
|
|
5
5
|
import { convertToTimestamp, validateTokenExpiry } from '../utils/date-converter.js';
|
|
6
6
|
// Import Zod schemas for input validation
|
|
@@ -196,7 +196,7 @@ export async function executeTool(api, toolName, args) {
|
|
|
196
196
|
};
|
|
197
197
|
}
|
|
198
198
|
catch (error) {
|
|
199
|
-
throw new Error(`Failed to convert date: ${error instanceof Error ? error.message : String(error)}
|
|
199
|
+
throw new Error(`Failed to convert date: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
200
200
|
}
|
|
201
201
|
}
|
|
202
202
|
case 'ebay_validate_token_expiry': {
|
|
@@ -217,7 +217,7 @@ export async function executeTool(api, toolName, args) {
|
|
|
217
217
|
};
|
|
218
218
|
}
|
|
219
219
|
catch (error) {
|
|
220
|
-
throw new Error(`Failed to validate token expiry: ${error instanceof Error ? error.message : String(error)}
|
|
220
|
+
throw new Error(`Failed to validate token expiry: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
221
221
|
}
|
|
222
222
|
}
|
|
223
223
|
case 'ebay_set_user_tokens_with_expiry': {
|
|
@@ -272,7 +272,7 @@ export async function executeTool(api, toolName, args) {
|
|
|
272
272
|
};
|
|
273
273
|
}
|
|
274
274
|
catch (error) {
|
|
275
|
-
throw new Error(`Failed to set user tokens: ${error instanceof Error ? error.message : String(error)}
|
|
275
|
+
throw new Error(`Failed to set user tokens: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
276
276
|
}
|
|
277
277
|
}
|
|
278
278
|
case 'ebay_display_credentials': {
|
|
@@ -370,7 +370,7 @@ export async function executeTool(api, toolName, args) {
|
|
|
370
370
|
};
|
|
371
371
|
}
|
|
372
372
|
catch (error) {
|
|
373
|
-
throw new Error(`Failed to exchange authorization code: ${error instanceof Error ? error.message : String(error)}
|
|
373
|
+
throw new Error(`Failed to exchange authorization code: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
374
374
|
}
|
|
375
375
|
}
|
|
376
376
|
case 'ebay_refresh_access_token': {
|
|
@@ -401,7 +401,7 @@ export async function executeTool(api, toolName, args) {
|
|
|
401
401
|
};
|
|
402
402
|
}
|
|
403
403
|
catch (error) {
|
|
404
|
-
throw new Error(`Failed to refresh access token: ${error instanceof Error ? error.message : String(error)}
|
|
404
|
+
throw new Error(`Failed to refresh access token: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
405
405
|
}
|
|
406
406
|
}
|
|
407
407
|
// Account Management
|
|
@@ -966,8 +966,33 @@ export async function executeTool(api, toolName, args) {
|
|
|
966
966
|
case 'ebay_get_notification_destinations':
|
|
967
967
|
return await api.notification.getDestinations(args.limit, args.continuationToken);
|
|
968
968
|
case 'ebay_create_notification_destination': {
|
|
969
|
-
|
|
970
|
-
|
|
969
|
+
// The MCP tool definition wraps inputs inside a `destination` field:
|
|
970
|
+
// { destination: { name, endpoint, verificationToken } }
|
|
971
|
+
// eBay API expects: { name, status, deliveryConfig: { endpoint, verificationToken } }
|
|
972
|
+
const dest = args.destination;
|
|
973
|
+
if (dest) {
|
|
974
|
+
const { name, endpoint, verificationToken, status } = dest;
|
|
975
|
+
const ebayPayload = {
|
|
976
|
+
name,
|
|
977
|
+
status: status ?? 'ENABLED',
|
|
978
|
+
deliveryConfig: { endpoint, verificationToken },
|
|
979
|
+
};
|
|
980
|
+
return await api.notification.createDestination(ebayPayload);
|
|
981
|
+
}
|
|
982
|
+
else {
|
|
983
|
+
// Fallback: legacy flat delivery_config format
|
|
984
|
+
const validated = createDestinationSchema.parse(args);
|
|
985
|
+
const { delivery_config, name, status } = validated;
|
|
986
|
+
const ebayPayload = {
|
|
987
|
+
name,
|
|
988
|
+
status: status ?? 'ENABLED',
|
|
989
|
+
deliveryConfig: {
|
|
990
|
+
endpoint: delivery_config?.endpoint,
|
|
991
|
+
verificationToken: delivery_config?.verification_token,
|
|
992
|
+
},
|
|
993
|
+
};
|
|
994
|
+
return await api.notification.createDestination(ebayPayload);
|
|
995
|
+
}
|
|
971
996
|
}
|
|
972
997
|
case 'ebay_get_notification_destination': {
|
|
973
998
|
const validated = getDestinationSchema.parse(args);
|
|
@@ -987,7 +1012,23 @@ export async function executeTool(api, toolName, args) {
|
|
|
987
1012
|
}
|
|
988
1013
|
case 'ebay_create_notification_subscription': {
|
|
989
1014
|
const validated = createSubscriptionSchema.parse(args);
|
|
990
|
-
|
|
1015
|
+
// Convert snake_case to camelCase for eBay API
|
|
1016
|
+
const subPayload = {};
|
|
1017
|
+
if (validated.destination_id !== undefined)
|
|
1018
|
+
subPayload.destinationId = validated.destination_id;
|
|
1019
|
+
if (validated.status !== undefined)
|
|
1020
|
+
subPayload.status = validated.status;
|
|
1021
|
+
if (validated.topic_id !== undefined)
|
|
1022
|
+
subPayload.topicId = validated.topic_id;
|
|
1023
|
+
if (validated.payload !== undefined) {
|
|
1024
|
+
const p = validated.payload;
|
|
1025
|
+
subPayload.payload = {
|
|
1026
|
+
...(p.delivery_protocol !== undefined && { deliveryProtocol: p.delivery_protocol }),
|
|
1027
|
+
...(p.format !== undefined && { format: p.format }),
|
|
1028
|
+
...(p.schema_version !== undefined && { schemaVersion: p.schema_version }),
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
return await api.notification.createSubscription(subPayload);
|
|
991
1032
|
}
|
|
992
1033
|
case 'ebay_get_notification_subscription': {
|
|
993
1034
|
const validated = getSubscriptionSchema.parse(args);
|
package/build/utils/version.js
CHANGED