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…
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:
- Intent selection
- 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:
- Connects to each router
- Runs
show ip interface brief - Parses the output
- 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:
메타데이터
- 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