/**
* @fileoverview VyOS Model Context Provider (MCP) Server - Complete API Implementation
*
* This is a comprehensive MCP server providing complete access to VyOS network operating
* system functionality through 25+ tools covering configuration management, operational
* commands, system administration, network diagnostics, and monitoring.
*
* The server implements the full VyOS HTTPS API surface area with type-safe Zod validation,
* comprehensive error handling, and OpenAPI v3.1 specification generation.
*
* ## Features
* - **Complete VyOS API Coverage**: All endpoints from VyOS HTTPS API
* - **Type Safety**: Full TypeScript with Zod schema validation
* - **MCP Compatible**: Works with Claude Desktop and MCP clients
* - **Streaming Support**: SSE transport for real-time communication
* - **OpenAPI Spec**: Auto-generated API documentation
* - **Error Handling**: Robust error management with proper HTTP status codes
*
* ## Tool Categories
* - **Authentication**: Connection and session management
* - **Configuration**: Show, set, delete, commit, save operations
* - **Operational**: Show commands, reset operations, generate utilities
* - **System**: Info, reboot, poweroff, health checks
* - **Interfaces**: Network interface configuration and monitoring
* - **Routing**: Static routes, BGP monitoring, routing tables
* - **Network Tools**: Ping, traceroute, connectivity testing
* - **Monitoring**: Statistics, health checks, system resources
*
* @author VyOS MCP Server
* @version 1.0.0
* @since 2025-01-13
*
* @example
* ```ts
* // Start the MCP server
* export default server;
*
* // Usage with Claude Desktop or MCP client:
* // 1. Connect to VyOS system
* // 2. Execute configuration operations
* // 3. Monitor system status
* // 4. Perform network diagnostics
* ```
*/
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import {
bridge,
describeTool,
muppet,
mValidator,
type ToolResponseType,
} from 'muppet';
import { SSEHonoTransport, streamSSE } from 'muppet/streaming';
import {
CommitRequestSchema,
ConfigExistsRequestSchema,
DeleteConfigRequestSchema,
GenerateRequestSchema,
HealthCheckRequestSchema,
InterfaceSchema,
PingRequestSchema,
ResetRequestSchema,
ReturnValuesRequestSchema,
SetConfigRequestSchema,
ShowConfigRequestSchema,
ShowOperationalRequestSchema,
StaticRouteSchema,
TracerouteRequestSchema,
VyOSAuthSchema,
} from './schemas/vyos-schemas';
import { VyOSClient } from './vyos-client';
/**
* Main Hono application instance for VyOS MCP tool endpoints.
* Each route corresponds to a specific VyOS management function.
*/
const app = new Hono();
/**
* Global VyOS client instance maintained for the session.
* Initialized by the vyos-connect tool and used by all subsequent operations.
*
* @internal
*/
let vyosClient: VyOSClient | null = null;
app.post(
'/connect',
describeTool({
name: 'vyos-connect',
description: 'Connect to a VyOS system using API credentials',
}),
mValidator('json', VyOSAuthSchema),
(c) => {
try {
const auth = c.req.valid('json');
vyosClient = new VyOSClient(auth);
return c.json<ToolResponseType>([
{
type: 'text',
text: `Successfully connected to VyOS system at ${auth.host}`,
},
]);
} catch (error) {
return c.json<ToolResponseType>(
[
{
type: 'text',
text: `Failed to connect to VyOS system: ${error instanceof Error ? error.message : String(error)}`,
},
],
{
status: 500,
},
);
}
},
);
app.post(
'/system/info',
describeTool({
name: 'vyos-system-info',
description:
'Get VyOS system information including version, hostname, and hardware details',
}),
async (c) => {
if (!vyosClient) {
return c.json<ToolResponseType>(
[
{
type: 'text',
text: 'Not connected to VyOS system. Use vyos-connect first.',
},
],
{
status: 400,
},
);
}
try {
const info = await vyosClient.getSystemInfo();
return c.json<ToolResponseType>([
{
type: 'text',
text: `VyOS System Information:\n${JSON.stringify(info, null, 2)}`,
},
]);
} catch (error) {
return c.json<ToolResponseType>(
[
{
type: 'text',
text: `Failed to get system info: ${error instanceof Error ? error.message : String(error)}`,
},
],
{
status: 500,
},
);
}
},
);
app.post(
'/config/show',
describeTool({
name: 'vyos-show-config',
description:
'Retrieve VyOS configuration for specified path or entire configuration',
}),
mValidator('json', ShowConfigRequestSchema),
async (c) => {
if (!vyosClient) {
return c.json<ToolResponseType>(
[
{
type: 'text',
text: 'Not connected to VyOS system. Use vyos-connect first.',
},
],
{
status: 400,
},
);
}
try {
const { path, format } = c.req.valid('json');
const config = await vyosClient.showConfig(path, format);
return c.json<ToolResponseType>([
{
type: 'text',
text:
format === 'json'
? JSON.stringify(config, null, 2)
: (
config as {
data?: string;
}
).data || String(config),
},
]);
} catch (error) {
return c.json<ToolResponseType>(
[
{
type: 'text',
text: `Failed to show config: ${error instanceof Error ? error.message : String(error)}`,
},
],
{
status: 500,
},
);
}
},
);
app.post(
'/config/set',
describeTool({
name: 'vyos-set-config',
description: 'Set configuration value at specified path',
}),
mValidator('json', SetConfigRequestSchema),
async (c) => {
if (!vyosClient) {
return c.json<ToolResponseType>(
[
{
type: 'text',
text: 'Not connected to VyOS system. Use vyos-connect first.',
},
],
{
status: 400,
},
);
}
try {
const { path, value } = c.req.valid('json');
await vyosClient.setConfig(path, value);
return c.json<ToolResponseType>([
{
type: 'text',
text: `Configuration set successfully: ${path.join(' ')} ${value || ''}`,
},
]);
} catch (error) {
return c.json<ToolResponseType>(
[
{
type: 'text',
text: `Failed to set config: ${error instanceof Error ? error.message : String(error)}`,
},
],
{
status: 500,
},
);
}
},
);
app.post(
'/config/delete',
describeTool({
name: 'vyos-delete-config',
description: 'Delete configuration at specified path',
}),
mValidator('json', DeleteConfigRequestSchema),
async (c) => {
if (!vyosClient) {
return c.json<ToolResponseType>(
[
{
type: 'text',
text: 'Not connected to VyOS system. Use vyos-connect first.',
},
],
{
status: 400,
},
);
}
try {
const { path } = c.req.valid('json');
await vyosClient.deleteConfig(path);
return c.json<ToolResponseType>([
{
type: 'text',
text: `Configuration deleted successfully: ${path.join(' ')}`,
},
]);
} catch (error) {
return c.json<ToolResponseType>(
[
{
type: 'text',
text: `Failed to delete config: ${error instanceof Error ? error.message : String(error)}`,
},
],
{
status: 500,
},
);
}
},
);
app.post(
'/config/exists',
describeTool({
name: 'vyos-config-exists',
description: 'Check if configuration path exists',
}),
mValidator('json', ConfigExistsRequestSchema),
async (c) => {
if (!vyosClient) {
return c.json<ToolResponseType>(
[
{
type: 'text',
text: 'Not connected to VyOS system. Use vyos-connect first.',
},
],
{
status: 400,
},
);
}
try {
const { path } = c.req.valid('json');
const exists = await vyosClient.configExists(path);
return c.json<ToolResponseType>([
{
type: 'text',
text: `Configuration path ${path.join(' ')} ${exists ? 'exists' : 'does not exist'}`,
},
]);
} catch (error) {
return c.json<ToolResponseType>(
[
{
type: 'text',
text: `Failed to check config existence: ${error instanceof Error ? error.message : String(error)}`,
},
],
{
status: 500,
},
);
}
},
);
app.post(
'/config/values',
describeTool({
name: 'vyos-return-values',
description: 'Get configuration values for specified path',
}),
mValidator('json', ReturnValuesRequestSchema),
async (c) => {
if (!vyosClient) {
return c.json<ToolResponseType>(
[
{
type: 'text',
text: 'Not connected to VyOS system. Use vyos-connect first.',
},
],
{
status: 400,
},
);
}
try {
const { path } = c.req.valid('json');
const values = await vyosClient.returnValues(path);
return c.json<ToolResponseType>([
{
type: 'text',
text: `Values for ${path.join(' ')}:\n${values.join('\n')}`,
},
]);
} catch (error) {
return c.json<ToolResponseType>(
[
{
type: 'text',
text: `Failed to get values: ${error instanceof Error ? error.message : String(error)}`,
},
],
{
status: 500,
},
);
}
},
);
app.post(
'/config/commit',
describeTool({
name: 'vyos-commit',
description: 'Commit pending configuration changes',
}),
mValidator('json', CommitRequestSchema),
async (c) => {
if (!vyosClient) {
return c.json<ToolResponseType>(
[
{
type: 'text',
text: 'Not connected to VyOS system. Use vyos-connect first.',
},
],
{
status: 400,
},
);
}
try {
const { comment, confirmTimeout } = c.req.valid('json');
await vyosClient.commit(comment, confirmTimeout);
return c.json<ToolResponseType>([
{
type: 'text',
text: confirmTimeout
? `Configuration committed with ${confirmTimeout} minute confirm timeout`
: 'Configuration committed successfully',
},
]);
} catch (error) {
return c.json<ToolResponseType>(
[
{
type: 'text',
text: `Failed to commit config: ${error instanceof Error ? error.message : String(error)}`,
},
],
{
status: 500,
},
);
}
},
);
app.post(
'/config/save',
describeTool({
name: 'vyos-save-config',
description: 'Save current configuration to startup config',
}),
async (c) => {
if (!vyosClient) {
return c.json<ToolResponseType>(
[
{
type: 'text',
text: 'Not connected to VyOS system. Use vyos-connect first.',
},
],
{
status: 400,
},
);
}
try {
await vyosClient.save();
return c.json<ToolResponseType>([
{
type: 'text',
text: 'Configuration saved successfully',
},
]);
} catch (error) {
return c.json<ToolResponseType>(
[
{
type: 'text',
text: `Failed to save config: ${error instanceof Error ? error.message : String(error)}`,
},
],
{
status: 500,
},
);
}
},
);
app.post(
'/operational/show',
describeTool({
name: 'vyos-show-operational',
description: 'Execute operational mode show commands',
}),
mValidator('json', ShowOperationalRequestSchema),
async (c) => {
if (!vyosClient) {
return c.json<ToolResponseType>(
[
{
type: 'text',
text: 'Not connected to VyOS system. Use vyos-connect first.',
},
],
{
status: 400,
},
);
}
try {
const { path, format } = c.req.valid('json');
const result = await vyosClient.show(path, format);
return c.json<ToolResponseType>([
{
type: 'text',
text:
format === 'json'
? JSON.stringify(result, null, 2)
: (
result as {
data?: string;
}
).data || String(result),
},
]);
} catch (error) {
return c.json<ToolResponseType>(
[
{
type: 'text',
text: `Failed to execute show command: ${error instanceof Error ? error.message : String(error)}`,
},
],
{
status: 500,
},
);
}
},
);
app.post(
'/operational/reset',
describeTool({
name: 'vyos-reset',
description: 'Execute operational mode reset commands',
}),
mValidator('json', ResetRequestSchema),
async (c) => {
if (!vyosClient) {
return c.json<ToolResponseType>(
[
{
type: 'text',
text: 'Not connected to VyOS system. Use vyos-connect first.',
},
],
{
status: 400,
},
);
}
try {
const { path } = c.req.valid('json');
await vyosClient.reset(path);
return c.json<ToolResponseType>([
{
type: 'text',
text: `Reset command executed: ${path.join(' ')}`,
},
]);
} catch (error) {
return c.json<ToolResponseType>(
[
{
type: 'text',
text: `Failed to execute reset command: ${error instanceof Error ? error.message : String(error)}`,
},
],
{
status: 500,
},
);
}
},
);
app.post(
'/operational/generate',
describeTool({
name: 'vyos-generate',
description: 'Execute operational mode generate commands',
}),
mValidator('json', GenerateRequestSchema),
async (c) => {
if (!vyosClient) {
return c.json<ToolResponseType>(
[
{
type: 'text',
text: 'Not connected to VyOS system. Use vyos-connect first.',
},
],
{
status: 400,
},
);
}
try {
const { path } = c.req.valid('json');
const result = await vyosClient.generate(path);
return c.json<ToolResponseType>([
{
type: 'text',
text: `Generate command result:\n${JSON.stringify(result, null, 2)}`,
},
]);
} catch (error) {
return c.json<ToolResponseType>(
[
{
type: 'text',
text: `Failed to execute generate command: ${error instanceof Error ? error.message : String(error)}`,
},
],
{
status: 500,
},
);
}
},
);
app.post(
'/system/reboot',
describeTool({
name: 'vyos-reboot',
description: 'Reboot the VyOS system',
}),
async (c) => {
if (!vyosClient) {
return c.json<ToolResponseType>(
[
{
type: 'text',
text: 'Not connected to VyOS system. Use vyos-connect first.',
},
],
{
status: 400,
},
);
}
try {
await vyosClient.reboot();
return c.json<ToolResponseType>([
{
type: 'text',
text: 'System reboot initiated',
},
]);
} catch (error) {
return c.json<ToolResponseType>(
[
{
type: 'text',
text: `Failed to reboot system: ${error instanceof Error ? error.message : String(error)}`,
},
],
{
status: 500,
},
);
}
},
);
app.post(
'/system/poweroff',
describeTool({
name: 'vyos-poweroff',
description: 'Power off the VyOS system',
}),
async (c) => {
if (!vyosClient) {
return c.json<ToolResponseType>(
[
{
type: 'text',
text: 'Not connected to VyOS system. Use vyos-connect first.',
},
],
{
status: 400,
},
);
}
try {
await vyosClient.poweroff();
return c.json<ToolResponseType>([
{
type: 'text',
text: 'System poweroff initiated',
},
]);
} catch (error) {
return c.json<ToolResponseType>(
[
{
type: 'text',
text: `Failed to poweroff system: ${error instanceof Error ? error.message : String(error)}`,
},
],
{
status: 500,
},
);
}
},
);
app.post(
'/interface/configure',
describeTool({
name: 'vyos-configure-interface',
description: 'Configure network interface settings',
}),
mValidator('json', InterfaceSchema),
async (c) => {
if (!vyosClient) {
return c.json<ToolResponseType>(
[
{
type: 'text',
text: 'Not connected to VyOS system. Use vyos-connect first.',
},
],
{
status: 400,
},
);
}
try {
const iface = c.req.valid('json');
const commands: string[] = [];
if (iface.address) {
await vyosClient.setConfig(
[
'interfaces',
'ethernet',
iface.name,
'address',
],
iface.address,
);
commands.push(
`set interfaces ethernet ${iface.name} address ${iface.address}`,
);
}
if (iface.description) {
await vyosClient.setConfig(
[
'interfaces',
'ethernet',
iface.name,
'description',
],
iface.description,
);
commands.push(
`set interfaces ethernet ${iface.name} description "${iface.description}"`,
);
}
if (iface.mtu) {
await vyosClient.setConfig(
[
'interfaces',
'ethernet',
iface.name,
'mtu',
],
iface.mtu.toString(),
);
commands.push(`set interfaces ethernet ${iface.name} mtu ${iface.mtu}`);
}
if (iface.enabled === false) {
await vyosClient.setConfig([
'interfaces',
'ethernet',
iface.name,
'disable',
]);
commands.push(`set interfaces ethernet ${iface.name} disable`);
}
return c.json<ToolResponseType>([
{
type: 'text',
text: `Interface ${iface.name} configured successfully:\n${commands.join('\n')}`,
},
]);
} catch (error) {
return c.json<ToolResponseType>(
[
{
type: 'text',
text: `Failed to configure interface: ${error instanceof Error ? error.message : String(error)}`,
},
],
{
status: 500,
},
);
}
},
);
app.post(
'/routing/static',
describeTool({
name: 'vyos-configure-static-route',
description: 'Configure static routing entries',
}),
mValidator('json', StaticRouteSchema),
async (c) => {
if (!vyosClient) {
return c.json<ToolResponseType>(
[
{
type: 'text',
text: 'Not connected to VyOS system. Use vyos-connect first.',
},
],
{
status: 400,
},
);
}
try {
const route = c.req.valid('json');
const basePath = [
'protocols',
'static',
'route',
route.destination,
];
await vyosClient.setConfig(
[
...basePath,
'next-hop',
],
route.nextHop,
);
if (route.distance) {
await vyosClient.setConfig(
[
...basePath,
'distance',
],
route.distance.toString(),
);
}
if (route.tag) {
await vyosClient.setConfig(
[
...basePath,
'tag',
],
route.tag.toString(),
);
}
return c.json<ToolResponseType>([
{
type: 'text',
text: `Static route configured: ${route.destination} via ${route.nextHop}`,
},
]);
} catch (error) {
return c.json<ToolResponseType>(
[
{
type: 'text',
text: `Failed to configure static route: ${error instanceof Error ? error.message : String(error)}`,
},
],
{
status: 500,
},
);
}
},
);
app.post(
'/network/ping',
describeTool({
name: 'vyos-ping',
description: 'Execute ping connectivity test',
}),
mValidator('json', PingRequestSchema),
async (c) => {
if (!vyosClient) {
return c.json<ToolResponseType>(
[
{
type: 'text',
text: 'Not connected to VyOS system. Use vyos-connect first.',
},
],
{
status: 400,
},
);
}
try {
const pingReq = c.req.valid('json');
const result = await vyosClient.ping(pingReq.host, pingReq);
return c.json<ToolResponseType>([
{
type: 'text',
text: `Ping results for ${pingReq.host}:\n${JSON.stringify(result, null, 2)}`,
},
]);
} catch (error) {
return c.json<ToolResponseType>(
[
{
type: 'text',
text: `Ping failed: ${error instanceof Error ? error.message : String(error)}`,
},
],
{
status: 500,
},
);
}
},
);
app.post(
'/network/traceroute',
describeTool({
name: 'vyos-traceroute',
description: 'Execute traceroute network path analysis',
}),
mValidator('json', TracerouteRequestSchema),
async (c) => {
if (!vyosClient) {
return c.json<ToolResponseType>(
[
{
type: 'text',
text: 'Not connected to VyOS system. Use vyos-connect first.',
},
],
{
status: 400,
},
);
}
try {
const traceReq = c.req.valid('json');
const result = await vyosClient.traceroute(traceReq.host, traceReq);
return c.json<ToolResponseType>([
{
type: 'text',
text: `Traceroute results for ${traceReq.host}:\n${JSON.stringify(result, null, 2)}`,
},
]);
} catch (error) {
return c.json<ToolResponseType>(
[
{
type: 'text',
text: `Traceroute failed: ${error instanceof Error ? error.message : String(error)}`,
},
],
{
status: 500,
},
);
}
},
);
app.post(
'/monitoring/interfaces',
describeTool({
name: 'vyos-interface-stats',
description: 'Get network interface statistics and status',
}),
async (c) => {
if (!vyosClient) {
return c.json<ToolResponseType>(
[
{
type: 'text',
text: 'Not connected to VyOS system. Use vyos-connect first.',
},
],
{
status: 400,
},
);
}
try {
const stats = await vyosClient.getInterfaceStatistics();
return c.json<ToolResponseType>([
{
type: 'text',
text: `Interface Statistics:\n${JSON.stringify(stats, null, 2)}`,
},
]);
} catch (error) {
return c.json<ToolResponseType>(
[
{
type: 'text',
text: `Failed to get interface stats: ${error instanceof Error ? error.message : String(error)}`,
},
],
{
status: 500,
},
);
}
},
);
app.post(
'/monitoring/routes',
describeTool({
name: 'vyos-routing-table',
description: 'Display routing table information',
}),
async (c) => {
if (!vyosClient) {
return c.json<ToolResponseType>(
[
{
type: 'text',
text: 'Not connected to VyOS system. Use vyos-connect first.',
},
],
{
status: 400,
},
);
}
try {
const routes = await vyosClient.getRoutes();
return c.json<ToolResponseType>([
{
type: 'text',
text: `Routing Table:\n${JSON.stringify(routes, null, 2)}`,
},
]);
} catch (error) {
return c.json<ToolResponseType>(
[
{
type: 'text',
text: `Failed to get routing table: ${error instanceof Error ? error.message : String(error)}`,
},
],
{
status: 500,
},
);
}
},
);
app.post(
'/monitoring/bgp',
describeTool({
name: 'vyos-bgp-status',
description: 'Get BGP protocol status and neighbor information',
}),
async (c) => {
if (!vyosClient) {
return c.json<ToolResponseType>(
[
{
type: 'text',
text: 'Not connected to VyOS system. Use vyos-connect first.',
},
],
{
status: 400,
},
);
}
try {
const [summary, neighbors] = await Promise.all([
vyosClient.getBGPSummary(),
vyosClient.getBGPNeighbors(),
]);
return c.json<ToolResponseType>([
{
type: 'text',
text: `BGP Status:\nSummary: ${JSON.stringify(summary, null, 2)}\nNeighbors: ${JSON.stringify(neighbors, null, 2)}`,
},
]);
} catch (error) {
return c.json<ToolResponseType>(
[
{
type: 'text',
text: `Failed to get BGP status: ${error instanceof Error ? error.message : String(error)}`,
},
],
{
status: 500,
},
);
}
},
);
app.post(
'/system/health',
describeTool({
name: 'vyos-health-check',
description: 'Comprehensive system health assessment',
}),
mValidator('json', HealthCheckRequestSchema),
async (c) => {
if (!vyosClient) {
return c.json<ToolResponseType>(
[
{
type: 'text',
text: 'Not connected to VyOS system. Use vyos-connect first.',
},
],
{
status: 400,
},
);
}
try {
const options = c.req.valid('json');
const health: Record<string, unknown> = {
timestamp: new Date().toISOString(),
system: {},
};
const [version, uptime] = await Promise.all([
vyosClient.getVersion(),
vyosClient.getUptime(),
]);
(health.system as Record<string, unknown>).version = version;
(health.system as Record<string, unknown>).uptime = uptime;
if (options.includeInterfaces) {
health.interfaces = await vyosClient.getInterfaces();
}
if (options.includeRouting) {
health.routing = {
routes: await vyosClient.getRoutes(),
bgp: await vyosClient.getBGPSummary().catch(() => null),
};
}
if (options.includeResources) {
const [cpu, memory, storage] = await Promise.all([
vyosClient.getSystemResources(),
vyosClient.getSystemMemory(),
vyosClient.getSystemStorage(),
]);
health.resources = {
cpu,
memory,
storage,
};
}
return c.json<ToolResponseType>([
{
type: 'text',
text: `System Health Check:\n${JSON.stringify(health, null, 2)}`,
},
]);
} catch (error) {
return c.json<ToolResponseType>(
[
{
type: 'text',
text: `Health check failed: ${error instanceof Error ? error.message : String(error)}`,
},
],
{
status: 500,
},
);
}
},
);
const mcp = muppet(app, {
name: 'VyOS MCP Server',
version: '1.0.0',
});
let transport: SSEHonoTransport | null = null;
const server = new Hono().use(
cors({
origin: (origin) => origin,
credentials: true,
}),
);
// Health check endpoint
server.get('/health', (c) => {
return c.json({
status: 'healthy',
timestamp: new Date().toISOString(),
version: '1.0.0',
transport: transport !== null ? 'connected' : 'disconnected',
});
});
server.get('/sse', (c) => {
return streamSSE(c, async (stream) => {
transport = new SSEHonoTransport('/messages');
transport.connectWithStream(stream);
await bridge({
mcp,
transport,
});
});
});
server.post('/messages', async (c) => {
if (!transport) {
throw new Error('Transport not initialized');
}
await transport.handlePostMessage(c);
return c.text('ok');
});
server.onError((err, c) => {
console.error(err);
return c.body(err.message, 500);
});
export default server;
export * from './schemas/vyos-schemas.ts';
// Export VyOS client and schemas for library usage
export { VyOSClient } from './vyos-client.ts';
// Export test utilities for resetting state in tests
export const __testUtils__ = {
resetTransport: () => {
transport = null;
},
getTransportStatus: () => transport !== null,
};