@striderlabs/mcp-gap 0.1.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/src/index.ts ADDED
@@ -0,0 +1,515 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import {
4
+ CallToolRequestSchema,
5
+ ListToolsRequestSchema,
6
+ } from '@modelcontextprotocol/sdk/types.js';
7
+ import {
8
+ Brand,
9
+ checkLoginStatus,
10
+ initiateLogin,
11
+ logout,
12
+ searchProducts,
13
+ getProductDetails,
14
+ checkStoreInventory,
15
+ addToCart,
16
+ viewCart,
17
+ proceedToCheckout,
18
+ getRewardsInfo,
19
+ trackOrder,
20
+ findStores,
21
+ initiateReturn,
22
+ getSizeGuide,
23
+ } from './browser.js';
24
+
25
+ const server = new Server(
26
+ {
27
+ name: 'mcp-gap',
28
+ version: '0.1.0',
29
+ },
30
+ {
31
+ capabilities: {
32
+ tools: {},
33
+ },
34
+ },
35
+ );
36
+
37
+ const BRANDS: Brand[] = ['gap', 'old-navy', 'banana-republic', 'athleta'];
38
+ const BRAND_DESCRIPTION = '"gap" | "old-navy" | "banana-republic" | "athleta"';
39
+
40
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
41
+ tools: [
42
+ {
43
+ name: 'gap_status',
44
+ description: 'Check your Gap Inc. account login status and session information.',
45
+ inputSchema: {
46
+ type: 'object',
47
+ properties: {},
48
+ required: [],
49
+ },
50
+ },
51
+ {
52
+ name: 'gap_login',
53
+ description: 'Get instructions and a URL to log in to your Gap Inc. account.',
54
+ inputSchema: {
55
+ type: 'object',
56
+ properties: {
57
+ brand: {
58
+ type: 'string',
59
+ description: `Brand login page to use: ${BRAND_DESCRIPTION}. Defaults to "gap".`,
60
+ },
61
+ },
62
+ required: [],
63
+ },
64
+ },
65
+ {
66
+ name: 'gap_logout',
67
+ description: 'Log out of your Gap Inc. account and clear saved session data.',
68
+ inputSchema: {
69
+ type: 'object',
70
+ properties: {},
71
+ required: [],
72
+ },
73
+ },
74
+ {
75
+ name: 'gap_search',
76
+ description:
77
+ 'Search for products across Gap Inc. brands (Gap, Old Navy, Banana Republic, Athleta). Returns product names, prices, and URLs.',
78
+ inputSchema: {
79
+ type: 'object',
80
+ properties: {
81
+ query: {
82
+ type: 'string',
83
+ description: 'Search query (e.g. "slim jeans", "floral dress", "running shorts")',
84
+ },
85
+ brand: {
86
+ type: 'string',
87
+ description: `Brand to search: ${BRAND_DESCRIPTION}. Defaults to "gap".`,
88
+ },
89
+ max_results: {
90
+ type: 'number',
91
+ description: 'Maximum number of results to return (default: 10, max: 30)',
92
+ },
93
+ },
94
+ required: ['query'],
95
+ },
96
+ },
97
+ {
98
+ name: 'gap_product_details',
99
+ description:
100
+ 'Get detailed product information including available sizes, colors, fit type, materials, and pricing.',
101
+ inputSchema: {
102
+ type: 'object',
103
+ properties: {
104
+ product_url: {
105
+ type: 'string',
106
+ description: 'Full product page URL (from gap_search results)',
107
+ },
108
+ brand: {
109
+ type: 'string',
110
+ description: `Product brand: ${BRAND_DESCRIPTION}. Defaults to "gap".`,
111
+ },
112
+ },
113
+ required: ['product_url'],
114
+ },
115
+ },
116
+ {
117
+ name: 'gap_store_inventory',
118
+ description: 'Check if a product is available in stores near a given zip code.',
119
+ inputSchema: {
120
+ type: 'object',
121
+ properties: {
122
+ product_id: {
123
+ type: 'string',
124
+ description: 'Product ID (from gap_product_details)',
125
+ },
126
+ zip_code: {
127
+ type: 'string',
128
+ description: 'Zip code to search near (e.g. "10001")',
129
+ },
130
+ brand: {
131
+ type: 'string',
132
+ description: `Product brand: ${BRAND_DESCRIPTION}. Defaults to "gap".`,
133
+ },
134
+ },
135
+ required: ['product_id', 'zip_code'],
136
+ },
137
+ },
138
+ {
139
+ name: 'gap_add_to_cart',
140
+ description: 'Add a product to your shopping bag with a specific size and color.',
141
+ inputSchema: {
142
+ type: 'object',
143
+ properties: {
144
+ product_url: {
145
+ type: 'string',
146
+ description: 'Full product page URL',
147
+ },
148
+ size: {
149
+ type: 'string',
150
+ description: 'Size to add (e.g. "M", "32x30", "8"). Use gap_product_details to see available sizes.',
151
+ },
152
+ color: {
153
+ type: 'string',
154
+ description: 'Color to select (e.g. "Navy", "Black"). Use gap_product_details to see available colors.',
155
+ },
156
+ quantity: {
157
+ type: 'number',
158
+ description: 'Quantity to add (default: 1)',
159
+ },
160
+ },
161
+ required: ['product_url', 'size', 'color'],
162
+ },
163
+ },
164
+ {
165
+ name: 'gap_view_cart',
166
+ description: 'View the contents of your shopping bag including items, quantities, and totals.',
167
+ inputSchema: {
168
+ type: 'object',
169
+ properties: {
170
+ brand: {
171
+ type: 'string',
172
+ description: `Brand cart to view: ${BRAND_DESCRIPTION}. Defaults to "gap".`,
173
+ },
174
+ },
175
+ required: [],
176
+ },
177
+ },
178
+ {
179
+ name: 'gap_checkout',
180
+ description:
181
+ 'Get checkout URL and instructions to complete your purchase. Reviews payment, shipping, and Gap Cash options.',
182
+ inputSchema: {
183
+ type: 'object',
184
+ properties: {
185
+ brand: {
186
+ type: 'string',
187
+ description: `Brand checkout: ${BRAND_DESCRIPTION}. Defaults to "gap".`,
188
+ },
189
+ },
190
+ required: [],
191
+ },
192
+ },
193
+ {
194
+ name: 'gap_rewards',
195
+ description:
196
+ 'Check your Gap Cash balance, rewards points, member tier, and upcoming rewards.',
197
+ inputSchema: {
198
+ type: 'object',
199
+ properties: {
200
+ brand: {
201
+ type: 'string',
202
+ description: `Brand rewards page: ${BRAND_DESCRIPTION}. Defaults to "gap".`,
203
+ },
204
+ },
205
+ required: [],
206
+ },
207
+ },
208
+ {
209
+ name: 'gap_track_order',
210
+ description: 'Track an order by order ID to see status, estimated delivery, and tracking information.',
211
+ inputSchema: {
212
+ type: 'object',
213
+ properties: {
214
+ order_id: {
215
+ type: 'string',
216
+ description: 'Order ID from your confirmation email or account order history',
217
+ },
218
+ brand: {
219
+ type: 'string',
220
+ description: `Brand the order was placed with: ${BRAND_DESCRIPTION}. Defaults to "gap".`,
221
+ },
222
+ },
223
+ required: ['order_id'],
224
+ },
225
+ },
226
+ {
227
+ name: 'gap_find_stores',
228
+ description: 'Find Gap Inc. store locations near a given zip code, with hours, phone, and distance.',
229
+ inputSchema: {
230
+ type: 'object',
231
+ properties: {
232
+ zip_code: {
233
+ type: 'string',
234
+ description: 'Zip code to search near (e.g. "10001")',
235
+ },
236
+ brand: {
237
+ type: 'string',
238
+ description: `Filter by brand: ${BRAND_DESCRIPTION}. Leave blank to show all brands.`,
239
+ },
240
+ radius: {
241
+ type: 'number',
242
+ description: 'Search radius in miles (default: 25)',
243
+ },
244
+ },
245
+ required: ['zip_code'],
246
+ },
247
+ },
248
+ {
249
+ name: 'gap_initiate_return',
250
+ description: 'Start a return for an item from a previous order. Provides return instructions and label information.',
251
+ inputSchema: {
252
+ type: 'object',
253
+ properties: {
254
+ order_id: {
255
+ type: 'string',
256
+ description: 'Order ID containing the item to return',
257
+ },
258
+ item_name: {
259
+ type: 'string',
260
+ description: 'Name or description of the item to return',
261
+ },
262
+ reason: {
263
+ type: 'string',
264
+ description:
265
+ 'Return reason (e.g. "Wrong size", "Changed my mind", "Defective", "Not as described")',
266
+ },
267
+ brand: {
268
+ type: 'string',
269
+ description: `Brand the order was placed with: ${BRAND_DESCRIPTION}. Defaults to "gap".`,
270
+ },
271
+ },
272
+ required: ['order_id', 'item_name', 'reason'],
273
+ },
274
+ },
275
+ {
276
+ name: 'gap_size_guide',
277
+ description:
278
+ 'Get size guide information for Gap Inc. clothing categories including measurements in US sizes.',
279
+ inputSchema: {
280
+ type: 'object',
281
+ properties: {
282
+ category: {
283
+ type: 'string',
284
+ description: 'Clothing category: "tops", "bottoms", "shoes", or "kids"',
285
+ },
286
+ brand: {
287
+ type: 'string',
288
+ description: `Brand (may affect sizing recommendations): ${BRAND_DESCRIPTION}. Defaults to "gap".`,
289
+ },
290
+ },
291
+ required: ['category'],
292
+ },
293
+ },
294
+ ],
295
+ }));
296
+
297
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
298
+ const { name, arguments: args } = request.params;
299
+
300
+ try {
301
+ switch (name) {
302
+ case 'gap_status': {
303
+ const status = await checkLoginStatus();
304
+ return {
305
+ content: [
306
+ {
307
+ type: 'text',
308
+ text: JSON.stringify(status, null, 2),
309
+ },
310
+ ],
311
+ };
312
+ }
313
+
314
+ case 'gap_login': {
315
+ const brand = (args?.brand as Brand) || 'gap';
316
+ if (!BRANDS.includes(brand)) {
317
+ throw new Error(`Invalid brand. Must be one of: ${BRANDS.join(', ')}`);
318
+ }
319
+ const result = await initiateLogin(brand);
320
+ return {
321
+ content: [{ type: 'text', text: `${result.instructions}\n\nLogin URL: ${result.url}` }],
322
+ };
323
+ }
324
+
325
+ case 'gap_logout': {
326
+ await logout();
327
+ return {
328
+ content: [{ type: 'text', text: 'Successfully logged out and cleared session data.' }],
329
+ };
330
+ }
331
+
332
+ case 'gap_search': {
333
+ if (!args?.query) throw new Error('query is required');
334
+ const brand = (args.brand as Brand) || 'gap';
335
+ if (!BRANDS.includes(brand)) throw new Error(`Invalid brand. Must be one of: ${BRANDS.join(', ')}`);
336
+ const maxResults = Math.min(Number(args.max_results) || 10, 30);
337
+ const products = await searchProducts(String(args.query), brand, maxResults);
338
+ if (products.length === 0) {
339
+ return {
340
+ content: [
341
+ {
342
+ type: 'text',
343
+ text: `No products found for "${args.query}" on ${brand}. Try a different search term or brand.`,
344
+ },
345
+ ],
346
+ };
347
+ }
348
+ return {
349
+ content: [{ type: 'text', text: JSON.stringify(products, null, 2) }],
350
+ };
351
+ }
352
+
353
+ case 'gap_product_details': {
354
+ if (!args?.product_url) throw new Error('product_url is required');
355
+ const brand = (args.brand as Brand) || 'gap';
356
+ const details = await getProductDetails(String(args.product_url), brand);
357
+ return {
358
+ content: [{ type: 'text', text: JSON.stringify(details, null, 2) }],
359
+ };
360
+ }
361
+
362
+ case 'gap_store_inventory': {
363
+ if (!args?.product_id) throw new Error('product_id is required');
364
+ if (!args?.zip_code) throw new Error('zip_code is required');
365
+ const brand = (args.brand as Brand) || 'gap';
366
+ const inventory = await checkStoreInventory(
367
+ String(args.product_id),
368
+ String(args.zip_code),
369
+ brand,
370
+ );
371
+ if (inventory.length === 0) {
372
+ return {
373
+ content: [
374
+ {
375
+ type: 'text',
376
+ text: `No store inventory results found for product ${args.product_id} near ${args.zip_code}. The item may only be available online.`,
377
+ },
378
+ ],
379
+ };
380
+ }
381
+ return {
382
+ content: [{ type: 'text', text: JSON.stringify(inventory, null, 2) }],
383
+ };
384
+ }
385
+
386
+ case 'gap_add_to_cart': {
387
+ if (!args?.product_url) throw new Error('product_url is required');
388
+ if (!args?.size) throw new Error('size is required');
389
+ if (!args?.color) throw new Error('color is required');
390
+ const quantity = Number(args.quantity) || 1;
391
+ const result = await addToCart(
392
+ String(args.product_url),
393
+ String(args.size),
394
+ String(args.color),
395
+ quantity,
396
+ );
397
+ return {
398
+ content: [
399
+ {
400
+ type: 'text',
401
+ text: result.success
402
+ ? `✓ ${result.message}${result.cartItemCount !== undefined ? ` (Bag now has ${result.cartItemCount} items)` : ''}`
403
+ : `⚠ ${result.message}`,
404
+ },
405
+ ],
406
+ };
407
+ }
408
+
409
+ case 'gap_view_cart': {
410
+ const brand = (args?.brand as Brand) || 'gap';
411
+ const cart = await viewCart(brand);
412
+ return {
413
+ content: [{ type: 'text', text: JSON.stringify(cart, null, 2) }],
414
+ };
415
+ }
416
+
417
+ case 'gap_checkout': {
418
+ const brand = (args?.brand as Brand) || 'gap';
419
+ const checkout = await proceedToCheckout(brand);
420
+ return {
421
+ content: [{ type: 'text', text: `${checkout.instructions}\n\nCheckout URL: ${checkout.url}` }],
422
+ };
423
+ }
424
+
425
+ case 'gap_rewards': {
426
+ const brand = (args?.brand as Brand) || 'gap';
427
+ const rewards = await getRewardsInfo(brand);
428
+ return {
429
+ content: [{ type: 'text', text: JSON.stringify(rewards, null, 2) }],
430
+ };
431
+ }
432
+
433
+ case 'gap_track_order': {
434
+ if (!args?.order_id) throw new Error('order_id is required');
435
+ const brand = (args.brand as Brand) || 'gap';
436
+ const order = await trackOrder(String(args.order_id), brand);
437
+ return {
438
+ content: [{ type: 'text', text: JSON.stringify(order, null, 2) }],
439
+ };
440
+ }
441
+
442
+ case 'gap_find_stores': {
443
+ if (!args?.zip_code) throw new Error('zip_code is required');
444
+ const brand = args.brand ? (args.brand as Brand) : undefined;
445
+ if (brand && !BRANDS.includes(brand)) {
446
+ throw new Error(`Invalid brand. Must be one of: ${BRANDS.join(', ')}`);
447
+ }
448
+ const radius = Number(args.radius) || 25;
449
+ const stores = await findStores(String(args.zip_code), brand, radius);
450
+ if (stores.length === 0) {
451
+ return {
452
+ content: [
453
+ {
454
+ type: 'text',
455
+ text: `No stores found near ${args.zip_code}${brand ? ` for ${brand}` : ''}. Try a different zip code or expand your radius.`,
456
+ },
457
+ ],
458
+ };
459
+ }
460
+ return {
461
+ content: [{ type: 'text', text: JSON.stringify(stores, null, 2) }],
462
+ };
463
+ }
464
+
465
+ case 'gap_initiate_return': {
466
+ if (!args?.order_id) throw new Error('order_id is required');
467
+ if (!args?.item_name) throw new Error('item_name is required');
468
+ if (!args?.reason) throw new Error('reason is required');
469
+ const brand = (args.brand as Brand) || 'gap';
470
+ const returnResult = await initiateReturn(
471
+ String(args.order_id),
472
+ String(args.item_name),
473
+ String(args.reason),
474
+ brand,
475
+ );
476
+ return {
477
+ content: [{ type: 'text', text: JSON.stringify(returnResult, null, 2) }],
478
+ };
479
+ }
480
+
481
+ case 'gap_size_guide': {
482
+ if (!args?.category) throw new Error('category is required');
483
+ const brand = (args.brand as Brand) || 'gap';
484
+ const guide = getSizeGuide(String(args.category), brand);
485
+ return {
486
+ content: [{ type: 'text', text: JSON.stringify(guide, null, 2) }],
487
+ };
488
+ }
489
+
490
+ default:
491
+ throw new Error(`Unknown tool: ${name}`);
492
+ }
493
+ } catch (error) {
494
+ const msg = error instanceof Error ? error.message : String(error);
495
+ return {
496
+ content: [
497
+ {
498
+ type: 'text',
499
+ text: `Error: ${msg}\n\nIf this is a login issue, use gap_login to authenticate first.`,
500
+ },
501
+ ],
502
+ isError: true,
503
+ };
504
+ }
505
+ });
506
+
507
+ async function main(): Promise<void> {
508
+ const transport = new StdioServerTransport();
509
+ await server.connect(transport);
510
+ }
511
+
512
+ main().catch((err) => {
513
+ console.error('Fatal error:', err);
514
+ process.exit(1);
515
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "declaration": true
12
+ },
13
+ "include": ["src/**/*"]
14
+ }