universalis-mcp-server 0.1.1 → 0.2.1
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/README.md +39 -2
- package/data/materia.json +1 -1
- package/dist/constants.js +2 -0
- package/dist/index.js +5 -0
- package/dist/instructions.js +27 -1
- package/dist/services/clients.js +13 -1
- package/dist/services/saddlebag.js +25 -0
- package/dist/tools/saddlebag.js +682 -0
- package/dist/tools/wiki.js +176 -0
- package/docs/wiki/index.json +153 -0
- package/docs/wiki/saddlebag/allagan-tools-inventory-analysis.md +94 -0
- package/docs/wiki/saddlebag/api-call-guide.md +173 -0
- package/docs/wiki/saddlebag/ffxiv-advanced-undercut-alert-options.md +123 -0
- package/docs/wiki/saddlebag/ffxiv-experimental-discount-price-sniper.md +7 -0
- package/docs/wiki/saddlebag/ffxiv-job-category-ids.md +13 -0
- package/docs/wiki/saddlebag/ffxiv-sale-alerts.md +48 -0
- package/docs/wiki/saddlebag/ffxiv-sale-leads.md +42 -0
- package/docs/wiki/saddlebag/how-to-trade-using-our-ffxiv-market-overview.md +102 -0
- package/docs/wiki/saddlebag/how-to-use-the-ffxiv-crafting-profit-simulator-craftsim.md +88 -0
- package/docs/wiki/saddlebag/item-categories-ids-and-list.md +217 -0
- package/docs/wiki/saddlebag/tldr-how-to-earn-gil-with-cross-server-trading.md +89 -0
- package/docs/wiki/universalis/api-app-overview.md +236 -0
- package/docs/wiki/universalis/how-to-help-update-the-data-on-universalis.md +21 -0
- package/docs/wiki/universalis/how-to-make-universalis-lists.md +24 -0
- package/docs/wiki/universalis/how-to-search-for-items-on-universalis-and-saddlebag-exchange.md +56 -0
- package/docs/wiki/universalis/how-to-setup-universalis-alerts.md +54 -0
- package/docs/wiki/universalis/how-to-use-universalis-favorites.md +17 -0
- package/docs/wiki/universalis/interactive-tutorials.md +3 -0
- package/docs/wiki/universalis/navigating-an-item-overview-on-universalis.md +72 -0
- package/package.json +5 -3
|
@@ -0,0 +1,682 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { BaseOutputSchema, ResponseFormatSchema } from "../schemas/common.js";
|
|
3
|
+
import { buildToolResponse } from "../utils/format.js";
|
|
4
|
+
const ServerSchema = z.string().min(1).describe("Server name.");
|
|
5
|
+
const RegionSchema = z.string().min(1).describe('Region name. Example: "Europe".');
|
|
6
|
+
const ItemIdSchema = z.number().int().min(1).describe("Item ID.");
|
|
7
|
+
const ItemIdsSchema = z
|
|
8
|
+
.array(ItemIdSchema)
|
|
9
|
+
.min(1)
|
|
10
|
+
.max(100)
|
|
11
|
+
.describe("Item IDs (max 100). Example: [5333, 5334].");
|
|
12
|
+
const RawStatsItemIdsSchema = z
|
|
13
|
+
.array(z.number().int().min(-1))
|
|
14
|
+
.min(1)
|
|
15
|
+
.describe("Item IDs (-1 for all items, may be very large).");
|
|
16
|
+
const FiltersSchema = z
|
|
17
|
+
.array(z.number().int())
|
|
18
|
+
.min(1)
|
|
19
|
+
.describe("Category filter IDs. See https://github.com/ff14-advanced-market-search/saddlebag-with-pockets/wiki/Item-categories-ids-and-list");
|
|
20
|
+
const DesiredStateSchema = z
|
|
21
|
+
.enum(["above", "below"])
|
|
22
|
+
.describe('Desired state: "above" or "below".');
|
|
23
|
+
const ShoppingListItemSchema = z
|
|
24
|
+
.object({
|
|
25
|
+
itemID: ItemIdSchema.describe("Item ID to craft."),
|
|
26
|
+
craft_amount: z.number().int().min(1).describe("Amount to craft."),
|
|
27
|
+
hq: z.boolean().describe("True for HQ crafting."),
|
|
28
|
+
job: z
|
|
29
|
+
.number()
|
|
30
|
+
.int()
|
|
31
|
+
.min(0)
|
|
32
|
+
.max(15)
|
|
33
|
+
.describe("Job ID (0 for any). See https://github.com/ff14-advanced-market-search/saddlebag-with-pockets/wiki/FFXIV-job-category-ids"),
|
|
34
|
+
})
|
|
35
|
+
.strict();
|
|
36
|
+
const PriceGroupSchema = z
|
|
37
|
+
.object({
|
|
38
|
+
name: z.string().min(1).max(64).describe("Name of the price group."),
|
|
39
|
+
hq_only: z.boolean().describe("If true, include only HQ items in this group."),
|
|
40
|
+
item_ids: z.array(ItemIdSchema).optional().describe("Explicit item IDs to include."),
|
|
41
|
+
categories: z
|
|
42
|
+
.array(z.number().int())
|
|
43
|
+
.optional()
|
|
44
|
+
.describe("Category IDs to include (see wiki for category IDs)."),
|
|
45
|
+
})
|
|
46
|
+
.strict()
|
|
47
|
+
.superRefine((value, ctx) => {
|
|
48
|
+
const hasItems = Array.isArray(value.item_ids) && value.item_ids.length > 0;
|
|
49
|
+
const hasCategories = Array.isArray(value.categories) && value.categories.length > 0;
|
|
50
|
+
if (!hasItems && !hasCategories) {
|
|
51
|
+
ctx.addIssue({
|
|
52
|
+
code: z.ZodIssueCode.custom,
|
|
53
|
+
message: "Provide at least one of item_ids or categories.",
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
const UserAuctionPriceSchema = z
|
|
58
|
+
.object({
|
|
59
|
+
itemID: ItemIdSchema.describe("Item ID to monitor."),
|
|
60
|
+
price: z.number().int().min(0).describe("Target price per unit."),
|
|
61
|
+
desired_state: DesiredStateSchema,
|
|
62
|
+
hq: z.boolean().describe("True for HQ items."),
|
|
63
|
+
})
|
|
64
|
+
.strict();
|
|
65
|
+
const UserAuctionQuantitySchema = z
|
|
66
|
+
.object({
|
|
67
|
+
itemID: ItemIdSchema.describe("Item ID to monitor."),
|
|
68
|
+
quantity: z.number().int().min(0).describe("Target quantity."),
|
|
69
|
+
desired_state: DesiredStateSchema,
|
|
70
|
+
hq: z.boolean().describe("True for HQ items."),
|
|
71
|
+
})
|
|
72
|
+
.strict();
|
|
73
|
+
export function registerSaddlebagTools(server, clients) {
|
|
74
|
+
server.registerTool("saddlebag_get_blog_description", {
|
|
75
|
+
title: "Saddlebag Item Description",
|
|
76
|
+
description: "Fetch the XIVAPI description text for an item ID.",
|
|
77
|
+
inputSchema: z
|
|
78
|
+
.object({
|
|
79
|
+
item_id: ItemIdSchema,
|
|
80
|
+
response_format: ResponseFormatSchema,
|
|
81
|
+
})
|
|
82
|
+
.strict(),
|
|
83
|
+
outputSchema: BaseOutputSchema,
|
|
84
|
+
annotations: {
|
|
85
|
+
readOnlyHint: true,
|
|
86
|
+
destructiveHint: false,
|
|
87
|
+
idempotentHint: true,
|
|
88
|
+
openWorldHint: true,
|
|
89
|
+
},
|
|
90
|
+
}, async ({ item_id, response_format }) => {
|
|
91
|
+
const data = await clients.saddlebag.post("/ffxiv/blog", { item_id });
|
|
92
|
+
return buildToolResponse({
|
|
93
|
+
title: "Saddlebag Item Description",
|
|
94
|
+
responseFormat: response_format,
|
|
95
|
+
data,
|
|
96
|
+
meta: { source: "saddlebag", endpoint: "/ffxiv/blog", item_id },
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
server.registerTool("saddlebag_get_raw_stats", {
|
|
100
|
+
title: "Saddlebag Raw Market Stats",
|
|
101
|
+
description: "Fetch daily snapshot stats (median, average, sales volume) for a list of item IDs.",
|
|
102
|
+
inputSchema: z
|
|
103
|
+
.object({
|
|
104
|
+
region: RegionSchema.describe('Region name (e.g. "North-America", "Europe").'),
|
|
105
|
+
item_ids: RawStatsItemIdsSchema,
|
|
106
|
+
response_format: ResponseFormatSchema,
|
|
107
|
+
})
|
|
108
|
+
.strict(),
|
|
109
|
+
outputSchema: BaseOutputSchema,
|
|
110
|
+
annotations: {
|
|
111
|
+
readOnlyHint: true,
|
|
112
|
+
destructiveHint: false,
|
|
113
|
+
idempotentHint: true,
|
|
114
|
+
openWorldHint: true,
|
|
115
|
+
},
|
|
116
|
+
}, async ({ region, item_ids, response_format }) => {
|
|
117
|
+
const data = await clients.saddlebag.post("/ffxivrawstats", { region, item_ids });
|
|
118
|
+
return buildToolResponse({
|
|
119
|
+
title: "Saddlebag Raw Market Stats",
|
|
120
|
+
responseFormat: response_format,
|
|
121
|
+
data,
|
|
122
|
+
meta: { source: "saddlebag", endpoint: "/ffxivrawstats", region, item_ids },
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
server.registerTool("saddlebag_get_listing_metrics", {
|
|
126
|
+
title: "Saddlebag Listing Metrics",
|
|
127
|
+
description: "Fetch listing competition metrics and current listings for an item.",
|
|
128
|
+
inputSchema: z
|
|
129
|
+
.object({
|
|
130
|
+
item_id: ItemIdSchema,
|
|
131
|
+
home_server: ServerSchema,
|
|
132
|
+
initial_days: z
|
|
133
|
+
.number()
|
|
134
|
+
.int()
|
|
135
|
+
.min(0)
|
|
136
|
+
.optional()
|
|
137
|
+
.describe("Deprecated age filter (days)."),
|
|
138
|
+
end_days: z
|
|
139
|
+
.number()
|
|
140
|
+
.int()
|
|
141
|
+
.min(0)
|
|
142
|
+
.optional()
|
|
143
|
+
.describe("Deprecated age filter (days)."),
|
|
144
|
+
response_format: ResponseFormatSchema,
|
|
145
|
+
})
|
|
146
|
+
.strict(),
|
|
147
|
+
outputSchema: BaseOutputSchema,
|
|
148
|
+
annotations: {
|
|
149
|
+
readOnlyHint: true,
|
|
150
|
+
destructiveHint: false,
|
|
151
|
+
idempotentHint: true,
|
|
152
|
+
openWorldHint: true,
|
|
153
|
+
},
|
|
154
|
+
}, async ({ item_id, home_server, initial_days, end_days, response_format }) => {
|
|
155
|
+
const payload = {
|
|
156
|
+
item_id,
|
|
157
|
+
home_server,
|
|
158
|
+
...(initial_days !== undefined ? { initial_days } : {}),
|
|
159
|
+
...(end_days !== undefined ? { end_days } : {}),
|
|
160
|
+
};
|
|
161
|
+
const data = await clients.saddlebag.post("/ffxiv/v2/listing", payload);
|
|
162
|
+
return buildToolResponse({
|
|
163
|
+
title: "Saddlebag Listing Metrics",
|
|
164
|
+
responseFormat: response_format,
|
|
165
|
+
data,
|
|
166
|
+
meta: { source: "saddlebag", endpoint: "/ffxiv/v2/listing", item_id, home_server },
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
server.registerTool("saddlebag_get_history_metrics", {
|
|
170
|
+
title: "Saddlebag History Metrics",
|
|
171
|
+
description: "Fetch aggregated history metrics and price distributions for an item.",
|
|
172
|
+
inputSchema: z
|
|
173
|
+
.object({
|
|
174
|
+
item_id: ItemIdSchema,
|
|
175
|
+
home_server: ServerSchema,
|
|
176
|
+
item_type: z
|
|
177
|
+
.enum(["hq_only", "nq_only", "all"])
|
|
178
|
+
.optional()
|
|
179
|
+
.describe('Type of history to return: "hq_only", "nq_only", or "all".'),
|
|
180
|
+
initial_days: z
|
|
181
|
+
.number()
|
|
182
|
+
.int()
|
|
183
|
+
.min(0)
|
|
184
|
+
.optional()
|
|
185
|
+
.describe("Deprecated age filter (days)."),
|
|
186
|
+
end_days: z
|
|
187
|
+
.number()
|
|
188
|
+
.int()
|
|
189
|
+
.min(0)
|
|
190
|
+
.optional()
|
|
191
|
+
.describe("Deprecated age filter (days)."),
|
|
192
|
+
response_format: ResponseFormatSchema,
|
|
193
|
+
})
|
|
194
|
+
.strict(),
|
|
195
|
+
outputSchema: BaseOutputSchema,
|
|
196
|
+
annotations: {
|
|
197
|
+
readOnlyHint: true,
|
|
198
|
+
destructiveHint: false,
|
|
199
|
+
idempotentHint: true,
|
|
200
|
+
openWorldHint: true,
|
|
201
|
+
},
|
|
202
|
+
}, async ({ item_id, home_server, item_type, initial_days, end_days, response_format }) => {
|
|
203
|
+
const payload = {
|
|
204
|
+
item_id,
|
|
205
|
+
home_server,
|
|
206
|
+
...(item_type ? { item_type } : {}),
|
|
207
|
+
...(initial_days !== undefined ? { initial_days } : {}),
|
|
208
|
+
...(end_days !== undefined ? { end_days } : {}),
|
|
209
|
+
};
|
|
210
|
+
const data = await clients.saddlebag.post("/ffxiv/v2/history", payload);
|
|
211
|
+
return buildToolResponse({
|
|
212
|
+
title: "Saddlebag History Metrics",
|
|
213
|
+
responseFormat: response_format,
|
|
214
|
+
data,
|
|
215
|
+
meta: { source: "saddlebag", endpoint: "/ffxiv/v2/history", item_id, home_server },
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
server.registerTool("saddlebag_get_scrip_exchange", {
|
|
219
|
+
title: "Saddlebag Scrip Exchange",
|
|
220
|
+
description: "Rank scrip exchange items by gil value per scrip.",
|
|
221
|
+
inputSchema: z
|
|
222
|
+
.object({
|
|
223
|
+
home_server: ServerSchema,
|
|
224
|
+
color: z
|
|
225
|
+
.enum([
|
|
226
|
+
"Orange Gatherers",
|
|
227
|
+
"Purple Gatherers",
|
|
228
|
+
"Purple Crafters",
|
|
229
|
+
"Orange Crafters",
|
|
230
|
+
])
|
|
231
|
+
.describe("Scrip category."),
|
|
232
|
+
response_format: ResponseFormatSchema,
|
|
233
|
+
})
|
|
234
|
+
.strict(),
|
|
235
|
+
outputSchema: BaseOutputSchema,
|
|
236
|
+
annotations: {
|
|
237
|
+
readOnlyHint: true,
|
|
238
|
+
destructiveHint: false,
|
|
239
|
+
idempotentHint: true,
|
|
240
|
+
openWorldHint: true,
|
|
241
|
+
},
|
|
242
|
+
}, async ({ home_server, color, response_format }) => {
|
|
243
|
+
const data = await clients.saddlebag.post("/ffxiv/scripexchange", { home_server, color });
|
|
244
|
+
return buildToolResponse({
|
|
245
|
+
title: "Saddlebag Scrip Exchange",
|
|
246
|
+
responseFormat: response_format,
|
|
247
|
+
data,
|
|
248
|
+
meta: { source: "saddlebag", endpoint: "/ffxiv/scripexchange", home_server, color },
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
server.registerTool("saddlebag_get_shopping_list", {
|
|
252
|
+
title: "Saddlebag Shopping List",
|
|
253
|
+
description: "Build a crafting shopping list across servers (may return an exception payload for some items).",
|
|
254
|
+
inputSchema: z
|
|
255
|
+
.object({
|
|
256
|
+
home_server: ServerSchema,
|
|
257
|
+
shopping_list: z
|
|
258
|
+
.array(ShoppingListItemSchema)
|
|
259
|
+
.min(1)
|
|
260
|
+
.max(10)
|
|
261
|
+
.describe("Up to 10 items to craft."),
|
|
262
|
+
region_wide: z
|
|
263
|
+
.boolean()
|
|
264
|
+
.describe("If true, search all data centers in your region."),
|
|
265
|
+
ignore_after_hours: z
|
|
266
|
+
.number()
|
|
267
|
+
.int()
|
|
268
|
+
.min(0)
|
|
269
|
+
.describe("Ignore listings older than this many hours."),
|
|
270
|
+
response_format: ResponseFormatSchema,
|
|
271
|
+
})
|
|
272
|
+
.strict(),
|
|
273
|
+
outputSchema: BaseOutputSchema,
|
|
274
|
+
annotations: {
|
|
275
|
+
readOnlyHint: true,
|
|
276
|
+
destructiveHint: false,
|
|
277
|
+
idempotentHint: true,
|
|
278
|
+
openWorldHint: true,
|
|
279
|
+
},
|
|
280
|
+
}, async ({ home_server, shopping_list, region_wide, ignore_after_hours, response_format }) => {
|
|
281
|
+
const data = await clients.saddlebag.post("/v2/shoppinglist", {
|
|
282
|
+
home_server,
|
|
283
|
+
shopping_list,
|
|
284
|
+
region_wide,
|
|
285
|
+
ignore_after_hours,
|
|
286
|
+
});
|
|
287
|
+
return buildToolResponse({
|
|
288
|
+
title: "Saddlebag Shopping List",
|
|
289
|
+
responseFormat: response_format,
|
|
290
|
+
data,
|
|
291
|
+
meta: { source: "saddlebag", endpoint: "/v2/shoppinglist", home_server },
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
server.registerTool("saddlebag_get_export_prices", {
|
|
295
|
+
title: "Saddlebag Export Prices",
|
|
296
|
+
description: "Compare item prices across multiple servers for export trading.",
|
|
297
|
+
inputSchema: z
|
|
298
|
+
.object({
|
|
299
|
+
home_server: ServerSchema,
|
|
300
|
+
export_servers: z
|
|
301
|
+
.array(z.string().min(1))
|
|
302
|
+
.min(1)
|
|
303
|
+
.describe("Servers to compare against."),
|
|
304
|
+
item_ids: ItemIdsSchema,
|
|
305
|
+
hq_only: z.boolean().describe("If true, include only HQ prices."),
|
|
306
|
+
response_format: ResponseFormatSchema,
|
|
307
|
+
})
|
|
308
|
+
.strict(),
|
|
309
|
+
outputSchema: BaseOutputSchema,
|
|
310
|
+
annotations: {
|
|
311
|
+
readOnlyHint: true,
|
|
312
|
+
destructiveHint: false,
|
|
313
|
+
idempotentHint: true,
|
|
314
|
+
openWorldHint: true,
|
|
315
|
+
},
|
|
316
|
+
}, async ({ home_server, export_servers, item_ids, hq_only, response_format }) => {
|
|
317
|
+
const data = await clients.saddlebag.post("/export", {
|
|
318
|
+
home_server,
|
|
319
|
+
export_servers,
|
|
320
|
+
item_ids,
|
|
321
|
+
hq_only,
|
|
322
|
+
});
|
|
323
|
+
return buildToolResponse({
|
|
324
|
+
title: "Saddlebag Export Prices",
|
|
325
|
+
responseFormat: response_format,
|
|
326
|
+
data,
|
|
327
|
+
meta: { source: "saddlebag", endpoint: "/export", home_server, item_ids },
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
server.registerTool("saddlebag_get_marketshare", {
|
|
331
|
+
title: "Saddlebag Marketshare",
|
|
332
|
+
description: "Fetch marketshare leaderboard data for a server.",
|
|
333
|
+
inputSchema: z
|
|
334
|
+
.object({
|
|
335
|
+
server: ServerSchema,
|
|
336
|
+
time_period: z.number().int().min(1).describe("Time period in hours."),
|
|
337
|
+
sales_amount: z.number().int().min(0).describe("Minimum sales amount."),
|
|
338
|
+
average_price: z.number().int().min(0).describe("Minimum average price."),
|
|
339
|
+
filters: FiltersSchema,
|
|
340
|
+
sort_by: z
|
|
341
|
+
.enum([
|
|
342
|
+
"avg",
|
|
343
|
+
"marketValue",
|
|
344
|
+
"median",
|
|
345
|
+
"purchaseAmount",
|
|
346
|
+
"quantitySold",
|
|
347
|
+
"percentChange",
|
|
348
|
+
])
|
|
349
|
+
.describe("Sort field."),
|
|
350
|
+
response_format: ResponseFormatSchema,
|
|
351
|
+
})
|
|
352
|
+
.strict(),
|
|
353
|
+
outputSchema: BaseOutputSchema,
|
|
354
|
+
annotations: {
|
|
355
|
+
readOnlyHint: true,
|
|
356
|
+
destructiveHint: false,
|
|
357
|
+
idempotentHint: true,
|
|
358
|
+
openWorldHint: true,
|
|
359
|
+
},
|
|
360
|
+
}, async ({ server, time_period, sales_amount, average_price, filters, sort_by, response_format }) => {
|
|
361
|
+
const data = await clients.saddlebag.post("/ffxivmarketshare", {
|
|
362
|
+
server,
|
|
363
|
+
time_period,
|
|
364
|
+
sales_amount,
|
|
365
|
+
average_price,
|
|
366
|
+
filters,
|
|
367
|
+
sort_by,
|
|
368
|
+
});
|
|
369
|
+
return buildToolResponse({
|
|
370
|
+
title: "Saddlebag Marketshare",
|
|
371
|
+
responseFormat: response_format,
|
|
372
|
+
data,
|
|
373
|
+
meta: { source: "saddlebag", endpoint: "/ffxivmarketshare", server },
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
server.registerTool("saddlebag_get_reselling_scan", {
|
|
377
|
+
title: "Saddlebag Reselling Scan",
|
|
378
|
+
description: "Find reselling opportunities across servers or vendors.",
|
|
379
|
+
inputSchema: z
|
|
380
|
+
.object({
|
|
381
|
+
preferred_roi: z.number().int().min(0).describe("Preferred ROI percentage."),
|
|
382
|
+
min_profit_amount: z.number().int().min(0).describe("Minimum profit per item."),
|
|
383
|
+
min_desired_avg_ppu: z.number().int().min(0).describe("Minimum average price."),
|
|
384
|
+
min_stack_size: z.number().int().min(1).describe("Minimum stack size."),
|
|
385
|
+
hours_ago: z.number().int().min(1).describe("Sales window in hours."),
|
|
386
|
+
min_sales: z.number().int().min(0).describe("Minimum sales in window."),
|
|
387
|
+
hq: z.boolean().describe("HQ-only if true."),
|
|
388
|
+
home_server: ServerSchema,
|
|
389
|
+
filters: FiltersSchema,
|
|
390
|
+
region_wide: z.boolean().describe("Search region-wide if true."),
|
|
391
|
+
include_vendor: z.boolean().describe("Include vendor prices if true."),
|
|
392
|
+
show_out_stock: z.boolean().describe("Include out-of-stock items if true."),
|
|
393
|
+
universalis_list_uid: z
|
|
394
|
+
.string()
|
|
395
|
+
.optional()
|
|
396
|
+
.describe("Universalis list UID (deprecated)."),
|
|
397
|
+
response_format: ResponseFormatSchema,
|
|
398
|
+
})
|
|
399
|
+
.strict(),
|
|
400
|
+
outputSchema: BaseOutputSchema,
|
|
401
|
+
annotations: {
|
|
402
|
+
readOnlyHint: true,
|
|
403
|
+
destructiveHint: false,
|
|
404
|
+
idempotentHint: true,
|
|
405
|
+
openWorldHint: true,
|
|
406
|
+
},
|
|
407
|
+
}, async ({ preferred_roi, min_profit_amount, min_desired_avg_ppu, min_stack_size, hours_ago, min_sales, hq, home_server, filters, region_wide, include_vendor, show_out_stock, universalis_list_uid, response_format, }) => {
|
|
408
|
+
const payload = {
|
|
409
|
+
preferred_roi,
|
|
410
|
+
min_profit_amount,
|
|
411
|
+
min_desired_avg_ppu,
|
|
412
|
+
min_stack_size,
|
|
413
|
+
hours_ago,
|
|
414
|
+
min_sales,
|
|
415
|
+
hq,
|
|
416
|
+
home_server,
|
|
417
|
+
filters,
|
|
418
|
+
region_wide,
|
|
419
|
+
include_vendor,
|
|
420
|
+
show_out_stock,
|
|
421
|
+
...(universalis_list_uid ? { universalis_list_uid } : {}),
|
|
422
|
+
};
|
|
423
|
+
const data = await clients.saddlebag.post("/scan", payload);
|
|
424
|
+
return buildToolResponse({
|
|
425
|
+
title: "Saddlebag Reselling Scan",
|
|
426
|
+
responseFormat: response_format,
|
|
427
|
+
data,
|
|
428
|
+
meta: { source: "saddlebag", endpoint: "/scan", home_server },
|
|
429
|
+
});
|
|
430
|
+
});
|
|
431
|
+
server.registerTool("saddlebag_get_weekly_price_group_delta", {
|
|
432
|
+
title: "Saddlebag Weekly Price Group Delta",
|
|
433
|
+
description: "Compute weekly price deltas for custom item groups.",
|
|
434
|
+
inputSchema: z
|
|
435
|
+
.object({
|
|
436
|
+
region: RegionSchema,
|
|
437
|
+
start_year: z.number().int().min(2022).describe("Start year (>= 2022)."),
|
|
438
|
+
start_month: z.number().int().min(1).max(12).describe("Start month (1-12)."),
|
|
439
|
+
start_day: z.number().int().min(1).max(31).describe("Start day (1-31)."),
|
|
440
|
+
end_year: z.number().int().min(2022).describe("End year (>= 2022)."),
|
|
441
|
+
end_month: z.number().int().min(1).max(12).describe("End month (1-12)."),
|
|
442
|
+
end_day: z.number().int().min(1).max(31).describe("End day (1-31)."),
|
|
443
|
+
price_groups: z.array(PriceGroupSchema).min(1).describe("Price groups to aggregate."),
|
|
444
|
+
price_setting: z.enum(["average", "median"]).describe("Price metric."),
|
|
445
|
+
quantity_setting: z
|
|
446
|
+
.enum(["quantitySold", "salesAmount"])
|
|
447
|
+
.describe("Quantity metric."),
|
|
448
|
+
minimum_marketshare: z.number().int().min(0).describe("Minimum marketshare."),
|
|
449
|
+
response_format: ResponseFormatSchema,
|
|
450
|
+
})
|
|
451
|
+
.strict(),
|
|
452
|
+
outputSchema: BaseOutputSchema,
|
|
453
|
+
annotations: {
|
|
454
|
+
readOnlyHint: true,
|
|
455
|
+
destructiveHint: false,
|
|
456
|
+
idempotentHint: true,
|
|
457
|
+
openWorldHint: true,
|
|
458
|
+
},
|
|
459
|
+
}, async ({ region, start_year, start_month, start_day, end_year, end_month, end_day, price_groups, price_setting, quantity_setting, minimum_marketshare, response_format, }) => {
|
|
460
|
+
const data = await clients.saddlebag.post("/ffxiv/weekly-price-group-delta", {
|
|
461
|
+
region,
|
|
462
|
+
start_year,
|
|
463
|
+
start_month,
|
|
464
|
+
start_day,
|
|
465
|
+
end_year,
|
|
466
|
+
end_month,
|
|
467
|
+
end_day,
|
|
468
|
+
price_groups,
|
|
469
|
+
price_setting,
|
|
470
|
+
quantity_setting,
|
|
471
|
+
minimum_marketshare,
|
|
472
|
+
});
|
|
473
|
+
return buildToolResponse({
|
|
474
|
+
title: "Saddlebag Weekly Price Group Delta",
|
|
475
|
+
responseFormat: response_format,
|
|
476
|
+
data,
|
|
477
|
+
meta: { source: "saddlebag", endpoint: "/ffxiv/weekly-price-group-delta", region },
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
server.registerTool("saddlebag_get_price_check", {
|
|
481
|
+
title: "Saddlebag Price Check",
|
|
482
|
+
description: "Check price alerts against current market listings.",
|
|
483
|
+
inputSchema: z
|
|
484
|
+
.object({
|
|
485
|
+
home_server: ServerSchema,
|
|
486
|
+
dc_only: z.boolean().optional().describe("If true, check data center only."),
|
|
487
|
+
user_auctions: z
|
|
488
|
+
.array(UserAuctionPriceSchema)
|
|
489
|
+
.min(1)
|
|
490
|
+
.describe("User auction alerts."),
|
|
491
|
+
response_format: ResponseFormatSchema,
|
|
492
|
+
})
|
|
493
|
+
.strict(),
|
|
494
|
+
outputSchema: BaseOutputSchema,
|
|
495
|
+
annotations: {
|
|
496
|
+
readOnlyHint: true,
|
|
497
|
+
destructiveHint: false,
|
|
498
|
+
idempotentHint: true,
|
|
499
|
+
openWorldHint: true,
|
|
500
|
+
},
|
|
501
|
+
}, async ({ home_server, dc_only, user_auctions, response_format }) => {
|
|
502
|
+
const payload = {
|
|
503
|
+
home_server,
|
|
504
|
+
user_auctions,
|
|
505
|
+
...(dc_only !== undefined ? { dc_only } : {}),
|
|
506
|
+
};
|
|
507
|
+
const data = await clients.saddlebag.post("/pricecheck", payload);
|
|
508
|
+
return buildToolResponse({
|
|
509
|
+
title: "Saddlebag Price Check",
|
|
510
|
+
responseFormat: response_format,
|
|
511
|
+
data,
|
|
512
|
+
meta: { source: "saddlebag", endpoint: "/pricecheck", home_server },
|
|
513
|
+
});
|
|
514
|
+
});
|
|
515
|
+
server.registerTool("saddlebag_get_quantity_check", {
|
|
516
|
+
title: "Saddlebag Quantity Check",
|
|
517
|
+
description: "Check quantity alerts against current market listings.",
|
|
518
|
+
inputSchema: z
|
|
519
|
+
.object({
|
|
520
|
+
home_server: ServerSchema,
|
|
521
|
+
user_auctions: z
|
|
522
|
+
.array(UserAuctionQuantitySchema)
|
|
523
|
+
.min(1)
|
|
524
|
+
.describe("User auction alerts."),
|
|
525
|
+
response_format: ResponseFormatSchema,
|
|
526
|
+
})
|
|
527
|
+
.strict(),
|
|
528
|
+
outputSchema: BaseOutputSchema,
|
|
529
|
+
annotations: {
|
|
530
|
+
readOnlyHint: true,
|
|
531
|
+
destructiveHint: false,
|
|
532
|
+
idempotentHint: true,
|
|
533
|
+
openWorldHint: true,
|
|
534
|
+
},
|
|
535
|
+
}, async ({ home_server, user_auctions, response_format }) => {
|
|
536
|
+
const data = await clients.saddlebag.post("/quantitycheck", {
|
|
537
|
+
home_server,
|
|
538
|
+
user_auctions,
|
|
539
|
+
});
|
|
540
|
+
return buildToolResponse({
|
|
541
|
+
title: "Saddlebag Quantity Check",
|
|
542
|
+
responseFormat: response_format,
|
|
543
|
+
data,
|
|
544
|
+
meta: { source: "saddlebag", endpoint: "/quantitycheck", home_server },
|
|
545
|
+
});
|
|
546
|
+
});
|
|
547
|
+
server.registerTool("saddlebag_get_sale_alert", {
|
|
548
|
+
title: "Saddlebag Sale Alerts",
|
|
549
|
+
description: "Check if tracked items have sold based on retainer listings.",
|
|
550
|
+
inputSchema: z
|
|
551
|
+
.object({
|
|
552
|
+
retainer_names: z
|
|
553
|
+
.array(z.string().min(1))
|
|
554
|
+
.min(1)
|
|
555
|
+
.describe("Retainer names to track."),
|
|
556
|
+
server: ServerSchema,
|
|
557
|
+
item_ids: ItemIdsSchema,
|
|
558
|
+
seller_id: z.string().optional().describe("Seller ID (deprecated)."),
|
|
559
|
+
response_format: ResponseFormatSchema,
|
|
560
|
+
})
|
|
561
|
+
.strict(),
|
|
562
|
+
outputSchema: BaseOutputSchema,
|
|
563
|
+
annotations: {
|
|
564
|
+
readOnlyHint: true,
|
|
565
|
+
destructiveHint: false,
|
|
566
|
+
idempotentHint: true,
|
|
567
|
+
openWorldHint: true,
|
|
568
|
+
},
|
|
569
|
+
}, async ({ retainer_names, server, item_ids, seller_id, response_format }) => {
|
|
570
|
+
const payload = {
|
|
571
|
+
retainer_names,
|
|
572
|
+
server,
|
|
573
|
+
item_ids,
|
|
574
|
+
...(seller_id ? { seller_id } : {}),
|
|
575
|
+
};
|
|
576
|
+
const data = await clients.saddlebag.post("/salealert", payload);
|
|
577
|
+
return buildToolResponse({
|
|
578
|
+
title: "Saddlebag Sale Alerts",
|
|
579
|
+
responseFormat: response_format,
|
|
580
|
+
data,
|
|
581
|
+
meta: { source: "saddlebag", endpoint: "/salealert", server },
|
|
582
|
+
});
|
|
583
|
+
});
|
|
584
|
+
server.registerTool("saddlebag_get_craftsim", {
|
|
585
|
+
title: "Saddlebag Craftsim",
|
|
586
|
+
description: "Calculate profitable crafting recipes. Results can be very large; max_results limits output size.",
|
|
587
|
+
inputSchema: z
|
|
588
|
+
.object({
|
|
589
|
+
home_server: ServerSchema,
|
|
590
|
+
cost_metric: z
|
|
591
|
+
.enum([
|
|
592
|
+
"material_avg_cost",
|
|
593
|
+
"material_median_cost",
|
|
594
|
+
"material_min_listing_cost",
|
|
595
|
+
])
|
|
596
|
+
.describe("Cost metric."),
|
|
597
|
+
revenue_metric: z
|
|
598
|
+
.enum([
|
|
599
|
+
"revenue_avg",
|
|
600
|
+
"revenue_median",
|
|
601
|
+
"revenue_home_min_listing",
|
|
602
|
+
"revenue_region_min_listing",
|
|
603
|
+
])
|
|
604
|
+
.describe("Revenue metric."),
|
|
605
|
+
sales_per_week: z.number().int().min(0).describe("Minimum sales per week."),
|
|
606
|
+
median_sale_price: z.number().int().min(0).describe("Minimum median sale price."),
|
|
607
|
+
max_material_cost: z.number().int().min(0).describe("Maximum material cost."),
|
|
608
|
+
filters: FiltersSchema,
|
|
609
|
+
jobs: z
|
|
610
|
+
.array(z.number().int())
|
|
611
|
+
.min(1)
|
|
612
|
+
.max(15)
|
|
613
|
+
.describe("Job IDs to scan ([0] for all jobs). See https://github.com/ff14-advanced-market-search/saddlebag-with-pockets/wiki/FFXIV-job-category-ids"),
|
|
614
|
+
stars: z
|
|
615
|
+
.number()
|
|
616
|
+
.int()
|
|
617
|
+
.min(-1)
|
|
618
|
+
.describe("Stars required (-1 for any)."),
|
|
619
|
+
lvl_lower_limit: z
|
|
620
|
+
.number()
|
|
621
|
+
.int()
|
|
622
|
+
.min(-1)
|
|
623
|
+
.max(99)
|
|
624
|
+
.describe("Lower level limit (max 99)."),
|
|
625
|
+
lvl_upper_limit: z
|
|
626
|
+
.number()
|
|
627
|
+
.int()
|
|
628
|
+
.min(2)
|
|
629
|
+
.describe("Upper level limit (min 2)."),
|
|
630
|
+
yields: z.number().int().min(-1).describe("Yield amount (-1 for any)."),
|
|
631
|
+
hide_expert_recipes: z.boolean().describe("Hide expert recipes if true."),
|
|
632
|
+
max_results: z
|
|
633
|
+
.number()
|
|
634
|
+
.int()
|
|
635
|
+
.min(1)
|
|
636
|
+
.max(5000)
|
|
637
|
+
.default(200)
|
|
638
|
+
.describe("Limit number of recipes returned (default: 200)."),
|
|
639
|
+
response_format: ResponseFormatSchema,
|
|
640
|
+
})
|
|
641
|
+
.strict(),
|
|
642
|
+
outputSchema: BaseOutputSchema,
|
|
643
|
+
annotations: {
|
|
644
|
+
readOnlyHint: true,
|
|
645
|
+
destructiveHint: false,
|
|
646
|
+
idempotentHint: true,
|
|
647
|
+
openWorldHint: true,
|
|
648
|
+
},
|
|
649
|
+
}, async ({ home_server, cost_metric, revenue_metric, sales_per_week, median_sale_price, max_material_cost, filters, jobs, stars, lvl_lower_limit, lvl_upper_limit, yields, hide_expert_recipes, max_results, response_format, }) => {
|
|
650
|
+
const data = await clients.saddlebag.post("/v2/craftsim", {
|
|
651
|
+
home_server,
|
|
652
|
+
cost_metric,
|
|
653
|
+
revenue_metric,
|
|
654
|
+
sales_per_week,
|
|
655
|
+
median_sale_price,
|
|
656
|
+
max_material_cost,
|
|
657
|
+
filters,
|
|
658
|
+
jobs,
|
|
659
|
+
stars,
|
|
660
|
+
lvl_lower_limit,
|
|
661
|
+
lvl_upper_limit,
|
|
662
|
+
yields,
|
|
663
|
+
hide_expert_recipes,
|
|
664
|
+
});
|
|
665
|
+
let output = data;
|
|
666
|
+
let summaryLines;
|
|
667
|
+
if (output && typeof output === "object" && Array.isArray(output.data)) {
|
|
668
|
+
const total = output.data.length;
|
|
669
|
+
if (total > max_results) {
|
|
670
|
+
output = { ...output, data: output.data.slice(0, max_results) };
|
|
671
|
+
summaryLines = [`Showing ${max_results} of ${total} recipes.`];
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
return buildToolResponse({
|
|
675
|
+
title: "Saddlebag Craftsim",
|
|
676
|
+
responseFormat: response_format,
|
|
677
|
+
data: output,
|
|
678
|
+
meta: { source: "saddlebag", endpoint: "/v2/craftsim", home_server, max_results },
|
|
679
|
+
summaryLines,
|
|
680
|
+
});
|
|
681
|
+
});
|
|
682
|
+
}
|