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.
- package/bin/easypost-mcp.js +14 -0
- package/package.json +71 -0
- package/src/adapters/easypost/EasyPostClient.js +75 -0
- package/src/adapters/easypost/responseMappers.js +105 -0
- package/src/cli/commands/start.js +47 -0
- package/src/cli/index.js +45 -0
- package/src/config/bootstrap.js +42 -0
- package/src/config/index.js +17 -0
- package/src/config/loadConfig.js +66 -0
- package/src/constants/toolCategories.js +10 -0
- package/src/errors/AppError.js +57 -0
- package/src/errors/easypostErrorMapper.js +21 -0
- package/src/index.js +6 -0
- package/src/logging/logger.js +77 -0
- package/src/middleware/errorHandler.js +49 -0
- package/src/middleware/rateLimiter.js +29 -0
- package/src/schemas/addressSchemas.js +27 -0
- package/src/schemas/batchSchemas.js +24 -0
- package/src/schemas/commonSchemas.js +15 -0
- package/src/schemas/orderSchemas.js +15 -0
- package/src/schemas/parcelSchemas.js +13 -0
- package/src/schemas/pickupSchemas.js +17 -0
- package/src/schemas/returnSchemas.js +14 -0
- package/src/schemas/shipmentSchemas.js +56 -0
- package/src/schemas/trackingSchemas.js +16 -0
- package/src/server/createServer.js +90 -0
- package/src/server/transports/http.js +102 -0
- package/src/server/transports/stdio.js +10 -0
- package/src/services/AddressService.js +36 -0
- package/src/services/BatchService.js +38 -0
- package/src/services/OrderService.js +25 -0
- package/src/services/PickupService.js +42 -0
- package/src/services/ReturnService.js +33 -0
- package/src/services/ShipmentService.js +97 -0
- package/src/services/TrackingService.js +34 -0
- package/src/services/index.js +24 -0
- package/src/tools/ToolDefinition.js +33 -0
- package/src/tools/definitions/address.js +26 -0
- package/src/tools/definitions/batches.js +34 -0
- package/src/tools/definitions/orders.js +26 -0
- package/src/tools/definitions/pickups.js +26 -0
- package/src/tools/definitions/returns.js +18 -0
- package/src/tools/definitions/shipments.js +74 -0
- package/src/tools/definitions/tracking.js +26 -0
- package/src/tools/index.js +24 -0
- package/src/tools/registry.js +37 -0
- package/src/utils/correlation.js +7 -0
- package/src/utils/sanitize.js +53 -0
- package/src/validators/antiHallucination.js +49 -0
- package/src/validators/validate.js +28 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { AppError } from "../errors/AppError.js";
|
|
2
|
+
import { getLoggerSync } from "../logging/logger.js";
|
|
3
|
+
|
|
4
|
+
function toSafeError(error, correlationId) {
|
|
5
|
+
if (error instanceof AppError) {
|
|
6
|
+
return {
|
|
7
|
+
ok: false,
|
|
8
|
+
error: {
|
|
9
|
+
code: error.code,
|
|
10
|
+
message: error.safeMessage,
|
|
11
|
+
details: error.details,
|
|
12
|
+
retryable: error.retryable,
|
|
13
|
+
correlation_id: correlationId,
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
getLoggerSync().error(
|
|
19
|
+
{ err: { message: error.message, stack: error.stack }, correlationId },
|
|
20
|
+
"Unhandled tool error"
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
ok: false,
|
|
25
|
+
error: {
|
|
26
|
+
code: "INTERNAL_ERROR",
|
|
27
|
+
message: "An unexpected server error occurred",
|
|
28
|
+
retryable: false,
|
|
29
|
+
correlation_id: correlationId,
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function withErrorHandling(context, operation) {
|
|
35
|
+
try {
|
|
36
|
+
return await operation();
|
|
37
|
+
} catch (error) {
|
|
38
|
+
context.logger?.error(
|
|
39
|
+
{
|
|
40
|
+
err: { name: error.name, message: error.message, code: error.code },
|
|
41
|
+
correlationId: context.correlationId,
|
|
42
|
+
},
|
|
43
|
+
"Tool execution failed"
|
|
44
|
+
);
|
|
45
|
+
return toSafeError(error, context.correlationId);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export { toSafeError, withErrorHandling };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { AppError } from "../errors/AppError.js";
|
|
2
|
+
|
|
3
|
+
function createInMemoryRateLimiter({ limit, windowMs }) {
|
|
4
|
+
const buckets = new Map();
|
|
5
|
+
|
|
6
|
+
return {
|
|
7
|
+
consume(key) {
|
|
8
|
+
const now = Date.now();
|
|
9
|
+
const bucket = buckets.get(key);
|
|
10
|
+
|
|
11
|
+
if (!bucket || bucket.resetAt <= now) {
|
|
12
|
+
buckets.set(key, { count: 1, resetAt: now + windowMs });
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
bucket.count += 1;
|
|
17
|
+
if (bucket.count > limit) {
|
|
18
|
+
throw new AppError("MCP rate limit exceeded", {
|
|
19
|
+
code: "RATE_LIMITED",
|
|
20
|
+
statusCode: 429,
|
|
21
|
+
retryable: true,
|
|
22
|
+
details: { reset_at: new Date(bucket.resetAt).toISOString() },
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export { createInMemoryRateLimiter };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { z, nonEmptyString, optionalNonEmptyString } from "./commonSchemas.js";
|
|
2
|
+
|
|
3
|
+
const addressSchema = z.object({
|
|
4
|
+
name: optionalNonEmptyString,
|
|
5
|
+
company: optionalNonEmptyString,
|
|
6
|
+
street1: nonEmptyString,
|
|
7
|
+
street2: optionalNonEmptyString,
|
|
8
|
+
city: nonEmptyString,
|
|
9
|
+
state: nonEmptyString,
|
|
10
|
+
zip: nonEmptyString,
|
|
11
|
+
country: z.string().trim().min(2).max(2).transform((value) => value.toUpperCase()),
|
|
12
|
+
phone: optionalNonEmptyString,
|
|
13
|
+
email: z.string().trim().email().optional(),
|
|
14
|
+
residential: z.boolean().optional(),
|
|
15
|
+
}).strict();
|
|
16
|
+
|
|
17
|
+
const verifyAddressSchema = z.object({
|
|
18
|
+
address: addressSchema,
|
|
19
|
+
verifications: z.array(z.enum(["delivery", "zip4"])).default(["delivery"]),
|
|
20
|
+
}).strict();
|
|
21
|
+
|
|
22
|
+
const createAddressSchema = z.object({
|
|
23
|
+
address: addressSchema,
|
|
24
|
+
verify: z.boolean().default(false),
|
|
25
|
+
}).strict();
|
|
26
|
+
|
|
27
|
+
export { addressSchema, verifyAddressSchema, createAddressSchema };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { z, easypostId } from "./commonSchemas.js";
|
|
2
|
+
import { createShipmentSchema } from "./shipmentSchemas.js";
|
|
3
|
+
|
|
4
|
+
const batchShipmentCreateSchema = createShipmentSchema.extend({
|
|
5
|
+
carrier: z.string().trim().min(1).optional(),
|
|
6
|
+
service: z.string().trim().min(1).optional(),
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
const batchShipmentSchema = z.union([
|
|
10
|
+
batchShipmentCreateSchema,
|
|
11
|
+
z.object({ shipment_id: easypostId }).strict(),
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
const createBatchSchema = z.object({
|
|
15
|
+
shipments: z.array(batchShipmentSchema).min(1).max(100),
|
|
16
|
+
}).strict();
|
|
17
|
+
|
|
18
|
+
const buyBatchSchema = z.object({
|
|
19
|
+
batch_id: easypostId,
|
|
20
|
+
}).strict();
|
|
21
|
+
|
|
22
|
+
const batchStatusSchema = buyBatchSchema;
|
|
23
|
+
|
|
24
|
+
export { createBatchSchema, buyBatchSchema, batchStatusSchema };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
const nonEmptyString = z.string().trim().min(1).max(255);
|
|
4
|
+
const optionalNonEmptyString = nonEmptyString.optional();
|
|
5
|
+
const easypostId = z.string().trim().regex(/^[a-z]+_[A-Za-z0-9]+$/, "Invalid EasyPost id format");
|
|
6
|
+
|
|
7
|
+
const currencyAmount = z.union([z.string().trim().regex(/^\d+(\.\d{1,2})?$/), z.number().nonnegative()]);
|
|
8
|
+
|
|
9
|
+
const paginationSchema = z.object({
|
|
10
|
+
page_size: z.number().int().min(1).max(100).optional(),
|
|
11
|
+
before_id: z.string().trim().optional(),
|
|
12
|
+
after_id: z.string().trim().optional(),
|
|
13
|
+
}).strict();
|
|
14
|
+
|
|
15
|
+
export { z, nonEmptyString, optionalNonEmptyString, easypostId, currencyAmount, paginationSchema };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { z, easypostId } from "./commonSchemas.js";
|
|
2
|
+
import { addressSchema } from "./addressSchemas.js";
|
|
3
|
+
import { parcelSchema } from "./parcelSchemas.js";
|
|
4
|
+
|
|
5
|
+
const createOrderSchema = z.object({
|
|
6
|
+
from_address: addressSchema,
|
|
7
|
+
to_address: addressSchema,
|
|
8
|
+
shipments: z.array(z.object({ parcel: parcelSchema }).strict()).min(1).max(100),
|
|
9
|
+
}).strict();
|
|
10
|
+
|
|
11
|
+
const getOrderSchema = z.object({
|
|
12
|
+
order_id: easypostId,
|
|
13
|
+
}).strict();
|
|
14
|
+
|
|
15
|
+
export { createOrderSchema, getOrderSchema };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { z, optionalNonEmptyString } from "./commonSchemas.js";
|
|
2
|
+
|
|
3
|
+
const parcelSchema = z.object({
|
|
4
|
+
length: z.number().positive().max(120),
|
|
5
|
+
width: z.number().positive().max(120),
|
|
6
|
+
height: z.number().positive().max(120),
|
|
7
|
+
weight: z.number().positive().max(1120),
|
|
8
|
+
predefined_package: optionalNonEmptyString,
|
|
9
|
+
}).strict().refine((parcel) => parcel.length * parcel.width * parcel.height > 0, {
|
|
10
|
+
message: "Parcel dimensions must be greater than zero",
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export { parcelSchema };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { z, easypostId, nonEmptyString } from "./commonSchemas.js";
|
|
2
|
+
import { addressSchema } from "./addressSchemas.js";
|
|
3
|
+
|
|
4
|
+
const schedulePickupSchema = z.object({
|
|
5
|
+
shipment_ids: z.array(easypostId).min(1),
|
|
6
|
+
address: addressSchema,
|
|
7
|
+
min_datetime: nonEmptyString,
|
|
8
|
+
max_datetime: nonEmptyString,
|
|
9
|
+
instructions: z.string().trim().max(500).optional(),
|
|
10
|
+
carrier_accounts: z.array(nonEmptyString).optional(),
|
|
11
|
+
}).strict();
|
|
12
|
+
|
|
13
|
+
const cancelPickupSchema = z.object({
|
|
14
|
+
pickup_id: easypostId,
|
|
15
|
+
}).strict();
|
|
16
|
+
|
|
17
|
+
export { schedulePickupSchema, cancelPickupSchema };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { z, easypostId } from "./commonSchemas.js";
|
|
2
|
+
import { addressSchema } from "./addressSchemas.js";
|
|
3
|
+
import { parcelSchema } from "./parcelSchemas.js";
|
|
4
|
+
|
|
5
|
+
const createReturnLabelSchema = z.object({
|
|
6
|
+
original_shipment_id: easypostId.optional(),
|
|
7
|
+
from_address: addressSchema,
|
|
8
|
+
to_address: addressSchema,
|
|
9
|
+
parcel: parcelSchema,
|
|
10
|
+
carrier: z.string().trim().optional(),
|
|
11
|
+
service: z.string().trim().optional(),
|
|
12
|
+
}).strict();
|
|
13
|
+
|
|
14
|
+
export { createReturnLabelSchema };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { z, easypostId, nonEmptyString, paginationSchema, currencyAmount } from "./commonSchemas.js";
|
|
2
|
+
import { addressSchema } from "./addressSchemas.js";
|
|
3
|
+
import { parcelSchema } from "./parcelSchemas.js";
|
|
4
|
+
|
|
5
|
+
const customsInfoSchema = z.record(z.any()).optional();
|
|
6
|
+
|
|
7
|
+
const shipmentOptionsSchema = z.object({
|
|
8
|
+
label_format: z.enum(["PDF", "PNG", "ZPL", "EPL2"]).optional(),
|
|
9
|
+
delivery_confirmation: z.string().trim().optional(),
|
|
10
|
+
print_custom_1: z.string().trim().max(255).optional(),
|
|
11
|
+
print_custom_2: z.string().trim().max(255).optional(),
|
|
12
|
+
}).strict().optional();
|
|
13
|
+
|
|
14
|
+
const createShipmentSchema = z.object({
|
|
15
|
+
from_address: addressSchema,
|
|
16
|
+
to_address: addressSchema,
|
|
17
|
+
parcel: parcelSchema,
|
|
18
|
+
carrier_accounts: z.array(nonEmptyString).optional(),
|
|
19
|
+
customs_info: customsInfoSchema,
|
|
20
|
+
options: shipmentOptionsSchema,
|
|
21
|
+
reference: z.string().trim().max(255).optional(),
|
|
22
|
+
}).strict();
|
|
23
|
+
|
|
24
|
+
const buyShippingLabelSchema = z.object({
|
|
25
|
+
shipment_id: easypostId,
|
|
26
|
+
rate_id: easypostId.optional(),
|
|
27
|
+
carrier: nonEmptyString.optional(),
|
|
28
|
+
service: nonEmptyString.optional(),
|
|
29
|
+
insurance: currencyAmount.optional(),
|
|
30
|
+
}).strict().refine((input) => input.rate_id || (input.carrier && input.service), {
|
|
31
|
+
message: "Provide either rate_id or carrier and service",
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const getShipmentSchema = z.object({
|
|
35
|
+
shipment_id: easypostId,
|
|
36
|
+
}).strict();
|
|
37
|
+
|
|
38
|
+
const listShipmentsSchema = paginationSchema.extend({
|
|
39
|
+
purchased: z.boolean().optional(),
|
|
40
|
+
include_children: z.boolean().optional(),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const estimateRatesSchema = createShipmentSchema;
|
|
44
|
+
|
|
45
|
+
const refundShipmentSchema = z.object({
|
|
46
|
+
shipment_id: easypostId,
|
|
47
|
+
}).strict();
|
|
48
|
+
|
|
49
|
+
const cancelShipmentSchema = refundShipmentSchema;
|
|
50
|
+
|
|
51
|
+
const insureShipmentSchema = z.object({
|
|
52
|
+
shipment_id: easypostId,
|
|
53
|
+
amount: currencyAmount,
|
|
54
|
+
}).strict();
|
|
55
|
+
|
|
56
|
+
export { createShipmentSchema, buyShippingLabelSchema, getShipmentSchema, listShipmentsSchema, estimateRatesSchema, refundShipmentSchema, cancelShipmentSchema, insureShipmentSchema };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { z, easypostId, nonEmptyString } from "./commonSchemas.js";
|
|
2
|
+
|
|
3
|
+
const trackPackageSchema = z.object({
|
|
4
|
+
tracking_code: nonEmptyString,
|
|
5
|
+
carrier: nonEmptyString.optional(),
|
|
6
|
+
}).strict();
|
|
7
|
+
|
|
8
|
+
const getTrackingHistorySchema = z.object({
|
|
9
|
+
tracker_id: easypostId.optional(),
|
|
10
|
+
tracking_code: nonEmptyString.optional(),
|
|
11
|
+
carrier: nonEmptyString.optional(),
|
|
12
|
+
}).strict().refine((input) => input.tracker_id || input.tracking_code, {
|
|
13
|
+
message: "Provide tracker_id or tracking_code",
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export { trackPackageSchema, getTrackingHistorySchema };
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { getConfig } from "../config/index.js";
|
|
7
|
+
import { getLoggerSync } from "../logging/logger.js";
|
|
8
|
+
import { createCorrelationId } from "../utils/correlation.js";
|
|
9
|
+
import { withErrorHandling } from "../middleware/errorHandler.js";
|
|
10
|
+
import { createInMemoryRateLimiter } from "../middleware/rateLimiter.js";
|
|
11
|
+
import { createServices } from "../services/index.js";
|
|
12
|
+
import { createToolRegistry } from "../tools/index.js";
|
|
13
|
+
import { redactForLogs } from "../utils/sanitize.js";
|
|
14
|
+
|
|
15
|
+
function readPackageVersion() {
|
|
16
|
+
const pkgPath = path.resolve(
|
|
17
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
18
|
+
"../../package.json"
|
|
19
|
+
);
|
|
20
|
+
return JSON.parse(readFileSync(pkgPath, "utf8")).version;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function asMcpJson(payload) {
|
|
24
|
+
return {
|
|
25
|
+
content: [
|
|
26
|
+
{
|
|
27
|
+
type: "text",
|
|
28
|
+
text: JSON.stringify(payload, null, 2),
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function createMcpServer() {
|
|
35
|
+
const config = getConfig();
|
|
36
|
+
const logger = getLoggerSync();
|
|
37
|
+
const services = createServices();
|
|
38
|
+
const registry = createToolRegistry(services);
|
|
39
|
+
const limiter = createInMemoryRateLimiter({
|
|
40
|
+
limit: config.rateLimit.perMinute,
|
|
41
|
+
windowMs: 60 * 1000,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const server = new Server(
|
|
45
|
+
{
|
|
46
|
+
name: "easypost-mcp",
|
|
47
|
+
version: readPackageVersion(),
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
capabilities: {
|
|
51
|
+
tools: {},
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
57
|
+
logger.debug({ toolCount: registry.list().length }, "Listing MCP tools");
|
|
58
|
+
return { tools: registry.list() };
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
62
|
+
const correlationId = createCorrelationId();
|
|
63
|
+
const toolName = request.params.name;
|
|
64
|
+
const childLogger = logger.child({ correlationId, toolName });
|
|
65
|
+
|
|
66
|
+
return asMcpJson(
|
|
67
|
+
await withErrorHandling({ correlationId, logger: childLogger }, async () => {
|
|
68
|
+
limiter.consume(toolName);
|
|
69
|
+
childLogger.info(
|
|
70
|
+
{ arguments: redactForLogs(request.params.arguments) },
|
|
71
|
+
"MCP tool called"
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const tool = registry.get(toolName);
|
|
75
|
+
const result = await tool.execute(request.params.arguments, {
|
|
76
|
+
correlationId,
|
|
77
|
+
logger: childLogger,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
childLogger.info({ result: redactForLogs(result) }, "MCP tool completed");
|
|
81
|
+
return {
|
|
82
|
+
...result,
|
|
83
|
+
correlation_id: correlationId,
|
|
84
|
+
};
|
|
85
|
+
})
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return server;
|
|
90
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { createServer } from "node:http";
|
|
3
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
4
|
+
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
|
|
5
|
+
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
6
|
+
import { createMcpServer } from "../createServer.js";
|
|
7
|
+
|
|
8
|
+
export async function startHttpTransport({ logger, config }) {
|
|
9
|
+
const app = createMcpExpressApp({ host: config.http.host });
|
|
10
|
+
const transports = new Map();
|
|
11
|
+
|
|
12
|
+
const mcpPostHandler = async (req, res) => {
|
|
13
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
let transport;
|
|
17
|
+
|
|
18
|
+
if (sessionId && transports.has(sessionId)) {
|
|
19
|
+
transport = transports.get(sessionId);
|
|
20
|
+
} else if (!sessionId && isInitializeRequest(req.body)) {
|
|
21
|
+
transport = new StreamableHTTPServerTransport({
|
|
22
|
+
sessionIdGenerator: () => randomUUID(),
|
|
23
|
+
onsessioninitialized: (id) => {
|
|
24
|
+
transports.set(id, transport);
|
|
25
|
+
logger.debug({ sessionId: id }, "MCP HTTP session initialized");
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
transport.onclose = () => {
|
|
30
|
+
const id = transport.sessionId;
|
|
31
|
+
if (id && transports.has(id)) {
|
|
32
|
+
transports.delete(id);
|
|
33
|
+
logger.debug({ sessionId: id }, "MCP HTTP session closed");
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const server = createMcpServer();
|
|
38
|
+
await server.connect(transport);
|
|
39
|
+
await transport.handleRequest(req, res, req.body);
|
|
40
|
+
return;
|
|
41
|
+
} else {
|
|
42
|
+
res.status(400).json({
|
|
43
|
+
jsonrpc: "2.0",
|
|
44
|
+
error: {
|
|
45
|
+
code: -32000,
|
|
46
|
+
message: "Bad Request: No valid session ID provided",
|
|
47
|
+
},
|
|
48
|
+
id: null,
|
|
49
|
+
});
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
await transport.handleRequest(req, res, req.body);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
logger.error(
|
|
56
|
+
{ err: { message: error.message, stack: error.stack } },
|
|
57
|
+
"MCP HTTP request failed"
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
if (!res.headersSent) {
|
|
61
|
+
res.status(500).json({
|
|
62
|
+
jsonrpc: "2.0",
|
|
63
|
+
error: {
|
|
64
|
+
code: -32603,
|
|
65
|
+
message: "Internal server error",
|
|
66
|
+
},
|
|
67
|
+
id: null,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const mcpGetHandler = async (req, res) => {
|
|
74
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
75
|
+
const transport = sessionId ? transports.get(sessionId) : undefined;
|
|
76
|
+
|
|
77
|
+
if (!transport) {
|
|
78
|
+
res.status(400).send("Invalid or missing session ID");
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
await transport.handleRequest(req, res);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
app.post("/mcp", mcpPostHandler);
|
|
86
|
+
app.get("/mcp", mcpGetHandler);
|
|
87
|
+
app.get("/health", (_req, res) => {
|
|
88
|
+
res.json({ ok: true, service: "easypost-mcp" });
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const httpServer = createServer(app);
|
|
92
|
+
const { port, host } = config.http;
|
|
93
|
+
|
|
94
|
+
await new Promise((resolve, reject) => {
|
|
95
|
+
httpServer.listen(port, host, (error) => {
|
|
96
|
+
if (error) reject(error);
|
|
97
|
+
else resolve();
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
logger.info({ host, port, path: "/mcp" }, "EasyPost MCP server running on HTTP");
|
|
102
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2
|
+
import { createMcpServer } from "../createServer.js";
|
|
3
|
+
|
|
4
|
+
export async function startStdioTransport({ logger }) {
|
|
5
|
+
const server = createMcpServer();
|
|
6
|
+
const transport = new StdioServerTransport();
|
|
7
|
+
|
|
8
|
+
await server.connect(transport);
|
|
9
|
+
logger.info("EasyPost MCP server running on stdio");
|
|
10
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { mapAddress } from "../adapters/easypost/responseMappers.js";
|
|
2
|
+
|
|
3
|
+
class AddressService {
|
|
4
|
+
constructor(easypostClient) {
|
|
5
|
+
this.easypost = easypostClient;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
async verifyAddress(input, context) {
|
|
9
|
+
const address = await this.easypost.execute(
|
|
10
|
+
"address.verify",
|
|
11
|
+
(client) => client.Address.createAndVerify
|
|
12
|
+
? client.Address.createAndVerify(input.address)
|
|
13
|
+
: client.Address.create({ ...input.address, verify: input.verifications }),
|
|
14
|
+
context
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
ok: true,
|
|
19
|
+
address: mapAddress(address),
|
|
20
|
+
verification_status: address.verifications?.delivery?.success === true ? "verified" : "review_required",
|
|
21
|
+
messages: address.verifications?.delivery?.errors || [],
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async createAddress(input, context) {
|
|
26
|
+
const address = await this.easypost.execute(
|
|
27
|
+
"address.create",
|
|
28
|
+
(client) => client.Address.create({ ...input.address, verify: input.verify }),
|
|
29
|
+
context
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
return { ok: true, address: mapAddress(address) };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export { AddressService };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
class BatchService {
|
|
2
|
+
constructor(easypostClient) {
|
|
3
|
+
this.easypost = easypostClient;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
async createBatch(input, context) {
|
|
7
|
+
const shipments = input.shipments.map((shipment) => (
|
|
8
|
+
shipment.shipment_id ? { id: shipment.shipment_id } : shipment
|
|
9
|
+
));
|
|
10
|
+
|
|
11
|
+
const batch = await this.easypost.execute(
|
|
12
|
+
"batch.create",
|
|
13
|
+
(client) => client.Batch.create({ shipments }),
|
|
14
|
+
context
|
|
15
|
+
);
|
|
16
|
+
return { ok: true, batch };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async buyBatch(input, context) {
|
|
20
|
+
const batch = await this.easypost.execute(
|
|
21
|
+
"batch.buy",
|
|
22
|
+
(client) => client.Batch.buy(input.batch_id),
|
|
23
|
+
context
|
|
24
|
+
);
|
|
25
|
+
return { ok: true, batch };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async batchStatus(input, context) {
|
|
29
|
+
const batch = await this.easypost.execute(
|
|
30
|
+
"batch.retrieve",
|
|
31
|
+
(client) => client.Batch.retrieve(input.batch_id),
|
|
32
|
+
context
|
|
33
|
+
);
|
|
34
|
+
return { ok: true, batch };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export { BatchService };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
class OrderService {
|
|
2
|
+
constructor(easypostClient) {
|
|
3
|
+
this.easypost = easypostClient;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
async createOrder(input, context) {
|
|
7
|
+
const order = await this.easypost.execute(
|
|
8
|
+
"order.create",
|
|
9
|
+
(client) => client.Order.create(input),
|
|
10
|
+
context
|
|
11
|
+
);
|
|
12
|
+
return { ok: true, order };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async getOrder(input, context) {
|
|
16
|
+
const order = await this.easypost.execute(
|
|
17
|
+
"order.retrieve",
|
|
18
|
+
(client) => client.Order.retrieve(input.order_id),
|
|
19
|
+
context
|
|
20
|
+
);
|
|
21
|
+
return { ok: true, order };
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export { OrderService };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
class PickupService {
|
|
2
|
+
constructor(easypostClient) {
|
|
3
|
+
this.easypost = easypostClient;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
async schedulePickup(input, context) {
|
|
7
|
+
const pickup = await this.easypost.execute(
|
|
8
|
+
"pickup.create",
|
|
9
|
+
(client) => client.Pickup.create({
|
|
10
|
+
shipment: { id: input.shipment_ids[0] },
|
|
11
|
+
address: input.address,
|
|
12
|
+
min_datetime: input.min_datetime,
|
|
13
|
+
max_datetime: input.max_datetime,
|
|
14
|
+
instructions: input.instructions,
|
|
15
|
+
carrier_accounts: input.carrier_accounts,
|
|
16
|
+
}),
|
|
17
|
+
context
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const pickupRate = pickup.lowestRate?.() || pickup.pickup_rates?.[0];
|
|
21
|
+
const scheduled = pickupRate
|
|
22
|
+
? await this.easypost.execute(
|
|
23
|
+
"pickup.buy",
|
|
24
|
+
(client) => client.Pickup.buy(pickup.id, pickupRate.carrier, pickupRate.service),
|
|
25
|
+
context
|
|
26
|
+
)
|
|
27
|
+
: pickup;
|
|
28
|
+
|
|
29
|
+
return { ok: true, pickup: scheduled, pickup_rate: pickupRate || null };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async cancelPickup(input, context) {
|
|
33
|
+
const pickup = await this.easypost.execute(
|
|
34
|
+
"pickup.cancel",
|
|
35
|
+
(client) => client.Pickup.cancel(input.pickup_id),
|
|
36
|
+
context
|
|
37
|
+
);
|
|
38
|
+
return { ok: true, pickup };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export { PickupService };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { mapShipment, mapRate } from "../adapters/easypost/responseMappers.js";
|
|
2
|
+
|
|
3
|
+
class ReturnService {
|
|
4
|
+
constructor(easypostClient) {
|
|
5
|
+
this.easypost = easypostClient;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
async createReturnLabel(input, context) {
|
|
9
|
+
const shipment = await this.easypost.execute(
|
|
10
|
+
"return.create",
|
|
11
|
+
(client) => client.Shipment.create({
|
|
12
|
+
from_address: input.from_address,
|
|
13
|
+
to_address: input.to_address,
|
|
14
|
+
parcel: input.parcel,
|
|
15
|
+
is_return: true,
|
|
16
|
+
options: input.original_shipment_id ? { original_shipment_id: input.original_shipment_id } : undefined,
|
|
17
|
+
}),
|
|
18
|
+
context
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const rate = input.carrier && input.service
|
|
22
|
+
? (shipment.rates || []).find((candidate) => candidate.carrier === input.carrier && candidate.service === input.service)
|
|
23
|
+
: shipment.lowestRate?.();
|
|
24
|
+
|
|
25
|
+
const bought = rate
|
|
26
|
+
? await this.easypost.execute("return.buy", (client) => client.Shipment.buy(shipment.id, rate), context)
|
|
27
|
+
: shipment;
|
|
28
|
+
|
|
29
|
+
return { ok: true, shipment: mapShipment(bought), purchased_rate: mapRate(rate) };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export { ReturnService };
|