NACC: Turning Your Entire Network Into a Single Intelligent AI Agent Using MCP

What if you could say “compress all the log files on the Kali VM, then move the results to the Ubuntu server, then send me a summary” — and your network just… did it?

No SSH windows. No manually copying paths. No scripting the same thing across three different machines. Just one sentence, spoken to an AI that has eyes and hands across your entire infrastructure.

That’s NACC — Network Agentic Command Control. An autonomous AI agent orchestrator that treats your local network not as a collection of separate machines, but as a single unified intelligent system.


The Problem With Multi-Machine Work

Anyone who manages more than one machine knows the mental overhead. You have a task that spans systems: fetch a file from the Linux server, process it on the Mac, run a security scan on the Kali VM, report back. In practice, this means:

  • Three SSH sessions
  • Manually copying file paths between windows
  • Remembering the state of each machine
  • Re-running something when you forgot a step

It’s the kind of work that is technically simple but cognitively expensive. The actual commands are trivial. The coordination is the hard part.

NACC’s thesis: AI is better at coordination than humans are. Give it tools. Give it a network. Get out of its way.


Architecture: Hub-and-Spoke with MCP

NACC uses a Hub-and-Spoke architecture. The Orchestrator (the Hub) is the brain. The Nodes (the Spokes) are the hands.

flowchart TD U["User\nNatural Language"] --> G subgraph O["Orchestrator — NACC Brain"] G["Gradio UI"] LLM["LLM\nClaude / Anthropic\nTool-calling loop"] G --> LLM end LLM -->|"MCP tool calls"| N1 LLM -->|"MCP tool calls"| N2 LLM -->|"MCP tool calls"| N3 subgraph N1["MacBook Node"] N1T["read_file\nwrite_file\nexecute_shell"] end subgraph N2["Kali VM Node"] N2T["nmap · Metasploit\nexecute_shell"] end subgraph N3["Ubuntu / Cloud Node"] N3T["backup_script\nlog management"] end LLM -->|"final summary"| U

The Orchestrator receives a natural language instruction, decomposes it into a multi-step execution plan, routes each step to the appropriate node, handles errors and retries, and returns a coherent summary to the user.

The protocol binding everything together is MCP — the Model Context Protocol.


What is MCP and Why It Matters

MCP (Model Context Protocol) is an open standard that lets AI models interact with external tools and systems through a standardised JSON-RPC interface. It was built to solve exactly this class of problem: giving AI agents a safe, structured way to take actions in the real world.

Under MCP, each Node in NACC exposes a set of tools — functions the AI can call:

  • read_file(path) → reads a file at the given path
  • write_file(path, content) → writes content to a file
  • execute_shell(command) → runs a shell command and returns output
  • list_directory(path) → lists directory contents
  • sync_to_node(target_node, source_path, dest_path) → copies a file to another node

The Orchestrator, powered by Claude (via Anthropic’s API) or any MCP-compatible LLM, calls these tools autonomously as it executes the plan.

What makes this powerful: the AI decides the sequence. I don’t script “step 1, step 2, step 3.” I express the goal, and the agent figures out the steps, executes them, handles errors mid-stream, and adjusts.


The Orchestrator: NACC’s Brain

The Orchestrator is a Python application with a Gradio web UI, making it accessible from any browser on the local network.

# orchestrator/app.py (core loop, simplified)
import anthropic
from mcp import ClientSession, StdioServerParameters

async def run_task(user_instruction: str, connected_nodes: list) -> str:
    client = anthropic.Anthropic()
    
    # Build tool list from all connected nodes
    tools = []
    for node in connected_nodes:
        tools.extend(await node.list_tools())
    
    messages = [{"role": "user", "content": user_instruction}]
    
    while True:
        response = client.messages.create(
            model="claude-opus-4-5",
            max_tokens=4096,
            tools=tools,
            messages=messages
        )
        
        if response.stop_reason == "end_turn":
            return response.content[-1].text
        
        # Execute tool calls autonomously
        for tool_call in response.tool_calls:
            node = find_node_for_tool(tool_call.name, connected_nodes)
            result = await node.call_tool(tool_call.name, tool_call.input)
            messages.append({"role": "tool", "content": result})

The loop continues until the agent signals it’s done. No fixed script. No hardcoded steps. Pure autonomous planning.


The Nodes: NACC’s Hands

Each machine in the network runs a lightweight NACC Node process — a Python MCP server that exposes local system capabilities.

# node/server.py (simplified)
from mcp.server import Server
from mcp.server.stdio import stdio_server
import subprocess, os

server = Server("nacc-node")

@server.tool()
async def execute_shell(command: str) -> str:
    """Execute a shell command and return stdout/stderr."""
    result = subprocess.run(
        command, shell=True, capture_output=True, text=True, timeout=30
    )
    return result.stdout + result.stderr

@server.tool()
async def read_file(path: str) -> str:
    """Read file contents at the given path."""
    with open(os.path.expanduser(path), 'r') as f:
        return f.read()

@server.tool()
async def write_file(path: str, content: str) -> str:
    """Write content to file at path."""
    with open(os.path.expanduser(path), 'w') as f:
        f.write(content)
    return f"Written {len(content)} bytes to {path}"

Nodes communicate with the Orchestrator over HTTP/MCP. Setup on each machine is pip install nacc-node && nacc-node start — that’s it.

The node process is minimal by design. The intelligence lives in the Orchestrator. The nodes are just dumb executors with a clean API.


Three Real Workflows From the Examples Doc

These aren’t synthetic demos — these are the three scenarios I documented in the repo’s examples.md.

Scenario 1: Basic File Operations (Any Single Node)

Prompt: “Create a directory called ‘test_data’, write a file named ‘notes.txt’ inside it with the text ‘Hello World’, and then read it back to me.”

What NACC does:

  1. Intent Parser identifies 3 steps: Create Dir → Write File → Read File
  2. Orchestrator executes them sequentially on the local node
  3. Returns the file content “Hello World” in the chat

This is the simplest case — single node, sequential steps. It works perfectly and is the first thing I demo to skeptics who think “the AI will just hallucinate the output.”

Scenario 2: Conditional Cross-Node Security Scan (Kali Node)

Prompt: “Switch to the Kali node. Run an nmap scan on 192.168.1.50 to find open ports. If you find port 80, try to curl the homepage.”

What NACC does:

  1. Routes the entire request to the kali-node
  2. Executes nmap -F 192.168.1.50
  3. Parses the output — if "80/tcp open" is found, executes curl http://192.168.1.50
  4. Summarises the open ports AND the web page content in one response

The conditional logic (if you find port 80) is handled entirely by the LLM reading the nmap output and deciding what to do next. I wrote zero conditional code.

Scenario 3: Multi-Node Conditional Orchestration

Prompt: “Check the CPU usage on the MacBook node, and if it’s below 50%, tell the Cloud node to start the backup process.”

What NACC does:

  1. Queries MacBook node: top -l 1 | grep CPU
  2. Analyses the result — extracts the CPU percentage
  3. If condition is met, sends python backup_script.py to the Cloud node
  4. Reports back: a coordinated action across two physical machines, triggered by a real-time system state

This last scenario is the one that made me realise how different this is from scripting. A bash script could do this — but it would be rigid, brittle, and require manual maintenance. NACC understands intent. If the CPU check returns in a different format than expected, the LLM figures it out. If backup_script.py raises an error, the LLM reports the error and suggests a fix.


Claude Desktop Integration

Because NACC is a proper MCP server, it integrates natively with Claude Desktop — Anthropic’s desktop app. You can add NACC as an MCP tool in Claude’s config, and suddenly Claude Desktop has live access to your entire network.

// claude_desktop_config.json
{
  "mcpServers": {
    "nacc": {
      "command": "python",
      "args": ["-m", "nacc.orchestrator"],
      "env": {
        "NACC_NODES": "mac:8080,kali:8081,ubuntu:8082"
      }
    }
  }
}

This is the integration I’m most excited about. Conversational AI with actual hands across a real network is a qualitatively different tool than a chatbot.


Security Model

Giving an AI shell access to multiple machines is not something to do naively. NACC’s security model:

Node-level:

  • Each node only exposes the tools you configure. execute_shell is opt-in and off by default.
  • Commands run as a dedicated low-privilege nacc user, not root.
  • Tool execution is logged with timestamps, command text, and output.

Network-level:

  • Nodes only accept connections from the Orchestrator IP (configured at startup).
  • All Orchestrator ↔ Node communication is over local network only (no internet exposure).

Future:

  • mTLS between Orchestrator and Nodes is on the roadmap.
  • A command allowlist (e.g., “only allow read operations”) for production hardening.

Multi-Backend AI: Pick Your LLM

One of the explicit design goals was backend-agnosticism. NACC ships with support for five different AI backends:

Backend Why Use It
Blaxel Serverless, zero-config, recommended for first-time setup
Anthropic (Claude) Best multi-step reasoning; what I use for complex tasks
OpenAI (GPT-4o) Fast, reliable, great tool-calling
Google (Gemini 1.5 Pro) Cost-effective for high-volume tasks
Ollama (local) 100% offline — Llama 3, Mistral, etc. No API key needed

Ollama mode is specifically compelling for security-sensitive environments. NACC controlling multiple machines, with the AI brain running locally, means your commands and responses never leave the LAN. Full orchestration intelligence, zero cloud dependency.

Getting It Running

The repo ships with a start_nacc.sh one-liner:

# Clone and run
git clone https://github.com/Vasanthadithya-mundrathi/NACC.git
cd NACC
./start_nacc.sh

The script handles venv creation, dependency install from requirements.txt, and launches the Gradio app. For multi-node setups, each remote machine runs:

pip install -r requirements.txt
python -m nacc.node --orchestrator-ip 192.168.1.10 --port 8001

Node config lives in node-config.yml — you define which tools each node exposes, what user they run as, and which IP ranges the orchestrator is allowed from.

The Gradio UI (SDK version 5.5.0, app_file: app.py) is also live on Hugging Face Spaces for a zero-install browser demo.

Hackathon Context

NACC was submitted to the Hugging Face MCP Birthday Hackathon across three tracks:

  • Enterprise (mcp-in-action-track-enterprise): Multi-node orchestration for distributed infrastructure management and automated deployments
  • Consumer (mcp-in-action-track-consumer): Natural language home lab management — NAS, home server, gaming rig
  • Creative (mcp-in-action-track-creative): Coordinated cross-machine creative workflows (render on one machine, edit on another, publish from a third)

And the secondary Building MCP track — because NACC isn’t just an MCP client, it’s also a proper MCP server that exposes tools to Claude Desktop and any other MCP-compatible client.


The Real Story: How This Actually Happened

The HuggingFace MCP Birthday Hackathon celebrated the first year of Anthropic’s Model Context Protocol. NACC started as a real solution to a real problem I face daily — not a hackathon project in the traditional “build something for the demo” sense. That distinction matters, because it’s what made the hard moments survivable.

The Timeline

November 14, 2024 — Hackathon opens. I have this idea about AI orchestrating an entire network. I’m excited.

November 18, 2024 — My college announces semester exams starting November 25th. Major project submission deadline: November 22nd.

November 20, 2024 — I’m debugging why Gradio and Docker Spaces can’t communicate with each other, while simultaneously studying for my Operating Systems exam and finishing my college IoT project.

November 23, 2024 — 2 AM. I finally crack the two-space architecture (more on this below). I have an exam at 9 AM. I study for 3 hours, sit the exam, come back and code for 12 hours straight.

November 28, 2024 — Exams are over. Two days left before the deadline. The demo works perfectly locally but fails on HuggingFace Spaces. Git LFS refuses to upload my 100MB demo video.

November 29, 2024 — Final commit pushed at 9 PM. Both Spaces are live. The demo works. I’m exhausted and oddly proud.

No co-working space. No team. Just a laptop, caffeine, and the refusal to ship a fake demo.

The Problem: You Can’t Demo a Network App in the Cloud

Here’s the wall I hit. The hackathon required submissions to run on HuggingFace Spaces. But NACC is fundamentally a local network tool — it needs SSH access to real machines, real filesystem operations, real shell execution across real nodes.

HuggingFace Spaces runs in isolated containers. You can’t SSH to other machines. You can’t discover nodes on a LAN. File operations are sandboxed. There’s one runtime per Space.

The easy path: strip NACC down to a single-machine mock, fake the multi-node responses, submit a video instead of a live demo.

I refused. That wouldn’t be NACC.

The Breakthrough: Two Spaces, One Brain

After days of fighting constraints, the idea hit at 2 AM: what if I use two separate HuggingFace Spaces and bridge them over HTTP?

┌─────────────────────────────────────┐
│  MAIN SPACE (Gradio)                │
│  Gradio UI + Orchestrator + AI Agent│
│  + hf-space-local node              │
└──────────────┬──────────────────────┘
               │ HTTP / MCP-style JSON-RPC
┌──────────────▼──────────────────────┐
│  VM SPACE (Docker)                  │
│  vm-node-01 — isolated container    │
│  RESTful API, whitelisted commands  │
└─────────────────────────────────────┘

The Main Space’s orchestrator treats the VM Space as just another node — identical to how it would treat a real machine on a LAN. The abstraction is seamless. The protocol is real MCP-style JSON-RPC, not simulated.

# Main Space calling the VM Space node — same as calling a LAN node
response = requests.post(
    "https://huggingface.co/spaces/MCP-1st-Birthday/NACC-VM/tools/execute-command",
    json={"command": ["ls", "-la", "/app"], "timeout": 30}
)

This was the first time two separate HuggingFace Spaces had been used to simulate a distributed system. The constraints didn’t kill the idea — they forced a better demo architecture than the original local version.

Building the MCP Stack From Scratch

I wrote the entire MCP implementation by hand — no pre-existing SDKs, no templates. Every JSON-RPC handler, every tool definition, every security handshake.

Why? Because a hackathon celebrating the Model Context Protocol’s first birthday deserved a submission that actually understood the protocol — not one that imported a library and called it a day.

class MCPServer:
    def __init__(self):
        self.tools = {}

    def register_tool(self, name: str, schema: dict, handler):
        self.tools[name] = {
            "name": name,
            "description": schema["description"],
            "inputSchema": schema["parameters"],
            "handler": handler
        }

    async def handle_call_tool(self, name: str, arguments: dict):
        if name not in self.tools:
            return {"error": "Tool not found"}
        result = await self.tools[name]["handler"](**arguments)
        return {"content": [{"type": "text", "text": result}]}

Custom additions on top of the spec: node aliases, session persistence across requests, and cross-space routing — none of which were in standard MCP SDKs at the time.

The Session Bug That Broke Everything

Week 4. The UI was nearly done. But after switching to vm-node-01, the file browser kept showing local files instead of the VM’s filesystem. Classic state bug.

# Before (broken) — returning the input session_id, not the session object's id
return history, "", dashboard, files, session_id

# After (fixed) — returning the actual session object's id
return history, "", dashboard, files, session.session_id

One character difference. State management in distributed systems breaks in exactly these ways — and finding it after days of debugging felt like pulling a splinter out of your brain.

The Honest Reflection

The HuggingFace Spaces demo is slower than the local version. Some features are constrained by sandboxing. There are rough edges I didn’t have time to polish between exams and the deadline.

But it’s real. The protocol is real. The orchestration is real. The two-space architecture is a genuine engineering solution to a genuine constraint — not a workaround, but a pattern that anyone building distributed demos on HF can reuse.

The full technical writeup is on HuggingFace if you want to go deeper. The demo Spaces are live. The GitHub repo has everything.


What I’m Building Next

Reactive mode. Currently NACC is request-response. I want an agent that monitors nodes continuously and acts proactively: “Disk on Ubuntu is at 91% — archiving logs automatically.”

Tool discovery. Right now, tools are defined manually per node. Automatic discovery (the agent enumerates what’s running on each machine and generates tools dynamically) would make NACC dramatically more powerful.

Permission gates. Before any destructive operation (rm, format, write to system paths), NACC should ask for explicit confirmation. “I want to delete 3.2GB of log files on Ubuntu. Proceed?”


NACC is a bet that the network is the computer — and the computer should be able to understand English. We’re not far from a world where managing a fleet of machines is as natural as having a conversation.

I’m trying to build that world.

— Vasanth