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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "easypost-mcp",
3
- "version": "2.1.0",
3
+ "version": "3.1.0",
4
4
  "description": "Production-grade EasyPost Model Context Protocol (MCP) server for Claude, Cursor, and programmatic agents",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -0,0 +1,20 @@
1
+ import { redactForLogs } from "../utils/sanitize.js";
2
+
3
+ class AuditLogger {
4
+ constructor(logger) {
5
+ this.logger = logger;
6
+ }
7
+
8
+ record(event, payload = {}) {
9
+ this.logger.info(
10
+ {
11
+ audit: true,
12
+ event,
13
+ ...redactForLogs(payload),
14
+ },
15
+ `audit.${event}`
16
+ );
17
+ }
18
+ }
19
+
20
+ export { AuditLogger };
@@ -0,0 +1,36 @@
1
+ const TOOL_RISK = Object.freeze({
2
+ LOW: "LOW",
3
+ MEDIUM: "MEDIUM",
4
+ HIGH: "HIGH",
5
+ });
6
+
7
+ const TOOL_RISK_BY_NAME = Object.freeze({
8
+ verify_address: TOOL_RISK.LOW,
9
+ create_address: TOOL_RISK.LOW,
10
+ estimate_rates: TOOL_RISK.LOW,
11
+ get_shipment: TOOL_RISK.LOW,
12
+ list_shipments: TOOL_RISK.LOW,
13
+ track_package: TOOL_RISK.LOW,
14
+ get_tracking_history: TOOL_RISK.LOW,
15
+ get_order: TOOL_RISK.LOW,
16
+ batch_status: TOOL_RISK.LOW,
17
+
18
+ create_shipment: TOOL_RISK.MEDIUM,
19
+ create_return_label: TOOL_RISK.MEDIUM,
20
+ insure_shipment: TOOL_RISK.MEDIUM,
21
+ create_batch: TOOL_RISK.MEDIUM,
22
+ create_order: TOOL_RISK.MEDIUM,
23
+
24
+ buy_shipping_label: TOOL_RISK.HIGH,
25
+ schedule_pickup: TOOL_RISK.HIGH,
26
+ cancel_pickup: TOOL_RISK.HIGH,
27
+ cancel_shipment: TOOL_RISK.HIGH,
28
+ refund_shipment: TOOL_RISK.HIGH,
29
+ buy_batch: TOOL_RISK.HIGH,
30
+ });
31
+
32
+ function riskForTool(name) {
33
+ return TOOL_RISK_BY_NAME[name] || TOOL_RISK.MEDIUM;
34
+ }
35
+
36
+ export { TOOL_RISK, TOOL_RISK_BY_NAME, riskForTool };
@@ -0,0 +1,37 @@
1
+ import { examplesForFields, getFieldMetadata } from "./fields/catalog.js";
2
+
3
+ function createFallbackResponse({
4
+ errorCode,
5
+ message,
6
+ missingFields = [],
7
+ ambiguousFields = [],
8
+ availableOptions = [],
9
+ examples,
10
+ nextAction,
11
+ workflowId,
12
+ validValues = {},
13
+ metadata = {},
14
+ }) {
15
+ return {
16
+ ok: false,
17
+ success: false,
18
+ error_code: errorCode,
19
+ message,
20
+ workflow_id: workflowId,
21
+ missing_fields: missingFields,
22
+ ambiguous_fields: ambiguousFields,
23
+ available_options: availableOptions,
24
+ examples: examples || examplesForFields(missingFields),
25
+ valid_values: validValues,
26
+ field_metadata: {
27
+ ...Object.fromEntries(
28
+ [...missingFields, ...ambiguousFields.map((field) => field.path || field.field).filter(Boolean)]
29
+ .map((path) => [path, getFieldMetadata(path)])
30
+ ),
31
+ ...metadata,
32
+ },
33
+ next_action: nextAction,
34
+ };
35
+ }
36
+
37
+ export { createFallbackResponse };
@@ -0,0 +1,171 @@
1
+ const FIELD_CATALOG = Object.freeze({
2
+ carrier: {
3
+ title: "Carrier",
4
+ description: "Exact supported carrier code or name. Do not use abbreviations unless listed.",
5
+ examples: ["USPS", "FedEx", "UPS"],
6
+ validation_rules: ["Must match an authoritative carrier exactly."],
7
+ formatting_hints: ["Use the value returned by get_carriers or validate_carrier."],
8
+ },
9
+ service: {
10
+ title: "Service level",
11
+ description: "Exact carrier service level returned by carrier resources or shipment rates.",
12
+ examples: ["GroundAdvantage", "FEDEX_GROUND", "Priority"],
13
+ validation_rules: ["Must be valid for the selected carrier."],
14
+ formatting_hints: ["Do not use vague terms like ground unless that exact service exists."],
15
+ },
16
+ predefined_package: {
17
+ title: "Package type",
18
+ description: "Carrier/package predefined package type.",
19
+ examples: ["Parcel", "FlatRateEnvelope", "FlatRateSmallBox"],
20
+ validation_rules: ["Must match a known package type exactly."],
21
+ },
22
+ country: {
23
+ title: "Country",
24
+ description: "ISO 3166-1 alpha-2 country code.",
25
+ examples: ["US", "CA", "GB"],
26
+ validation_rules: ["Two-letter country code."],
27
+ formatting_hints: ["Use uppercase country codes."],
28
+ },
29
+ state: {
30
+ title: "State or province",
31
+ description: "State, province, or region code appropriate for the destination country.",
32
+ examples: ["CA", "NY", "ON"],
33
+ validation_rules: ["Use country-specific state/province format where required."],
34
+ },
35
+ zip: {
36
+ title: "Postal code",
37
+ description: "Country-specific postal or ZIP code.",
38
+ examples: ["94104", "10001", "M5V 2T6"],
39
+ validation_rules: ["Must match the destination country postal format."],
40
+ formatting_hints: ["For US addresses use 5-digit or ZIP+4 format."],
41
+ },
42
+ street1: {
43
+ title: "Street address",
44
+ description: "Primary street address line.",
45
+ examples: ["417 Montgomery St"],
46
+ validation_rules: ["Must be a real operational shipping address, not a placeholder."],
47
+ },
48
+ city: {
49
+ title: "City",
50
+ description: "Destination or origin city.",
51
+ examples: ["San Francisco", "New York"],
52
+ validation_rules: ["Required for address creation and shipment workflows."],
53
+ },
54
+ weight: {
55
+ title: "Weight",
56
+ description: "Parcel weight in ounces.",
57
+ examples: [16, 32.5],
58
+ validation_rules: ["Must be greater than 0 and at most 1120 ounces."],
59
+ formatting_hints: ["EasyPost parcel weight is in ounces."],
60
+ },
61
+ length: {
62
+ title: "Length",
63
+ description: "Parcel length in inches.",
64
+ examples: [10],
65
+ validation_rules: ["Must be greater than 0 and at most 120 inches."],
66
+ },
67
+ width: {
68
+ title: "Width",
69
+ description: "Parcel width in inches.",
70
+ examples: [8],
71
+ validation_rules: ["Must be greater than 0 and at most 120 inches."],
72
+ },
73
+ height: {
74
+ title: "Height",
75
+ description: "Parcel height in inches.",
76
+ examples: [4],
77
+ validation_rules: ["Must be greater than 0 and at most 120 inches."],
78
+ },
79
+ rate_option: {
80
+ title: "Rate option",
81
+ description: "Numbered option from the shipment's actual returned rates.",
82
+ examples: [1, 2],
83
+ validation_rules: ["Must refer to one of the returned rate options for the shipment."],
84
+ },
85
+ confirm: {
86
+ title: "Confirmation",
87
+ description: "Explicit approval for high-risk or irreversible operations.",
88
+ examples: [true],
89
+ validation_rules: ["Must be true before execution continues."],
90
+ },
91
+ insurance_amount: {
92
+ title: "Insurance amount",
93
+ description: "Shipment insurance amount in USD unless otherwise specified.",
94
+ examples: ["100.00", "250.00"],
95
+ validation_rules: ["Must be non-negative currency with at most two decimals."],
96
+ },
97
+ tracking_code: {
98
+ title: "Tracking code",
99
+ description: "Carrier tracking number.",
100
+ examples: ["EZ1000000001"],
101
+ validation_rules: ["Must be supplied by the carrier or EasyPost."],
102
+ },
103
+ min_datetime: {
104
+ title: "Pickup start",
105
+ description: "Earliest pickup datetime.",
106
+ examples: ["2026-05-25T10:00:00-07:00"],
107
+ validation_rules: ["Must be an operational pickup datetime accepted by the carrier."],
108
+ },
109
+ max_datetime: {
110
+ title: "Pickup end",
111
+ description: "Latest pickup datetime.",
112
+ examples: ["2026-05-25T14:00:00-07:00"],
113
+ validation_rules: ["Must be after pickup start."],
114
+ },
115
+ });
116
+
117
+ function getFieldMetadata(path) {
118
+ const parts = String(path || "").split(".");
119
+ const last = parts[parts.length - 1];
120
+ if (last === "zip") return FIELD_CATALOG.zip;
121
+ if (last === "country") return FIELD_CATALOG.country;
122
+ if (last === "state") return FIELD_CATALOG.state;
123
+ if (last === "predefined_package") return FIELD_CATALOG.predefined_package;
124
+ if (last === "amount") return FIELD_CATALOG.insurance_amount;
125
+ return FIELD_CATALOG[last] || {
126
+ title: last || "Field",
127
+ description: `Required field: ${path}`,
128
+ examples: [],
129
+ validation_rules: [],
130
+ };
131
+ }
132
+
133
+ function schemaForField(path) {
134
+ const metadata = getFieldMetadata(path);
135
+ const last = String(path).split(".").at(-1);
136
+
137
+ if (["length", "width", "height", "weight", "rate_option"].includes(last)) {
138
+ return {
139
+ type: last === "rate_option" ? "integer" : "number",
140
+ title: metadata.title,
141
+ description: metadata.description,
142
+ minimum: last === "rate_option" ? 1 : 0.01,
143
+ ...(last !== "rate_option" ? { maximum: last === "weight" ? 1120 : 120 } : {}),
144
+ };
145
+ }
146
+
147
+ if (last === "confirm") {
148
+ return {
149
+ type: "boolean",
150
+ title: metadata.title,
151
+ description: metadata.description,
152
+ default: false,
153
+ };
154
+ }
155
+
156
+ return {
157
+ type: "string",
158
+ title: metadata.title,
159
+ description: metadata.description,
160
+ minLength: 1,
161
+ maxLength: 255,
162
+ };
163
+ }
164
+
165
+ function examplesForFields(paths) {
166
+ return Object.fromEntries(
167
+ paths.map((path) => [path, getFieldMetadata(path).examples || []])
168
+ );
169
+ }
170
+
171
+ export { FIELD_CATALOG, getFieldMetadata, schemaForField, examplesForFields };
@@ -0,0 +1,226 @@
1
+ import { validateInput } from "../validators/validate.js";
2
+ import { riskForTool, TOOL_RISK } from "../constants/toolRisk.js";
3
+ import { AntiHallucinationError, ValidationError } from "../errors/AppError.js";
4
+ import { createFallbackResponse } from "../elicitation/fallback.js";
5
+
6
+ class ExecutionPipeline {
7
+ constructor({ resourceManager, elicitationService, workflowState, auditLogger }) {
8
+ this.resourceManager = resourceManager;
9
+ this.elicitation = elicitationService;
10
+ this.workflowState = workflowState;
11
+ this.auditLogger = auditLogger;
12
+ }
13
+
14
+ async run(tool, args, context) {
15
+ const risk = tool.risk || riskForTool(tool.name);
16
+ this.auditLogger.record("intent_extracted", {
17
+ correlationId: context.correlationId,
18
+ toolName: tool.name,
19
+ risk,
20
+ });
21
+
22
+ await this.resourceManager?.ensureFresh(context);
23
+ const originalArgs = args || {};
24
+ const workflowId = originalArgs.workflow_id;
25
+ const mergedArgs = this.workflowState?.merge(workflowId, originalArgs) || originalArgs;
26
+
27
+ const input = await this.validateOrElicit(tool, mergedArgs, {
28
+ ...context,
29
+ risk,
30
+ workflowId,
31
+ });
32
+
33
+ if (input?.__elicitationFallback) {
34
+ return input.__elicitationFallback;
35
+ }
36
+
37
+ this.auditLogger.record("validation_succeeded", {
38
+ correlationId: context.correlationId,
39
+ toolName: tool.name,
40
+ risk,
41
+ });
42
+
43
+ if (risk === TOOL_RISK.HIGH && input.confirm !== true) {
44
+ this.auditLogger.record("confirmation_missing", {
45
+ correlationId: context.correlationId,
46
+ toolName: tool.name,
47
+ });
48
+ }
49
+
50
+ const result = await tool.handler(input, {
51
+ ...context,
52
+ risk,
53
+ resources: this.resourceManager,
54
+ elicitation: this.elicitation,
55
+ audit: this.auditLogger,
56
+ });
57
+
58
+ this.auditLogger.record("execution_completed", {
59
+ correlationId: context.correlationId,
60
+ toolName: tool.name,
61
+ risk,
62
+ ok: result?.ok === true,
63
+ });
64
+
65
+ return result;
66
+ }
67
+
68
+ async validateOrElicit(tool, args, context) {
69
+ const validationArgs = stripWorkflowId(args);
70
+ try {
71
+ return validateInput(tool.schema, validationArgs, {
72
+ resourceManager: this.resourceManager,
73
+ toolName: tool.name,
74
+ risk: context.risk,
75
+ });
76
+ } catch (error) {
77
+ if (error instanceof ValidationError) {
78
+ return this.handleValidationFailure(tool, args, context, error);
79
+ }
80
+
81
+ if (error instanceof AntiHallucinationError) {
82
+ return this.handleAmbiguityFailure(tool, args, context, error);
83
+ }
84
+
85
+ throw error;
86
+ }
87
+ }
88
+
89
+ async handleValidationFailure(tool, args, context, error) {
90
+ const validationArgs = stripWorkflowId(args);
91
+ const missingFields = extractMissingFields(error.details);
92
+ if (!missingFields.length) throw error;
93
+
94
+ const workflowId = context.workflowId || this.workflowState?.create({
95
+ toolName: tool.name,
96
+ input: validationArgs,
97
+ reason: "MISSING_FIELDS",
98
+ correlationId: context.correlationId,
99
+ });
100
+
101
+ this.auditLogger.record("missing_fields_detected", {
102
+ correlationId: context.correlationId,
103
+ toolName: tool.name,
104
+ missingFields,
105
+ workflowId,
106
+ });
107
+
108
+ const elicited = await this.elicitation?.requestMissingFields({
109
+ server: context.server,
110
+ toolName: tool.name,
111
+ missingFields,
112
+ partialInput: validationArgs,
113
+ });
114
+
115
+ if (elicited?.completed) {
116
+ this.workflowState?.update(workflowId, elicited.input);
117
+ return this.validateOrElicit(tool, elicited.input, { ...context, workflowId });
118
+ }
119
+
120
+ return {
121
+ __elicitationFallback: createFallbackResponse({
122
+ errorCode: "MISSING_REQUIRED_FIELDS",
123
+ message: "Required fields are missing. Provide only the missing fields and retry with workflow_id to resume.",
124
+ missingFields,
125
+ workflowId,
126
+ nextAction: "Provide missing_fields and include workflow_id in the next call, or use a client that supports elicitation.form.",
127
+ }),
128
+ };
129
+ }
130
+
131
+ async handleAmbiguityFailure(tool, args, context, error) {
132
+ const details = error.details || {};
133
+ const ambiguousFields = (details.issues || [])
134
+ .filter((issue) => Array.isArray(issue.suggestions) && issue.suggestions.length > 0)
135
+ .map((issue) => ({
136
+ path: issue.path,
137
+ code: issue.code,
138
+ message: issue.message,
139
+ suggestions: issue.suggestions,
140
+ }));
141
+
142
+ if (!ambiguousFields.length) throw error;
143
+
144
+ const workflowId = context.workflowId || this.workflowState?.create({
145
+ toolName: tool.name,
146
+ input: validationArgs,
147
+ reason: "AMBIGUOUS_FIELDS",
148
+ correlationId: context.correlationId,
149
+ });
150
+
151
+ const first = ambiguousFields[0];
152
+ this.auditLogger.record("ambiguity_detected", {
153
+ correlationId: context.correlationId,
154
+ toolName: tool.name,
155
+ ambiguousFields,
156
+ workflowId,
157
+ });
158
+
159
+ const clarified = await this.elicitation?.clarify({
160
+ server: context.server,
161
+ field: first.path,
162
+ suggestions: first.suggestions,
163
+ message: first.message || `Clarify ${first.path}`,
164
+ });
165
+
166
+ if (clarified?.selected) {
167
+ const clarifiedInput = setPath(validationArgs, first.path, clarified.value);
168
+ this.workflowState?.update(workflowId, clarifiedInput);
169
+ return this.validateOrElicit(tool, clarifiedInput, { ...context, workflowId });
170
+ }
171
+
172
+ return {
173
+ __elicitationFallback: createFallbackResponse({
174
+ errorCode: "AMBIGUOUS_OR_UNSUPPORTED_VALUES",
175
+ message: "One or more values are ambiguous or unsupported. Choose an exact suggested value or provide a different exact value.",
176
+ ambiguousFields,
177
+ availableOptions: ambiguousFields.flatMap((field) => field.suggestions || []),
178
+ workflowId,
179
+ nextAction: "Confirm one suggestion explicitly and include workflow_id in the next call, or use a client that supports elicitation.form.",
180
+ }),
181
+ };
182
+ }
183
+ }
184
+
185
+ function extractMissingFields(issues = []) {
186
+ const fields = [];
187
+ for (const issue of issues) {
188
+ const isMissing = issue.code === "invalid_type" && issue.received === "undefined";
189
+ if (!isMissing) continue;
190
+ fields.push(...expandRequiredPath(issue.path || []));
191
+ }
192
+ return [...new Set(fields)];
193
+ }
194
+
195
+ function expandRequiredPath(path) {
196
+ const joined = path.join(".");
197
+ if (["from_address", "to_address", "address"].includes(joined)) {
198
+ return ["street1", "city", "state", "zip", "country"].map((field) => `${joined}.${field}`);
199
+ }
200
+ if (joined === "parcel") {
201
+ return ["length", "width", "height", "weight"].map((field) => `parcel.${field}`);
202
+ }
203
+ if (!joined) return [];
204
+ return [joined];
205
+ }
206
+
207
+ function setPath(input, path, value) {
208
+ const output = structuredClone(input || {});
209
+ const parts = String(path).split(".");
210
+ let target = output;
211
+ for (const part of parts.slice(0, -1)) {
212
+ target[part] = target[part] && typeof target[part] === "object" ? target[part] : {};
213
+ target = target[part];
214
+ }
215
+ target[parts.at(-1)] = value;
216
+ return output;
217
+ }
218
+
219
+ function stripWorkflowId(input = {}) {
220
+ if (!input || typeof input !== "object" || Array.isArray(input)) return input;
221
+ const output = { ...input };
222
+ delete output.workflow_id;
223
+ return output;
224
+ }
225
+
226
+ export { ExecutionPipeline };