easypost-mcp 2.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 (50) hide show
  1. package/bin/easypost-mcp.js +14 -0
  2. package/package.json +71 -0
  3. package/src/adapters/easypost/EasyPostClient.js +75 -0
  4. package/src/adapters/easypost/responseMappers.js +105 -0
  5. package/src/cli/commands/start.js +47 -0
  6. package/src/cli/index.js +45 -0
  7. package/src/config/bootstrap.js +42 -0
  8. package/src/config/index.js +17 -0
  9. package/src/config/loadConfig.js +66 -0
  10. package/src/constants/toolCategories.js +10 -0
  11. package/src/errors/AppError.js +57 -0
  12. package/src/errors/easypostErrorMapper.js +21 -0
  13. package/src/index.js +6 -0
  14. package/src/logging/logger.js +77 -0
  15. package/src/middleware/errorHandler.js +49 -0
  16. package/src/middleware/rateLimiter.js +29 -0
  17. package/src/schemas/addressSchemas.js +27 -0
  18. package/src/schemas/batchSchemas.js +24 -0
  19. package/src/schemas/commonSchemas.js +15 -0
  20. package/src/schemas/orderSchemas.js +15 -0
  21. package/src/schemas/parcelSchemas.js +13 -0
  22. package/src/schemas/pickupSchemas.js +17 -0
  23. package/src/schemas/returnSchemas.js +14 -0
  24. package/src/schemas/shipmentSchemas.js +56 -0
  25. package/src/schemas/trackingSchemas.js +16 -0
  26. package/src/server/createServer.js +90 -0
  27. package/src/server/transports/http.js +102 -0
  28. package/src/server/transports/stdio.js +10 -0
  29. package/src/services/AddressService.js +36 -0
  30. package/src/services/BatchService.js +38 -0
  31. package/src/services/OrderService.js +25 -0
  32. package/src/services/PickupService.js +42 -0
  33. package/src/services/ReturnService.js +33 -0
  34. package/src/services/ShipmentService.js +97 -0
  35. package/src/services/TrackingService.js +34 -0
  36. package/src/services/index.js +24 -0
  37. package/src/tools/ToolDefinition.js +33 -0
  38. package/src/tools/definitions/address.js +26 -0
  39. package/src/tools/definitions/batches.js +34 -0
  40. package/src/tools/definitions/orders.js +26 -0
  41. package/src/tools/definitions/pickups.js +26 -0
  42. package/src/tools/definitions/returns.js +18 -0
  43. package/src/tools/definitions/shipments.js +74 -0
  44. package/src/tools/definitions/tracking.js +26 -0
  45. package/src/tools/index.js +24 -0
  46. package/src/tools/registry.js +37 -0
  47. package/src/utils/correlation.js +7 -0
  48. package/src/utils/sanitize.js +53 -0
  49. package/src/validators/antiHallucination.js +49 -0
  50. package/src/validators/validate.js +28 -0
@@ -0,0 +1,97 @@
1
+ import { mapShipment, mapRate, mapCollection } from "../adapters/easypost/responseMappers.js";
2
+ import { NotFoundError } from "../errors/AppError.js";
3
+
4
+ class ShipmentService {
5
+ constructor(easypostClient) {
6
+ this.easypost = easypostClient;
7
+ }
8
+
9
+ async createShipment(input, context) {
10
+ const shipment = await this.easypost.execute(
11
+ "shipment.create",
12
+ (client) => client.Shipment.create({
13
+ from_address: input.from_address,
14
+ to_address: input.to_address,
15
+ parcel: input.parcel,
16
+ carrier_accounts: input.carrier_accounts,
17
+ customs_info: input.customs_info,
18
+ options: input.options,
19
+ reference: input.reference,
20
+ }),
21
+ context
22
+ );
23
+
24
+ return { ok: true, shipment: mapShipment(shipment), rates: (shipment.rates || []).map(mapRate) };
25
+ }
26
+
27
+ async buyShippingLabel(input, context) {
28
+ const shipment = await this.easypost.execute(
29
+ "shipment.retrieve",
30
+ (client) => client.Shipment.retrieve(input.shipment_id),
31
+ context
32
+ );
33
+
34
+ if (!shipment) throw new NotFoundError("shipment", input.shipment_id);
35
+
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}`);
41
+
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) };
49
+ }
50
+
51
+ async getShipment(input, context) {
52
+ const shipment = await this.easypost.execute(
53
+ "shipment.retrieve",
54
+ (client) => client.Shipment.retrieve(input.shipment_id),
55
+ context
56
+ );
57
+ return { ok: true, shipment: mapShipment(shipment) };
58
+ }
59
+
60
+ async listShipments(input, context) {
61
+ const collection = await this.easypost.execute(
62
+ "shipment.list",
63
+ (client) => client.Shipment.all(input),
64
+ context
65
+ );
66
+ return { ok: true, ...mapCollection(collection, mapShipment) };
67
+ }
68
+
69
+ async estimateRates(input, context) {
70
+ const result = await this.createShipment(input, context);
71
+ return { ok: true, shipment_id: result.shipment.id, rates: result.rates };
72
+ }
73
+
74
+ async refundShipment(input, context) {
75
+ const shipment = await this.easypost.execute(
76
+ "shipment.refund",
77
+ (client) => client.Shipment.refund(input.shipment_id),
78
+ context
79
+ );
80
+ return { ok: true, shipment: mapShipment(shipment), refund_status: shipment.refund_status };
81
+ }
82
+
83
+ async cancelShipment(input, context) {
84
+ return this.refundShipment(input, context);
85
+ }
86
+
87
+ async insureShipment(input, context) {
88
+ const shipment = await this.easypost.execute(
89
+ "shipment.insure",
90
+ (client) => client.Shipment.insure(input.shipment_id, input.amount),
91
+ context
92
+ );
93
+ return { ok: true, shipment: mapShipment(shipment), insured_amount: input.amount };
94
+ }
95
+ }
96
+
97
+ export { ShipmentService };
@@ -0,0 +1,34 @@
1
+ import { mapTracker } from "../adapters/easypost/responseMappers.js";
2
+
3
+ class TrackingService {
4
+ constructor(easypostClient) {
5
+ this.easypost = easypostClient;
6
+ }
7
+
8
+ async trackPackage(input, context) {
9
+ const tracker = await this.easypost.execute(
10
+ "tracker.create",
11
+ (client) => client.Tracker.create({
12
+ tracking_code: input.tracking_code,
13
+ carrier: input.carrier,
14
+ }),
15
+ context
16
+ );
17
+
18
+ return { ok: true, tracker: mapTracker(tracker) };
19
+ }
20
+
21
+ async getTrackingHistory(input, context) {
22
+ const tracker = await this.easypost.execute(
23
+ "tracker.retrieve",
24
+ (client) => input.tracker_id
25
+ ? client.Tracker.retrieve(input.tracker_id)
26
+ : client.Tracker.create({ tracking_code: input.tracking_code, carrier: input.carrier }),
27
+ context
28
+ );
29
+
30
+ return { ok: true, tracker: mapTracker(tracker), history: mapTracker(tracker)?.tracking_details || [] };
31
+ }
32
+ }
33
+
34
+ export { TrackingService };
@@ -0,0 +1,24 @@
1
+ import { EasyPostClient } from "../adapters/easypost/EasyPostClient.js";
2
+ import { ShipmentService } from "./ShipmentService.js";
3
+ import { AddressService } from "./AddressService.js";
4
+ import { TrackingService } from "./TrackingService.js";
5
+ import { ReturnService } from "./ReturnService.js";
6
+ import { PickupService } from "./PickupService.js";
7
+ import { BatchService } from "./BatchService.js";
8
+ import { OrderService } from "./OrderService.js";
9
+
10
+ function createServices() {
11
+ const easypostClient = new EasyPostClient();
12
+
13
+ return {
14
+ shipments: new ShipmentService(easypostClient),
15
+ addresses: new AddressService(easypostClient),
16
+ tracking: new TrackingService(easypostClient),
17
+ returns: new ReturnService(easypostClient),
18
+ pickups: new PickupService(easypostClient),
19
+ batches: new BatchService(easypostClient),
20
+ orders: new OrderService(easypostClient),
21
+ };
22
+ }
23
+
24
+ export { createServices };
@@ -0,0 +1,33 @@
1
+ import { toInputSchema, validateInput } from "../validators/validate.js";
2
+
3
+ class ToolDefinition {
4
+ constructor({ name, title, description, category, schema, handler, annotations = {} }) {
5
+ this.name = name;
6
+ this.title = title;
7
+ this.description = description;
8
+ this.category = category;
9
+ this.schema = schema;
10
+ this.handler = handler;
11
+ this.annotations = annotations;
12
+ }
13
+
14
+ toMcpTool() {
15
+ return {
16
+ name: this.name,
17
+ title: this.title,
18
+ description: this.description,
19
+ inputSchema: toInputSchema(this.schema, this.name),
20
+ annotations: {
21
+ category: this.category,
22
+ ...this.annotations,
23
+ },
24
+ };
25
+ }
26
+
27
+ async execute(args, context) {
28
+ const input = validateInput(this.schema, args);
29
+ return this.handler(input, context);
30
+ }
31
+ }
32
+
33
+ export { ToolDefinition };
@@ -0,0 +1,26 @@
1
+ import { ToolDefinition } from "../ToolDefinition.js";
2
+ import categories from "../../constants/toolCategories.js";
3
+ import { verifyAddressSchema, createAddressSchema } from "../../schemas/addressSchemas.js";
4
+
5
+ function addressTools(services) {
6
+ return [
7
+ new ToolDefinition({
8
+ name: "verify_address",
9
+ title: "Verify Address",
10
+ category: categories.ADDRESS,
11
+ description: "Verify a real shipping address and return normalized verification status and messages.",
12
+ schema: verifyAddressSchema,
13
+ handler: (input, context) => services.addresses.verifyAddress(input, context),
14
+ }),
15
+ new ToolDefinition({
16
+ name: "create_address",
17
+ title: "Create Address",
18
+ category: categories.ADDRESS,
19
+ description: "Create an EasyPost address object, optionally requesting provider verification.",
20
+ schema: createAddressSchema,
21
+ handler: (input, context) => services.addresses.createAddress(input, context),
22
+ }),
23
+ ];
24
+ }
25
+
26
+ export { addressTools };
@@ -0,0 +1,34 @@
1
+ import { ToolDefinition } from "../ToolDefinition.js";
2
+ import categories from "../../constants/toolCategories.js";
3
+ import { createBatchSchema, buyBatchSchema, batchStatusSchema } from "../../schemas/batchSchemas.js";
4
+
5
+ function batchTools(services) {
6
+ return [
7
+ new ToolDefinition({
8
+ name: "create_batch",
9
+ title: "Create Batch",
10
+ category: categories.BATCHES,
11
+ description: "Create a batch from multiple shipments.",
12
+ schema: createBatchSchema,
13
+ handler: (input, context) => services.batches.createBatch(input, context),
14
+ }),
15
+ new ToolDefinition({
16
+ name: "buy_batch",
17
+ title: "Buy Batch",
18
+ category: categories.BATCHES,
19
+ description: "Buy labels for all purchasable shipments in a batch.",
20
+ schema: buyBatchSchema,
21
+ handler: (input, context) => services.batches.buyBatch(input, context),
22
+ }),
23
+ new ToolDefinition({
24
+ name: "batch_status",
25
+ title: "Batch Status",
26
+ category: categories.BATCHES,
27
+ description: "Get current batch processing and purchase status.",
28
+ schema: batchStatusSchema,
29
+ handler: (input, context) => services.batches.batchStatus(input, context),
30
+ }),
31
+ ];
32
+ }
33
+
34
+ export { batchTools };
@@ -0,0 +1,26 @@
1
+ import { ToolDefinition } from "../ToolDefinition.js";
2
+ import categories from "../../constants/toolCategories.js";
3
+ import { createOrderSchema, getOrderSchema } from "../../schemas/orderSchemas.js";
4
+
5
+ function orderTools(services) {
6
+ return [
7
+ new ToolDefinition({
8
+ name: "create_order",
9
+ title: "Create Order",
10
+ category: categories.ORDERS,
11
+ description: "Create an EasyPost order with multiple shipments using business order fields.",
12
+ schema: createOrderSchema,
13
+ handler: (input, context) => services.orders.createOrder(input, context),
14
+ }),
15
+ new ToolDefinition({
16
+ name: "get_order",
17
+ title: "Get Order",
18
+ category: categories.ORDERS,
19
+ description: "Retrieve an order by id.",
20
+ schema: getOrderSchema,
21
+ handler: (input, context) => services.orders.getOrder(input, context),
22
+ }),
23
+ ];
24
+ }
25
+
26
+ export { orderTools };
@@ -0,0 +1,26 @@
1
+ import { ToolDefinition } from "../ToolDefinition.js";
2
+ import categories from "../../constants/toolCategories.js";
3
+ import { schedulePickupSchema, cancelPickupSchema } from "../../schemas/pickupSchemas.js";
4
+
5
+ function pickupTools(services) {
6
+ return [
7
+ new ToolDefinition({
8
+ name: "schedule_pickup",
9
+ title: "Schedule Pickup",
10
+ category: categories.PICKUPS,
11
+ description: "Schedule a carrier pickup for one or more shipments.",
12
+ schema: schedulePickupSchema,
13
+ handler: (input, context) => services.pickups.schedulePickup(input, context),
14
+ }),
15
+ new ToolDefinition({
16
+ name: "cancel_pickup",
17
+ title: "Cancel Pickup",
18
+ category: categories.PICKUPS,
19
+ description: "Cancel a scheduled carrier pickup.",
20
+ schema: cancelPickupSchema,
21
+ handler: (input, context) => services.pickups.cancelPickup(input, context),
22
+ }),
23
+ ];
24
+ }
25
+
26
+ export { pickupTools };
@@ -0,0 +1,18 @@
1
+ import { ToolDefinition } from "../ToolDefinition.js";
2
+ import categories from "../../constants/toolCategories.js";
3
+ import { createReturnLabelSchema } from "../../schemas/returnSchemas.js";
4
+
5
+ function returnTools(services) {
6
+ return [
7
+ new ToolDefinition({
8
+ name: "create_return_label",
9
+ title: "Create Return Label",
10
+ category: categories.RETURNS,
11
+ description: "Create and optionally buy a return label using business return fields.",
12
+ schema: createReturnLabelSchema,
13
+ handler: (input, context) => services.returns.createReturnLabel(input, context),
14
+ }),
15
+ ];
16
+ }
17
+
18
+ export { returnTools };
@@ -0,0 +1,74 @@
1
+ import { ToolDefinition } from "../ToolDefinition.js";
2
+ import categories from "../../constants/toolCategories.js";
3
+ import * as schemas from "../../schemas/shipmentSchemas.js";
4
+
5
+ function shipmentTools(services) {
6
+ return [
7
+ new ToolDefinition({
8
+ name: "create_shipment",
9
+ title: "Create Shipment",
10
+ category: categories.SHIPMENTS,
11
+ description: "Create an EasyPost shipment and return normalized shipment details plus available carrier rates. Does not buy a label.",
12
+ schema: schemas.createShipmentSchema,
13
+ handler: (input, context) => services.shipments.createShipment(input, context),
14
+ }),
15
+ new ToolDefinition({
16
+ name: "buy_shipping_label",
17
+ title: "Buy Shipping Label",
18
+ category: categories.SHIPMENTS,
19
+ description: "Buy a shipping label for an existing shipment using a rate id or carrier/service pair.",
20
+ schema: schemas.buyShippingLabelSchema,
21
+ handler: (input, context) => services.shipments.buyShippingLabel(input, context),
22
+ }),
23
+ new ToolDefinition({
24
+ name: "get_shipment",
25
+ title: "Get Shipment",
26
+ category: categories.SHIPMENTS,
27
+ description: "Retrieve a shipment by id with normalized label, rate, parcel, and tracking fields.",
28
+ schema: schemas.getShipmentSchema,
29
+ handler: (input, context) => services.shipments.getShipment(input, context),
30
+ }),
31
+ new ToolDefinition({
32
+ name: "list_shipments",
33
+ title: "List Shipments",
34
+ category: categories.SHIPMENTS,
35
+ description: "List shipments with bounded pagination.",
36
+ schema: schemas.listShipmentsSchema,
37
+ handler: (input, context) => services.shipments.listShipments(input, context),
38
+ }),
39
+ new ToolDefinition({
40
+ name: "cancel_shipment",
41
+ title: "Cancel Shipment",
42
+ category: categories.SHIPMENTS,
43
+ description: "Cancel or void a shipment when supported by the carrier. Uses EasyPost refund semantics for purchased labels.",
44
+ schema: schemas.cancelShipmentSchema,
45
+ handler: (input, context) => services.shipments.cancelShipment(input, context),
46
+ }),
47
+ new ToolDefinition({
48
+ name: "refund_shipment",
49
+ title: "Refund Shipment",
50
+ category: categories.SHIPMENTS,
51
+ description: "Request a refund for a purchased shipment label.",
52
+ schema: schemas.refundShipmentSchema,
53
+ handler: (input, context) => services.shipments.refundShipment(input, context),
54
+ }),
55
+ new ToolDefinition({
56
+ name: "estimate_rates",
57
+ title: "Estimate Rates",
58
+ category: categories.SHIPMENTS,
59
+ description: "Create an unrated purchase-free shipment quote and return available carrier rates.",
60
+ schema: schemas.estimateRatesSchema,
61
+ handler: (input, context) => services.shipments.estimateRates(input, context),
62
+ }),
63
+ new ToolDefinition({
64
+ name: "insure_shipment",
65
+ title: "Insure Shipment",
66
+ category: categories.INSURANCE,
67
+ description: "Add insurance to an existing shipment.",
68
+ schema: schemas.insureShipmentSchema,
69
+ handler: (input, context) => services.shipments.insureShipment(input, context),
70
+ }),
71
+ ];
72
+ }
73
+
74
+ export { shipmentTools };
@@ -0,0 +1,26 @@
1
+ import { ToolDefinition } from "../ToolDefinition.js";
2
+ import categories from "../../constants/toolCategories.js";
3
+ import { trackPackageSchema, getTrackingHistorySchema } from "../../schemas/trackingSchemas.js";
4
+
5
+ function trackingTools(services) {
6
+ return [
7
+ new ToolDefinition({
8
+ name: "track_package",
9
+ title: "Track Package",
10
+ category: categories.TRACKING,
11
+ description: "Create or refresh tracking for a package using a tracking code and optional carrier.",
12
+ schema: trackPackageSchema,
13
+ handler: (input, context) => services.tracking.trackPackage(input, context),
14
+ }),
15
+ new ToolDefinition({
16
+ name: "get_tracking_history",
17
+ title: "Get Tracking History",
18
+ category: categories.TRACKING,
19
+ description: "Return normalized tracking events by tracker id or tracking code.",
20
+ schema: getTrackingHistorySchema,
21
+ handler: (input, context) => services.tracking.getTrackingHistory(input, context),
22
+ }),
23
+ ];
24
+ }
25
+
26
+ export { trackingTools };
@@ -0,0 +1,24 @@
1
+ import { ToolRegistry } from "./registry.js";
2
+ import { shipmentTools } from "./definitions/shipments.js";
3
+ import { addressTools } from "./definitions/address.js";
4
+ import { trackingTools } from "./definitions/tracking.js";
5
+ import { returnTools } from "./definitions/returns.js";
6
+ import { pickupTools } from "./definitions/pickups.js";
7
+ import { batchTools } from "./definitions/batches.js";
8
+ import { orderTools } from "./definitions/orders.js";
9
+
10
+ function createToolRegistry(services) {
11
+ const registry = new ToolRegistry();
12
+ registry.registerMany([
13
+ ...shipmentTools(services),
14
+ ...trackingTools(services),
15
+ ...addressTools(services),
16
+ ...returnTools(services),
17
+ ...pickupTools(services),
18
+ ...batchTools(services),
19
+ ...orderTools(services),
20
+ ]);
21
+ return registry;
22
+ }
23
+
24
+ export { createToolRegistry };
@@ -0,0 +1,37 @@
1
+ import { AppError } from "../errors/AppError.js";
2
+
3
+ class ToolRegistry {
4
+ constructor() {
5
+ this.tools = new Map();
6
+ }
7
+
8
+ register(tool) {
9
+ if (this.tools.has(tool.name)) {
10
+ throw new Error(`Duplicate tool registered: ${tool.name}`);
11
+ }
12
+
13
+ this.tools.set(tool.name, tool);
14
+ }
15
+
16
+ registerMany(tools) {
17
+ tools.forEach((tool) => this.register(tool));
18
+ }
19
+
20
+ list() {
21
+ return Array.from(this.tools.values()).map((tool) => tool.toMcpTool());
22
+ }
23
+
24
+ get(name) {
25
+ const tool = this.tools.get(name);
26
+ if (!tool) {
27
+ throw new AppError(`Unknown tool: ${name}`, {
28
+ code: "UNKNOWN_TOOL",
29
+ statusCode: 404,
30
+ safeMessage: `Unknown tool: ${name}`,
31
+ });
32
+ }
33
+ return tool;
34
+ }
35
+ }
36
+
37
+ export { ToolRegistry };
@@ -0,0 +1,7 @@
1
+ import { randomUUID } from "node:crypto";
2
+
3
+ function createCorrelationId(prefix = "mcp") {
4
+ return `${prefix}_${randomUUID()}`;
5
+ }
6
+
7
+ export { createCorrelationId };
@@ -0,0 +1,53 @@
1
+ const SECRET_KEYS = new Set([
2
+ "authorization",
3
+ "api_key",
4
+ "apiKey",
5
+ "token",
6
+ "password",
7
+ "EASYPOST_API_KEY",
8
+ ]);
9
+
10
+ const PII_KEYS = new Set([
11
+ "street1",
12
+ "street2",
13
+ "name",
14
+ "company",
15
+ "phone",
16
+ "email",
17
+ ]);
18
+
19
+ function maskValue(value) {
20
+ if (typeof value !== "string") return value;
21
+ if (value.length <= 4) return "***";
22
+ return `${value.slice(0, 2)}***${value.slice(-2)}`;
23
+ }
24
+
25
+ function sanitizeString(value) {
26
+ return value.trim().replace(/\s+/g, " ");
27
+ }
28
+
29
+ function sanitizeInput(value) {
30
+ if (Array.isArray(value)) return value.map(sanitizeInput);
31
+ if (!value || typeof value !== "object") {
32
+ return typeof value === "string" ? sanitizeString(value) : value;
33
+ }
34
+
35
+ return Object.fromEntries(
36
+ Object.entries(value).map(([key, nested]) => [key, sanitizeInput(nested)])
37
+ );
38
+ }
39
+
40
+ function redactForLogs(value) {
41
+ if (Array.isArray(value)) return value.map(redactForLogs);
42
+ if (!value || typeof value !== "object") return value;
43
+
44
+ return Object.fromEntries(
45
+ Object.entries(value).map(([key, nested]) => {
46
+ if (SECRET_KEYS.has(key)) return [key, "***REDACTED***"];
47
+ if (PII_KEYS.has(key)) return [key, maskValue(nested)];
48
+ return [key, redactForLogs(nested)];
49
+ })
50
+ );
51
+ }
52
+
53
+ export { sanitizeInput, redactForLogs };
@@ -0,0 +1,49 @@
1
+ import { AntiHallucinationError } from "../errors/AppError.js";
2
+
3
+ const PLACEHOLDER_PATTERNS = [
4
+ /\b(unknown|n\/a|na|null|none|tbd|todo|test|sample|example|placeholder)\b/i,
5
+ /\b(123 main|main street|fake street|any street|your address|shipping address)\b/i,
6
+ /\b(john doe|jane doe|acme|foo|bar)\b/i,
7
+ /^(.)\1{4,}$/,
8
+ ];
9
+
10
+ function inspectString(path, value, issues) {
11
+ if (!value.trim()) {
12
+ issues.push({ path, reason: "Empty string" });
13
+ return;
14
+ }
15
+
16
+ if (PLACEHOLDER_PATTERNS.some((pattern) => pattern.test(value))) {
17
+ issues.push({ path, reason: "Looks like autogenerated placeholder data" });
18
+ }
19
+ }
20
+
21
+ function inspectObject(value, path, issues) {
22
+ if (Array.isArray(value)) {
23
+ value.forEach((item, index) => inspectObject(item, `${path}[${index}]`, issues));
24
+ return;
25
+ }
26
+
27
+ if (!value || typeof value !== "object") {
28
+ if (typeof value === "string") inspectString(path, value, issues);
29
+ return;
30
+ }
31
+
32
+ for (const [key, nested] of Object.entries(value)) {
33
+ inspectObject(nested, path ? `${path}.${key}` : key, issues);
34
+ }
35
+ }
36
+
37
+ function assertNotHallucinated(input) {
38
+ const issues = [];
39
+ inspectObject(input, "", issues);
40
+
41
+ if (issues.length) {
42
+ throw new AntiHallucinationError(
43
+ "Input appears to contain placeholder or AI-generated shipping data. Provide real, verified shipment details.",
44
+ issues
45
+ );
46
+ }
47
+ }
48
+
49
+ export { assertNotHallucinated };
@@ -0,0 +1,28 @@
1
+ import { zodToJsonSchema } from "zod-to-json-schema";
2
+ import { ValidationError } from "../errors/AppError.js";
3
+ import { sanitizeInput } from "../utils/sanitize.js";
4
+ import { assertNotHallucinated } from "./antiHallucination.js";
5
+
6
+ function validateInput(schema, input) {
7
+ const sanitized = sanitizeInput(input || {});
8
+ const result = schema.safeParse(sanitized);
9
+
10
+ if (!result.success) {
11
+ throw new ValidationError("Tool input validation failed", result.error.issues);
12
+ }
13
+
14
+ assertNotHallucinated(result.data);
15
+ return result.data;
16
+ }
17
+
18
+ function toInputSchema(schema, name) {
19
+ const jsonSchema = zodToJsonSchema(schema, {
20
+ name,
21
+ target: "jsonSchema7",
22
+ $refStrategy: "none",
23
+ });
24
+
25
+ return jsonSchema.definitions?.[name] || jsonSchema;
26
+ }
27
+
28
+ export { validateInput, toInputSchema };