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,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { runCli } from "../src/cli/index.js";
|
|
4
|
+
|
|
5
|
+
runCli(process.argv).catch((error) => {
|
|
6
|
+
process.stderr.write(
|
|
7
|
+
`${JSON.stringify({
|
|
8
|
+
level: "fatal",
|
|
9
|
+
msg: error.message,
|
|
10
|
+
stack: error.stack,
|
|
11
|
+
})}\n`
|
|
12
|
+
);
|
|
13
|
+
process.exit(1);
|
|
14
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "easypost-mcp",
|
|
3
|
+
"version": "2.1.0",
|
|
4
|
+
"description": "Production-grade EasyPost Model Context Protocol (MCP) server for Claude, Cursor, and programmatic agents",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js",
|
|
9
|
+
"./package.json": "./package.json"
|
|
10
|
+
},
|
|
11
|
+
"bin": {
|
|
12
|
+
"easypost-mcp": "./bin/easypost-mcp.js"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"bin",
|
|
16
|
+
"src",
|
|
17
|
+
"README.md",
|
|
18
|
+
"PUBLISHING.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18.0.0"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"start": "node bin/easypost-mcp.js start",
|
|
26
|
+
"start:stdio": "node bin/easypost-mcp.js start --mode=stdio",
|
|
27
|
+
"start:http": "node bin/easypost-mcp.js start --mode=http",
|
|
28
|
+
"test": "node --test",
|
|
29
|
+
"prepack": "node scripts/verify-pack.js",
|
|
30
|
+
"check:mcp:safe": "node scripts/manual-safe-mcp-check.js",
|
|
31
|
+
"check:mcp:full": "node scripts/full-mcp-tool-check.js",
|
|
32
|
+
"inspect:tools": "node bin/easypost-mcp.js --help"
|
|
33
|
+
},
|
|
34
|
+
"keywords": [
|
|
35
|
+
"mcp",
|
|
36
|
+
"model-context-protocol",
|
|
37
|
+
"easypost",
|
|
38
|
+
"shipping",
|
|
39
|
+
"logistics",
|
|
40
|
+
"claude",
|
|
41
|
+
"cursor",
|
|
42
|
+
"stdio",
|
|
43
|
+
"ai-agent"
|
|
44
|
+
],
|
|
45
|
+
"author": "Surya",
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
49
|
+
"license": "ISC",
|
|
50
|
+
"repository": {
|
|
51
|
+
"type": "git",
|
|
52
|
+
"url": "git+https://github.com/your-org/easypost-mcp.git"
|
|
53
|
+
},
|
|
54
|
+
"bugs": {
|
|
55
|
+
"url": "https://github.com/your-org/easypost-mcp/issues"
|
|
56
|
+
},
|
|
57
|
+
"homepage": "https://github.com/your-org/easypost-mcp#readme",
|
|
58
|
+
"dependencies": {
|
|
59
|
+
"@easypost/api": "^8.8.0",
|
|
60
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
61
|
+
"commander": "^13.1.0",
|
|
62
|
+
"dotenv": "^16.4.7",
|
|
63
|
+
"express": "^5.2.1",
|
|
64
|
+
"pino": "^9.6.0",
|
|
65
|
+
"zod": "^3.24.1",
|
|
66
|
+
"zod-to-json-schema": "^3.24.1"
|
|
67
|
+
},
|
|
68
|
+
"devDependencies": {
|
|
69
|
+
"@modelcontextprotocol/inspector": "^0.16.6"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import EasyPost from "@easypost/api";
|
|
2
|
+
import { getConfig } from "../../config/index.js";
|
|
3
|
+
import { getLoggerSync } from "../../logging/logger.js";
|
|
4
|
+
import { mapEasyPostError } from "../../errors/easypostErrorMapper.js";
|
|
5
|
+
import { redactForLogs } from "../../utils/sanitize.js";
|
|
6
|
+
|
|
7
|
+
function wait(ms) {
|
|
8
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function withTimeout(promise, timeoutMs) {
|
|
12
|
+
let timeout;
|
|
13
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
14
|
+
timeout = setTimeout(() => {
|
|
15
|
+
const error = new Error("EasyPost request timed out");
|
|
16
|
+
error.statusCode = 504;
|
|
17
|
+
error.code = "EASYPOST_TIMEOUT";
|
|
18
|
+
reject(error);
|
|
19
|
+
}, timeoutMs);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
return Promise.race([promise, timeoutPromise]).finally(() => clearTimeout(timeout));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
class EasyPostClient {
|
|
26
|
+
constructor(options = {}) {
|
|
27
|
+
const config = getConfig();
|
|
28
|
+
const {
|
|
29
|
+
apiKey = config.easypost.apiKey,
|
|
30
|
+
timeoutMs = config.easypost.timeoutMs,
|
|
31
|
+
retryAttempts = config.easypost.retryAttempts,
|
|
32
|
+
} = options;
|
|
33
|
+
|
|
34
|
+
this.client = new EasyPost(apiKey);
|
|
35
|
+
this.timeoutMs = timeoutMs;
|
|
36
|
+
this.retryAttempts = retryAttempts;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async execute(operation, fn, context = {}) {
|
|
40
|
+
const childLogger = getLoggerSync().child({
|
|
41
|
+
correlationId: context.correlationId,
|
|
42
|
+
operation,
|
|
43
|
+
provider: "easypost",
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
for (let attempt = 0; attempt <= this.retryAttempts; attempt += 1) {
|
|
47
|
+
const startedAt = Date.now();
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
childLogger.debug({ attempt }, "EasyPost request started");
|
|
51
|
+
const result = await withTimeout(fn(this.client), this.timeoutMs);
|
|
52
|
+
childLogger.debug({ attempt, durationMs: Date.now() - startedAt }, "EasyPost request completed");
|
|
53
|
+
return result;
|
|
54
|
+
} catch (error) {
|
|
55
|
+
const mapped = mapEasyPostError(error);
|
|
56
|
+
|
|
57
|
+
childLogger.warn(
|
|
58
|
+
{ attempt, retryable: mapped.retryable, err: redactForLogs(mapped.details) },
|
|
59
|
+
"EasyPost request failed"
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
if (!mapped.retryable || attempt === this.retryAttempts) {
|
|
63
|
+
throw mapped;
|
|
64
|
+
}
|
|
65
|
+
await wait(100 * 2 ** attempt);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
get raw() {
|
|
71
|
+
return this.client;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export { EasyPostClient };
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
function mapRate(rate) {
|
|
2
|
+
if (!rate) return null;
|
|
3
|
+
return {
|
|
4
|
+
id: rate.id,
|
|
5
|
+
carrier: rate.carrier,
|
|
6
|
+
service: rate.service,
|
|
7
|
+
rate: rate.rate,
|
|
8
|
+
currency: rate.currency || "USD",
|
|
9
|
+
delivery_days: rate.delivery_days,
|
|
10
|
+
delivery_date: rate.delivery_date,
|
|
11
|
+
delivery_date_guaranteed: rate.delivery_date_guaranteed,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function mapAddress(address) {
|
|
16
|
+
if (!address) return null;
|
|
17
|
+
return {
|
|
18
|
+
id: address.id,
|
|
19
|
+
street1: address.street1,
|
|
20
|
+
street2: address.street2,
|
|
21
|
+
city: address.city,
|
|
22
|
+
state: address.state,
|
|
23
|
+
zip: address.zip,
|
|
24
|
+
country: address.country,
|
|
25
|
+
residential: address.residential,
|
|
26
|
+
verifications: address.verifications,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function mapShipment(shipment) {
|
|
31
|
+
if (!shipment) return null;
|
|
32
|
+
return {
|
|
33
|
+
id: shipment.id,
|
|
34
|
+
mode: shipment.mode,
|
|
35
|
+
status: shipment.status,
|
|
36
|
+
tracking_code: shipment.tracking_code,
|
|
37
|
+
reference: shipment.reference,
|
|
38
|
+
from_address: mapAddress(shipment.from_address),
|
|
39
|
+
to_address: mapAddress(shipment.to_address),
|
|
40
|
+
parcel: shipment.parcel
|
|
41
|
+
? {
|
|
42
|
+
id: shipment.parcel.id,
|
|
43
|
+
length: shipment.parcel.length,
|
|
44
|
+
width: shipment.parcel.width,
|
|
45
|
+
height: shipment.parcel.height,
|
|
46
|
+
weight: shipment.parcel.weight,
|
|
47
|
+
}
|
|
48
|
+
: null,
|
|
49
|
+
selected_rate: mapRate(shipment.selected_rate),
|
|
50
|
+
rates: Array.isArray(shipment.rates) ? shipment.rates.map(mapRate) : [],
|
|
51
|
+
postage_label: shipment.postage_label
|
|
52
|
+
? {
|
|
53
|
+
label_url: shipment.postage_label.label_url,
|
|
54
|
+
label_pdf_url: shipment.postage_label.label_pdf_url,
|
|
55
|
+
label_zpl_url: shipment.postage_label.label_zpl_url,
|
|
56
|
+
label_file_type: shipment.postage_label.label_file_type,
|
|
57
|
+
}
|
|
58
|
+
: null,
|
|
59
|
+
tracker: mapTracker(shipment.tracker),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function mapTracker(tracker) {
|
|
64
|
+
if (!tracker) return null;
|
|
65
|
+
return {
|
|
66
|
+
id: tracker.id,
|
|
67
|
+
tracking_code: tracker.tracking_code,
|
|
68
|
+
carrier: tracker.carrier,
|
|
69
|
+
status: tracker.status,
|
|
70
|
+
status_detail: tracker.status_detail,
|
|
71
|
+
estimated_delivery_date: tracker.est_delivery_date,
|
|
72
|
+
signed_by: tracker.signed_by,
|
|
73
|
+
public_url: tracker.public_url,
|
|
74
|
+
tracking_details: Array.isArray(tracker.tracking_details)
|
|
75
|
+
? tracker.tracking_details.map((detail) => ({
|
|
76
|
+
message: detail.message,
|
|
77
|
+
status: detail.status,
|
|
78
|
+
status_detail: detail.status_detail,
|
|
79
|
+
datetime: detail.datetime,
|
|
80
|
+
source: detail.source,
|
|
81
|
+
tracking_location: detail.tracking_location
|
|
82
|
+
? {
|
|
83
|
+
city: detail.tracking_location.city,
|
|
84
|
+
state: detail.tracking_location.state,
|
|
85
|
+
country: detail.tracking_location.country,
|
|
86
|
+
zip: detail.tracking_location.zip,
|
|
87
|
+
}
|
|
88
|
+
: null,
|
|
89
|
+
}))
|
|
90
|
+
: [],
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function mapCollection(collection, mapper) {
|
|
95
|
+
return {
|
|
96
|
+
has_more: Boolean(collection?.has_more),
|
|
97
|
+
items: Array.isArray(collection?.shipments)
|
|
98
|
+
? collection.shipments.map(mapper)
|
|
99
|
+
: Array.isArray(collection)
|
|
100
|
+
? collection.map(mapper)
|
|
101
|
+
: [],
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export { mapAddress, mapRate, mapShipment, mapTracker, mapCollection };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { applyBootstrapOptions } from "../../config/bootstrap.js";
|
|
2
|
+
import { getConfig } from "../../config/index.js";
|
|
3
|
+
import { getLogger, initLogger } from "../../logging/logger.js";
|
|
4
|
+
import { startStdioTransport } from "../../server/transports/stdio.js";
|
|
5
|
+
import { startHttpTransport } from "../../server/transports/http.js";
|
|
6
|
+
|
|
7
|
+
function normalizeTransportMode(mode) {
|
|
8
|
+
const value = String(mode || "stdio").toLowerCase();
|
|
9
|
+
if (value === "stdio" || value === "http") {
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
throw new Error(`Invalid --mode "${mode}". Use "stdio" or "http".`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function runStartCommand(options) {
|
|
16
|
+
applyBootstrapOptions({
|
|
17
|
+
apiKey: options.apiKey,
|
|
18
|
+
mode: options.easypostMode,
|
|
19
|
+
logLevel: options.logLevel,
|
|
20
|
+
timeoutMs: options.timeoutMs ? Number(options.timeoutMs) : undefined,
|
|
21
|
+
retryAttempts: options.retryAttempts ? Number(options.retryAttempts) : undefined,
|
|
22
|
+
rateLimitPerMinute: options.rateLimit ? Number(options.rateLimit) : undefined,
|
|
23
|
+
httpPort: options.port ? Number(options.port) : undefined,
|
|
24
|
+
httpHost: options.host,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const config = getConfig();
|
|
28
|
+
const logger = initLogger(config);
|
|
29
|
+
const transportMode = normalizeTransportMode(options.mode);
|
|
30
|
+
|
|
31
|
+
logger.info(
|
|
32
|
+
{
|
|
33
|
+
transport: transportMode,
|
|
34
|
+
easypostMode: config.easypost.mode,
|
|
35
|
+
nodeEnv: config.env,
|
|
36
|
+
http: transportMode === "http" ? config.http : undefined,
|
|
37
|
+
},
|
|
38
|
+
"Starting EasyPost MCP server"
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
if (transportMode === "stdio") {
|
|
42
|
+
await startStdioTransport({ logger });
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
await startHttpTransport({ logger, config });
|
|
47
|
+
}
|
package/src/cli/index.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { runStartCommand } from "./commands/start.js";
|
|
6
|
+
|
|
7
|
+
function readPackageVersion() {
|
|
8
|
+
const pkgPath = path.resolve(
|
|
9
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
10
|
+
"../../package.json"
|
|
11
|
+
);
|
|
12
|
+
return JSON.parse(readFileSync(pkgPath, "utf8")).version;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function createProgram() {
|
|
16
|
+
const program = new Command();
|
|
17
|
+
|
|
18
|
+
program
|
|
19
|
+
.name("easypost-mcp")
|
|
20
|
+
.description("EasyPost Model Context Protocol (MCP) server")
|
|
21
|
+
.version(readPackageVersion(), "-V, --version", "Show version");
|
|
22
|
+
|
|
23
|
+
program
|
|
24
|
+
.command("start")
|
|
25
|
+
.description("Start the EasyPost MCP server")
|
|
26
|
+
.option("--api-key <key>", "EasyPost API key (or set EASYPOST_API_KEY)")
|
|
27
|
+
.option("--mode <transport>", "Transport mode: stdio or http", "stdio")
|
|
28
|
+
.option("--easypost-mode <mode>", "EasyPost environment: sandbox or production", "sandbox")
|
|
29
|
+
.option("--log-level <level>", "Log level (trace, debug, info, warn, error)")
|
|
30
|
+
.option("--port <port>", "HTTP port when --mode=http", "3000")
|
|
31
|
+
.option("--host <host>", "HTTP bind host when --mode=http", "127.0.0.1")
|
|
32
|
+
.option("--timeout-ms <ms>", "EasyPost request timeout in milliseconds", "30000")
|
|
33
|
+
.option("--retry-attempts <n>", "EasyPost retry attempts", "2")
|
|
34
|
+
.option("--rate-limit <n>", "MCP tool calls per minute", "60")
|
|
35
|
+
.action(async (options) => {
|
|
36
|
+
await runStartCommand(options);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
return program;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function runCli(argv) {
|
|
43
|
+
const program = createProgram();
|
|
44
|
+
await program.parseAsync(argv);
|
|
45
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Apply CLI options to process.env before config validation runs.
|
|
3
|
+
* Priority at runtime: CLI flags → existing env → dotenv file (loaded earlier).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export function applyBootstrapOptions(options = {}) {
|
|
7
|
+
if (options.apiKey) {
|
|
8
|
+
process.env.EASYPOST_API_KEY = options.apiKey;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (options.mode) {
|
|
12
|
+
process.env.EASYPOST_MODE = options.mode;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (options.logLevel) {
|
|
16
|
+
process.env.LOG_LEVEL = options.logLevel;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (options.nodeEnv) {
|
|
20
|
+
process.env.NODE_ENV = options.nodeEnv;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (options.timeoutMs != null) {
|
|
24
|
+
process.env.EASYPOST_TIMEOUT_MS = String(options.timeoutMs);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (options.retryAttempts != null) {
|
|
28
|
+
process.env.EASYPOST_RETRY_ATTEMPTS = String(options.retryAttempts);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (options.rateLimitPerMinute != null) {
|
|
32
|
+
process.env.MCP_RATE_LIMIT_PER_MINUTE = String(options.rateLimitPerMinute);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (options.httpPort != null) {
|
|
36
|
+
process.env.MCP_HTTP_PORT = String(options.httpPort);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (options.httpHost) {
|
|
40
|
+
process.env.MCP_HTTP_HOST = options.httpHost;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { loadConfig } from "./loadConfig.js";
|
|
2
|
+
|
|
3
|
+
let cachedConfig = null;
|
|
4
|
+
|
|
5
|
+
export function getConfig() {
|
|
6
|
+
if (!cachedConfig) {
|
|
7
|
+
cachedConfig = loadConfig();
|
|
8
|
+
}
|
|
9
|
+
return cachedConfig;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function resetConfigForTests() {
|
|
13
|
+
cachedConfig = null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export { loadConfig } from "./loadConfig.js";
|
|
17
|
+
export { applyBootstrapOptions } from "./bootstrap.js";
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { config as loadDotenv } from "dotenv";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
|
|
8
|
+
let dotenvLoaded = false;
|
|
9
|
+
|
|
10
|
+
function ensureDotenv() {
|
|
11
|
+
if (dotenvLoaded) return;
|
|
12
|
+
dotenvLoaded = true;
|
|
13
|
+
loadDotenv({
|
|
14
|
+
path: path.resolve(__dirname, "../../.env"),
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const envSchema = z.object({
|
|
19
|
+
NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
|
|
20
|
+
EASYPOST_API_KEY: z.string().min(1, "EASYPOST_API_KEY is required (use --api-key or env)"),
|
|
21
|
+
EASYPOST_MODE: z.enum(["sandbox", "production"]).default("sandbox"),
|
|
22
|
+
LOG_LEVEL: z.enum(["trace", "debug", "info", "warn", "error", "fatal", "silent"]).default("info"),
|
|
23
|
+
DEBUG: z.string().optional(),
|
|
24
|
+
EASYPOST_TIMEOUT_MS: z.coerce.number().int().positive().default(30000),
|
|
25
|
+
EASYPOST_RETRY_ATTEMPTS: z.coerce.number().int().min(0).max(5).default(2),
|
|
26
|
+
MCP_RATE_LIMIT_PER_MINUTE: z.coerce.number().int().positive().default(60),
|
|
27
|
+
MCP_HTTP_PORT: z.coerce.number().int().positive().default(3000),
|
|
28
|
+
MCP_HTTP_HOST: z.string().default("127.0.0.1"),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export function loadConfig() {
|
|
32
|
+
ensureDotenv();
|
|
33
|
+
|
|
34
|
+
const parsed = envSchema.safeParse(process.env);
|
|
35
|
+
|
|
36
|
+
if (!parsed.success) {
|
|
37
|
+
const message = parsed.error.issues
|
|
38
|
+
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
|
|
39
|
+
.join("; ");
|
|
40
|
+
throw new Error(`Invalid configuration: ${message}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const env = parsed.data;
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
env: env.NODE_ENV,
|
|
47
|
+
isProduction: env.NODE_ENV === "production",
|
|
48
|
+
debug: env.DEBUG === "true" || env.LOG_LEVEL === "debug",
|
|
49
|
+
easypost: {
|
|
50
|
+
apiKey: env.EASYPOST_API_KEY,
|
|
51
|
+
mode: env.EASYPOST_MODE,
|
|
52
|
+
timeoutMs: env.EASYPOST_TIMEOUT_MS,
|
|
53
|
+
retryAttempts: env.EASYPOST_RETRY_ATTEMPTS,
|
|
54
|
+
},
|
|
55
|
+
logging: {
|
|
56
|
+
level: env.LOG_LEVEL,
|
|
57
|
+
},
|
|
58
|
+
rateLimit: {
|
|
59
|
+
perMinute: env.MCP_RATE_LIMIT_PER_MINUTE,
|
|
60
|
+
},
|
|
61
|
+
http: {
|
|
62
|
+
port: env.MCP_HTTP_PORT,
|
|
63
|
+
host: env.MCP_HTTP_HOST,
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
class AppError extends Error {
|
|
2
|
+
constructor(message, options = {}) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = this.constructor.name;
|
|
5
|
+
this.code = options.code || "APP_ERROR";
|
|
6
|
+
this.statusCode = options.statusCode || 500;
|
|
7
|
+
this.details = options.details;
|
|
8
|
+
this.retryable = Boolean(options.retryable);
|
|
9
|
+
this.safeMessage = options.safeMessage || message;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
class ValidationError extends AppError {
|
|
14
|
+
constructor(message, details) {
|
|
15
|
+
super(message, {
|
|
16
|
+
code: "VALIDATION_ERROR",
|
|
17
|
+
statusCode: 400,
|
|
18
|
+
details,
|
|
19
|
+
safeMessage: "Input validation failed",
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
class AntiHallucinationError extends AppError {
|
|
25
|
+
constructor(message, details) {
|
|
26
|
+
super(message, {
|
|
27
|
+
code: "SUSPICIOUS_INPUT",
|
|
28
|
+
statusCode: 400,
|
|
29
|
+
details,
|
|
30
|
+
safeMessage: message,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
class ExternalServiceError extends AppError {
|
|
36
|
+
constructor(message, options = {}) {
|
|
37
|
+
super(message, {
|
|
38
|
+
code: options.code || "EASYPOST_ERROR",
|
|
39
|
+
statusCode: options.statusCode || 502,
|
|
40
|
+
details: options.details,
|
|
41
|
+
retryable: options.retryable,
|
|
42
|
+
safeMessage: options.safeMessage || "Shipping provider request failed",
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
class NotFoundError extends AppError {
|
|
48
|
+
constructor(resource, id) {
|
|
49
|
+
super(`${resource} not found`, {
|
|
50
|
+
code: "NOT_FOUND",
|
|
51
|
+
statusCode: 404,
|
|
52
|
+
details: { resource, id },
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export { AppError, ValidationError, AntiHallucinationError, ExternalServiceError, NotFoundError };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ExternalServiceError } from "./AppError.js";
|
|
2
|
+
|
|
3
|
+
function mapEasyPostError(error) {
|
|
4
|
+
const statusCode = error.statusCode || error.status || error.response?.statusCode || 502;
|
|
5
|
+
const code = error.code || error.error?.code || "EASYPOST_ERROR";
|
|
6
|
+
const message = error.message || error.error?.message || "EasyPost request failed";
|
|
7
|
+
const retryable = statusCode === 429 || statusCode >= 500;
|
|
8
|
+
|
|
9
|
+
return new ExternalServiceError(message, {
|
|
10
|
+
code,
|
|
11
|
+
statusCode,
|
|
12
|
+
retryable,
|
|
13
|
+
details: {
|
|
14
|
+
statusCode,
|
|
15
|
+
code,
|
|
16
|
+
errors: error.errors || error.error?.errors,
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export { mapEasyPostError };
|
package/src/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { createMcpServer } from "./server/createServer.js";
|
|
2
|
+
export { createToolRegistry } from "./tools/index.js";
|
|
3
|
+
export { createServices } from "./services/index.js";
|
|
4
|
+
export { getConfig, loadConfig, resetConfigForTests, applyBootstrapOptions } from "./config/index.js";
|
|
5
|
+
export { runCli, createProgram } from "./cli/index.js";
|
|
6
|
+
export { runStartCommand } from "./cli/commands/start.js";
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import pino from "pino";
|
|
2
|
+
import { redactForLogs } from "../utils/sanitize.js";
|
|
3
|
+
|
|
4
|
+
let loggerInstance = null;
|
|
5
|
+
|
|
6
|
+
function createFallbackLogger(config) {
|
|
7
|
+
const write = (level, obj, msg) => {
|
|
8
|
+
const payload = {
|
|
9
|
+
level,
|
|
10
|
+
time: new Date().toISOString(),
|
|
11
|
+
msg,
|
|
12
|
+
...redactForLogs(obj || {}),
|
|
13
|
+
};
|
|
14
|
+
process.stderr.write(`${JSON.stringify(payload)}\n`);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const logger = {
|
|
18
|
+
trace: (obj, msg) => write("trace", obj, msg),
|
|
19
|
+
debug: (obj, msg) => write("debug", obj, msg),
|
|
20
|
+
info: (obj, msg) => write("info", obj, msg),
|
|
21
|
+
warn: (obj, msg) => write("warn", obj, msg),
|
|
22
|
+
error: (obj, msg) => write("error", obj, msg),
|
|
23
|
+
fatal: (obj, msg) => write("fatal", obj, msg),
|
|
24
|
+
child: (bindings) => {
|
|
25
|
+
const child = createFallbackLogger(config);
|
|
26
|
+
for (const level of ["trace", "debug", "info", "warn", "error", "fatal"]) {
|
|
27
|
+
const original = child[level];
|
|
28
|
+
child[level] = (obj, msg) => original({ ...bindings, ...(obj || {}) }, msg);
|
|
29
|
+
}
|
|
30
|
+
return child;
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return logger;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function initLogger(config) {
|
|
38
|
+
loggerInstance = pino(
|
|
39
|
+
{
|
|
40
|
+
level: config.logging.level,
|
|
41
|
+
redact: {
|
|
42
|
+
paths: [
|
|
43
|
+
"*.authorization",
|
|
44
|
+
"*.api_key",
|
|
45
|
+
"*.apiKey",
|
|
46
|
+
"*.token",
|
|
47
|
+
"*.password",
|
|
48
|
+
"req.headers.authorization",
|
|
49
|
+
],
|
|
50
|
+
censor: "***REDACTED***",
|
|
51
|
+
},
|
|
52
|
+
base: {
|
|
53
|
+
service: "easypost-mcp",
|
|
54
|
+
environment: config.env,
|
|
55
|
+
easypostMode: config.easypost.mode,
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
process.stderr
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
return loggerInstance;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function getLogger() {
|
|
65
|
+
if (!loggerInstance) {
|
|
66
|
+
throw new Error("Logger not initialized. Call initLogger() during server startup.");
|
|
67
|
+
}
|
|
68
|
+
return loggerInstance;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Sync getter used after initLogger in startup path
|
|
72
|
+
export function getLoggerSync() {
|
|
73
|
+
if (!loggerInstance) {
|
|
74
|
+
throw new Error("Logger not initialized. Call initLogger() during server startup.");
|
|
75
|
+
}
|
|
76
|
+
return loggerInstance;
|
|
77
|
+
}
|