@open-loyalty/mcp-server 1.8.0 → 1.13.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 (74) hide show
  1. package/dist/config.d.ts +0 -9
  2. package/dist/config.js +0 -23
  3. package/dist/instructions.d.ts +1 -1
  4. package/dist/instructions.js +58 -22
  5. package/dist/tools/apps/rewards-catalog/handlers.js +2 -1
  6. package/dist/tools/campaign/index.d.ts +16 -3
  7. package/dist/tools/campaign/index.js +15 -4
  8. package/dist/tools/campaign/member-handlers.d.ts +12 -0
  9. package/dist/tools/campaign/member-handlers.js +33 -0
  10. package/dist/tools/campaign/schemas.d.ts +6 -0
  11. package/dist/tools/campaign/schemas.js +6 -0
  12. package/dist/tools/channel/handlers.d.ts +32 -0
  13. package/dist/tools/channel/handlers.js +130 -0
  14. package/dist/tools/channel/index.d.ts +68 -0
  15. package/dist/tools/channel/index.js +59 -0
  16. package/dist/tools/channel/schemas.d.ts +29 -0
  17. package/dist/tools/channel/schemas.js +30 -0
  18. package/dist/tools/context/handlers.d.ts +49 -0
  19. package/dist/tools/context/handlers.js +131 -0
  20. package/dist/tools/context/index.d.ts +15 -0
  21. package/dist/tools/context/index.js +20 -0
  22. package/dist/tools/context/schemas.d.ts +7 -0
  23. package/dist/tools/context/schemas.js +4 -0
  24. package/dist/tools/group-of-values/handlers.d.ts +39 -0
  25. package/dist/tools/group-of-values/handlers.js +133 -0
  26. package/dist/tools/group-of-values/index.d.ts +82 -0
  27. package/dist/tools/group-of-values/index.js +72 -0
  28. package/dist/tools/group-of-values/schemas.d.ts +36 -0
  29. package/dist/tools/group-of-values/schemas.js +39 -0
  30. package/dist/tools/index.js +12 -0
  31. package/dist/tools/language/handlers.d.ts +24 -0
  32. package/dist/tools/language/handlers.js +127 -0
  33. package/dist/tools/language/index.d.ts +64 -0
  34. package/dist/tools/language/index.js +60 -0
  35. package/dist/tools/language/schemas.d.ts +25 -0
  36. package/dist/tools/language/schemas.js +25 -0
  37. package/dist/tools/member/handlers.d.ts +4 -0
  38. package/dist/tools/member/handlers.js +27 -0
  39. package/dist/tools/member/index.d.ts +14 -2
  40. package/dist/tools/member/index.js +15 -2
  41. package/dist/tools/points/fraud-handlers.d.ts +21 -0
  42. package/dist/tools/points/fraud-handlers.js +96 -0
  43. package/dist/tools/points/index.d.ts +50 -1
  44. package/dist/tools/points/index.js +45 -2
  45. package/dist/tools/points/schemas.d.ts +11 -0
  46. package/dist/tools/points/schemas.js +11 -0
  47. package/dist/tools/reward/category-handlers.d.ts +27 -0
  48. package/dist/tools/reward/category-handlers.js +70 -0
  49. package/dist/tools/reward/handlers.d.ts +0 -12
  50. package/dist/tools/reward/handlers.js +0 -28
  51. package/dist/tools/reward/index.d.ts +76 -3
  52. package/dist/tools/reward/index.js +63 -4
  53. package/dist/tools/reward/photo-handlers.d.ts +10 -0
  54. package/dist/tools/reward/photo-handlers.js +97 -0
  55. package/dist/tools/reward/redemption-handlers.d.ts +23 -0
  56. package/dist/tools/reward/redemption-handlers.js +50 -0
  57. package/dist/tools/reward/schemas.d.ts +31 -0
  58. package/dist/tools/reward/schemas.js +33 -0
  59. package/dist/tools/segment/handlers.js +14 -10
  60. package/dist/tools/segment/index.js +1 -1
  61. package/dist/tools/segment/schemas.js +3 -3
  62. package/dist/tools/store/handlers.d.ts +24 -0
  63. package/dist/tools/store/handlers.js +29 -1
  64. package/dist/tools/store/index.d.ts +41 -3
  65. package/dist/tools/store/index.js +27 -4
  66. package/dist/tools/store/schemas.d.ts +24 -0
  67. package/dist/tools/store/schemas.js +24 -0
  68. package/package.json +2 -12
  69. package/dist/auth/provider.d.ts +0 -33
  70. package/dist/auth/provider.js +0 -383
  71. package/dist/auth/storage.d.ts +0 -16
  72. package/dist/auth/storage.js +0 -120
  73. package/dist/http.d.ts +0 -2
  74. package/dist/http.js +0 -319
@@ -1,383 +0,0 @@
1
- import crypto from "crypto";
2
- import { getStorage, KEYS } from "./storage.js";
3
- // Expiration times
4
- const AUTH_CODE_TTL_MS = 10 * 60 * 1000; // 10 minutes
5
- const ACCESS_TOKEN_TTL_MS = 60 * 60 * 1000; // 1 hour
6
- const CLIENT_TTL_MS = 365 * 24 * 60 * 60 * 1000; // 1 year
7
- const CONFIG_TTL_MS = 365 * 24 * 60 * 60 * 1000; // 1 year
8
- /**
9
- * Storage-backed OAuth clients store
10
- */
11
- class StorageClientsStore {
12
- async getClient(clientId) {
13
- const storage = getStorage();
14
- const client = await storage.get(KEYS.client(clientId));
15
- return client ?? undefined;
16
- }
17
- async registerClient(client) {
18
- const storage = getStorage();
19
- const clientId = crypto.randomBytes(16).toString("hex");
20
- const clientIdIssuedAt = Math.floor(Date.now() / 1000);
21
- const fullClient = {
22
- ...client,
23
- client_id: clientId,
24
- client_id_issued_at: clientIdIssuedAt,
25
- };
26
- await storage.set(KEYS.client(clientId), fullClient, CLIENT_TTL_MS);
27
- return fullClient;
28
- }
29
- }
30
- /**
31
- * Creates the OAuth server provider
32
- */
33
- export function createOAuthProvider(issuerUrl) {
34
- const clientsStore = new StorageClientsStore();
35
- return {
36
- get clientsStore() {
37
- return clientsStore;
38
- },
39
- /**
40
- * Handles authorization by showing a configuration form
41
- */
42
- async authorize(client, params, res) {
43
- const storage = getStorage();
44
- // Generate session ID to track this authorization flow
45
- const sessionId = crypto.randomBytes(16).toString("hex");
46
- // Store the pending authorization
47
- const sessionData = {
48
- clientId: client.client_id,
49
- redirectUri: params.redirectUri,
50
- codeChallenge: params.codeChallenge,
51
- state: params.state,
52
- scope: params.scopes?.join(" "),
53
- expiresAt: Date.now() + AUTH_CODE_TTL_MS,
54
- };
55
- await storage.set(KEYS.session(sessionId), sessionData, AUTH_CODE_TTL_MS);
56
- // Render the configuration form
57
- const html = renderAuthorizationForm({
58
- sessionId,
59
- state: params.state,
60
- clientName: client.client_name || "ChatGPT",
61
- issuerUrl,
62
- });
63
- res.setHeader("Content-Type", "text/html");
64
- res.send(html);
65
- },
66
- /**
67
- * Returns the code challenge for a given authorization code
68
- */
69
- async challengeForAuthorizationCode(_client, authorizationCode) {
70
- const storage = getStorage();
71
- const codeData = await storage.get(KEYS.authCode(authorizationCode));
72
- if (!codeData || codeData.expiresAt < Date.now()) {
73
- await storage.delete(KEYS.authCode(authorizationCode));
74
- throw new Error("Authorization code not found or expired");
75
- }
76
- return codeData.codeChallenge;
77
- },
78
- /**
79
- * Exchanges authorization code for tokens
80
- */
81
- async exchangeAuthorizationCode(client, authorizationCode) {
82
- const storage = getStorage();
83
- const codeData = await storage.get(KEYS.authCode(authorizationCode));
84
- if (!codeData || codeData.expiresAt < Date.now()) {
85
- await storage.delete(KEYS.authCode(authorizationCode));
86
- throw new Error("Authorization code not found or expired");
87
- }
88
- if (codeData.clientId !== client.client_id) {
89
- throw new Error("Authorization code was not issued to this client");
90
- }
91
- // Delete the code (one-time use)
92
- await storage.delete(KEYS.authCode(authorizationCode));
93
- // Store the client config if provided
94
- if (codeData.pendingConfig) {
95
- await storage.set(KEYS.config(client.client_id), codeData.pendingConfig, CONFIG_TTL_MS);
96
- }
97
- // Generate access token
98
- const accessToken = crypto.randomBytes(32).toString("hex");
99
- const expiresAt = Date.now() + ACCESS_TOKEN_TTL_MS;
100
- const tokenData = {
101
- clientId: client.client_id,
102
- scope: codeData.scope,
103
- expiresAt,
104
- };
105
- await storage.set(KEYS.token(accessToken), tokenData, ACCESS_TOKEN_TTL_MS);
106
- return {
107
- access_token: accessToken,
108
- token_type: "Bearer",
109
- expires_in: Math.floor(ACCESS_TOKEN_TTL_MS / 1000),
110
- scope: codeData.scope,
111
- };
112
- },
113
- /**
114
- * Exchanges refresh token (not supported)
115
- */
116
- async exchangeRefreshToken() {
117
- throw new Error("Refresh tokens are not supported");
118
- },
119
- /**
120
- * Verifies an access token
121
- */
122
- async verifyAccessToken(token) {
123
- const storage = getStorage();
124
- const tokenData = await storage.get(KEYS.token(token));
125
- if (!tokenData) {
126
- throw new Error("Invalid access token");
127
- }
128
- if (tokenData.expiresAt < Date.now()) {
129
- await storage.delete(KEYS.token(token));
130
- throw new Error("Access token has expired");
131
- }
132
- return {
133
- token,
134
- clientId: tokenData.clientId,
135
- scopes: tokenData.scope ? tokenData.scope.split(" ") : [],
136
- expiresAt: Math.floor(tokenData.expiresAt / 1000),
137
- };
138
- },
139
- };
140
- }
141
- /**
142
- * Completes authorization after form submission
143
- */
144
- export async function completeAuthorization(sessionId, config) {
145
- const storage = getStorage();
146
- const sessionData = await storage.get(KEYS.session(sessionId));
147
- if (!sessionData || sessionData.expiresAt < Date.now()) {
148
- await storage.delete(KEYS.session(sessionId));
149
- return { error: "Session expired. Please start the authorization process again." };
150
- }
151
- // Delete session
152
- await storage.delete(KEYS.session(sessionId));
153
- // Generate authorization code
154
- const authorizationCode = crypto.randomBytes(32).toString("hex");
155
- // Store with pending config
156
- const codeData = {
157
- ...sessionData,
158
- pendingConfig: config,
159
- expiresAt: Date.now() + AUTH_CODE_TTL_MS,
160
- };
161
- await storage.set(KEYS.authCode(authorizationCode), codeData, AUTH_CODE_TTL_MS);
162
- // Build redirect URL
163
- const redirectUrl = new URL(sessionData.redirectUri);
164
- redirectUrl.searchParams.set("code", authorizationCode);
165
- if (sessionData.state) {
166
- redirectUrl.searchParams.set("state", sessionData.state);
167
- }
168
- return { redirectUrl: redirectUrl.toString() };
169
- }
170
- /**
171
- * Gets the Open Loyalty config for a client
172
- */
173
- export async function getClientConfig(clientId) {
174
- const storage = getStorage();
175
- const config = await storage.get(KEYS.config(clientId));
176
- return config ?? undefined;
177
- }
178
- /**
179
- * Validates Open Loyalty credentials
180
- * Uses the member list endpoint to validate both API token and store code
181
- */
182
- export async function validateOpenLoyaltyCredentials(config) {
183
- try {
184
- // Use member list endpoint with limit=1 to validate credentials
185
- // This validates both the API token and the store code existence
186
- const response = await fetch(`${config.apiUrl}/${config.storeCode}/member?_itemsOnPage=1`, {
187
- method: "GET",
188
- headers: {
189
- "Content-Type": "application/json",
190
- "X-AUTH-TOKEN": config.apiToken,
191
- },
192
- });
193
- if (response.status === 401) {
194
- return { valid: false, error: "Invalid API token" };
195
- }
196
- if (response.status === 403) {
197
- return { valid: false, error: "API token does not have required permissions" };
198
- }
199
- if (response.status === 404) {
200
- return { valid: false, error: "Store code not found or invalid API URL" };
201
- }
202
- if (!response.ok) {
203
- return { valid: false, error: `API returned status ${response.status}` };
204
- }
205
- return { valid: true };
206
- }
207
- catch (error) {
208
- const message = error instanceof Error ? error.message : "Unknown error";
209
- return { valid: false, error: `Failed to connect: ${message}` };
210
- }
211
- }
212
- /**
213
- * Renders the authorization form HTML
214
- */
215
- function renderAuthorizationForm(params) {
216
- const { sessionId, state, clientName, issuerUrl } = params;
217
- return `<!DOCTYPE html>
218
- <html lang="en">
219
- <head>
220
- <meta charset="UTF-8">
221
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
222
- <title>Connect to Open Loyalty</title>
223
- <style>
224
- * { box-sizing: border-box; }
225
- body {
226
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
227
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
228
- min-height: 100vh;
229
- margin: 0;
230
- padding: 20px;
231
- display: flex;
232
- justify-content: center;
233
- align-items: center;
234
- }
235
- .container {
236
- background: white;
237
- border-radius: 16px;
238
- box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
239
- padding: 40px;
240
- width: 100%;
241
- max-width: 420px;
242
- }
243
- h1 {
244
- color: #1a1a2e;
245
- font-size: 24px;
246
- text-align: center;
247
- margin: 0 0 8px 0;
248
- }
249
- .subtitle {
250
- color: #6b7280;
251
- text-align: center;
252
- font-size: 14px;
253
- margin-bottom: 32px;
254
- }
255
- .client-name { color: #667eea; font-weight: 500; }
256
- .form-group { margin-bottom: 20px; }
257
- label {
258
- display: block;
259
- color: #374151;
260
- font-size: 14px;
261
- font-weight: 500;
262
- margin-bottom: 6px;
263
- }
264
- input {
265
- width: 100%;
266
- padding: 12px 16px;
267
- border: 2px solid #e5e7eb;
268
- border-radius: 8px;
269
- font-size: 14px;
270
- }
271
- input:focus {
272
- outline: none;
273
- border-color: #667eea;
274
- }
275
- .help-text { color: #6b7280; font-size: 12px; margin-top: 4px; }
276
- button {
277
- width: 100%;
278
- padding: 14px 24px;
279
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
280
- color: white;
281
- border: none;
282
- border-radius: 8px;
283
- font-size: 16px;
284
- font-weight: 600;
285
- cursor: pointer;
286
- }
287
- button:hover { opacity: 0.9; }
288
- button:disabled { opacity: 0.7; cursor: not-allowed; }
289
- .error { background: #fef2f2; border: 1px solid #fecaca; color: #dc2626; padding: 12px; border-radius: 8px; margin-bottom: 20px; display: none; }
290
- .error.visible { display: block; }
291
- </style>
292
- </head>
293
- <body>
294
- <div class="container">
295
- <h1>Connect to Open Loyalty</h1>
296
- <p class="subtitle">
297
- <span class="client-name">${escapeHtml(clientName)}</span> wants to access your Open Loyalty account
298
- </p>
299
-
300
- <div id="error" class="error"></div>
301
-
302
- <form id="authForm">
303
- <input type="hidden" name="session_id" value="${escapeHtml(sessionId)}">
304
- ${state ? `<input type="hidden" name="state" value="${escapeHtml(state)}">` : ""}
305
-
306
- <div class="form-group">
307
- <label for="apiUrl">API URL</label>
308
- <input type="url" id="apiUrl" name="api_url" placeholder="https://api.openloyalty.io" required>
309
- <p class="help-text">Your Open Loyalty API endpoint</p>
310
- </div>
311
-
312
- <div class="form-group">
313
- <label for="apiToken">API Token</label>
314
- <input type="password" id="apiToken" name="api_token" required>
315
- <p class="help-text">From your Open Loyalty admin panel</p>
316
- </div>
317
-
318
- <div class="form-group">
319
- <label for="storeCode">Store Code</label>
320
- <input type="text" id="storeCode" name="store_code" value="default" required>
321
- <p class="help-text">Usually "default"</p>
322
- </div>
323
-
324
- <button type="submit" id="submitBtn">Connect Account</button>
325
- </form>
326
- </div>
327
-
328
- <script>
329
- const form = document.getElementById('authForm');
330
- const errorEl = document.getElementById('error');
331
- const submitBtn = document.getElementById('submitBtn');
332
-
333
- form.addEventListener('submit', async (e) => {
334
- e.preventDefault();
335
- errorEl.classList.remove('visible');
336
- submitBtn.disabled = true;
337
- submitBtn.textContent = 'Connecting...';
338
-
339
- try {
340
- const formData = new FormData(form);
341
- const response = await fetch('${issuerUrl}/authorize/submit', {
342
- method: 'POST',
343
- headers: { 'Content-Type': 'application/json' },
344
- body: JSON.stringify({
345
- session_id: formData.get('session_id'),
346
- state: formData.get('state'),
347
- api_url: formData.get('api_url'),
348
- api_token: formData.get('api_token'),
349
- store_code: formData.get('store_code'),
350
- }),
351
- });
352
-
353
- const result = await response.json();
354
-
355
- if (result.redirect_url) {
356
- window.location.href = result.redirect_url;
357
- } else if (result.error) {
358
- errorEl.textContent = result.error;
359
- errorEl.classList.add('visible');
360
- submitBtn.disabled = false;
361
- submitBtn.textContent = 'Connect Account';
362
- }
363
- } catch (err) {
364
- errorEl.textContent = 'Connection failed. Please try again.';
365
- errorEl.classList.add('visible');
366
- submitBtn.disabled = false;
367
- submitBtn.textContent = 'Connect Account';
368
- }
369
- });
370
- </script>
371
- </body>
372
- </html>`;
373
- }
374
- function escapeHtml(text) {
375
- const escapes = {
376
- "&": "&amp;",
377
- "<": "&lt;",
378
- ">": "&gt;",
379
- '"': "&quot;",
380
- "'": "&#39;",
381
- };
382
- return text.replace(/[&<>"']/g, (c) => escapes[c]);
383
- }
@@ -1,16 +0,0 @@
1
- export interface StorageBackend {
2
- get<T>(key: string): Promise<T | null>;
3
- set<T>(key: string, value: T, ttlMs?: number): Promise<void>;
4
- delete(key: string): Promise<void>;
5
- }
6
- /**
7
- * Get the storage backend (Redis if available, otherwise in-memory)
8
- */
9
- export declare function getStorage(): StorageBackend;
10
- export declare const KEYS: {
11
- client: (id: string) => string;
12
- authCode: (code: string) => string;
13
- session: (id: string) => string;
14
- token: (token: string) => string;
15
- config: (clientId: string) => string;
16
- };
@@ -1,120 +0,0 @@
1
- /**
2
- * Storage abstraction for OAuth data
3
- * Uses Redis if REDIS_URL is set, otherwise falls back to in-memory storage
4
- */
5
- import { Redis } from "ioredis";
6
- /**
7
- * In-memory storage for local development
8
- * Includes periodic cleanup to prevent memory leaks from expired entries
9
- */
10
- class InMemoryStorage {
11
- data = new Map();
12
- cleanupInterval;
13
- constructor() {
14
- // Periodic cleanup every 5 minutes to remove expired entries
15
- this.cleanupInterval = setInterval(() => this.cleanup(), 5 * 60 * 1000);
16
- this.cleanupInterval.unref(); // Don't prevent process exit
17
- }
18
- cleanup() {
19
- const now = Date.now();
20
- for (const [key, entry] of this.data) {
21
- if (entry.expiresAt && entry.expiresAt < now) {
22
- this.data.delete(key);
23
- }
24
- }
25
- }
26
- async get(key) {
27
- const entry = this.data.get(key);
28
- if (!entry)
29
- return null;
30
- if (entry.expiresAt && entry.expiresAt < Date.now()) {
31
- this.data.delete(key);
32
- return null;
33
- }
34
- return entry.value;
35
- }
36
- async set(key, value, ttlMs) {
37
- this.data.set(key, {
38
- value,
39
- expiresAt: ttlMs ? Date.now() + ttlMs : undefined,
40
- });
41
- }
42
- async delete(key) {
43
- this.data.delete(key);
44
- }
45
- /**
46
- * Close the storage and clean up resources
47
- */
48
- close() {
49
- clearInterval(this.cleanupInterval);
50
- this.data.clear();
51
- }
52
- }
53
- /**
54
- * Redis storage for production
55
- */
56
- class RedisStorage {
57
- client;
58
- constructor(redisUrl) {
59
- this.client = new Redis(redisUrl, {
60
- maxRetriesPerRequest: 3,
61
- retryStrategy: (times) => Math.min(times * 100, 3000),
62
- });
63
- this.client.on("error", (err) => {
64
- console.error("Redis connection error:", err.message);
65
- });
66
- this.client.on("connect", () => {
67
- console.log("Connected to Redis");
68
- });
69
- }
70
- async get(key) {
71
- const data = await this.client.get(key);
72
- if (!data)
73
- return null;
74
- try {
75
- return JSON.parse(data);
76
- }
77
- catch {
78
- return null;
79
- }
80
- }
81
- async set(key, value, ttlMs) {
82
- const data = JSON.stringify(value);
83
- if (ttlMs) {
84
- await this.client.set(key, data, "PX", ttlMs);
85
- }
86
- else {
87
- await this.client.set(key, data);
88
- }
89
- }
90
- async delete(key) {
91
- await this.client.del(key);
92
- }
93
- }
94
- // Singleton storage instance
95
- let storage = null;
96
- /**
97
- * Get the storage backend (Redis if available, otherwise in-memory)
98
- */
99
- export function getStorage() {
100
- if (storage)
101
- return storage;
102
- const redisUrl = process.env.REDIS_URL;
103
- if (redisUrl) {
104
- console.log("Using Redis for OAuth storage");
105
- storage = new RedisStorage(redisUrl);
106
- }
107
- else {
108
- console.log("Using in-memory storage for OAuth (set REDIS_URL for persistence)");
109
- storage = new InMemoryStorage();
110
- }
111
- return storage;
112
- }
113
- // Storage key prefixes
114
- export const KEYS = {
115
- client: (id) => `oauth:client:${id}`,
116
- authCode: (code) => `oauth:code:${code}`,
117
- session: (id) => `oauth:session:${id}`,
118
- token: (token) => `oauth:token:${token}`,
119
- config: (clientId) => `oauth:config:${clientId}`,
120
- };
package/dist/http.d.ts DELETED
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env node
2
- import "dotenv/config";