@codespar/mcp-andreani 0.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/README.md ADDED
@@ -0,0 +1,53 @@
1
+ # MCP Andreani
2
+
3
+ MCP server for **Andreani** — the largest Argentine courier and logistics company, providing domestic and international shipping services.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ # Set your credentials
9
+ export ANDREANI_API_KEY="your-api-key"
10
+ export ANDREANI_USER="your-username"
11
+ export ANDREANI_PASSWORD="your-password"
12
+
13
+ # Run via stdio
14
+ npx tsx packages/argentina/andreani/src/index.ts
15
+
16
+ # Run via HTTP
17
+ npx tsx packages/argentina/andreani/src/index.ts --http
18
+ ```
19
+
20
+ ## Environment Variables
21
+
22
+ | Variable | Required | Description |
23
+ |----------|----------|-------------|
24
+ | `ANDREANI_API_KEY` | Yes | API key from Andreani |
25
+ | `ANDREANI_USER` | Yes | Username for authentication |
26
+ | `ANDREANI_PASSWORD` | Yes | Password for authentication |
27
+ | `MCP_HTTP` | No | Set to `"true"` to enable HTTP transport |
28
+ | `MCP_PORT` | No | HTTP port (default: 3000) |
29
+
30
+ ## Tools
31
+
32
+ | Tool | Description |
33
+ |------|-------------|
34
+ | `create_shipment` | Create a new shipment |
35
+ | `get_shipment` | Get shipment details by ID |
36
+ | `track_shipment` | Track a shipment by tracking number |
37
+ | `get_rates` | Get shipping rates/quotes |
38
+ | `list_branches` | List Andreani branches/sucursales |
39
+ | `create_label` | Generate a shipping label |
40
+ | `get_tracking_history` | Get full tracking history |
41
+ | `cancel_shipment` | Cancel a shipment |
42
+
43
+ ## Auth
44
+
45
+ Uses **Bearer token** authentication. The server logs in with username/password to obtain a JWT token, which is used for subsequent API calls. An API key header is also included.
46
+
47
+ ## API Reference
48
+
49
+ - [Andreani API Docs](https://api.andreani.com/v2/docs)
50
+
51
+ ---
52
+
53
+ **Enterprise?** Contact us at [codespar.com](https://codespar.com) for dedicated support, custom integrations, and SLAs.
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * MCP Server for Andreani — largest Argentine courier/logistics provider.
4
+ *
5
+ * Tools:
6
+ * - create_shipment: Create a new shipment
7
+ * - get_shipment: Get shipment details by ID
8
+ * - track_shipment: Track a shipment by tracking number
9
+ * - get_rates: Get shipping rates/quotes
10
+ * - list_branches: List Andreani branches/sucursales
11
+ * - create_label: Generate a shipping label
12
+ * - get_tracking_history: Get full tracking history
13
+ * - cancel_shipment: Cancel a shipment
14
+ *
15
+ * Environment:
16
+ * ANDREANI_API_KEY — API key
17
+ * ANDREANI_USER — Username
18
+ * ANDREANI_PASSWORD — Password
19
+ */
20
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,288 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * MCP Server for Andreani — largest Argentine courier/logistics provider.
4
+ *
5
+ * Tools:
6
+ * - create_shipment: Create a new shipment
7
+ * - get_shipment: Get shipment details by ID
8
+ * - track_shipment: Track a shipment by tracking number
9
+ * - get_rates: Get shipping rates/quotes
10
+ * - list_branches: List Andreani branches/sucursales
11
+ * - create_label: Generate a shipping label
12
+ * - get_tracking_history: Get full tracking history
13
+ * - cancel_shipment: Cancel a shipment
14
+ *
15
+ * Environment:
16
+ * ANDREANI_API_KEY — API key
17
+ * ANDREANI_USER — Username
18
+ * ANDREANI_PASSWORD — Password
19
+ */
20
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
21
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
22
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
23
+ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
24
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
25
+ const API_KEY = process.env.ANDREANI_API_KEY || "";
26
+ const USER = process.env.ANDREANI_USER || "";
27
+ const PASSWORD = process.env.ANDREANI_PASSWORD || "";
28
+ const BASE_URL = "https://api.andreani.com/v2";
29
+ let cachedToken = null;
30
+ async function getToken() {
31
+ if (cachedToken)
32
+ return cachedToken;
33
+ const res = await fetch(`${BASE_URL}/login`, {
34
+ method: "GET",
35
+ headers: {
36
+ "Authorization": `Basic ${Buffer.from(USER + ":" + PASSWORD).toString("base64")}`,
37
+ },
38
+ });
39
+ if (!res.ok)
40
+ throw new Error(`Andreani login failed: ${res.status}`);
41
+ const token = res.headers.get("x-authorization-token") || "";
42
+ cachedToken = token;
43
+ return token;
44
+ }
45
+ async function andreaniRequest(method, path, body) {
46
+ const token = await getToken();
47
+ const headers = {
48
+ "Content-Type": "application/json",
49
+ "Accept": "application/json",
50
+ "x-authorization-token": token,
51
+ };
52
+ if (API_KEY)
53
+ headers["x-api-key"] = API_KEY;
54
+ const res = await fetch(`${BASE_URL}${path}`, {
55
+ method,
56
+ headers,
57
+ body: body ? JSON.stringify(body) : undefined,
58
+ });
59
+ if (!res.ok) {
60
+ const err = await res.text();
61
+ throw new Error(`Andreani API ${res.status}: ${err}`);
62
+ }
63
+ return res.json();
64
+ }
65
+ const server = new Server({ name: "mcp-andreani", version: "0.1.0" }, { capabilities: { tools: {} } });
66
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
67
+ tools: [
68
+ {
69
+ name: "create_shipment",
70
+ description: "Create a new shipment",
71
+ inputSchema: {
72
+ type: "object",
73
+ properties: {
74
+ contract: { type: "string", description: "Contract/account number" },
75
+ origin: {
76
+ type: "object",
77
+ description: "Origin address",
78
+ properties: {
79
+ postal_code: { type: "string", description: "Postal code" },
80
+ street: { type: "string", description: "Street name" },
81
+ number: { type: "string", description: "Street number" },
82
+ city: { type: "string", description: "City" },
83
+ province: { type: "string", description: "Province" },
84
+ },
85
+ required: ["postal_code"],
86
+ },
87
+ destination: {
88
+ type: "object",
89
+ description: "Destination address",
90
+ properties: {
91
+ postal_code: { type: "string", description: "Postal code" },
92
+ street: { type: "string", description: "Street name" },
93
+ number: { type: "string", description: "Street number" },
94
+ city: { type: "string", description: "City" },
95
+ province: { type: "string", description: "Province" },
96
+ contact_name: { type: "string", description: "Recipient name" },
97
+ contact_phone: { type: "string", description: "Recipient phone" },
98
+ },
99
+ required: ["postal_code", "contact_name"],
100
+ },
101
+ packages: {
102
+ type: "array",
103
+ description: "Packages to ship",
104
+ items: {
105
+ type: "object",
106
+ properties: {
107
+ weight: { type: "number", description: "Weight in grams" },
108
+ height: { type: "number", description: "Height in cm" },
109
+ width: { type: "number", description: "Width in cm" },
110
+ length: { type: "number", description: "Length in cm" },
111
+ },
112
+ required: ["weight"],
113
+ },
114
+ },
115
+ },
116
+ required: ["contract", "destination", "packages"],
117
+ },
118
+ },
119
+ {
120
+ name: "get_shipment",
121
+ description: "Get shipment details by ID",
122
+ inputSchema: {
123
+ type: "object",
124
+ properties: { shipmentId: { type: "string", description: "Shipment ID (numero de envio)" } },
125
+ required: ["shipmentId"],
126
+ },
127
+ },
128
+ {
129
+ name: "track_shipment",
130
+ description: "Track a shipment by tracking number",
131
+ inputSchema: {
132
+ type: "object",
133
+ properties: { trackingNumber: { type: "string", description: "Tracking number" } },
134
+ required: ["trackingNumber"],
135
+ },
136
+ },
137
+ {
138
+ name: "get_rates",
139
+ description: "Get shipping rates/quotes",
140
+ inputSchema: {
141
+ type: "object",
142
+ properties: {
143
+ contract: { type: "string", description: "Contract number" },
144
+ origin_postal_code: { type: "string", description: "Origin postal code" },
145
+ destination_postal_code: { type: "string", description: "Destination postal code" },
146
+ weight: { type: "number", description: "Weight in grams" },
147
+ volume: { type: "number", description: "Volume in cm3" },
148
+ declared_value: { type: "number", description: "Declared value in ARS" },
149
+ },
150
+ required: ["contract", "origin_postal_code", "destination_postal_code", "weight"],
151
+ },
152
+ },
153
+ {
154
+ name: "list_branches",
155
+ description: "List Andreani branches/sucursales",
156
+ inputSchema: {
157
+ type: "object",
158
+ properties: {
159
+ province: { type: "string", description: "Filter by province" },
160
+ locality: { type: "string", description: "Filter by locality" },
161
+ },
162
+ },
163
+ },
164
+ {
165
+ name: "create_label",
166
+ description: "Generate a shipping label for a shipment",
167
+ inputSchema: {
168
+ type: "object",
169
+ properties: { shipmentId: { type: "string", description: "Shipment ID" } },
170
+ required: ["shipmentId"],
171
+ },
172
+ },
173
+ {
174
+ name: "get_tracking_history",
175
+ description: "Get full tracking history for a shipment",
176
+ inputSchema: {
177
+ type: "object",
178
+ properties: { shipmentId: { type: "string", description: "Shipment ID" } },
179
+ required: ["shipmentId"],
180
+ },
181
+ },
182
+ {
183
+ name: "cancel_shipment",
184
+ description: "Cancel a shipment",
185
+ inputSchema: {
186
+ type: "object",
187
+ properties: { shipmentId: { type: "string", description: "Shipment ID" } },
188
+ required: ["shipmentId"],
189
+ },
190
+ },
191
+ ],
192
+ }));
193
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
194
+ const { name, arguments: args } = request.params;
195
+ try {
196
+ switch (name) {
197
+ case "create_shipment": {
198
+ const payload = {
199
+ contrato: args?.contract,
200
+ origen: args?.origin,
201
+ destino: args?.destination,
202
+ bultos: args?.packages,
203
+ };
204
+ return { content: [{ type: "text", text: JSON.stringify(await andreaniRequest("POST", "/ordenes-de-envio", payload), null, 2) }] };
205
+ }
206
+ case "get_shipment":
207
+ return { content: [{ type: "text", text: JSON.stringify(await andreaniRequest("GET", `/envios/${args?.shipmentId}`), null, 2) }] };
208
+ case "track_shipment":
209
+ return { content: [{ type: "text", text: JSON.stringify(await andreaniRequest("GET", `/envios/${args?.trackingNumber}/trazas`), null, 2) }] };
210
+ case "get_rates": {
211
+ const params = new URLSearchParams({
212
+ contrato: args?.contract,
213
+ cpOrigen: args?.origin_postal_code,
214
+ cpDestino: args?.destination_postal_code,
215
+ peso: String(args?.weight),
216
+ });
217
+ if (args?.volume)
218
+ params.set("volumen", String(args.volume));
219
+ if (args?.declared_value)
220
+ params.set("valorDeclarado", String(args.declared_value));
221
+ return { content: [{ type: "text", text: JSON.stringify(await andreaniRequest("GET", `/tarifas?${params}`), null, 2) }] };
222
+ }
223
+ case "list_branches": {
224
+ const params = new URLSearchParams();
225
+ if (args?.province)
226
+ params.set("provincia", args.province);
227
+ if (args?.locality)
228
+ params.set("localidad", args.locality);
229
+ return { content: [{ type: "text", text: JSON.stringify(await andreaniRequest("GET", `/sucursales?${params}`), null, 2) }] };
230
+ }
231
+ case "create_label":
232
+ return { content: [{ type: "text", text: JSON.stringify(await andreaniRequest("GET", `/envios/${args?.shipmentId}/etiquetas`), null, 2) }] };
233
+ case "get_tracking_history":
234
+ return { content: [{ type: "text", text: JSON.stringify(await andreaniRequest("GET", `/envios/${args?.shipmentId}/trazas`), null, 2) }] };
235
+ case "cancel_shipment":
236
+ return { content: [{ type: "text", text: JSON.stringify(await andreaniRequest("DELETE", `/envios/${args?.shipmentId}`), null, 2) }] };
237
+ default:
238
+ return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
239
+ }
240
+ }
241
+ catch (err) {
242
+ return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
243
+ }
244
+ });
245
+ async function main() {
246
+ if (process.argv.includes("--http") || process.env.MCP_HTTP === "true") {
247
+ const { default: express } = await import("express");
248
+ const { randomUUID } = await import("node:crypto");
249
+ const app = express();
250
+ app.use(express.json());
251
+ const transports = new Map();
252
+ app.get("/health", (_req, res) => res.json({ status: "ok", sessions: transports.size }));
253
+ app.post("/mcp", async (req, res) => {
254
+ const sid = req.headers["mcp-session-id"];
255
+ if (sid && transports.has(sid)) {
256
+ await transports.get(sid).handleRequest(req, res, req.body);
257
+ return;
258
+ }
259
+ if (!sid && isInitializeRequest(req.body)) {
260
+ const t = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (id) => { transports.set(id, t); } });
261
+ t.onclose = () => { if (t.sessionId)
262
+ transports.delete(t.sessionId); };
263
+ const s = new Server({ name: "mcp-andreani", version: "0.1.0" }, { capabilities: { tools: {} } });
264
+ server._requestHandlers.forEach((v, k) => s._requestHandlers.set(k, v));
265
+ server._notificationHandlers?.forEach((v, k) => s._notificationHandlers.set(k, v));
266
+ await s.connect(t);
267
+ await t.handleRequest(req, res, req.body);
268
+ return;
269
+ }
270
+ res.status(400).json({ jsonrpc: "2.0", error: { code: -32000, message: "Bad Request" }, id: null });
271
+ });
272
+ app.get("/mcp", async (req, res) => { const sid = req.headers["mcp-session-id"]; if (sid && transports.has(sid))
273
+ await transports.get(sid).handleRequest(req, res);
274
+ else
275
+ res.status(400).send("Invalid session"); });
276
+ app.delete("/mcp", async (req, res) => { const sid = req.headers["mcp-session-id"]; if (sid && transports.has(sid))
277
+ await transports.get(sid).handleRequest(req, res);
278
+ else
279
+ res.status(400).send("Invalid session"); });
280
+ const port = Number(process.env.MCP_PORT) || 3000;
281
+ app.listen(port, () => { console.error(`MCP HTTP server on http://localhost:${port}/mcp`); });
282
+ }
283
+ else {
284
+ const transport = new StdioServerTransport();
285
+ await server.connect(transport);
286
+ }
287
+ }
288
+ main().catch(console.error);
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@codespar/mcp-andreani",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for Andreani — Argentine courier and logistics",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "mcp-andreani": "dist/index.js"
9
+ },
10
+ "files": ["dist"],
11
+ "scripts": {
12
+ "build": "tsc",
13
+ "start": "node dist/index.js"
14
+ },
15
+ "dependencies": {
16
+ "@modelcontextprotocol/sdk": "^1.0.0"
17
+ },
18
+ "devDependencies": {
19
+ "@types/node": "^22.0.0",
20
+ "typescript": "^5.8.0"
21
+ },
22
+ "license": "MIT",
23
+ "keywords": ["andreani", "courier", "logistics", "argentina", "shipping", "mcp"],
24
+ "mcpName": "io.github.codespar/mcp-andreani"
25
+ }