← Back to list

Scaling an AI Network Assistant with Inventory and SQLite

In my previous story — https://medium.com/@aks001235/building-an-mcp-based-ai-network-engineer-with-python-b93fdbcce810 , I built a small…

Aman Singh · 2026-05-24 06:40 · 3 claps · 4.5 min read
#mcp-server #cisco #python #networking
Open on Medium ↗
Wiki topics: AGT · AI Agents

Scaling an AI Network Assistant with Inventory and SQLite

In my previous story — https://medium.com/@aks001235/building-an-mcp-based-ai-network-engineer-with-python-b93fdbcce810 , I built a small MCP-based AI network assistant that could:

  • understand networking questions in natural language
  • select the correct network tool
  • connect to a Cisco router
  • retrieve operational data
  • let an LLM analyze the results

But the original implementation had a few limitations:

The first limitation is that it only worked against a single hardcoded router.

That approach worked for learning LLM orchestration, but it was not realistic for actual network environments where automation systems need to operate across multiple devices dynamically.

So I extended the project to support:

  • inventory-driven device selection
  • dynamic router connectivity
  • AI-driven device-aware tool execution

The result was a much more scalable architecture.

The Problem with Hardcoded Devices

Initially, my tools looked like this:

router = {
    "device_type": "cisco_ios_telnet",
    "host": "192.168.223.131",
    "port": 32770,
    "username": "admin",
    "password": "eve",
}

Every tool execution always connected to the same device.

That meant:

  • no network-wide flexibility
  • no multi-device support
  • no inventory abstraction
  • no dynamic execution logic

The LLM could understand intent, but the infrastructure layer was still static.

The Architectural Shift

The major change was introducing an inventory layer.

Instead of embedding device details directly inside Python functions, device information was externalised into a datasource.

Example inventory:

{
  "R1": {
    "host": "192.168.223.131",
    "port": 32769
  },
  "R2": {
    "host": "192.168.223.131",
    "port": 32774
  },
  "R3": {
    "host": "192.168.223.131",
    "port": 32774
  }
}

This changed the architecture significantly.

Instead of:

User ↓ LLM ↓ Single Hardcoded Router

The workflow became:

User ↓ Groq LLM ↓ Tool Selection ↓ Device Selection ↓ Inventory Lookup ↓ FastMCP Tool ↓ Netmiko ↓ Target Router

The assistant could now dynamically choose which device to interact with.

Building the Inventory Layer

The inventory is loaded once during startup:

with open("devices.json") as f:
    DEVICES = json.load(f)

I then added a helper function to dynamically construct device connection parameters.

def get_device(device_name):
  if device_name not in DEVICES:
        raise ValueError(
            f"Device {device_name} not found"
        )
    device = DEVICES[device_name]
    return {
        "device_type": "cisco_ios_telnet",
        "host": device["host"],
        "port": device["port"],
        "username": "admin",
        "password": "eve",
    }

This removed all hardcoded device dependencies from the tool layer.

Now the same MCP tool could operate against any router present in the inventory.

Extending FastMCP Tools

Previously, tools had no awareness of device context.

Old version:

@mcp.tool()
def show_bgp():

New version:

@mcp.tool()
def show_bgp(device_name: str):

The tool now becomes parameterised and inventory-aware.

Example:

@mcp.tool()
def show_bgp(device_name: str):
    router = get_device(device_name)
    conn = ConnectHandler(**router)
    output = conn.send_command(
        "show ip bgp summary"
    )
    conn.disconnect()
    return output

This small architectural change dramatically improves scalability.

Instead of creating separate tools for every router, a single reusable tool can now operate across the entire inventory.

Making the LLM Device-Aware

The next challenge was allowing the LLM to determine not only:

  • which tool to use

but also:

  • which device should be queried

The system prompt was updated to force structured responses.

Example:

SYSTEM_PROMPT = """
Return EXACTLY in this format:
TOOL:<tool_name>:<device_name>
Examples:
TOOL:show_bgp:R1
TOOL:show_interfaces:R2
"""

Now the LLM performs two reasoning tasks simultaneously:

  1. Intent selection
  2. Device extraction

Example interaction:

User:

show bgp summary on R1

LLM response:

TOOL:show_bgp:R1

The response is then parsed programmatically:

parts = llm_decision.strip().split(":")
tool_name = parts[1]
device_name = parts[2]

This effectively converts natural language into executable infrastructure actions.

End-to-End Execution Flow

The complete flow now looks like this:

User Question ↓ Groq LLM ↓ Tool + Device Decision ↓ Inventory Lookup ↓ FastMCP Tool Execution ↓ Netmiko Session ↓ Router CLI Output ↓ Second LLM Analysis Pass ↓ Human-Friendly Response

The second LLM stage still performs operational analysis.

For example:

User:

How many BGP neighbors are established on R1?

The assistant:

  • selects the BGP tool
  • extracts device R1
  • retrieves live router state
  • feeds raw output back into the LLM
  • generates a summarized operational answer

This creates a much more natural interaction model compared to traditional CLI automation.

Also, the data can stay anywhere; here it is stored in a JSON file, but it can also be stored in Excel or a database.

Storing Network State in SQLite

Initially, the setup was directly querying devices every time the AI needed information. That works for a few routers, but it becomes slow and inefficient once the environment grows.

To improve this, I added a lightweight SQLite-based inventory cache where interface data is periodically collected and stored.

Database Schema

I created a simple interfaces table:

import sqlite3
conn = sqlite3.connect("network.db")
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS interfaces (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    device_name TEXT,
    interface_name TEXT,
    ip_address TEXT,
    status TEXT,
    protocol TEXT,
    collected_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
conn.commit()
conn.close()

This stores:

  • Device name
  • Interface name
  • IP address
  • Interface status
  • Protocol state
  • Collection timestamp

Polling Devices Periodically

Instead of querying routers live for every request, I built a small collector loop.

The collector:

  1. Connects to each router
  2. Runs show ip interface brief
  3. Parses the output
  4. Stores it into SQLite
while True:
    for device in DEVICES:
        output = show_interfaces(device)
        parsed = parse_interfaces(output)
        save_interfaces(device, parsed)
     print("Pass")
     time.sleep(300)

This gives me a continuously updated local dataset of interface states across the lab.

Parsing Router Output

The raw CLI output is converted into structured data before insertion.

def parse_interfaces(output):
    interfaces = []
    lines = output.splitlines()[1:]
    for line in lines:
        parts = line.split()
        if len(parts) >= 6:
            interfaces.append({
                "interface_name": parts[0],
                "ip_address": parts[1],
                "status": parts[-2],
                "protocol": parts[-1]
            })
    return interfaces

This makes the router output easier to search, analyze, and expose to the AI assistant.

Accessing Network Data Through MCP Tools

Once the data is stored locally, the AI assistant can query SQLite instead of logging into routers every time.

Example MCP tool:

@mcp.tool()
def get_interfaces(device_name: str):
    conn = sqlite3.connect("network.db")
    cursor = conn.cursor()
    cursor.execute("""
    SELECT interface_name,
           ip_address,
           status,
           protocol
    FROM interfaces
    WHERE device_name = ?
    """, (device_name,))
    rows = cursor.fetchall()
    conn.close()
    return rows

I also added a network-wide query:

@mcp.tool()
def get_interfaces_all():
    conn = sqlite3.connect("network.db")
    cursor = conn.cursor()
    cursor.execute("""
    SELECT device_name,
           interface_name,
           ip_address,
           status,
           protocol
    FROM interfaces
    """)
    rows = cursor.fetchall()
    conn.close()
    return rows

Why This Helps

This changed the setup significantly.

Instead of:

  • AI → Router login → CLI command → Parse output

The workflow became:

  • AI → SQLite query → Immediate response

Benefits:

  • Faster responses
  • Reduced device load
  • Easier scaling to larger environments
  • Historical data capability
  • Simpler multi-device analysis

This also opens the door for:

  • Trend analysis
  • Interface flap detection
  • Alerting pipelines
  • AI-based anomaly detection
  • Natural language queries across the entire network

This gives a much better and faster result:

A better approach would be to push data from devices through various available mechanisms, but it is a little difficult to setup that in lab so I am doing pull model.

If you enjoyed reading this or found the project useful, you can support my work here:

buymeacoffee.com/Amansingh1202


메타데이터
post_id
8c555d9fc10a
slug
scaling-an-ai-network-assistant-with-inventory-and-sqlite-8c555d9fc10a
url
https://medium.com/@aks001235/scaling-an-ai-network-assistant-with-inventory-and-sqlite-8c555d9fc10a
canonical_url
https://medium.com/@aks001235/scaling-an-ai-network-assistant-with-inventory-and-sqlite-8c555d9fc10a
author_url
https://medium.com/@aks001235
status
ok
fetched_at
2026-07-16 00:50:09