import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
import fetch from 'node-fetch';
const ILP_API_BASE = process.env.ILP_API_URL || 'http://localhost:8080/api/v1';
const TOOLS = [
{
name: 'list_available_drones',
description: 'Get a list of all available drones with their capabilities (capacity, heating, cooling, cost, max moves)',
inputSchema: {
type: 'object',
properties: {
hasCooling: {
type: 'boolean',
description: 'Filter drones by cooling capability (true = only cooling drones, false = all drones)'
}
}
}
},
{
name: 'get_drone_details',
description: 'Get detailed information about a specific drone by its ID',
inputSchema: {
type: 'object',
properties: {
droneId: {
type: 'number',
description: 'The drone ID to get details for'
}
},
required: ['droneId']
}
},
{
name: 'plan_delivery',
description: 'Plan a delivery route including cost estimate, number of moves, and drone assignment. Returns complete path planning.',
inputSchema: {
type: 'object',
properties: {
deliveryLng: {
type: 'number',
description: 'Delivery location longitude (e.g., -3.188 for Edinburgh)'
},
deliveryLat: {
type: 'number',
description: 'Delivery location latitude (e.g., 55.945 for Edinburgh)'
},
capacity: {
type: 'number',
description: 'Required cargo capacity in kilograms (e.g., 4.5)'
},
heating: {
type: 'boolean',
description: 'Whether the cargo requires heating'
},
cooling: {
type: 'boolean',
description: 'Whether the cargo requires cooling'
},
date: {
type: 'string',
description: 'Delivery date in YYYY-MM-DD format (defaults to today)'
}
},
required: ['deliveryLng', 'deliveryLat', 'capacity']
}
},
{
name: 'check_drone_availability',
description: 'Check which drones are available to handle a delivery with specific requirements',
inputSchema: {
type: 'object',
properties: {
capacity: {
type: 'number',
description: 'Required capacity in kg'
},
heating: {
type: 'boolean',
description: 'Requires heating capability'
},
cooling: {
type: 'boolean',
description: 'Requires cooling capability'
},
date: {
type: 'string',
description: 'Date in YYYY-MM-DD format'
}
},
required: ['capacity']
}
},
{
name: 'get_delivery_geojson',
description: 'Get the delivery path in GeoJSON format for map visualization. Can be used with geojson.io',
inputSchema: {
type: 'object',
properties: {
deliveryLng: {
type: 'number',
description: 'Delivery longitude'
},
deliveryLat: {
type: 'number',
description: 'Delivery latitude'
},
capacity: {
type: 'number',
description: 'Capacity in kg'
},
heating: {
type: 'boolean',
description: 'Requires heating'
},
cooling: {
type: 'boolean',
description: 'Requires cooling'
}
},
required: ['deliveryLng', 'deliveryLat', 'capacity']
}
},
{
name: 'plan_multiple_deliveries',
description: 'Plan routes for multiple deliveries simultaneously with multi-drone optimization',
inputSchema: {
type: 'object',
properties: {
deliveries: {
type: 'array',
description: 'Array of delivery objects',
items: {
type: 'object',
properties: {
lng: { type: 'number' },
lat: { type: 'number' },
capacity: { type: 'number' },
heating: { type: 'boolean' },
cooling: { type: 'boolean' }
},
required: ['lng', 'lat', 'capacity']
}
}
},
required: ['deliveries']
}
}
];
async function handleToolCall(name, args) {
try {
switch (name) {
case 'list_available_drones':
return await listAvailableDrones(args);
case 'get_drone_details':
return await getDroneDetails(args);
case 'plan_delivery':
return await planDelivery(args);
case 'check_drone_availability':
return await checkDroneAvailability(args);
case 'get_delivery_geojson':
return await getDeliveryGeoJSON(args);
case 'plan_multiple_deliveries':
return await planMultipleDeliveries(args);
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
return {
content: [{
type: 'text',
text: `Error calling tool ${name}: ${error.message}\n\nMake sure the ILP service is running at ${ILP_API_BASE}`
}],
isError: true
};
}
}
async function listAvailableDrones(args) {
const hasCooling = args.hasCooling ?? false;
// Get drone IDs
const response = await fetch(`${ILP_API_BASE}/dronesWithCooling/${hasCooling}`);
if (!response.ok) {
throw new Error(`API returned ${response.status}: ${response.statusText}`);
}
const droneIds = await response.json();
// Fetch details for each drone
const droneDetails = await Promise.all(
droneIds.map(async (id) => {
const detailResponse = await fetch(`${ILP_API_BASE}/droneDetails/${id}`);
if (!detailResponse.ok) {
throw new Error(`Failed to get details for drone ${id}`);
}
return detailResponse.json();
})
);
// Format nicely for LLM
let text = `Found ${droneDetails.length} available drone(s):\n\n`;
droneDetails.forEach(drone => {
text += `Drone ${drone.id}\n`;
text += ` Capacity: ${drone.capability.capacity} kg\n`;
text += ` Max Moves: ${drone.capability.maxMoves}\n`;
text += ` Cost per Move: $${drone.capability.costPerMove.toFixed(3)}\n`;
text += ` Initial Cost: $${drone.capability.costInitial.toFixed(2)}\n`;
text += ` Final Cost: $${drone.capability.costFinal.toFixed(2)}\n`;
const features = [];
if (drone.capability.heating) features.push('🔥 Heating');
if (drone.capability.cooling) features.push('❄️ Cooling');
if (features.length > 0) {
text += ` Features: ${features.join(', ')}\n`;
}
text += `\n`;
});
return {
content: [{
type: 'text',
text: text
}]
};
}
async function getDroneDetails(args) {
const response = await fetch(`${ILP_API_BASE}/droneDetails/${args.droneId}`);
if (!response.ok) {
throw new Error(`API returned ${response.status}: ${response.statusText}`);
}
const drone = await response.json();
let text = ` Drone ${drone.id} Details:\n\n`;
text += `Capacity: ${drone.capability.capacity} kg\n`;
text += `Max Moves: ${drone.capability.maxMoves}\n`;
text += `Cost per Move: $${drone.capability.costPerMove.toFixed(3)}\n`;
text += `Initial Cost: $${drone.capability.costInitial.toFixed(2)}\n`;
text += `Final Cost: $${drone.capability.costFinal.toFixed(2)}\n`;
const features = [];
if (drone.capability.heating) features.push('Heating');
if (drone.capability.cooling) features.push('Cooling');
text += `Features: ${features.join(', ') || 'None'}\n`;
return {
content: [{
type: 'text',
text: text
}]
};
}
async function planDelivery(args) {
const dispatch = {
id: Math.floor(Math.random() * 10000),
date: args.date || new Date().toISOString().split('T')[0],
requirements: {
capacity: args.capacity,
...(args.heating && { heating: true }),
...(args.cooling && { cooling: true })
},
delivery: {
lng: args.deliveryLng,
lat: args.deliveryLat
}
};
const response = await fetch(`${ILP_API_BASE}/calcDeliveryPath`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify([dispatch])
});
if (!response.ok) {
throw new Error(`API returned ${response.status}: ${response.statusText}`);
}
const result = await response.json();
if (!result.dronePaths || result.dronePaths.length === 0) {
return {
content: [{
type: 'text',
text: 'No drones available for this delivery. Check requirements and try again.'
}]
};
}
const dronePath = result.dronePaths[0];
const estimatedTime = Math.ceil(result.totalMoves * 0.5 / 60); // 0.5s per move
let text = ` Delivery Plan Created!\n\n`;
text += ` Delivery Details:\n`;
text += ` Location: (${args.deliveryLng.toFixed(4)}, ${args.deliveryLat.toFixed(4)})\n`;
text += ` Capacity: ${args.capacity} kg\n`;
if (args.heating) text += ` Heating Required\n`;
if (args.cooling) text += ` Cooling Required\n`;
text += `\n`;
text += `Assigned Drone: ${dronePath.droneId}\n\n`;
text += `Cost Analysis:\n`;
text += ` Total Cost: $${result.totalCost.toFixed(2)}\n`;
text += ` Total Moves: ${result.totalMoves}\n`;
text += ` Estimated Time: ~${estimatedTime} minutes\n\n`;
text += `Route Summary:\n`;
text += ` Number of Deliveries: ${dronePath.deliveries.length}\n`;
dronePath.deliveries.forEach((delivery, idx) => {
const pathLength = delivery.flightPath.length;
if (delivery.deliveryId === 0) {
text += ` ${idx + 1}. Return to Service Point (${pathLength} positions)\n`;
} else {
text += ` ${idx + 1}. Delivery #${delivery.deliveryId} (${pathLength} positions)\n`;
}
});
return {
content: [{
type: 'text',
text: text
}]
};
}
async function checkDroneAvailability(args) {
const dispatch = {
id: 1,
date: args.date || new Date().toISOString().split('T')[0],
requirements: {
capacity: args.capacity,
...(args.heating && { heating: true }),
...(args.cooling && { cooling: true })
}
};
const response = await fetch(`${ILP_API_BASE}/queryAvailableDrones`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify([dispatch])
});
if (!response.ok) {
throw new Error(`API returned ${response.status}: ${response.statusText}`);
}
const result = await response.json();
if (!result || result.length === 0) {
return {
content: [{
type: 'text',
text: 'No drones available matching these requirements.'
}]
};
}
let text = `Found ${result.length} available drone(s) for:\n`;
text += ` Capacity: ${args.capacity} kg\n`;
if (args.heating) text += ` Heating Required\n`;
if (args.cooling) text += ` Cooling Required\n`;
text += ` Date: ${dispatch.date}\n\n`;
result.forEach(droneId => {
text += `🚁 Drone ${droneId}\n`;
});
return {
content: [{
type: 'text',
text: text
}]
};
}
async function getDeliveryGeoJSON(args) {
const dispatch = {
id: 1,
requirements: {
capacity: args.capacity,
...(args.heating && { heating: true }),
...(args.cooling && { cooling: true })
},
delivery: {
lng: args.deliveryLng,
lat: args.deliveryLat
}
};
const response = await fetch(`${ILP_API_BASE}/calcDeliveryPathAsGeoJson`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify([dispatch])
});
if (!response.ok) {
throw new Error(`API returned ${response.status}: ${response.statusText}`);
}
const geojson = await response.json();
let text = `GeoJSON Path Generated!\n\n`;
text += `Features: ${geojson.features.length}\n`;
text += `Type: ${geojson.type}\n\n`;
text += `To visualize:\n`;
text += `1. Copy the JSON below\n`;
text += `2. Go to https://geojson.io\n`;
text += `3. Paste into the JSON editor\n\n`;
text += `GeoJSON Data:\n`;
text += '```json\n';
text += JSON.stringify(geojson, null, 2);
text += '\n```';
return {
content: [{
type: 'text',
text: text
}]
};
}
async function planMultipleDeliveries(args) {
const dispatches = args.deliveries.map((d, idx) => ({
id: 1000 + idx,
requirements: {
capacity: d.capacity,
...(d.heating && { heating: true }),
...(d.cooling && { cooling: true })
},
delivery: {
lng: d.lng,
lat: d.lat
}
}));
const response = await fetch(`${ILP_API_BASE}/calcDeliveryPath`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(dispatches)
});
if (!response.ok) {
throw new Error(`API returned ${response.status}: ${response.statusText}`);
}
const result = await response.json();
let text = ` Multi-Delivery Plan Created!\n\n`;
text += `Planning ${args.deliveries.length} deliveries\n`;
text += `Total Cost: $${result.totalCost.toFixed(2)}\n`;
text += `Total Moves: ${result.totalMoves}\n`;
text += `Drones Used: ${result.dronePaths.length}\n\n`;
result.dronePaths.forEach((dronePath, idx) => {
text += `Drone ${dronePath.droneId}:\n`;
text += ` Deliveries: ${dronePath.deliveries.filter(d => d.deliveryId !== 0).length}\n`;
const totalPositions = dronePath.deliveries.reduce((sum, d) => sum + d.flightPath.length, 0);
text += ` Total Positions: ${totalPositions}\n\n`;
});
return {
content: [{
type: 'text',
text: text
}]
};
}
async function main() {
const server = new Server(
{
name: 'ilp-drone-server',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: TOOLS,
};
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
return await handleToolCall(name, args || {});
});
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('ILP Drone Delivery MCP Server started');
console.error(`Connecting to ILP API at: ${ILP_API_BASE}`);
}
main().catch((error) => {
console.error('Fatal error:', error);
process.exit(1);
});