universalis-mcp-server 0.1.1 → 0.2.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/README.md +24 -2
- package/data/materia.json +1 -1
- package/dist/constants.js +2 -0
- package/dist/index.js +3 -0
- package/dist/instructions.js +21 -1
- package/dist/services/clients.js +13 -1
- package/dist/services/saddlebag.js +25 -0
- package/dist/tools/saddlebag.js +682 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# universalis-mcp-server
|
|
2
2
|
|
|
3
|
-
MCP server for
|
|
3
|
+
MCP server for FFXIV market data via Universalis + Saddlebag Exchange, plus XIVAPI item lookup.
|
|
4
4
|
|
|
5
5
|
## Capabilities
|
|
6
6
|
|
|
7
|
-
This MCP server exposes read-only tools for Universalis market data and XIVAPI item lookup.
|
|
7
|
+
This MCP server exposes read-only tools for Universalis market data, Saddlebag Exchange analytics, and XIVAPI item lookup.
|
|
8
8
|
All tools support `response_format` as `markdown` or `json`.
|
|
9
9
|
|
|
10
10
|
### Market board data (Universalis)
|
|
@@ -24,6 +24,25 @@ All tools support `response_format` as `markdown` or `json`.
|
|
|
24
24
|
|
|
25
25
|
- `universalis_rank_items_by_profitability`: Resolve names, fetch aggregated data, and rank by demand and profit.
|
|
26
26
|
|
|
27
|
+
### Market analytics (Saddlebag Exchange)
|
|
28
|
+
|
|
29
|
+
- `saddlebag_get_listing_metrics`: Listing competition metrics and current listings.
|
|
30
|
+
- `saddlebag_get_history_metrics`: Aggregated history metrics and price distributions.
|
|
31
|
+
- `saddlebag_get_raw_stats`: Daily snapshot stats (median, average, sales volume).
|
|
32
|
+
- `saddlebag_get_scrip_exchange`: Gil-per-scrip rankings.
|
|
33
|
+
- `saddlebag_get_shopping_list`: Crafting shopping list across servers (may return exception payloads).
|
|
34
|
+
- `saddlebag_get_export_prices`: Cross-world price comparison for export trading.
|
|
35
|
+
- `saddlebag_get_marketshare`: Marketshare leaderboard snapshot for a server.
|
|
36
|
+
- `saddlebag_get_reselling_scan`: Reselling opportunities (region/DC/vendor filters).
|
|
37
|
+
- `saddlebag_get_weekly_price_group_delta`: Weekly deltas for custom item groups.
|
|
38
|
+
- `saddlebag_get_price_check`: Price alert checks (user-supplied auctions).
|
|
39
|
+
- `saddlebag_get_quantity_check`: Quantity alert checks (user-supplied auctions).
|
|
40
|
+
- `saddlebag_get_sale_alert`: Sale alerts for retainer listings.
|
|
41
|
+
- `saddlebag_get_craftsim`: Crafting profitability scan (use `max_results` to limit payloads).
|
|
42
|
+
- `saddlebag_get_blog_description`: Item description text lookup.
|
|
43
|
+
|
|
44
|
+
Best-deals is excluded (premium gated + inconsistent API validation).
|
|
45
|
+
|
|
27
46
|
### Reference data (Universalis)
|
|
28
47
|
|
|
29
48
|
- `universalis_list_worlds`: List worlds with pagination.
|
|
@@ -97,9 +116,11 @@ If you change code, re-run `pnpm build` and restart the MCP connection.
|
|
|
97
116
|
|
|
98
117
|
- `UNIVERSALIS_BASE_URL`: Override Universalis base URL (default: `https://universalis.app/api/v2`).
|
|
99
118
|
- `XIVAPI_BASE_URL`: Override XIVAPI base URL (default: `https://v2.xivapi.com/api`).
|
|
119
|
+
- `SADDLEBAG_BASE_URL`: Override Saddlebag base URL (default: `https://docs.saddlebagexchange.com/api`).
|
|
100
120
|
- `UNIVERSALIS_MCP_USER_AGENT`: Custom User-Agent header.
|
|
101
121
|
- `UNIVERSALIS_TIMEOUT_MS`: Request timeout for Universalis (default: 30000).
|
|
102
122
|
- `XIVAPI_TIMEOUT_MS`: Request timeout for XIVAPI (default: 30000).
|
|
123
|
+
- `SADDLEBAG_TIMEOUT_MS`: Request timeout for Saddlebag (default: 30000).
|
|
103
124
|
- `XIVAPI_LANGUAGE`: Default XIVAPI language (default: `en`).
|
|
104
125
|
- `XIVAPI_VERSION`: Default XIVAPI version (default: `latest`).
|
|
105
126
|
|
|
@@ -107,3 +128,4 @@ If you change code, re-run `pnpm build` and restart the MCP connection.
|
|
|
107
128
|
|
|
108
129
|
- Rate limits are enforced client-side for Universalis and XIVAPI.
|
|
109
130
|
- Tools support `response_format` as `markdown` or `json`.
|
|
131
|
+
- Some Saddlebag endpoints proxy Universalis data; avoid excessive polling.
|
package/data/materia.json
CHANGED
package/dist/constants.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
export const UNIVERSALIS_BASE_URL = process.env.UNIVERSALIS_BASE_URL ?? "https://universalis.app/api/v2";
|
|
2
2
|
export const XIVAPI_BASE_URL = process.env.XIVAPI_BASE_URL ?? "https://v2.xivapi.com/api";
|
|
3
|
+
export const SADDLEBAG_BASE_URL = process.env.SADDLEBAG_BASE_URL ?? "https://docs.saddlebagexchange.com/api";
|
|
3
4
|
export const DEFAULT_UNIVERSALIS_TIMEOUT_MS = 30000;
|
|
4
5
|
export const DEFAULT_XIVAPI_TIMEOUT_MS = 30000;
|
|
6
|
+
export const DEFAULT_SADDLEBAG_TIMEOUT_MS = 30000;
|
|
5
7
|
export const DEFAULT_TIMEOUT_MS = 30000;
|
|
6
8
|
export const DEFAULT_MATERIA_CACHE_TTL_MS = 1000 * 60 * 60 * 24;
|
|
7
9
|
export const CHARACTER_LIMIT = 25000;
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import { createClients } from "./services/clients.js";
|
|
|
7
7
|
import { registerLookupTools } from "./tools/lookup.js";
|
|
8
8
|
import { registerMarketTools } from "./tools/market.js";
|
|
9
9
|
import { registerReferenceTools } from "./tools/reference.js";
|
|
10
|
+
import { registerSaddlebagTools } from "./tools/saddlebag.js";
|
|
10
11
|
import { registerStatsTools } from "./tools/stats.js";
|
|
11
12
|
import { registerWorkflowTools } from "./tools/workflows.js";
|
|
12
13
|
function toNumber(value) {
|
|
@@ -27,6 +28,7 @@ async function main() {
|
|
|
27
28
|
userAgent,
|
|
28
29
|
universalisTimeoutMs: toNumber(process.env.UNIVERSALIS_TIMEOUT_MS),
|
|
29
30
|
xivapiTimeoutMs: toNumber(process.env.XIVAPI_TIMEOUT_MS),
|
|
31
|
+
saddlebagTimeoutMs: toNumber(process.env.SADDLEBAG_TIMEOUT_MS),
|
|
30
32
|
xivapiLanguage: process.env.XIVAPI_LANGUAGE,
|
|
31
33
|
xivapiVersion: process.env.XIVAPI_VERSION,
|
|
32
34
|
});
|
|
@@ -34,6 +36,7 @@ async function main() {
|
|
|
34
36
|
registerReferenceTools(server, clients);
|
|
35
37
|
registerStatsTools(server, clients);
|
|
36
38
|
registerLookupTools(server, clients);
|
|
39
|
+
registerSaddlebagTools(server, clients);
|
|
37
40
|
registerWorkflowTools(server, clients);
|
|
38
41
|
server.registerPrompt("universalis_usage_guide", {
|
|
39
42
|
title: "Universalis Usage Guide",
|
package/dist/instructions.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const SERVER_INSTRUCTIONS = `Use this server to analyze FFXIV market data via Universalis and item data via XIVAPI.
|
|
1
|
+
export const SERVER_INSTRUCTIONS = `Use this server to analyze FFXIV market data via Universalis and Saddlebag Exchange, plus item data via XIVAPI.
|
|
2
2
|
|
|
3
3
|
Tool guide:
|
|
4
4
|
- universalis_rank_items_by_profitability: best for "most profitable to farm" questions with cost inputs; ranks by demand and profit (defaults to best HQ/NQ price, can filter unmarketable items, and includes supply metrics).
|
|
@@ -25,6 +25,22 @@ Tool guide:
|
|
|
25
25
|
- universalis_get_upload_counts_by_world: upload counts by world.
|
|
26
26
|
- universalis_get_upload_history: daily upload totals for the last 30 days.
|
|
27
27
|
|
|
28
|
+
Saddlebag tools (FFXIV):
|
|
29
|
+
- saddlebag_get_listing_metrics: listing competition metrics and current listings.
|
|
30
|
+
- saddlebag_get_history_metrics: aggregated history metrics and price distributions.
|
|
31
|
+
- saddlebag_get_raw_stats: daily snapshot stats (median, average, sales volume).
|
|
32
|
+
- saddlebag_get_scrip_exchange: gil-per-scrip rankings.
|
|
33
|
+
- saddlebag_get_shopping_list: crafting shopping list (may return exception payloads).
|
|
34
|
+
- saddlebag_get_export_prices: cross-world price comparison for export trading.
|
|
35
|
+
- saddlebag_get_marketshare: marketshare leaderboard snapshot for a server.
|
|
36
|
+
- saddlebag_get_reselling_scan: reselling opportunities (region/DC/vendor filters).
|
|
37
|
+
- saddlebag_get_weekly_price_group_delta: weekly deltas for custom item groups.
|
|
38
|
+
- saddlebag_get_price_check: price alert checks (user-supplied auctions).
|
|
39
|
+
- saddlebag_get_quantity_check: quantity alert checks (user-supplied auctions).
|
|
40
|
+
- saddlebag_get_sale_alert: sale alerts for retainer listings.
|
|
41
|
+
- saddlebag_get_craftsim: crafting profitability scan (use max_results to limit payloads).
|
|
42
|
+
- saddlebag_get_blog_description: item description text lookup.
|
|
43
|
+
|
|
28
44
|
Notes:
|
|
29
45
|
- world_dc_region accepts a world name/ID, data center name, or region.
|
|
30
46
|
- response_format defaults to markdown; use json for structured processing.
|
|
@@ -33,4 +49,8 @@ Notes:
|
|
|
33
49
|
- Materia expansion uses cached XIVAPI data; refresh is controlled by MATERIA_CACHE_TTL_MS and MATERIA_REFRESH.
|
|
34
50
|
- Profitability tool options: marketable_only (default true), min_velocity threshold, include_supply (default true).
|
|
35
51
|
- price_variant defaults to "best".
|
|
52
|
+
- Prefer Universalis for raw listings/sales history; use Saddlebag for competition metrics, rankings, and aggregate distributions.
|
|
53
|
+
- Saddlebag best-deals is excluded (premium gated + inconsistent API validation).
|
|
54
|
+
- Some Saddlebag endpoints call Universalis directly; be mindful of rate limits.
|
|
55
|
+
- saddlebag_get_raw_stats supports item_ids = -1 for all items and can return very large payloads.
|
|
36
56
|
`;
|
package/dist/services/clients.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import Bottleneck from "bottleneck";
|
|
2
|
-
import { DEFAULT_UNIVERSALIS_TIMEOUT_MS, DEFAULT_XIVAPI_TIMEOUT_MS } from "../constants.js";
|
|
2
|
+
import { DEFAULT_SADDLEBAG_TIMEOUT_MS, DEFAULT_UNIVERSALIS_TIMEOUT_MS, DEFAULT_XIVAPI_TIMEOUT_MS, } from "../constants.js";
|
|
3
|
+
import { SaddlebagClient } from "./saddlebag.js";
|
|
3
4
|
import { UniversalisClient } from "./universalis.js";
|
|
4
5
|
import { XivapiClient } from "./xivapi.js";
|
|
5
6
|
export function createClients(options = {}) {
|
|
@@ -15,7 +16,18 @@ export function createClients(options = {}) {
|
|
|
15
16
|
reservoirRefreshAmount: 10,
|
|
16
17
|
reservoirRefreshInterval: 1000,
|
|
17
18
|
});
|
|
19
|
+
const saddlebagLimiter = new Bottleneck({
|
|
20
|
+
maxConcurrent: 4,
|
|
21
|
+
reservoir: 20,
|
|
22
|
+
reservoirRefreshAmount: 10,
|
|
23
|
+
reservoirRefreshInterval: 1000,
|
|
24
|
+
});
|
|
18
25
|
return {
|
|
26
|
+
saddlebag: new SaddlebagClient({
|
|
27
|
+
limiter: saddlebagLimiter,
|
|
28
|
+
timeoutMs: options.saddlebagTimeoutMs ?? DEFAULT_SADDLEBAG_TIMEOUT_MS,
|
|
29
|
+
userAgent: options.userAgent,
|
|
30
|
+
}),
|
|
19
31
|
universalis: new UniversalisClient({
|
|
20
32
|
limiter: universalisLimiter,
|
|
21
33
|
timeoutMs: options.universalisTimeoutMs ?? DEFAULT_UNIVERSALIS_TIMEOUT_MS,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { SADDLEBAG_BASE_URL } from "../constants.js";
|
|
2
|
+
import { requestJson } from "./http.js";
|
|
3
|
+
export class SaddlebagClient {
|
|
4
|
+
baseUrl;
|
|
5
|
+
timeoutMs;
|
|
6
|
+
limiter;
|
|
7
|
+
userAgent;
|
|
8
|
+
constructor(options = {}) {
|
|
9
|
+
this.baseUrl = options.baseUrl ?? SADDLEBAG_BASE_URL;
|
|
10
|
+
this.timeoutMs = options.timeoutMs;
|
|
11
|
+
this.limiter = options.limiter;
|
|
12
|
+
this.userAgent = options.userAgent;
|
|
13
|
+
}
|
|
14
|
+
async post(path, body) {
|
|
15
|
+
return requestJson({
|
|
16
|
+
baseUrl: this.baseUrl,
|
|
17
|
+
path,
|
|
18
|
+
method: "POST",
|
|
19
|
+
body,
|
|
20
|
+
limiter: this.limiter,
|
|
21
|
+
timeoutMs: this.timeoutMs,
|
|
22
|
+
userAgent: this.userAgent,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -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
|
+
}
|