easypost-mcp 2.1.0 → 3.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.
Files changed (35) hide show
  1. package/package.json +1 -1
  2. package/src/audit/AuditLogger.js +20 -0
  3. package/src/constants/toolRisk.js +36 -0
  4. package/src/elicitation/fallback.js +37 -0
  5. package/src/elicitation/fields/catalog.js +171 -0
  6. package/src/pipeline/ExecutionPipeline.js +226 -0
  7. package/src/resources/ResourceManager.js +244 -0
  8. package/src/resources/cache.js +41 -0
  9. package/src/resources/fuzzy.js +57 -0
  10. package/src/resources/staticResources.js +89 -0
  11. package/src/schemas/addressSchemas.js +3 -1
  12. package/src/schemas/batchSchemas.js +2 -1
  13. package/src/schemas/commonSchemas.js +5 -1
  14. package/src/schemas/pickupSchemas.js +4 -1
  15. package/src/schemas/resourceSchemas.js +16 -0
  16. package/src/schemas/returnSchemas.js +5 -1
  17. package/src/schemas/shipmentSchemas.js +8 -6
  18. package/src/server/createServer.js +25 -1
  19. package/src/services/AddressService.js +53 -3
  20. package/src/services/BatchService.js +8 -1
  21. package/src/services/ConfirmationService.js +45 -0
  22. package/src/services/ElicitationService.js +230 -0
  23. package/src/services/IdempotencyStore.js +40 -0
  24. package/src/services/PickupService.js +47 -20
  25. package/src/services/ReturnService.js +37 -7
  26. package/src/services/ShipmentService.js +149 -17
  27. package/src/services/WorkflowStateStore.js +70 -0
  28. package/src/services/index.js +20 -5
  29. package/src/tools/ToolDefinition.js +13 -2
  30. package/src/tools/definitions/pickups.js +2 -2
  31. package/src/tools/definitions/resources.js +59 -0
  32. package/src/tools/definitions/shipments.js +3 -3
  33. package/src/tools/index.js +2 -0
  34. package/src/validators/antiHallucination.js +109 -8
  35. package/src/validators/validate.js +2 -2
@@ -1,13 +1,25 @@
1
1
  import { mapShipment, mapRate, mapCollection } from "../adapters/easypost/responseMappers.js";
2
2
  import { NotFoundError } from "../errors/AppError.js";
3
+ import { createFallbackResponse } from "../elicitation/fallback.js";
4
+
5
+ function mapSelectableRates(rates) {
6
+ return rates.map((rate, index) => ({
7
+ option: index + 1,
8
+ ...mapRate(rate),
9
+ }));
10
+ }
3
11
 
4
12
  class ShipmentService {
5
- constructor(easypostClient) {
13
+ constructor(easypostClient, { confirmations, elicitation, idempotency } = {}) {
6
14
  this.easypost = easypostClient;
15
+ this.confirmations = confirmations;
16
+ this.elicitation = elicitation;
17
+ this.idempotency = idempotency;
7
18
  }
8
19
 
9
20
  async createShipment(input, context) {
10
- const shipment = await this.easypost.execute(
21
+ const operation = async () => {
22
+ const shipment = await this.easypost.execute(
11
23
  "shipment.create",
12
24
  (client) => client.Shipment.create({
13
25
  from_address: input.from_address,
@@ -19,9 +31,30 @@ class ShipmentService {
19
31
  reference: input.reference,
20
32
  }),
21
33
  context
22
- );
23
-
24
- return { ok: true, shipment: mapShipment(shipment), rates: (shipment.rates || []).map(mapRate) };
34
+ );
35
+
36
+ const rawRates = shipment.rates || [];
37
+ const rates = mapSelectableRates(rawRates);
38
+ context.audit?.record("rates_returned", {
39
+ correlationId: context.correlationId,
40
+ shipment_id: shipment.id,
41
+ rate_count: rates.length,
42
+ rate_ids: rates.map((rate) => rate.id),
43
+ });
44
+
45
+ return {
46
+ ok: true,
47
+ shipment: mapShipment(shipment),
48
+ rates,
49
+ next_step: rates.length
50
+ ? "Choose a numbered rate option or exact rate_id from rates and call buy_shipping_label with confirm=true."
51
+ : "No rates were returned by the carrier accounts for this shipment.",
52
+ };
53
+ };
54
+
55
+ if (!this.idempotency || !input.idempotency_key) return operation();
56
+ const { reused, result } = await this.idempotency.run(`create_shipment:${input.idempotency_key}`, operation);
57
+ return reused ? { ...result, idempotent_replay: true } : result;
25
58
  }
26
59
 
27
60
  async buyShippingLabel(input, context) {
@@ -33,19 +66,104 @@ class ShipmentService {
33
66
 
34
67
  if (!shipment) throw new NotFoundError("shipment", input.shipment_id);
35
68
 
36
- const rate = input.rate_id
37
- ? (shipment.rates || []).find((candidate) => candidate.id === input.rate_id)
38
- : (shipment.rates || []).find((candidate) => candidate.carrier === input.carrier && candidate.service === input.service);
39
-
40
- if (!rate) throw new NotFoundError("rate", input.rate_id || `${input.carrier}/${input.service}`);
69
+ const rates = shipment.rates || [];
70
+ if (!input.rate_id && !input.rate_option) {
71
+ const elicited = await this.elicitation?.selectRateForLabel({
72
+ server: context.server,
73
+ shipmentId: input.shipment_id,
74
+ rates,
75
+ confirmDefault: input.confirm === true,
76
+ });
77
+
78
+ if (elicited?.selected) {
79
+ input.rate_option = elicited.rate_option;
80
+ input.confirm = elicited.confirm;
81
+ context.audit?.record("rate_selection_elicited", {
82
+ correlationId: context.correlationId,
83
+ shipment_id: input.shipment_id,
84
+ rate_option: input.rate_option,
85
+ confirmed: input.confirm,
86
+ });
87
+ } else {
88
+ context.audit?.record("rate_selection_fallback", {
89
+ correlationId: context.correlationId,
90
+ shipment_id: input.shipment_id,
91
+ reason: elicited?.reason,
92
+ });
93
+ return {
94
+ ...createFallbackResponse({
95
+ errorCode: "RATE_SELECTION_REQUIRED",
96
+ message: "Select a rate using the client form if available, or provide one numbered rate_option from available_rates.",
97
+ missingFields: ["rate_option"],
98
+ availableOptions: mapSelectableRates(rates),
99
+ examples: { rate_option: [1, 2] },
100
+ nextAction: "Choose one available option and retry with rate_option, or use a client that supports elicitation.form.",
101
+ metadata: {
102
+ elicitation_supported: this.elicitation?.supportsForm(context.server) === true,
103
+ fallback_reason: elicited?.reason,
104
+ },
105
+ }),
106
+ };
107
+ }
108
+ }
41
109
 
42
- const bought = await this.easypost.execute(
43
- "shipment.buy",
44
- (client) => client.Shipment.buy(input.shipment_id, rate, input.insurance),
45
- context
46
- );
47
-
48
- return { ok: true, shipment: mapShipment(bought), purchased_rate: mapRate(rate) };
110
+ const rate = input.rate_id
111
+ ? rates.find((candidate) => candidate.id === input.rate_id)
112
+ : rates[input.rate_option - 1];
113
+
114
+ if (!rate) {
115
+ context.audit?.record("rate_selection_failed", {
116
+ correlationId: context.correlationId,
117
+ shipment_id: input.shipment_id,
118
+ requested_rate_id: input.rate_id,
119
+ requested_rate_option: input.rate_option,
120
+ available_rate_ids: rates.map((candidate) => candidate.id),
121
+ });
122
+ return {
123
+ ...createFallbackResponse({
124
+ errorCode: "RATE_ID_NOT_AVAILABLE",
125
+ message: "The requested rate selection is not available on this shipment. Select one option number or exact id from available_rates.",
126
+ missingFields: ["rate_option"],
127
+ availableOptions: mapSelectableRates(rates),
128
+ examples: { rate_option: [1], rate_id: rates[0]?.id ? [rates[0].id] : [] },
129
+ nextAction: "Retry with a valid rate_option or exact rate_id from available_options.",
130
+ metadata: {
131
+ requested_rate_id: input.rate_id,
132
+ requested_rate_option: input.rate_option,
133
+ },
134
+ }),
135
+ };
136
+ }
137
+
138
+ await this.confirmations?.requireConfirmed(input, {
139
+ action: "buy_shipping_label",
140
+ message: `You are about to buy a ${rate.carrier} ${rate.service} label for ${rate.rate} ${rate.currency || "USD"}. Proceed?`,
141
+ details: {
142
+ shipment_id: input.shipment_id,
143
+ rate: mapRate(rate),
144
+ },
145
+ }, context);
146
+
147
+ const operation = async () => {
148
+ context.audit?.record("label_purchase_confirmed", {
149
+ correlationId: context.correlationId,
150
+ shipment_id: input.shipment_id,
151
+ rate_id: rate.id,
152
+ rate_option: input.rate_option,
153
+ });
154
+
155
+ const bought = await this.easypost.execute(
156
+ "shipment.buy",
157
+ (client) => client.Shipment.buy(input.shipment_id, rate, input.insurance),
158
+ context
159
+ );
160
+
161
+ return { ok: true, shipment: mapShipment(bought), purchased_rate: mapRate(rate) };
162
+ };
163
+
164
+ if (!this.idempotency || !input.idempotency_key) return operation();
165
+ const { reused, result } = await this.idempotency.run(`buy_shipping_label:${input.idempotency_key}`, operation);
166
+ return reused ? { ...result, idempotent_replay: true } : result;
49
167
  }
50
168
 
51
169
  async getShipment(input, context) {
@@ -72,6 +190,13 @@ class ShipmentService {
72
190
  }
73
191
 
74
192
  async refundShipment(input, context) {
193
+ await this.confirmations?.requireConfirmed(input, {
194
+ action: "refund_shipment",
195
+ message: `You are about to void/refund shipment ${input.shipment_id}. Proceed?`,
196
+ details: { shipment_id: input.shipment_id },
197
+ typedConfirmation: "REFUND",
198
+ }, context);
199
+
75
200
  const shipment = await this.easypost.execute(
76
201
  "shipment.refund",
77
202
  (client) => client.Shipment.refund(input.shipment_id),
@@ -85,6 +210,13 @@ class ShipmentService {
85
210
  }
86
211
 
87
212
  async insureShipment(input, context) {
213
+ await this.confirmations?.requireConfirmed(input, {
214
+ action: "insure_shipment",
215
+ message: `You are about to add insurance for ${input.amount} to shipment ${input.shipment_id}. Proceed?`,
216
+ details: { shipment_id: input.shipment_id, amount: input.amount },
217
+ typedConfirmation: Number(input.amount) >= 500 ? "INSURE" : undefined,
218
+ }, context);
219
+
88
220
  const shipment = await this.easypost.execute(
89
221
  "shipment.insure",
90
222
  (client) => client.Shipment.insure(input.shipment_id, input.amount),
@@ -0,0 +1,70 @@
1
+ import { randomUUID } from "node:crypto";
2
+
3
+ class WorkflowStateStore {
4
+ constructor({ ttlMs = 30 * 60 * 1000 } = {}) {
5
+ this.ttlMs = ttlMs;
6
+ this.entries = new Map();
7
+ }
8
+
9
+ _purgeExpired() {
10
+ const now = Date.now();
11
+ for (const [key, entry] of this.entries.entries()) {
12
+ if (entry.expiresAt <= now) this.entries.delete(key);
13
+ }
14
+ }
15
+
16
+ create({ toolName, input = {}, reason, correlationId }) {
17
+ this._purgeExpired();
18
+ const workflowId = `wf_${randomUUID()}`;
19
+ this.entries.set(workflowId, {
20
+ toolName,
21
+ input,
22
+ reason,
23
+ correlationId,
24
+ createdAt: new Date().toISOString(),
25
+ expiresAt: Date.now() + this.ttlMs,
26
+ });
27
+ return workflowId;
28
+ }
29
+
30
+ get(workflowId) {
31
+ if (!workflowId) return null;
32
+ this._purgeExpired();
33
+ return this.entries.get(workflowId) || null;
34
+ }
35
+
36
+ merge(workflowId, input = {}) {
37
+ const state = this.get(workflowId);
38
+ if (!state) return input;
39
+ return deepMerge(state.input, input);
40
+ }
41
+
42
+ update(workflowId, input) {
43
+ const state = this.get(workflowId);
44
+ if (!state) return;
45
+ this.entries.set(workflowId, {
46
+ ...state,
47
+ input,
48
+ expiresAt: Date.now() + this.ttlMs,
49
+ });
50
+ }
51
+ }
52
+
53
+ function isPlainObject(value) {
54
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
55
+ }
56
+
57
+ function deepMerge(base, override) {
58
+ const output = { ...(isPlainObject(base) ? base : {}) };
59
+ for (const [key, value] of Object.entries(override || {})) {
60
+ if (key === "workflow_id") continue;
61
+ if (isPlainObject(value) && isPlainObject(output[key])) {
62
+ output[key] = deepMerge(output[key], value);
63
+ } else {
64
+ output[key] = value;
65
+ }
66
+ }
67
+ return output;
68
+ }
69
+
70
+ export { WorkflowStateStore, deepMerge };
@@ -1,4 +1,9 @@
1
1
  import { EasyPostClient } from "../adapters/easypost/EasyPostClient.js";
2
+ import { ResourceManager } from "../resources/ResourceManager.js";
3
+ import { ConfirmationService } from "./ConfirmationService.js";
4
+ import { ElicitationService } from "./ElicitationService.js";
5
+ import { IdempotencyStore } from "./IdempotencyStore.js";
6
+ import { WorkflowStateStore } from "./WorkflowStateStore.js";
2
7
  import { ShipmentService } from "./ShipmentService.js";
3
8
  import { AddressService } from "./AddressService.js";
4
9
  import { TrackingService } from "./TrackingService.js";
@@ -9,14 +14,24 @@ import { OrderService } from "./OrderService.js";
9
14
 
10
15
  function createServices() {
11
16
  const easypostClient = new EasyPostClient();
17
+ const resources = new ResourceManager(easypostClient);
18
+ const confirmations = new ConfirmationService();
19
+ const elicitation = new ElicitationService();
20
+ const idempotency = new IdempotencyStore();
21
+ const workflowState = new WorkflowStateStore();
12
22
 
13
23
  return {
14
- shipments: new ShipmentService(easypostClient),
15
- addresses: new AddressService(easypostClient),
24
+ resources,
25
+ confirmations,
26
+ elicitation,
27
+ idempotency,
28
+ workflowState,
29
+ shipments: new ShipmentService(easypostClient, { confirmations, elicitation, idempotency }),
30
+ addresses: new AddressService(easypostClient, { confirmations }),
16
31
  tracking: new TrackingService(easypostClient),
17
- returns: new ReturnService(easypostClient),
18
- pickups: new PickupService(easypostClient),
19
- batches: new BatchService(easypostClient),
32
+ returns: new ReturnService(easypostClient, { confirmations, elicitation }),
33
+ pickups: new PickupService(easypostClient, { confirmations, idempotency }),
34
+ batches: new BatchService(easypostClient, { confirmations }),
20
35
  orders: new OrderService(easypostClient),
21
36
  };
22
37
  }
@@ -1,13 +1,15 @@
1
1
  import { toInputSchema, validateInput } from "../validators/validate.js";
2
+ import { riskForTool } from "../constants/toolRisk.js";
2
3
 
3
4
  class ToolDefinition {
4
- constructor({ name, title, description, category, schema, handler, annotations = {} }) {
5
+ constructor({ name, title, description, category, schema, handler, risk, annotations = {} }) {
5
6
  this.name = name;
6
7
  this.title = title;
7
8
  this.description = description;
8
9
  this.category = category;
9
10
  this.schema = schema;
10
11
  this.handler = handler;
12
+ this.risk = risk || riskForTool(name);
11
13
  this.annotations = annotations;
12
14
  }
13
15
 
@@ -19,13 +21,22 @@ class ToolDefinition {
19
21
  inputSchema: toInputSchema(this.schema, this.name),
20
22
  annotations: {
21
23
  category: this.category,
24
+ risk: this.risk,
22
25
  ...this.annotations,
23
26
  },
24
27
  };
25
28
  }
26
29
 
27
30
  async execute(args, context) {
28
- const input = validateInput(this.schema, args);
31
+ if (context?.pipeline) {
32
+ return context.pipeline.run(this, args, context);
33
+ }
34
+
35
+ const input = validateInput(this.schema, args, {
36
+ resourceManager: context?.resources,
37
+ toolName: this.name,
38
+ risk: this.risk,
39
+ });
29
40
  return this.handler(input, context);
30
41
  }
31
42
  }
@@ -8,7 +8,7 @@ function pickupTools(services) {
8
8
  name: "schedule_pickup",
9
9
  title: "Schedule Pickup",
10
10
  category: categories.PICKUPS,
11
- description: "Schedule a carrier pickup for one or more shipments.",
11
+ description: "Create a carrier pickup request for one or more shipments. Requires confirm=true and never auto-buys a pickup rate.",
12
12
  schema: schedulePickupSchema,
13
13
  handler: (input, context) => services.pickups.schedulePickup(input, context),
14
14
  }),
@@ -16,7 +16,7 @@ function pickupTools(services) {
16
16
  name: "cancel_pickup",
17
17
  title: "Cancel Pickup",
18
18
  category: categories.PICKUPS,
19
- description: "Cancel a scheduled carrier pickup.",
19
+ description: "Cancel a scheduled carrier pickup. Requires confirm=true.",
20
20
  schema: cancelPickupSchema,
21
21
  handler: (input, context) => services.pickups.cancelPickup(input, context),
22
22
  }),
@@ -0,0 +1,59 @@
1
+ import { ToolDefinition } from "../ToolDefinition.js";
2
+ import categories from "../../constants/toolCategories.js";
3
+ import { TOOL_RISK } from "../../constants/toolRisk.js";
4
+ import { listResourcesSchema, validateCarrierSchema, validateServiceSchema } from "../../schemas/resourceSchemas.js";
5
+
6
+ function resourceTools(services) {
7
+ return [
8
+ new ToolDefinition({
9
+ name: "get_carriers",
10
+ title: "Get Carriers",
11
+ category: categories.SHIPMENTS,
12
+ risk: TOOL_RISK.LOW,
13
+ description: "Return compact authoritative carrier metadata from the server-side resource cache.",
14
+ schema: listResourcesSchema,
15
+ handler: async (input, context) => {
16
+ if (input.refresh) await services.resources.refresh(context);
17
+ return {
18
+ ok: true,
19
+ carriers: services.resources.getCarriers(),
20
+ resource_context: services.resources.getOperationalContext(),
21
+ };
22
+ },
23
+ }),
24
+ new ToolDefinition({
25
+ name: "validate_carrier",
26
+ title: "Validate Carrier",
27
+ category: categories.SHIPMENTS,
28
+ risk: TOOL_RISK.LOW,
29
+ description: "Validate a carrier with exact matching. Fuzzy matches are returned only as suggestions.",
30
+ schema: validateCarrierSchema,
31
+ handler: async (input) => {
32
+ const result = services.resources.validateCarrier(input.carrier);
33
+ return {
34
+ ok: result.valid,
35
+ validation: result,
36
+ clarification_required: !result.valid && result.suggestions.length > 0,
37
+ };
38
+ },
39
+ }),
40
+ new ToolDefinition({
41
+ name: "validate_service",
42
+ title: "Validate Service",
43
+ category: categories.SHIPMENTS,
44
+ risk: TOOL_RISK.LOW,
45
+ description: "Validate an exact carrier/service combination against cached authoritative resources.",
46
+ schema: validateServiceSchema,
47
+ handler: async (input) => {
48
+ const result = services.resources.validateService(input.carrier, input.service);
49
+ return {
50
+ ok: result.valid,
51
+ validation: result,
52
+ clarification_required: !result.valid && result.suggestions.length > 0,
53
+ };
54
+ },
55
+ }),
56
+ ];
57
+ }
58
+
59
+ export { resourceTools };
@@ -16,7 +16,7 @@ function shipmentTools(services) {
16
16
  name: "buy_shipping_label",
17
17
  title: "Buy Shipping Label",
18
18
  category: categories.SHIPMENTS,
19
- description: "Buy a shipping label for an existing shipment using a rate id or carrier/service pair.",
19
+ description: "Buy a shipping label for an existing shipment using client elicitation, exact returned rate_id, or numbered rate_option. Requires confirmation and idempotency_key.",
20
20
  schema: schemas.buyShippingLabelSchema,
21
21
  handler: (input, context) => services.shipments.buyShippingLabel(input, context),
22
22
  }),
@@ -40,7 +40,7 @@ function shipmentTools(services) {
40
40
  name: "cancel_shipment",
41
41
  title: "Cancel Shipment",
42
42
  category: categories.SHIPMENTS,
43
- description: "Cancel or void a shipment when supported by the carrier. Uses EasyPost refund semantics for purchased labels.",
43
+ description: "Cancel or void a shipment when supported by the carrier. Requires confirm=true and uses EasyPost refund semantics for purchased labels.",
44
44
  schema: schemas.cancelShipmentSchema,
45
45
  handler: (input, context) => services.shipments.cancelShipment(input, context),
46
46
  }),
@@ -48,7 +48,7 @@ function shipmentTools(services) {
48
48
  name: "refund_shipment",
49
49
  title: "Refund Shipment",
50
50
  category: categories.SHIPMENTS,
51
- description: "Request a refund for a purchased shipment label.",
51
+ description: "Request a refund for a purchased shipment label. Requires confirm=true.",
52
52
  schema: schemas.refundShipmentSchema,
53
53
  handler: (input, context) => services.shipments.refundShipment(input, context),
54
54
  }),
@@ -6,6 +6,7 @@ import { returnTools } from "./definitions/returns.js";
6
6
  import { pickupTools } from "./definitions/pickups.js";
7
7
  import { batchTools } from "./definitions/batches.js";
8
8
  import { orderTools } from "./definitions/orders.js";
9
+ import { resourceTools } from "./definitions/resources.js";
9
10
 
10
11
  function createToolRegistry(services) {
11
12
  const registry = new ToolRegistry();
@@ -17,6 +18,7 @@ function createToolRegistry(services) {
17
18
  ...pickupTools(services),
18
19
  ...batchTools(services),
19
20
  ...orderTools(services),
21
+ ...resourceTools(services),
20
22
  ]);
21
23
  return registry;
22
24
  }
@@ -4,17 +4,31 @@ const PLACEHOLDER_PATTERNS = [
4
4
  /\b(unknown|n\/a|na|null|none|tbd|todo|test|sample|example|placeholder)\b/i,
5
5
  /\b(123 main|main street|fake street|any street|your address|shipping address)\b/i,
6
6
  /\b(john doe|jane doe|acme|foo|bar)\b/i,
7
- /^(.)\1{4,}$/,
7
+ /^(.)\1{4,}$/i,
8
+ /^(0{6,}|1{6,}|9{6,}|123456\d*|987654\d*)$/i,
9
+ /\b(lorem ipsum|asdf|qwerty|abcdef)\b/i,
8
10
  ];
9
11
 
12
+ function issue(path, code, message, confidence = 0.2, suggestions = []) {
13
+ return { path, code, message, confidence, suggestions };
14
+ }
15
+
10
16
  function inspectString(path, value, issues) {
11
17
  if (!value.trim()) {
12
- issues.push({ path, reason: "Empty string" });
18
+ issues.push(issue(path, "EMPTY_STRING", "Empty string", 0));
13
19
  return;
14
20
  }
15
21
 
16
22
  if (PLACEHOLDER_PATTERNS.some((pattern) => pattern.test(value))) {
17
- issues.push({ path, reason: "Looks like autogenerated placeholder data" });
23
+ issues.push(issue(path, "SUSPICIOUS_PLACEHOLDER", "Looks like autogenerated placeholder data", 0.1));
24
+ }
25
+
26
+ if (/(.)\1{5,}/i.test(value.replace(/\s+/g, ""))) {
27
+ issues.push(issue(path, "REPEATED_CHARACTER_GARBAGE", "Contains repeated-character garbage", 0.1));
28
+ }
29
+
30
+ if (value.length > 8 && /^[^a-z0-9]+$/i.test(value)) {
31
+ issues.push(issue(path, "NON_SEMANTIC_STRING", "String contains no semantic content", 0.1));
18
32
  }
19
33
  }
20
34
 
@@ -34,16 +48,103 @@ function inspectObject(value, path, issues) {
34
48
  }
35
49
  }
36
50
 
37
- function assertNotHallucinated(input) {
51
+ function inspectResources(input, resourceManager, issues) {
52
+ if (!resourceManager || !input || typeof input !== "object") return;
53
+
54
+ inspectResourceFields(input, "", resourceManager, issues);
55
+
56
+ if (Array.isArray(input.carrier_accounts)) {
57
+ for (const [index, carrierAccount] of input.carrier_accounts.entries()) {
58
+ const result = resourceManager.validateCarrierAccount(carrierAccount);
59
+ if (!result.valid) {
60
+ issues.push(issue(`carrier_accounts[${index}]`, "UNKNOWN_CARRIER_ACCOUNT", `Unknown carrier account: ${carrierAccount}`, 0, result.suggestions));
61
+ }
62
+ }
63
+ }
64
+
65
+ }
66
+
67
+ function inspectResourceFields(value, path, resourceManager, issues) {
68
+ if (Array.isArray(value)) {
69
+ value.forEach((item, index) => inspectResourceFields(item, `${path}[${index}]`, resourceManager, issues));
70
+ return;
71
+ }
72
+
73
+ if (!value || typeof value !== "object") return;
74
+
75
+ if (value.country) {
76
+ const result = resourceManager.validateCountry(value.country);
77
+ if (!result.valid) {
78
+ issues.push(issue(path ? `${path}.country` : "country", "UNKNOWN_COUNTRY", `Unknown country: ${value.country}`, 0, result.suggestions));
79
+ }
80
+ }
81
+
82
+ if (value.country && value.state) {
83
+ const result = resourceManager.validateState(value.country, value.state);
84
+ if (!result.valid) {
85
+ issues.push(issue(path ? `${path}.state` : "state", "UNKNOWN_STATE", `Unknown state/province for ${value.country}: ${value.state}`, 0, result.suggestions));
86
+ }
87
+ }
88
+
89
+ if (value.payment_method) {
90
+ const result = resourceManager.validatePaymentMethod(value.payment_method);
91
+ if (!result.valid) {
92
+ issues.push(issue(path ? `${path}.payment_method` : "payment_method", "UNKNOWN_PAYMENT_METHOD", `Unknown payment method: ${value.payment_method}`, 0, result.suggestions));
93
+ }
94
+ }
95
+
96
+ if (value.predefined_package) {
97
+ const result = resourceManager.validatePackageType(value.predefined_package);
98
+ if (!result.valid) {
99
+ issues.push(issue(path ? `${path}.predefined_package` : "predefined_package", "UNKNOWN_PACKAGE_TYPE", `Unknown package type: ${value.predefined_package}`, 0, result.suggestions));
100
+ }
101
+ }
102
+
103
+ if (value.carrier) {
104
+ const result = resourceManager.validateCarrier(value.carrier);
105
+ if (!result.valid) {
106
+ issues.push(issue(path ? `${path}.carrier` : "carrier", "UNKNOWN_CARRIER", `Unsupported carrier: ${value.carrier}`, 0, result.suggestions));
107
+ } else if (value.service) {
108
+ const serviceResult = resourceManager.validateService(value.carrier, value.service);
109
+ if (!serviceResult.valid) {
110
+ issues.push(issue(path ? `${path}.service` : "service", "UNSUPPORTED_SERVICE_FOR_CARRIER", serviceResult.issues?.[0]?.message || "Unsupported carrier/service combination", 0, serviceResult.suggestions));
111
+ }
112
+ }
113
+ }
114
+
115
+ for (const [key, nested] of Object.entries(value)) {
116
+ inspectResourceFields(nested, path ? `${path}.${key}` : key, resourceManager, issues);
117
+ }
118
+ }
119
+
120
+ function validateAntiHallucination(input, options = {}) {
38
121
  const issues = [];
39
122
  inspectObject(input, "", issues);
123
+ if (!["validate_carrier", "validate_service"].includes(options.toolName)) {
124
+ inspectResources(input, options.resourceManager, issues);
125
+ }
126
+
127
+ const confidence = issues.length
128
+ ? Math.min(...issues.map((item) => item.confidence ?? 0.2))
129
+ : 1;
130
+
131
+ return {
132
+ valid: issues.length === 0,
133
+ confidence,
134
+ issues,
135
+ suggestions: issues.flatMap((item) => item.suggestions || []),
136
+ };
137
+ }
138
+
139
+ function assertNotHallucinated(input, options = {}) {
140
+ const result = validateAntiHallucination(input, options);
40
141
 
41
- if (issues.length) {
142
+ if (!result.valid) {
42
143
  throw new AntiHallucinationError(
43
- "Input appears to contain placeholder or AI-generated shipping data. Provide real, verified shipment details.",
44
- issues
144
+ "Input appears unsupported, ambiguous, or AI-generated. Provide exact values from verified resources.",
145
+ result
45
146
  );
46
147
  }
47
148
  }
48
149
 
49
- export { assertNotHallucinated };
150
+ export { assertNotHallucinated, validateAntiHallucination };
@@ -3,7 +3,7 @@ import { ValidationError } from "../errors/AppError.js";
3
3
  import { sanitizeInput } from "../utils/sanitize.js";
4
4
  import { assertNotHallucinated } from "./antiHallucination.js";
5
5
 
6
- function validateInput(schema, input) {
6
+ function validateInput(schema, input, options = {}) {
7
7
  const sanitized = sanitizeInput(input || {});
8
8
  const result = schema.safeParse(sanitized);
9
9
 
@@ -11,7 +11,7 @@ function validateInput(schema, input) {
11
11
  throw new ValidationError("Tool input validation failed", result.error.issues);
12
12
  }
13
13
 
14
- assertNotHallucinated(result.data);
14
+ assertNotHallucinated(result.data, options);
15
15
  return result.data;
16
16
  }
17
17