import { z } from 'zod';
import * as unifi from '../unifi-client.js';
export const clientTools = {
// ========================================
// Client Listing
// ========================================
list_all_clients: {
description: 'List all connected clients across all hosts and sites',
schema: z.object({}),
handler: async () => {
const clients = await unifi.listAllClients();
return {
content: [{ type: 'text', text: JSON.stringify(clients, null, 2) }]
};
}
},
get_clients_for_host: {
description: 'Get all clients connected to a specific host',
schema: z.object({
hostId: z.string().describe('The host ID')
}),
handler: async ({ hostId }) => {
const clients = await unifi.getClientsForHost(hostId);
return {
content: [{ type: 'text', text: JSON.stringify(clients, null, 2) }]
};
}
},
get_clients_for_site: {
description: 'Get all clients connected to a specific site',
schema: z.object({
hostId: z.string().describe('The host ID'),
siteId: z.string().describe('The site ID')
}),
handler: async ({ hostId, siteId }) => {
const clients = await unifi.getClientsForSite(hostId, siteId);
return {
content: [{ type: 'text', text: JSON.stringify(clients, null, 2) }]
};
}
},
// ========================================
// Client Actions
// ========================================
block_client: {
description: 'Block a client from the network. The client will be disconnected and prevented from reconnecting.',
schema: z.object({
hostId: z.string().describe('The host ID'),
siteId: z.string().describe('The site ID'),
mac: z.string().describe('The client MAC address'),
confirm: z.boolean().describe('Confirm you want to block this client')
}),
handler: async ({ hostId, siteId, mac, confirm }) => {
if (!confirm) {
return {
content: [{
type: 'text',
text: 'Client block cancelled. Set confirm=true to proceed. Note: You can unblock the client later using unblock_client.'
}]
};
}
const result = await unifi.blockClient(hostId, siteId, mac);
return {
content: [{
type: 'text',
text: `Client ${mac} has been blocked. Use unblock_client to reverse this action. ${JSON.stringify(result, null, 2)}`
}]
};
}
},
unblock_client: {
description: 'Unblock a previously blocked client, allowing them to reconnect',
schema: z.object({
hostId: z.string().describe('The host ID'),
siteId: z.string().describe('The site ID'),
mac: z.string().describe('The client MAC address')
}),
handler: async ({ hostId, siteId, mac }) => {
const result = await unifi.unblockClient(hostId, siteId, mac);
return {
content: [{ type: 'text', text: `Client ${mac} has been unblocked. ${JSON.stringify(result, null, 2)}` }]
};
}
},
reconnect_client: {
description: 'Force a client to reconnect (kick and allow immediate reconnection)',
schema: z.object({
hostId: z.string().describe('The host ID'),
siteId: z.string().describe('The site ID'),
mac: z.string().describe('The client MAC address')
}),
handler: async ({ hostId, siteId, mac }) => {
const result = await unifi.reconnectClient(hostId, siteId, mac);
return {
content: [{ type: 'text', text: `Client ${mac} has been reconnected. ${JSON.stringify(result, null, 2)}` }]
};
}
},
// ========================================
// Client Search & Analysis
// ========================================
find_client_by_mac: {
description: 'Find a client by MAC address',
schema: z.object({
mac: z.string().describe('The MAC address to search for')
}),
handler: async ({ mac }) => {
const normalizedMac = mac.toLowerCase().replace(/[:-]/g, '');
const allClients = await unifi.listAllClients();
const clients = allClients.data || [];
const matches = clients.filter(c => {
const clientMac = (c.mac || '').toLowerCase().replace(/[:-]/g, '');
return clientMac === normalizedMac || clientMac.includes(normalizedMac);
});
return {
content: [{
type: 'text',
text: JSON.stringify({
searchMac: mac,
matchCount: matches.length,
matches
}, null, 2)
}]
};
}
},
find_client_by_name: {
description: 'Search for clients by hostname or name',
schema: z.object({
query: z.string().describe('The search query for client name/hostname')
}),
handler: async ({ query }) => {
const queryLower = query.toLowerCase();
const allClients = await unifi.listAllClients();
const clients = allClients.data || [];
const matches = clients.filter(c => {
const name = (c.name || c.hostname || c.displayName || '').toLowerCase();
return name.includes(queryLower);
});
return {
content: [{
type: 'text',
text: JSON.stringify({
searchQuery: query,
matchCount: matches.length,
matches
}, null, 2)
}]
};
}
},
get_client_bandwidth_summary: {
description: 'Get bandwidth usage summary for all clients',
schema: z.object({
hostId: z.string().optional().describe('Optional: Filter by host ID')
}),
handler: async ({ hostId }) => {
let clients;
if (hostId) {
const response = await unifi.getClientsForHost(hostId);
clients = response.data || [];
} else {
const response = await unifi.listAllClients();
clients = response.data || [];
}
// Calculate bandwidth stats
const summary = {
totalClients: clients.length,
totalRxBytes: 0,
totalTxBytes: 0,
byConnectionType: {
wired: { count: 0, rxBytes: 0, txBytes: 0 },
wireless: { count: 0, rxBytes: 0, txBytes: 0 }
},
topByUsage: []
};
const clientsWithUsage = clients.map(c => {
const rx = c.rxBytes || c.rx_bytes || 0;
const tx = c.txBytes || c.tx_bytes || 0;
summary.totalRxBytes += rx;
summary.totalTxBytes += tx;
const isWired = c.isWired || c.is_wired;
if (isWired) {
summary.byConnectionType.wired.count++;
summary.byConnectionType.wired.rxBytes += rx;
summary.byConnectionType.wired.txBytes += tx;
} else {
summary.byConnectionType.wireless.count++;
summary.byConnectionType.wireless.rxBytes += rx;
summary.byConnectionType.wireless.txBytes += tx;
}
return {
name: c.name || c.hostname || c.mac,
mac: c.mac,
totalUsage: rx + tx,
rxBytes: rx,
txBytes: tx
};
});
// Sort by usage and get top 10
summary.topByUsage = clientsWithUsage
.sort((a, b) => b.totalUsage - a.totalUsage)
.slice(0, 10);
// Format bytes
const formatBytes = (bytes) => {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`;
if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
return `${(bytes / 1024 / 1024 / 1024).toFixed(2)} GB`;
};
summary.totalRxFormatted = formatBytes(summary.totalRxBytes);
summary.totalTxFormatted = formatBytes(summary.totalTxBytes);
return {
content: [{ type: 'text', text: JSON.stringify(summary, null, 2) }]
};
}
},
get_wireless_clients: {
description: 'Get all wireless (WiFi) clients',
schema: z.object({}),
handler: async () => {
const allClients = await unifi.listAllClients();
const clients = allClients.data || [];
const wireless = clients.filter(c => !c.isWired && !c.is_wired);
return {
content: [{
type: 'text',
text: JSON.stringify({
wirelessCount: wireless.length,
totalClients: clients.length,
wirelessClients: wireless
}, null, 2)
}]
};
}
},
get_wired_clients: {
description: 'Get all wired (Ethernet) clients',
schema: z.object({}),
handler: async () => {
const allClients = await unifi.listAllClients();
const clients = allClients.data || [];
const wired = clients.filter(c => c.isWired || c.is_wired);
return {
content: [{
type: 'text',
text: JSON.stringify({
wiredCount: wired.length,
totalClients: clients.length,
wiredClients: wired
}, null, 2)
}]
};
}
}
};
export default clientTools;