top of page

Control your Network with AI - Building your first MCP Server with a local LLM

  • Oct 1
  • 8 min read
Title Page


Table of Contents





Introduction:


I have previously written a Blog post explaining what MCP is. If you're unfamiliar with the MCP concept, I strongly suggest reading that first. In this post, I'll demonstrate how to set up your first MCP Server and provide important side notes along the way! Additionally, I made a Blog Post about configuring Meraki networks using ChatGPT natively. I received a lot of feedback expressing discomfort with exposing the API key to a public service, particularly in Germany. Therefore, this time, I focused on having a local LLM and operating a local MCP Server.


For this post my aim is to set up my own MCP Server locally to collect data about my infrastructure via a Catalyst Center.

Since this alone would be a pretty boring use-case, I plan to utilize the Catalyst Center MCP to gather information on all the IOS-XE Versions in use. Subsequently, I'll use a pre-built NIS MCP Server to query about known CVEs associated with these IOS-XE Versions. Finally, I'll enhance my code to gather the running configuration and ask the LLM to check if I'm impacted by the CVE and, if necessary, how to mitigate any risks.



Chapter 1: Setting up a local LLM


There are numerous options for running a pre-trained LLM. Hugging Face is undoubtedly the most well-known. Regarding Client/Agent software, VS-Code, Claude Desktop, and LM Studio are three of the most popular, at least from what I an see. I've chosen for LM Studio. One reason is that I can select a model from Hugging Face directly within the app. Additionally, it has MCP fully integrated, and I've chosen the MLX version of OpenAI's GPT-OSS 20B model. Thanks to Apple Silicon’s unified memory, integrated GPU, and Neural Engine, Macs can run large language models surprisingly well without needing a dedicated gaming GPU. Apple’s MLX (Machine Learning eXchange) framework enables this by efficiently distributing LLM workloads across the CPU, GPU, and Neural Engine. This combination provides strong performance and energy efficiency right on a laptop. However, you need an Apple Silicon (M1-M4) to take advantage of this.



Chapter 2: Write first lines of Python code


Let's begin by initializing a new project with uv init, as usual.

Since I plan to use the SDK, I'll add the package immediately with: uv add catalystcentersdk


It's crucial not to include your credentials directly in the code. For this project, I've created a file named config.py to store all credentials and included this file in the .gitignore.


Below is a simple code snippet that retrieves all devices and their respective details:


from catalystcentersdk import CatalystCenterAPI
import config

CatCen = CatalystCenterAPI(username = config.USERNAME,
                            password = config.PASSWORD,
                            base_url=config.URL,
                            verify=False)


def getDevices():
    devices = CatCen.devices.devices()
    return devices


print(getDevices())


Lets make this usable for our AI Agent that it can work with this informations.

The fastest and most convenient way currently is, as the name may already reveal, FastMCP.

FastMCP is very easy to use:


  • uv add fastmcp

  • import fastmcp within the Python code

  • instantiate

  • decorate your function (@mcp.tool())

  • make a good docstring explaining to the Agent what he can do with this function

  • add annotations

  • optional: make the function async


Doing all this will result in a code like this:

from catalystcentersdk import CatalystCenterAPI
from typing import Any
from fastmcp import FastMCP
import config
import asyncio

mcp = FastMCP("CatCen MCP")

CatCen = CatalystCenterAPI(username = config.USERNAME,
                                  password = config.PASSWORD,
                                  base_url=config.URL,
                                  verify=False)


@mcp.tool()   
async def getDevices() -> list[dict[str, Any]]:
  """
  tool for accessing Cisco devices over the Catalyst Center (former  
  DNA Center). You also get a lot of attributes per device
  using this you will get a list of all devices. within the list   
  you can read the attribues like the device name, IP Adress or
  Name
  example output:
  [{'airQualityHealth': {}, 'band': {}, 'clientCount': {},     
  'cpuHealth': -1, 'deviceFamily': 'SWITCHES_AND_HUBS', 
  'deviceType':   'Cisco Catalyst 9000 UADP 8 Port Virtual <…>
   """
   devices = await asyncio.to_thread(CatCen.devices.devices)
   return devices.response

if __name__ == "__main__":
   mcp.run(transport="http", port=8002, log_level="DEBUG")



Let's break down some special parts.


Every function decorated with @mcp.tool() becomes a tool provided by an MCP Server, ready to be utilized by the AI Agent.


The first thing that was somewhat unusual for me was the annotation, which declares the expected data type. If you're wondering why only 'Any' needs to be imported while others don't, it's because, since Python 3.9 and later, most of these constructs are included by default.

Examples are: Included in Python 3.12:

  • int, str, float, bool, bytes

  • list[T], dict[K, V], tuple[T, ...], set[T]

  • e.g. str | None

Comes from typing:

  • Any


But why is it necessary to specify this? Let's recall the JSON body when a client requests tools/list:

{
  "jsonrpc": "2.0",
  "id": 52,
  "result": {
    "tools": [
      {
        "name": "inventory_list",
        "description": "List all managed devices; optional filter
                        by device family {SWITCH, AP, WLC}.",
        "inputSchema": {
          "type": "object",
          "properties": {
            "family": {
              "type": "string",
              "enum": ["SWITCH", "ROUTER", "AP", "WLC"]
       }
     }
   }
}


Given that all data must be in JSON format, FastMCP needs to determine the type for this request and, conversely, how the result should be converted back into JSON.


Finally, you specify whether the MCP is accessible via STDIO or streamable HTTP. If you choose HTTP, there are some parameters associated with this setting.


Making the function asynchronous allows the MCP Server to execute multiple tools simultaneously, such as collecting data from several devices at once. Otherwise, calling a tool would essentially freeze the entire MCP server until the request is complete. However, this is not mandatory. FastMCP will manage and handle it as needed.

This may look as follows (highly simplified):


if inspect.iscoroutinefunction(tool_func):
    result = await tool_func(*args)
else:
    result = tool_func(*args) 


To complete this action, starting the MCP Server by executing uv run CatCen.py.


FastMPC 2.0 starting screen


Chapter 3: Testing with the MCP Inspector


The best current method for testing the running MCP Server is using the MCP Inspector.

I will not go over the installation process here. Please refer to the official guide.


MCP Inspector


You should be able to connect to your server and view a list of all tools. At this point in the guide, you should see your tool: getDevices. Along with all related information, such as the docstring


When you test it, you will get a response similar to this:


{
  "content": [
    {
      "type": "text",
      "text": "Output validation error: None is not of type 'array'"
    }
  ],
  "isError": true
}

By printing the type of the response we will get following:


<class 'catalystcentersdk.models.mydict.MyDict'>

Since FastMCP cannot manage SDK-specific objects, it's necessary to convert this object into a basic JSON object.

Here's how I've accomplished this:


If the object is a Pydantic model and have the dict attribute, use the dict() function to obtain the plain JSON object.

Enhancing the return statement:


  return json.loads(json.dumps(
                     devices.response,  
                     default=_json_deobjectations))


After creating the function accordingly:


def _json_deobjectations(o: Any) -> Any:
    """
    take the mydict object and convert it to plain dict if it is a 
    pydantic object with dict
    """
    if hasattr(o, "dict"):
      return o.dict()

This can be applied to every new MCP tool, as it is able of handling both JSON objects and Pydantic objects.


Once the code is adapted, you will be able to view all the devices managed by the Catalyst Center.


Chapter 4: Action!


Since we have confirmed that the MCP Server functions as expected, we will proceed to use it with our Agent.


Configuring LM Studio

To enable access to the MCP Server, we need to modify the configuration file. Simply click on the power plug symbol, select Install from the dropdown menu, and edit mcp.json.


mcp.json edit in LM Studio

Since we have already started the MCP server, the configuration is quite straightforward:


{
  "mcpServers": {
    "CatCenMCP": {
      "url": "http://127.0.0.1:8002/mcp"
    }
  }
}

Once you've chosen your MCP Server from the list, you'll see that the prompt field now features the button:


MCP tools in LM Studio


So lets test it:

local LLM output in LM Studio


Hell yeah! We've successfully created our first MCP Server! Congratulations!


Chapter 5: Taking it further - Real World Use Case


The final step is to leverage what has been learned!

In this example, I want to take the initial output and take it to another MCP Server in order see if there is a known CVE for the listed IOS Versions.

If there is, then obtain the IP Address of the device, gather its configuration, and verify if we are affected by this CVE. For instance, a BGP CVE is not a big problem for us if we don't use BGP.


MCP for searching CVE in the NVD


For this task i used this MCP Server. You need to request an API key and augment the mcp.json file in LM Studio.


{
  "mcpServers": {
    "CatCenMCP": {
      "url": "http://127.0.0.1:8002/mcp"
    },
    "mcp-nvd": {
      "command": "/opt/homebrew/bin/uvx",
      "args": [
        "mcp-nvd"
      ],
      "env": {
        "NVD_API_KEY": "<never share you API key>"
      }
    }
  }
}


Adding Tool for gathering running configuration


I've developed a straightforward tool using Netmike to SSH into a device using its IP address in order to obtain the running configuration. Additionally, I've updated the config.py file to include the username and password needed for remote access.


from netmiko import ConnectHandler

mcp.tool()
async def getDeviceConfig(host: str, ) -> str:
   """
   use the IP Adress of a device to ssh into it. Username and 
   Password are already present in this tool.
   It will return the full running-configuration of the Cisco 
   Device
   """
   device = {
    "device_type": "cisco_ios",      
    "host": host,
    "username": config.SSH_USERNAME,
    "password": config.SSH_PASSWORD
    }
   
   try:
    with ConnectHandler(**device) as conn:
        # If the device needs enable mode:
        # if device.get("secret"): conn.enable()

        running_cfg = conn.send_command("show running-config", 
                                         use_textfsm=False)
        #return running_cfg
        return json.loads(json.dumps(running_cfg, 
                          default=_json_deobjectations))

   except Exception as e:
      return f"Connection failed. Reason: {e}"
  

Let's do a test:


This time, I compiled a list of all switches along with their IP addresses, names, and IOS versions, and asked about any known CVEs. In blue, we can see that the agent is utilizing the search_cve tool from the CVE MCP.




local LLM output in LM Stuio 2


CVE-2018 appears to be interesting. I want to know if we are impacted by this CVE. Therefore, I requested a configuration check. It should review the tool description, which states that the configuration can be obtained through our MCP server, and it will require the device's IP address to do so. Understand that in the Cisco configuration, if there are no CDP lines present, it means CDP is enabled. This is because it is a default setting, and defaults are not visible in the running configuration.


Table displays "Edge02 – Impact Assessment for CVE-2018-0471," detailing device info, CVE, version, config check, and conclusion on vulnerability.

and the Agent also immediately recommended mitigation actions:


Table of recommended mitigations and next steps for CDP vulnerability. Dark background with text instructions for disabling and upgrading.


Security considerations

When integrating MCP into enterprise environments, it is crucial to consider security implementations beyond this simple lab. Not all MCP agents softwares are capable of securely storing credentials or API keys within the mcp.json file, which creates a huge risk. Furthermore, the MCP server itself runs with read/write permissions on you system, making it an attractive to abuse. To reduce risk, enterprises should ensure the LLM in use is not compromised. Cisco AI Defense, for example, provides threat detection and guardrails against malicious prompts, data exfiltration, or model abuse. Strong authentication to the MCP server must be mandatory, with OAuth 2.0 enforced to protect access tokens and session integrity. Additional hardening measures include restricting file system privileges, centralizing secret storage through enterprise-grade secret managers, enforcing TLS with mutual certificate validation between clients and the MCP server, and enabling granular audit logging to identify suspicious access or privilege escalation attempts. Security teams should also monitor MCP processes in the same way they would critical middleware or identity services, as its compromise could serve as a pivot into sensitive data and systems.







 
 
 

Comments


Beitrag: Blog2 Post
  • LinkedIn

©2022 Marco Networking. Erstellt mit Wix.com

bottom of page