ebay-mcp-remote-edition 1.0.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.
Files changed (129) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +755 -0
  3. package/build/api/account-management/account.js +301 -0
  4. package/build/api/analytics-and-report/analytics.js +102 -0
  5. package/build/api/client-trading.js +96 -0
  6. package/build/api/client.js +173 -0
  7. package/build/api/communication/feedback.js +119 -0
  8. package/build/api/communication/message.js +131 -0
  9. package/build/api/communication/negotiation.js +97 -0
  10. package/build/api/communication/notification.js +373 -0
  11. package/build/api/developer/developer.js +81 -0
  12. package/build/api/index.js +109 -0
  13. package/build/api/listing-management/inventory.js +640 -0
  14. package/build/api/listing-metadata/metadata.js +485 -0
  15. package/build/api/listing-metadata/taxonomy.js +58 -0
  16. package/build/api/marketing-and-promotions/marketing.js +768 -0
  17. package/build/api/marketing-and-promotions/recommendation.js +32 -0
  18. package/build/api/order-management/dispute.js +69 -0
  19. package/build/api/order-management/fulfillment.js +89 -0
  20. package/build/api/other/compliance.js +47 -0
  21. package/build/api/other/edelivery.js +219 -0
  22. package/build/api/other/identity.js +24 -0
  23. package/build/api/other/translation.js +22 -0
  24. package/build/api/other/vero.js +48 -0
  25. package/build/api/trading/trading.js +78 -0
  26. package/build/auth/kv-store.js +40 -0
  27. package/build/auth/multi-user-store.js +120 -0
  28. package/build/auth/oauth-metadata.js +59 -0
  29. package/build/auth/oauth-middleware.js +99 -0
  30. package/build/auth/oauth-types.js +4 -0
  31. package/build/auth/oauth.js +235 -0
  32. package/build/auth/scope-utils.js +304 -0
  33. package/build/auth/token-store.js +46 -0
  34. package/build/auth/token-verifier.js +172 -0
  35. package/build/config/environment.js +297 -0
  36. package/build/index.d.ts +1 -0
  37. package/build/index.js +129 -0
  38. package/build/schemas/account-management/account.js +375 -0
  39. package/build/schemas/analytics/analytics.js +191 -0
  40. package/build/schemas/communication/messages.js +345 -0
  41. package/build/schemas/fulfillment/orders.js +338 -0
  42. package/build/schemas/index.js +68 -0
  43. package/build/schemas/inventory-management/inventory.js +471 -0
  44. package/build/schemas/marketing/marketing.js +1103 -0
  45. package/build/schemas/metadata/metadata.js +618 -0
  46. package/build/schemas/other/other-apis.js +390 -0
  47. package/build/schemas/taxonomy/taxonomy.js +575 -0
  48. package/build/scripts/auto-setup.js +364 -0
  49. package/build/scripts/dev-sync.js +512 -0
  50. package/build/scripts/diagnostics.js +301 -0
  51. package/build/scripts/download-specs.js +116 -0
  52. package/build/scripts/interactive-setup.js +757 -0
  53. package/build/scripts/setup.js +1515 -0
  54. package/build/scripts/update-api-status-doc.js +44 -0
  55. package/build/server-http.d.ts +1 -0
  56. package/build/server-http.js +581 -0
  57. package/build/tools/definitions/account-with-schemas.js +170 -0
  58. package/build/tools/definitions/account.js +428 -0
  59. package/build/tools/definitions/analytics.js +66 -0
  60. package/build/tools/definitions/communication.js +394 -0
  61. package/build/tools/definitions/developer.js +195 -0
  62. package/build/tools/definitions/fulfillment.js +326 -0
  63. package/build/tools/definitions/index.js +41 -0
  64. package/build/tools/definitions/inventory.js +464 -0
  65. package/build/tools/definitions/marketing.js +1486 -0
  66. package/build/tools/definitions/metadata.js +188 -0
  67. package/build/tools/definitions/other.js +309 -0
  68. package/build/tools/definitions/taxonomy.js +64 -0
  69. package/build/tools/definitions/token-management.js +148 -0
  70. package/build/tools/definitions/trading.js +71 -0
  71. package/build/tools/index.js +1200 -0
  72. package/build/tools/schemas.js +667 -0
  73. package/build/tools/tool-definitions.js +3534 -0
  74. package/build/types/application-settings/developerAnalyticsV1BetaOas3.js +5 -0
  75. package/build/types/application-settings/developerClientRegistrationV1Oas3.js +5 -0
  76. package/build/types/application-settings/developerKeyManagementV1Oas3.js +5 -0
  77. package/build/types/ebay-enums.js +1330 -0
  78. package/build/types/ebay.js +123 -0
  79. package/build/types/index.js +10 -0
  80. package/build/types/sell-apps/account-management/sellAccountV1Oas3.js +5 -0
  81. package/build/types/sell-apps/analytics-and-report/sellAnalyticsV1Oas3.js +5 -0
  82. package/build/types/sell-apps/communication/commerceFeedbackV1BetaOas3.js +5 -0
  83. package/build/types/sell-apps/communication/commerceMessageV1Oas3.js +5 -0
  84. package/build/types/sell-apps/communication/commerceNotificationV1Oas3.js +5 -0
  85. package/build/types/sell-apps/communication/sellNegotiationV1Oas3.js +5 -0
  86. package/build/types/sell-apps/listing-management/sellInventoryV1Oas3.js +5 -0
  87. package/build/types/sell-apps/listing-metadata/sellMetadataV1Oas3.js +5 -0
  88. package/build/types/sell-apps/markeitng-and-promotions/sellMarketingV1Oas3.js +5 -0
  89. package/build/types/sell-apps/markeitng-and-promotions/sellRecommendationV1Oas3.js +5 -0
  90. package/build/types/sell-apps/order-management/sellFulfillmentV1Oas3.js +5 -0
  91. package/build/types/sell-apps/other-apis/commerceIdentityV1Oas3.js +5 -0
  92. package/build/types/sell-apps/other-apis/commerceTranslationV1BetaOas3.js +5 -0
  93. package/build/types/sell-apps/other-apis/commerceVeroV1Oas3.js +5 -0
  94. package/build/types/sell-apps/other-apis/sellComplianceV1Oas3.js +5 -0
  95. package/build/types/sell-apps/other-apis/sellEdeliveryInternationalShippingOas3.js +5 -0
  96. package/build/types/sell-apps/other-apis/sellMarketingV1Oas3.js +5 -0
  97. package/build/types/sell-apps/other-apis/sellRecommendationV1Oas3.js +5 -0
  98. package/build/utils/account-management/account.js +831 -0
  99. package/build/utils/api-status-feed.js +83 -0
  100. package/build/utils/communication/feedback.js +216 -0
  101. package/build/utils/communication/message.js +242 -0
  102. package/build/utils/communication/negotiation.js +150 -0
  103. package/build/utils/communication/notification.js +369 -0
  104. package/build/utils/date-converter.js +160 -0
  105. package/build/utils/llm-client-detector.js +758 -0
  106. package/build/utils/logger.js +198 -0
  107. package/build/utils/oauth-helper.js +315 -0
  108. package/build/utils/order-management/dispute.js +369 -0
  109. package/build/utils/order-management/fulfillment.js +205 -0
  110. package/build/utils/other/compliance.js +76 -0
  111. package/build/utils/other/edelivery.js +241 -0
  112. package/build/utils/other/identity.js +13 -0
  113. package/build/utils/other/translation.js +41 -0
  114. package/build/utils/other/vero.js +90 -0
  115. package/build/utils/scope-helper.js +207 -0
  116. package/build/utils/security-checker.js +248 -0
  117. package/build/utils/setup-validator.js +305 -0
  118. package/build/utils/token-utils.js +40 -0
  119. package/build/utils/version.js +56 -0
  120. package/docs/auth/production_scopes.json +111 -0
  121. package/docs/auth/sandbox_scopes.json +142 -0
  122. package/package.json +122 -0
  123. package/public/icons/1024x1024.png +0 -0
  124. package/public/icons/128x128.png +0 -0
  125. package/public/icons/16x16.png +0 -0
  126. package/public/icons/256x256.png +0 -0
  127. package/public/icons/32x32.png +0 -0
  128. package/public/icons/48x48.png +0 -0
  129. package/public/icons/512x512.png +0 -0
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Fetches the eBay API Status RSS feed and writes the latest items to docs/API_STATUS.md.
3
+ * Used by the GitHub Action to keep an in-repo snapshot. Run with: npx tsx src/scripts/update-api-status-doc.ts
4
+ */
5
+ import { writeFileSync } from 'node:fs';
6
+ import { join } from 'node:path';
7
+ import { getApiStatusFeed } from '../utils/api-status-feed.js';
8
+ const DEFAULT_LIMIT = 15;
9
+ const OUT_PATH = join(process.cwd(), 'docs', 'API_STATUS.md');
10
+ function escapeCell(s) {
11
+ return s.replace(/\|/g, '\\|').replace(/\n/g, ' ');
12
+ }
13
+ function buildMarkdown(items) {
14
+ const lines = [
15
+ '# eBay API Status (latest)',
16
+ '',
17
+ 'Auto-updated snapshot from the [eBay API Status RSS feed](https://developer.ebay.com/rss/api-status).',
18
+ 'Full list: [developer.ebay.com/support/api-status](https://developer.ebay.com/support/api-status).',
19
+ '',
20
+ `*Last updated: ${new Date().toISOString()}*`,
21
+ '',
22
+ '| Title | API | Site | Status | Last updated | Link |',
23
+ '|-------|-----|------|--------|--------------|------|',
24
+ ];
25
+ for (const item of items) {
26
+ const link = item.link ? `[Details](${item.link})` : '';
27
+ lines.push(`| ${escapeCell(item.title)} | ${escapeCell(item.api)} | ${escapeCell(item.site)} | ${escapeCell(item.status)} | ${escapeCell(item.lastUpdated)} | ${link} |`);
28
+ }
29
+ lines.push('', '---', '', '*Generated by [ebay-mcp-remote-edition](https://github.com/mrnajiboy/ebay-mcp-remote-edition) API status sync.*');
30
+ return lines.join('\n');
31
+ }
32
+ async function main() {
33
+ const { items, error } = await getApiStatusFeed({ limit: DEFAULT_LIMIT });
34
+ if (error && items.length === 0) {
35
+ throw new Error(`Failed to fetch API status feed: ${error}`);
36
+ }
37
+ const markdown = buildMarkdown(items);
38
+ writeFileSync(OUT_PATH, markdown, 'utf8');
39
+ console.log(`Wrote ${items.length} items to ${OUT_PATH}`);
40
+ }
41
+ main().catch((err) => {
42
+ console.error(err);
43
+ process.exitCode = 1;
44
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,581 @@
1
+ import express from 'express';
2
+ import helmet from 'helmet';
3
+ import cors from 'cors';
4
+ import { dirname, join, resolve } from 'path';
5
+ import { fileURLToPath } from 'url';
6
+ import { randomUUID, createHash } from 'crypto';
7
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
8
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
9
+ import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
10
+ import { EbaySellerApi } from './api/index.js';
11
+ import { getConfiguredEnvironment, getHostedOauthScopes, getEbayConfig, getOAuthAuthorizationUrl, } from './config/environment.js';
12
+ import { getToolDefinitions, executeTool } from './tools/index.js';
13
+ import { getVersion } from './utils/version.js';
14
+ import { serverLogger } from './utils/logger.js';
15
+ import { MultiUserAuthStore } from './auth/multi-user-store.js';
16
+ const CONFIG = {
17
+ host: process.env.MCP_HOST || '0.0.0.0',
18
+ port: Number(process.env.PORT || process.env.MCP_PORT || 3000),
19
+ publicBaseUrl: (process.env.PUBLIC_BASE_URL || '').replace(/\/$/, ''),
20
+ adminApiKey: process.env.ADMIN_API_KEY || '',
21
+ oauthStartKey: process.env.OAUTH_START_KEY || '',
22
+ };
23
+ const authStore = new MultiUserAuthStore();
24
+ function getServerBaseUrl() {
25
+ return CONFIG.publicBaseUrl || `http://localhost:${CONFIG.port}`;
26
+ }
27
+ function htmlEscape(value) {
28
+ return value
29
+ .replace(/&/g, '&')
30
+ .replace(/</g, '&lt;')
31
+ .replace(/>/g, '&gt;')
32
+ .replace(/"/g, '&quot;')
33
+ .replace(/'/g, '&#39;');
34
+ }
35
+ function requireAdmin(req, res, next) {
36
+ if (!CONFIG.adminApiKey) {
37
+ res.status(500).json({ error: 'ADMIN_API_KEY is not configured' });
38
+ return;
39
+ }
40
+ const header = req.headers['x-admin-api-key'];
41
+ if (header !== CONFIG.adminApiKey) {
42
+ res.status(401).json({ error: 'unauthorized' });
43
+ return;
44
+ }
45
+ next();
46
+ }
47
+ function requireOauthStartKey(req, res, next) {
48
+ if (!CONFIG.oauthStartKey) {
49
+ next();
50
+ return;
51
+ }
52
+ const header = req.headers['x-oauth-start-key'];
53
+ const queryKey = typeof req.query.key === 'string' ? req.query.key : '';
54
+ if (header !== CONFIG.oauthStartKey && queryKey !== CONFIG.oauthStartKey) {
55
+ res.status(401).json({ error: 'unauthorized_oauth_start' });
56
+ return;
57
+ }
58
+ next();
59
+ }
60
+ async function createUserScopedApi(userId, environment) {
61
+ const api = new EbaySellerApi(getEbayConfig(environment), { userId, environment });
62
+ await api.initialize();
63
+ return api;
64
+ }
65
+ function createApp() {
66
+ const app = express();
67
+ app.disable('x-powered-by');
68
+ const __filename = fileURLToPath(import.meta.url);
69
+ const __dirname = dirname(__filename);
70
+ const projectRoot = join(__dirname, '..');
71
+ app.use(cors({ origin: '*', exposedHeaders: ['Mcp-Session-Id'] }));
72
+ app.use(express.json());
73
+ // OAuth token endpoint sends application/x-www-form-urlencoded per RFC 6749
74
+ app.use(express.urlencoded({ extended: false }));
75
+ app.use(helmet({ xPoweredBy: false }));
76
+ app.use('/icons', express.static(join(projectRoot, 'public', 'icons')));
77
+ app.use((req, res, next) => {
78
+ const start = Date.now();
79
+ res.on('finish', () => {
80
+ serverLogger.info(`${req.method} ${req.path} -> ${res.statusCode}`, {
81
+ durationMs: Date.now() - start,
82
+ });
83
+ });
84
+ next();
85
+ });
86
+ const serverUrl = getServerBaseUrl();
87
+ const iconBaseUrl = `${serverUrl}/icons`;
88
+ app.get('/', (_req, res) => {
89
+ res.json({
90
+ name: 'ebay-mcp-remote-edition',
91
+ version: getVersion(),
92
+ mode: 'multi-user-hosted',
93
+ oauth_start: `${serverUrl}/oauth/start?env=${getConfiguredEnvironment()}`,
94
+ mcp_endpoint: `${serverUrl}/mcp`,
95
+ });
96
+ });
97
+ app.get('/health', (_req, res) => {
98
+ res.json({ status: 'healthy', timestamp: new Date().toISOString(), version: getVersion() });
99
+ });
100
+ // ── MCP OAuth 2.1 Authorization Server endpoints ─────────────────────────
101
+ // Required so MCP clients (e.g. Cline) can discover and use this server's
102
+ // built-in OAuth flow instead of hitting a 404 on /register.
103
+ /** RFC 8414 – Authorization Server Metadata */
104
+ app.get('/.well-known/oauth-authorization-server', (_req, res) => {
105
+ res.json({
106
+ issuer: serverUrl,
107
+ authorization_endpoint: `${serverUrl}/authorize`,
108
+ token_endpoint: `${serverUrl}/token`,
109
+ registration_endpoint: `${serverUrl}/register`,
110
+ response_types_supported: ['code'],
111
+ grant_types_supported: ['authorization_code'],
112
+ code_challenge_methods_supported: ['S256'],
113
+ token_endpoint_auth_methods_supported: ['none'],
114
+ scopes_supported: ['mcp'],
115
+ });
116
+ });
117
+ /** RFC 7591 – Dynamic Client Registration */
118
+ app.post('/register', async (req, res) => {
119
+ const body = req.body;
120
+ const redirectUris = body.redirect_uris;
121
+ const clientName = typeof body.client_name === 'string' ? body.client_name : undefined;
122
+ if (!Array.isArray(redirectUris) || redirectUris.length === 0) {
123
+ res.status(400).json({
124
+ error: 'invalid_client_metadata',
125
+ error_description: 'redirect_uris is required and must be a non-empty array',
126
+ });
127
+ return;
128
+ }
129
+ const uris = redirectUris;
130
+ const client = await authStore.registerClient(uris, clientName);
131
+ res.status(201).json({
132
+ client_id: client.clientId,
133
+ redirect_uris: client.redirectUris,
134
+ client_name: client.clientName,
135
+ client_id_issued_at: Math.floor(new Date(client.createdAt).getTime() / 1000),
136
+ token_endpoint_auth_method: 'none',
137
+ grant_types: ['authorization_code'],
138
+ response_types: ['code'],
139
+ });
140
+ });
141
+ /**
142
+ * RFC 6749 – Authorization Endpoint (PKCE required)
143
+ * Validates the MCP client, stores context in state, then forward to eBay OAuth.
144
+ */
145
+ app.get('/authorize', requireOauthStartKey, async (req, res) => {
146
+ try {
147
+ const q = req.query;
148
+ const { client_id, redirect_uri, response_type, state: mcpState, code_challenge, code_challenge_method } = q;
149
+ const environment = (q.env === 'sandbox' || q.env === 'production'
150
+ ? q.env
151
+ : getConfiguredEnvironment());
152
+ if (response_type !== 'code') {
153
+ res.status(400).json({ error: 'unsupported_response_type' });
154
+ return;
155
+ }
156
+ if (!client_id) {
157
+ res.status(400).json({ error: 'invalid_request', error_description: 'client_id is required' });
158
+ return;
159
+ }
160
+ const client = await authStore.getClient(client_id);
161
+ if (!client) {
162
+ res.status(400).json({ error: 'invalid_client', error_description: 'Unknown client_id' });
163
+ return;
164
+ }
165
+ if (!redirect_uri || !client.redirectUris.includes(redirect_uri)) {
166
+ res.status(400).json({ error: 'invalid_request', error_description: 'redirect_uri not registered for this client' });
167
+ return;
168
+ }
169
+ if (!code_challenge || code_challenge_method !== 'S256') {
170
+ res.status(400).json({ error: 'invalid_request', error_description: 'PKCE with S256 code_challenge is required' });
171
+ return;
172
+ }
173
+ const ebayConfig = getEbayConfig(environment);
174
+ if (!ebayConfig.clientId || !ebayConfig.clientSecret || !ebayConfig.redirectUri) {
175
+ res.status(500).json({ error: 'server_error', error_description: `Missing eBay configuration for ${environment}` });
176
+ return;
177
+ }
178
+ const stateRecord = await authStore.createOAuthState(environment, undefined, {
179
+ mcpClientId: client_id,
180
+ mcpRedirectUri: redirect_uri,
181
+ mcpState,
182
+ mcpCodeChallenge: code_challenge,
183
+ mcpCodeChallengeMethod: code_challenge_method,
184
+ });
185
+ const oauthUrl = getOAuthAuthorizationUrl(ebayConfig.clientId, ebayConfig.redirectUri, environment, getHostedOauthScopes(environment), undefined, stateRecord.state);
186
+ res.redirect(oauthUrl);
187
+ }
188
+ catch (error) {
189
+ res.status(500).json({ error: 'server_error', error_description: error instanceof Error ? error.message : String(error) });
190
+ }
191
+ });
192
+ /**
193
+ * RFC 6749 – Token Endpoint
194
+ * Exchanges a short-lived MCP authorization code (+ PKCE verifier) for a session token.
195
+ */
196
+ app.post('/token', async (req, res) => {
197
+ // Body may be form-encoded (RFC 6749 §4.1.3) or JSON — both are parsed by middleware.
198
+ // Guard against unparsed bodies (missing Content-Type header etc.)
199
+ if (!req.body || typeof req.body !== 'object') {
200
+ res.status(400).json({
201
+ error: 'invalid_request',
202
+ error_description: 'Request body is missing or unparseable. Use application/x-www-form-urlencoded or application/json.',
203
+ });
204
+ return;
205
+ }
206
+ const body = req.body;
207
+ const { grant_type, code, redirect_uri, client_id, code_verifier } = body;
208
+ if (grant_type !== 'authorization_code') {
209
+ res.status(400).json({ error: 'unsupported_grant_type' });
210
+ return;
211
+ }
212
+ if (!code) {
213
+ res.status(400).json({ error: 'invalid_request', error_description: 'code is required' });
214
+ return;
215
+ }
216
+ const authCode = await authStore.consumeAuthCode(code);
217
+ if (!authCode) {
218
+ res.status(400).json({ error: 'invalid_grant', error_description: 'Invalid or expired authorization code' });
219
+ return;
220
+ }
221
+ if (authCode.clientId !== client_id) {
222
+ res.status(400).json({ error: 'invalid_client', error_description: 'client_id mismatch' });
223
+ return;
224
+ }
225
+ if (authCode.redirectUri !== redirect_uri) {
226
+ res.status(400).json({ error: 'invalid_grant', error_description: 'redirect_uri mismatch' });
227
+ return;
228
+ }
229
+ if (!code_verifier) {
230
+ res.status(400).json({ error: 'invalid_request', error_description: 'code_verifier is required' });
231
+ return;
232
+ }
233
+ // Verify PKCE S256: BASE64URL(SHA256(code_verifier)) must equal code_challenge
234
+ const expectedChallenge = createHash('sha256').update(code_verifier).digest('base64url');
235
+ if (expectedChallenge !== authCode.codeChallenge) {
236
+ res.status(400).json({ error: 'invalid_grant', error_description: 'PKCE verification failed' });
237
+ return;
238
+ }
239
+ const session = await authStore.createSession(authCode.userId, authCode.environment);
240
+ res.json({
241
+ access_token: session.sessionToken,
242
+ token_type: 'bearer',
243
+ scope: 'mcp',
244
+ });
245
+ });
246
+ // ── End MCP OAuth 2.1 endpoints ───────────────────────────────────────────
247
+ app.get('/oauth/start', requireOauthStartKey, async (req, res) => {
248
+ try {
249
+ const environment = ((typeof req.query.env === 'string' ? req.query.env : undefined) ||
250
+ getConfiguredEnvironment());
251
+ const returnTo = typeof req.query.returnTo === 'string' ? req.query.returnTo : undefined;
252
+ const ebayConfig = getEbayConfig(environment);
253
+ if (!ebayConfig.clientId || !ebayConfig.clientSecret || !ebayConfig.redirectUri) {
254
+ res.status(500).json({ error: `Missing eBay configuration for ${environment}` });
255
+ return;
256
+ }
257
+ const stateRecord = await authStore.createOAuthState(environment, returnTo);
258
+ const oauthUrl = getOAuthAuthorizationUrl(ebayConfig.clientId, ebayConfig.redirectUri, environment, getHostedOauthScopes(environment), undefined, stateRecord.state);
259
+ res.redirect(oauthUrl);
260
+ }
261
+ catch (error) {
262
+ res.status(400).json({ error: error instanceof Error ? error.message : String(error) });
263
+ }
264
+ });
265
+ app.get('/oauth/callback', async (req, res) => {
266
+ try {
267
+ const code = typeof req.query.code === 'string' ? req.query.code : undefined;
268
+ const state = typeof req.query.state === 'string' ? req.query.state : undefined;
269
+ const envFromQuery = typeof req.query.env === 'string' ? req.query.env : undefined;
270
+ const oauthError = typeof req.query.error === 'string' ? req.query.error : undefined;
271
+ const errorDescription = typeof req.query.error_description === 'string' ? req.query.error_description : undefined;
272
+ if (oauthError) {
273
+ res
274
+ .status(400)
275
+ .send(`<h1>OAuth failed</h1><p>${htmlEscape(errorDescription || oauthError)}</p>`);
276
+ return;
277
+ }
278
+ if (!code) {
279
+ res.status(400).send('<h1>Missing authorization code</h1>');
280
+ return;
281
+ }
282
+ let environment;
283
+ let stateRecord = null;
284
+ if (state) {
285
+ stateRecord = await authStore.consumeOAuthState(state);
286
+ if (!stateRecord) {
287
+ res.status(400).send('<h1>Invalid or expired OAuth state</h1>');
288
+ return;
289
+ }
290
+ environment = stateRecord.environment;
291
+ }
292
+ else {
293
+ environment =
294
+ envFromQuery === 'sandbox' || envFromQuery === 'production'
295
+ ? envFromQuery
296
+ : getConfiguredEnvironment();
297
+ serverLogger.warn('OAuth callback received without state; falling back to configured/query environment', { environment });
298
+ }
299
+ const userId = randomUUID();
300
+ const api = await createUserScopedApi(userId, environment);
301
+ const oauthClient = api.getAuthClient().getOAuthClient();
302
+ const tokenData = await oauthClient.exchangeCodeForToken(code);
303
+ // ── MCP OAuth flow: redirect back to the registered MCP client ─────────
304
+ if (stateRecord?.mcpClientId && stateRecord.mcpRedirectUri && stateRecord.mcpCodeChallenge) {
305
+ const authCodeRecord = await authStore.createAuthCode(stateRecord.mcpClientId, stateRecord.mcpRedirectUri, stateRecord.mcpCodeChallenge, stateRecord.mcpCodeChallengeMethod ?? 'S256', userId, environment);
306
+ const redirectUrl = new URL(stateRecord.mcpRedirectUri);
307
+ redirectUrl.searchParams.set('code', authCodeRecord.code);
308
+ if (stateRecord.mcpState) {
309
+ redirectUrl.searchParams.set('state', stateRecord.mcpState);
310
+ }
311
+ serverLogger.info('MCP OAuth flow complete, redirecting to client', {
312
+ clientId: stateRecord.mcpClientId,
313
+ redirectUri: stateRecord.mcpRedirectUri,
314
+ userId,
315
+ });
316
+ res.redirect(redirectUrl.toString());
317
+ return;
318
+ }
319
+ // ── End MCP OAuth flow ─────────────────────────────────────────────────
320
+ const session = await authStore.createSession(userId, environment);
321
+ res.status(200).send(`<!doctype html>
322
+ <html>
323
+ <head>
324
+ <meta charset="utf-8">
325
+ <title>eBay MCP Connected</title>
326
+ <style>
327
+ body { font-family: Inter, Arial, sans-serif; max-width: 760px; margin: 40px auto; line-height: 1.5; padding: 0 16px; }
328
+ .card { background: #f7f7f8; padding: 16px; border-radius: 10px; margin: 16px 0; }
329
+ pre { white-space: pre-wrap; word-break: break-all; background: #fff; padding: 14px; border-radius: 8px; border: 1px solid #e5e7eb; }
330
+ .copy-btn { background: #111827; color: white; border: none; border-radius: 8px; padding: 10px 14px; cursor: pointer; transition: transform 120ms ease, opacity 120ms ease, background 120ms ease; }
331
+ .copy-btn:hover { background: #1f2937; }
332
+ .copy-btn:active { transform: scale(0.97); opacity: 0.92; }
333
+ .copy-status { margin-left: 12px; color: #065f46; font-weight: 600; }
334
+ .muted { color: #6b7280; }
335
+ a { color: #2563eb; }
336
+ </style>
337
+ <script>
338
+ async function copySessionToken() {
339
+ const token = document.getElementById('session-token').innerText;
340
+ const status = document.getElementById('copy-status');
341
+ try {
342
+ if (navigator.clipboard && window.isSecureContext) {
343
+ await navigator.clipboard.writeText(token);
344
+ } else {
345
+ const temp = document.createElement('textarea');
346
+ temp.value = token;
347
+ temp.setAttribute('readonly', '');
348
+ temp.style.position = 'absolute';
349
+ temp.style.left = '-9999px';
350
+ document.body.appendChild(temp);
351
+ temp.select();
352
+ document.execCommand('copy');
353
+ document.body.removeChild(temp);
354
+ }
355
+ status.textContent = 'Copied!';
356
+ setTimeout(() => { status.textContent = ''; }, 1800);
357
+ } catch (err) {
358
+ status.textContent = 'Copy failed — select manually';
359
+ setTimeout(() => { status.textContent = ''; }, 2500);
360
+ }
361
+ }
362
+ </script>
363
+ </head>
364
+ <body>
365
+ <h1>eBay account connected</h1>
366
+ <p>Your <strong>${htmlEscape(environment)}</strong> account has been connected successfully.</p>
367
+ <div class="card">
368
+ <p><strong>User ID:</strong> <code>${htmlEscape(userId)}</code></p>
369
+ <p><strong>Paste this session token into Make or TypingMind as your API Key / Access token.</strong></p>
370
+ <pre id="session-token">${htmlEscape(session.sessionToken)}</pre>
371
+ <button class="copy-btn" onclick="copySessionToken()">Copy session token</button><span id="copy-status" class="copy-status"></span>
372
+ </div>
373
+ <div class="card">
374
+ <p><strong>Authorization header format</strong></p>
375
+ <pre>Authorization: Bearer ${htmlEscape(session.sessionToken)}</pre>
376
+ </div>
377
+ <p><strong>Scopes granted:</strong> ${htmlEscape(tokenData.scope || 'Not returned by eBay in token response')} — <a href="https://developer.ebay.com/my/keys" target="_blank" rel="noopener noreferrer">See full account scope list on the developer platform</a>.</p>
378
+ <p class="muted">Keep this token private. If it is exposed, revoke it using the admin session endpoints and create a new one.</p>
379
+ </body>
380
+ </html>`);
381
+ }
382
+ catch (error) {
383
+ res
384
+ .status(500)
385
+ .send(`<h1>OAuth callback failed</h1><pre>${htmlEscape(error instanceof Error ? error.message : String(error))}</pre>`);
386
+ }
387
+ });
388
+ app.get('/admin/session/:sessionToken', requireAdmin, async (req, res) => {
389
+ const session = await authStore.getSession(req.params.sessionToken);
390
+ if (!session) {
391
+ res.status(404).json({ error: 'not_found' });
392
+ return;
393
+ }
394
+ res.json(session);
395
+ });
396
+ app.get('/whoami', async (req, res) => {
397
+ const authHeader = req.headers.authorization;
398
+ if (!authHeader?.startsWith('Bearer ')) {
399
+ res.status(401).json({ error: 'missing_session_token' });
400
+ return;
401
+ }
402
+ const sessionToken = authHeader.slice('Bearer '.length).trim();
403
+ const session = await authStore.getSession(sessionToken);
404
+ if (!session || session.revokedAt) {
405
+ res.status(401).json({ error: 'invalid_session_token' });
406
+ return;
407
+ }
408
+ res.json({
409
+ userId: session.userId,
410
+ environment: session.environment,
411
+ createdAt: session.createdAt,
412
+ lastUsedAt: session.lastUsedAt,
413
+ revokedAt: session.revokedAt ?? null,
414
+ });
415
+ });
416
+ app.post('/admin/session/:sessionToken/revoke', requireAdmin, async (req, res) => {
417
+ await authStore.revokeSession(req.params.sessionToken);
418
+ res.json({ ok: true, revoked: true });
419
+ });
420
+ app.delete('/admin/session/:sessionToken', requireAdmin, async (req, res) => {
421
+ await authStore.deleteSession(req.params.sessionToken);
422
+ res.json({ ok: true, deleted: true });
423
+ });
424
+ const transports = new Map();
425
+ const authenticateSession = async (req, res, next) => {
426
+ const authHeader = req.headers.authorization;
427
+ const requestedEnv = ((typeof req.query.env === 'string' ? req.query.env : undefined) ||
428
+ (typeof req.headers['x-ebay-env'] === 'string' ? req.headers['x-ebay-env'] : undefined) ||
429
+ getConfiguredEnvironment());
430
+ const sendAuthorizationRequired = (reason) => {
431
+ const oauthUrl = new URL(`${getServerBaseUrl()}/oauth/start`);
432
+ oauthUrl.searchParams.set('env', requestedEnv);
433
+ if (CONFIG.oauthStartKey) {
434
+ oauthUrl.searchParams.set('key', CONFIG.oauthStartKey);
435
+ }
436
+ if (req.method === 'GET') {
437
+ res.redirect(oauthUrl.toString());
438
+ return;
439
+ }
440
+ res.status(401).json({
441
+ error: reason,
442
+ authorization_required: true,
443
+ environment: requestedEnv,
444
+ authorization_url: oauthUrl.toString(),
445
+ message: 'No valid hosted session token was provided. Complete the browser OAuth flow using authorization_url, then retry with Authorization: Bearer <session-token>.',
446
+ });
447
+ };
448
+ if (!authHeader?.startsWith('Bearer ')) {
449
+ sendAuthorizationRequired('missing_session_token');
450
+ return;
451
+ }
452
+ const sessionToken = authHeader.slice('Bearer '.length).trim();
453
+ const session = await authStore.getSession(sessionToken);
454
+ if (!session || session.revokedAt) {
455
+ sendAuthorizationRequired('invalid_session_token');
456
+ return;
457
+ }
458
+ await authStore.touchSession(sessionToken);
459
+ req.userContext = {
460
+ userId: session.userId,
461
+ environment: session.environment,
462
+ sessionToken,
463
+ };
464
+ next();
465
+ };
466
+ async function createMcpServer(userId, environment) {
467
+ const api = await createUserScopedApi(userId, environment);
468
+ const server = new McpServer({
469
+ name: 'ebay-mcp-remote-edition',
470
+ version: getVersion(),
471
+ title: 'eBay API MCP Server',
472
+ websiteUrl: 'https://github.com/mrnajiboy/ebay-mcp-remote-edition',
473
+ icons: [
474
+ { src: `${iconBaseUrl}/16x16.png`, mimeType: 'image/png', sizes: ['16x16'] },
475
+ { src: `${iconBaseUrl}/32x32.png`, mimeType: 'image/png', sizes: ['32x32'] },
476
+ { src: `${iconBaseUrl}/48x48.png`, mimeType: 'image/png', sizes: ['48x48'] },
477
+ ],
478
+ });
479
+ const tools = getToolDefinitions();
480
+ for (const toolDef of tools) {
481
+ server.registerTool(toolDef.name, { description: toolDef.description, inputSchema: toolDef.inputSchema }, async (args) => {
482
+ try {
483
+ const result = await executeTool(api, toolDef.name, args);
484
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
485
+ }
486
+ catch (error) {
487
+ return {
488
+ content: [
489
+ {
490
+ type: 'text',
491
+ text: JSON.stringify({ error: error instanceof Error ? error.message : String(error) }, null, 2),
492
+ },
493
+ ],
494
+ isError: true,
495
+ };
496
+ }
497
+ });
498
+ }
499
+ return server;
500
+ }
501
+ const mcpPostHandler = async (req, res) => {
502
+ const userContext = req.userContext;
503
+ if (!userContext) {
504
+ res.status(401).json({ error: 'missing_user_context' });
505
+ return;
506
+ }
507
+ const sessionId = req.headers['mcp-session-id'];
508
+ let transport;
509
+ if (sessionId && transports.has(sessionId)) {
510
+ transport = transports.get(sessionId);
511
+ }
512
+ else if (!sessionId && isInitializeRequest(req.body)) {
513
+ transport = new StreamableHTTPServerTransport({
514
+ sessionIdGenerator: () => randomUUID(),
515
+ onsessioninitialized: (newSessionId) => {
516
+ transports.set(newSessionId, transport);
517
+ serverLogger.info('New MCP session initialized', {
518
+ sessionId: newSessionId,
519
+ userId: userContext.userId,
520
+ });
521
+ },
522
+ });
523
+ transport.onclose = () => {
524
+ if (transport.sessionId) {
525
+ transports.delete(transport.sessionId);
526
+ }
527
+ };
528
+ const server = await createMcpServer(userContext.userId, userContext.environment);
529
+ await server.connect(transport);
530
+ }
531
+ else {
532
+ res.status(400).json({
533
+ jsonrpc: '2.0',
534
+ error: { code: -32000, message: 'Bad Request: No valid session ID provided' },
535
+ id: null,
536
+ });
537
+ return;
538
+ }
539
+ await transport.handleRequest(req, res, req.body);
540
+ };
541
+ const handleSessionRequest = async (req, res) => {
542
+ const sessionId = req.headers['mcp-session-id'];
543
+ if (!sessionId || !transports.has(sessionId)) {
544
+ res
545
+ .status(400)
546
+ .json({ error: 'invalid_session', error_description: 'Invalid or missing session ID' });
547
+ return;
548
+ }
549
+ const transport = transports.get(sessionId);
550
+ await transport.handleRequest(req, res);
551
+ };
552
+ app.post('/mcp', authenticateSession, mcpPostHandler);
553
+ app.get('/mcp', authenticateSession, handleSessionRequest);
554
+ app.delete('/mcp', authenticateSession, handleSessionRequest);
555
+ return app;
556
+ }
557
+ // eslint-disable-next-line @typescript-eslint/require-await
558
+ async function main() {
559
+ try {
560
+ const app = createApp();
561
+ const server = app.listen(CONFIG.port, CONFIG.host, () => {
562
+ const serverUrl = getServerBaseUrl();
563
+ console.log(`Server running at ${serverUrl}`);
564
+ console.log(`OAuth start: ${serverUrl}/oauth/start?env=production`);
565
+ console.log(`OAuth start sandbox: ${serverUrl}/oauth/start?env=sandbox`);
566
+ console.log(`MCP endpoint: ${serverUrl}/mcp`);
567
+ });
568
+ process.on('SIGINT', () => {
569
+ server.close(() => process.exit(0));
570
+ });
571
+ }
572
+ catch (error) {
573
+ console.error('Fatal error starting server:', error);
574
+ process.exit(1);
575
+ }
576
+ }
577
+ const entryPath = process.argv[1] ? resolve(process.argv[1]) : undefined;
578
+ const modulePath = resolve(fileURLToPath(import.meta.url));
579
+ if (entryPath && modulePath === entryPath) {
580
+ await main();
581
+ }