@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 +53 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +288 -0
- package/package.json +25 -0
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.
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|