AI assistants become significantly more useful when they can interact with the systems we use every day. Asking an AI model a general networking question is useful. Asking it to inspect your actual Meraki environment, retrieve live information, and help you troubleshoot what is happening right now is something entirely different.
That is where the Model Context Protocol, or MCP, comes in.
In this series, we’re going to build a working MCP platform from the ground up using Cisco Meraki as our example. We won’t jump directly into Kubernetes, Terraform, or production infrastructure. Instead, we’ll build the system progressively so that every layer makes sense before we add the next one.
We’ll begin with the simplest possible architecture: a Python MCP server running directly on a Windows workstation and communicating locally with Claude Desktop.
Then we’ll evolve it.
By the end of the complete series, the architecture will progress through six stages:
- Local development — run the Meraki MCP server directly on our workstation and connect it to Claude Desktop using the local stdio transport.
- Docker — package the MCP server into a portable Linux container and change its transport from local stdio to Streamable HTTP.
- Azure Container Registry and Azure Container Apps — build the container in Azure, store it in ACR, and expose it as a remotely accessible MCP service.
- Authentication and API Management — protect the remote MCP endpoint with Microsoft Entra ID and Azure API Management and move toward per-user authentication.
- Azure Kubernetes Service — deploy the same MCP workload to AKS and explore Kubernetes Deployments, Pods, Services, Secrets, ConfigMaps, probes, resource controls, scaling, self-healing, and declarative YAML.
- Terraform and Azure DevOps — replace the manually built infrastructure with Infrastructure as Code and automate building, validating, and deploying the platform through CI/CD pipelines.
The important part is that we’re not going to treat those as six unrelated projects.
We’re going to take the same MCP server and evolve it one layer at a time.
By doing that, concepts such as containers, registries, managed identities, Kubernetes Pods, Services, Secrets, ingress, OAuth, Terraform state, and deployment pipelines have a reason to exist instead of appearing as a giant collection of cloud technologies we’re expected to understand all at once.
For Part 1, we’re keeping things intentionally simple.
Our goal is:
Claude Desktop → local MCP server → Meraki Dashboard API
By the end of this article, you’ll be able to ask Claude questions about your Meraki environment and have Claude call MCP tools running directly on your workstation to retrieve the information.
And we’ll do it in read-only mode so we can explore the integration without giving the MCP server permission to modify our Meraki environment.
What Is MCP?
Before installing anything, it’s worth understanding what we’re actually building.
MCP stands for Model Context Protocol.
At a high level, MCP provides a standardized way for an AI application to interact with external tools and data sources.
Without MCP, an AI assistant generally knows only what was included in its training data, what you type into the conversation, and whatever capabilities its application provides.
It doesn’t automatically know:
- what networks exist in your Meraki organization;
- whether an access point is currently online;
- which VLANs are configured;
- what devices are connected;
- what organizations your Meraki account can access;
- or what the current state of your network is.
More importantly, we shouldn’t solve that by simply handing an AI model an API key and hoping it figures everything out.
Instead, MCP introduces a server between the AI client and the external system.
In our case:
Claude Desktop
|
| MCP
v
Meraki MCP Server
|
| Meraki Dashboard API
v
Cisco Meraki
Claude is the MCP client.
The Python application we’re about to run is the MCP server.
Cisco Meraki is the external system that the server knows how to communicate with.
The MCP server exposes a collection of tools. Claude can discover those tools, determine which one is appropriate for a request, supply the required parameters, receive the result, and then use that result in its response.
That separation becomes extremely important later in this series.
Today the MCP server will run as a local Python process.
Later it will run inside Docker.
Then Azure Container Apps.
Then Kubernetes.
The location of the server changes dramatically, but the fundamental idea does not.
Architecture for Part 1
Our first architecture is about as simple as an MCP deployment can get.
Everything except the Meraki API itself runs on our workstation.
┌───────────────────────────────┐
│ Windows Workstation │
│ │
│ ┌───────────────────────┐ │
│ │ Claude Desktop │ │
│ └───────────┬───────────┘ │
│ │ │
│ stdio │
│ │ │
│ ┌───────────▼───────────┐ │
│ │ Meraki MCP Server │ │
│ │ Python │ │
│ └───────────┬───────────┘ │
│ │ │
└───────────────┼───────────────┘
│ HTTPS
▼
Meraki Dashboard API
There’s an important term in that diagram:
stdio.
It stands for standard input/output.
In this configuration, we’re not hosting a website and we’re not opening a network port for Claude to contact.
Claude launches a local process and communicates with it through the process’s input and output streams.
Conceptually:
Claude
↓ request
stdin
↓
MCP Server
↓ result
stdout
↓
Claude
That makes stdio particularly convenient for local MCP development.
There is no:
- DNS;
- TLS certificate;
- load balancer;
- ingress controller;
- public endpoint;
- Azure resource;
- Kubernetes Service;
- or firewall rule.
The client and server live on the same computer.
That changes in Part 2 when we put the application inside a container.
For now, stdio gives us the shortest possible path between Claude and our MCP server.
What We’re Using
The MCP server we’ll use is Cisco DevNet’s Meraki Magic MCP Community project.
Rather than writing dozens of Meraki API integrations from scratch, this gives us an existing MCP implementation designed to expose Meraki functionality as MCP tools.
We’re going to clone the source code locally, create an isolated Python environment, configure our Meraki credentials, run the server independently, and finally tell Claude Desktop how to launch it.
Before doing any of that, however, we need a few prerequisites.
Step 1: Install the Prerequisites
For this walkthrough you’ll need:
- a Windows workstation;
- Git;
- Python;
- Claude Desktop;
- access to a Cisco Meraki organization;
- and a Meraki Dashboard API key.
The original build used Python 3.13, so that is the version I’ll use throughout this walkthrough.
We’ll add Docker Desktop in Part 2. You do not need Docker for Part 1.
Likewise, you don’t need an Azure subscription, Azure CLI, Kubernetes, Terraform, or Azure DevOps yet.
That’s intentional.
We’re going to prove that the application works before adding infrastructure underneath it.
Step 2: Verify Python
Open PowerShell and run:
python --version
You should receive output similar to:
Python 3.13.x
If Windows doesn’t recognize python, install Python and make sure the installer adds Python to your PATH under System Variables in Environment Variables.
You can also determine which executable Windows is finding with:
where.exe python
This becomes useful later if you have multiple Python installations on the same machine.
Step 3: Verify Git
From PowerShell:
git --version
You should receive a Git version number.
For example:
git version 2.x.x.windows.x
Git is what we’ll use to retrieve the MCP project’s source code.
Step 4: Obtain a Meraki Dashboard API Key
The MCP server needs a way to authenticate to the Meraki Dashboard API.
For this project, that credential is a Meraki API key.
This is important:
Your Meraki API key is a secret.
Do not paste it into source code.
Do not put it in the Dockerfile we’ll create later.
Do not commit it to Git.
Do not include it in screenshots.
Do not publish it in a blog post.
We’ll place the key in a local environment file that the application reads at runtime.
That same separation between application code and secrets will follow us through the entire series.
The storage mechanism will eventually change:
Local development
↓
.env
Docker
↓
Runtime environment variable
Azure
↓
Container App secret / Key Vault
Kubernetes
↓
Kubernetes Secret
Production
↓
Managed secret architecture
But the principle remains the same:
credentials are configuration, not application source code.
Step 5: Clone the MCP Repository
Now we’re ready to get the actual application.
Choose a directory where you keep development projects.
Then run:
git clone https://github.com/CiscoDevNet/meraki-magic-mcp-community.git
Git will download the repository into a new directory named:
meraki-magic-mcp-community
Move into it:
cd meraki-magic-mcp-community
Before doing anything else, take a moment to look at what was downloaded.
You can use:
Get-ChildItem
or:
dir
One of the important files for our walkthrough is:
meraki-mcp-dynamic.py
That’s the Python MCP server we’ll eventually run.
You’ll also find the dependency information and example environment configuration we’ll use shortly.
Step 6: Create a Python Virtual Environment
We could install the project’s Python packages globally.
We’re not going to.
Instead, create a virtual environment inside the project:
python -m venv .venv
After the command completes, you’ll have a directory named:
.venv
Why use a virtual environment?
Python applications depend on packages.
Application A might need one version of a package while Application B needs another. Installing every dependency globally eventually creates conflicts and makes troubleshooting much harder.
A virtual environment gives this project its own isolated Python environment.
Conceptually:
Windows
│
├── Global Python
│
├── Project A
│ └── .venv
│ └── Project A dependencies
│
└── Meraki MCP
└── .venv
└── Meraki MCP dependencies
This also explains something that will become important when we configure Claude.
The executable Claude needs isn’t necessarily somewhere in the global Windows PATH.
It lives inside this project’s .venv.
Step 7: Activate the Virtual Environment
Run:
.venv\Scripts\Activate.ps1
Once activated, your PowerShell prompt should indicate that you’re working inside the virtual environment.
Typically you’ll see something like:
(.venv) PS C:\...\meraki-magic-mcp-community>
From this point forward, Python and pip commands run against the project’s isolated environment.
PowerShell blocks the activation script
Depending on your PowerShell execution policy, Windows may refuse to run Activate.ps1.
If that happens, don’t assume Python or the virtual environment is broken. Check the error first—it may simply be PowerShell’s script execution policy preventing activation.
The important thing is that .venv was successfully created.
Step 8: Install the Project Dependencies
With the virtual environment active, run:
pip install -r requirements.txt
The requirements.txt file tells pip which Python packages the application requires.
This command reads that file and installs those packages inside .venv.
That means we’re moving from:
Source code only
to:
Source code
+
Python runtime
+
Required libraries
Once installation completes, we have the pieces necessary to execute the MCP server.
We still don’t have its configuration.
That’s next.
Step 9: Create the Environment File
The repository provides an example environment file.
Copy it:
copy .env-example .env
We now have:
.env-example
and:
.env
The distinction matters.
.env-example is a template showing which settings the application expects.
.env is our local configuration and can contain secrets.
Open .env in your editor.
At minimum, we’re interested in the settings controlling:
- Meraki authentication;
- the target organization;
- MCP transport;
- and read-only behavior.
The actual secret values should remain private.
A simplified representation looks like:
MERAKI_API_KEY=<your-api-key>
MERAKI_ORG_ID=<your-organization-id>
READ_ONLY_MODE=true
MCP_TRANSPORT=stdio
The exact variable names and available options should follow the version of the repository you’re using.
The important part for this stage is that the server has the credentials it needs and is configured for local stdio operation.
Step 10: Keep the API Key Out of Git
Before going any further, verify that .env won’t accidentally end up in source control.
Run:
gitstatus
Check the repository’s .gitignore as well.
The goal is simple:
.env-example safe to commit
.env DO NOT COMMIT
This seems like a small detail during local development.
It isn’t.
Once we begin using Docker, Azure, Kubernetes, Terraform, and CI/CD, secret management becomes one of the most important parts of the architecture.
Starting with good habits now prevents much more serious problems later.
Step 11: Enable Read-Only Mode
For this walkthrough, set:
READ_ONLY_MODE=true
This deserves more attention than a single configuration line.
We’re connecting an AI client to infrastructure tooling.
There is an enormous difference between allowing an AI system to answer:
Which Meraki devices are offline?
and allowing it to perform:
Change the configuration of these Meraki networks.
During our initial build, we don’t need write access.
So we’re deliberately constraining the MCP server to read operations.
The source project enforces this server-side rather than relying on Claude to “remember” not to make changes. That distinction is critical.
A safety control should not be:
Please don’t call dangerous tools.
It should be:
Dangerous operations are rejected by the server.
We’ll verify that behavior before considering Part 1 complete.
Step 12: Run the MCP Server Before Connecting Claude
One of the most useful troubleshooting habits in this entire project is separating layers.
Don’t configure Claude, see that something fails, and immediately assume Claude is the problem.
First prove the MCP server itself can start.
From the activated virtual environment, run the project’s server:
python meraki-mcp-dynamic.py
We’re looking for the application to initialize without:
- missing-module errors;
- Python exceptions;
- missing environment variables;
- authentication configuration failures;
- or immediate process termination.
If it doesn’t start here, connecting Claude won’t fix it.
This pattern will repeat throughout the series.
Before Docker:
Prove Python works.
Before Azure:
Prove Docker works.
Before Kubernetes:
Prove the image works.
Before CI/CD:
Prove the deployment works manually.
That approach gives us a known-good layer every time we introduce something new.
Step 13: Understand Why the Server Doesn’t Look Like a Website
This is where stdio matters again.
When the MCP server runs in stdio mode, it isn’t listening on:
http://localhost:8000
There is no browser URL to open.
There is no HTTP endpoint to curl.
There is no port mapping.
The process is intended to communicate through standard input and output with an MCP client.
That’s exactly what Claude Desktop is going to do.
In Part 2, this changes substantially.
We’ll containerize the application and switch from:
MCP_TRANSPORT=stdio
to an HTTP-based transport.
That transition is one of the most important architectural changes in the series because it turns our local process into something that can eventually become a remote network service.
But we don’t need that yet.
Step 14: Locate Claude Desktop’s Configuration
Now that we know the MCP server can run, we can configure Claude Desktop to launch it.
This is one place where Windows installation methods matter.
A traditional Claude Desktop installation may use a configuration location under the user’s roaming application data.
During the actual build, however, Claude had been installed through the Microsoft Store.
That changed where its application data lived.
The configuration was found under a path resembling:
%LocalAppData%\Packages\Claude_<hash>\LocalCache\Roaming\Claude\claude_desktop_config.json
The <hash> portion varies by installation, so don’t copy somebody else’s complete path blindly.
This is an excellent example of why definitive walkthroughs need troubleshooting details. A perfectly valid MCP configuration is useless if you edit a configuration file Claude isn’t actually reading.
The file we’re ultimately looking for is:
claude_desktop_config.json
Step 15: Configure Claude to Launch the MCP Server
Claude Desktop needs to know two fundamental things:
- What executable should I launch?
- What arguments should I pass to it?
Because FastMCP was installed inside our virtual environment, its executable lives under .venv.
On Windows, the path resembles:
C:\path\to\meraki-magic-mcp-community\.venv\Scripts\fastmcp.exe
Our configuration therefore points Claude at the FastMCP executable inside the virtual environment and tells it to run our Python server using stdio transport.
The local configuration used during the build followed this structure:
{
"mcpServers": {
"Meraki_Magic_MCP": {
"command": "C:/path/to/meraki-magic-mcp-community/.venv/Scripts/fastmcp.exe",
"args": [
"run",
"-t",
"stdio",
"C:/path/to/meraki-magic-mcp-community/meraki-mcp-dynamic.py"
]
}
}
}
Replace:
C:/path/to/
with the actual location of the repository on your workstation.
Why use forward slashes?
JSON uses backslashes for escape sequences, which can make Windows paths awkward.
Using:
C:/Users/...
avoids much of that escaping complexity and works well for this configuration.
Step 16: Understand What Claude Will Actually Do
The configuration is easier to troubleshoot if we translate it into plain English.
This:
"command": ".../.venv/Scripts/fastmcp.exe"
means:
Launch the FastMCP executable installed inside this project’s Python virtual environment.
This:
"run"
means:
Run an MCP server.
This:
"-t",
"stdio"
means:
Communicate with that server through standard input/output.
And this:
".../meraki-mcp-dynamic.py"
means:
This is the MCP server application FastMCP should run.
So the complete chain is:
Claude Desktop
│
│ launches
▼
fastmcp.exe
│
│ runs
▼
meraki-mcp-dynamic.py
│
│ reads
▼
.env
│
│ authenticates to
▼
Meraki Dashboard API
Understanding this chain makes troubleshooting far easier than treating the JSON as magic.
Step 17: Restart Claude Desktop
After saving the configuration file, completely exit Claude Desktop and start it again.
Simply closing a window may not always terminate the application, depending on how it’s running.
We want Claude to start fresh and reload its MCP configuration.
When Claude launches, it should process the mcpServers configuration and attempt to start our Meraki MCP server.
If everything is configured correctly, the Meraki tools should become available to Claude.
Step 18: Verify MCP Tool Discovery
Before asking complicated questions, confirm that Claude actually sees the server and its tools.
This is another layer boundary.
At this point we’ve separately proven:
Python works
↓
Dependencies work
↓
MCP server starts
↓
Claude can launch MCP server
↓
Claude discovers tools
Only after all of those are true should we test Meraki data retrieval.
If the tools don’t appear, investigate:
- whether Claude is reading the correct configuration file;
- whether the JSON is valid;
- whether the fastmcp.exe path exists;
- whether the Python file path exists;
- whether .venv contains the required packages;
- whether the server starts independently;
- and whether Claude was fully restarted.
This layered troubleshooting approach saves an enormous amount of time.
Step 19: Ask Claude a Real Meraki Question
Now comes the part that makes all of the setup worthwhile.
Start with a simple read-only request appropriate to your environment.
For example:
List the Meraki organizations available to me.
Or:
Show me the networks in my Meraki organization.
Or:
Which Meraki devices are currently offline?
The exact available capabilities depend on the tools exposed by the server and the permissions associated with your Meraki API key.
What matters is the execution chain.
When Claude determines that your question requires Meraki data:
You
│
│ natural-language question
▼
Claude
│
│ chooses MCP tool
▼
Meraki MCP Server
│
│ calls API
▼
Meraki Dashboard
│
│ returns data
▼
MCP Server
│
│ returns tool result
▼
Claude
│
│ interprets result
▼
You
We’ve crossed an important line.
Claude isn’t answering only from model knowledge anymore.
It has a controlled mechanism for obtaining live information from an external system.
Step 20: Verify Read-Only Protection
Don’t stop after proving that reads work.
We deliberately configured:
READ_ONLY_MODE=true
Now verify the control.
Attempt an operation that would require modifying the Meraki environment.
The server should reject the write operation.
That’s the behavior we want.
Our architecture should enforce:
READ operation
↓
Allowed
WRITE operation
↓
Blocked by MCP server
This demonstrates an important principle we’ll carry all the way into the production architecture:
Authorization and safety controls belong as close as possible to the system enforcing them.
We don’t want to depend exclusively on a prompt telling an AI model what it should or shouldn’t do.
The infrastructure itself should impose boundaries.
Troubleshooting Part 1
A definitive walkthrough should tell you what to do when things don’t behave like the screenshots.
Here are the most important failure points at this stage.
Claude doesn’t show the MCP server
First verify the server runs independently:
python meraki-mcp-dynamic.py
If that fails, troubleshoot Python/application configuration before Claude.
If it succeeds, check the Claude configuration.
Make sure:
claude_desktop_config.json
is the file your particular Claude installation actually uses.
Microsoft Store installations can use the packaged application-data path described earlier.
fastmcp.exe cannot be found
Check:
.venv\Scripts\
The configuration must point to the executable in the virtual environment where the dependencies were installed.
Do not assume a globally installed fastmcp is the same environment.
The virtual environment won’t activate
Confirm .venv exists:
Get-ChildItem .venv
If it exists but PowerShell refuses to execute Activate.ps1, inspect your PowerShell execution-policy error.
The server starts but can’t reach Meraki
Check the .env configuration.
Verify:
- the API key is valid;
- the organization information is correct;
- the account associated with the API key has access to the organization;
- and the environment variables are actually being loaded.
Do not print or paste the API key while troubleshooting.
Claude launches but tools don’t appear
Validate the JSON.
One missing comma, quote, or brace can invalidate the configuration.
Also verify the paths independently.
For example:
Test-Path ".\.venv\Scripts\fastmcp.exe"
and:
Test-Path ".\meraki-mcp-dynamic.py"
Both should return:
True
What We Built
At the beginning of this article we had:
Claude Desktop
and
a Meraki environment
with no connection between them.
We now have:
┌────────────────────────────────────┐
│ Windows Workstation │
│ │
│ Claude Desktop │
│ │ │
│ │ MCP / stdio │
│ ▼ │
│ FastMCP │
│ │ │
│ ▼ │
│ meraki-mcp-dynamic.py │
│ │ │
│ │ .env │
│ │ READ_ONLY_MODE=true │
│ │ │
└───────┼────────────────────────────┘
│
│ HTTPS / Meraki API
▼
┌────────────────────────────────────┐
│ Cisco Meraki Dashboard │
└────────────────────────────────────┘
That’s already a functional MCP integration.
But it has a major limitation.
Everything depends on this workstation.
The Python environment exists here.
The source code exists here.
The MCP process runs here.
Claude launches it using an absolute filesystem path tied to this computer.
If another engineer wants to use the same server, they need their own copy of the repository, their own Python environment, their own dependencies, their own configuration, and their own local process.
That isn’t how we want to operate a shared service.
And that’s exactly the problem containers are going to solve.
Coming Next: Put the MCP Server in Docker
In Part 2, we’re going to take the working Python application we just built and package it into a Docker image.
That will force our first major architectural change.
Today:
Claude
│
stdio
│
Python process
Next:
MCP Client
│
│ HTTP
▼
Docker Container
│
▼
Meraki MCP Server
We’ll install Docker Desktop and Windows Subsystem for Linux 2 (WSL2), examine the Dockerfile, build the image, understand the difference between an image and a container, inject our secrets at runtime, expose port 8000, switch FastMCP from stdio to HTTP, and test the MCP endpoint directly.
We’ll also reproduce and fix two particularly useful problems from the original build: a Windows CRLF line-ending issue that caused Linux to report:
exec ./entrypoint.sh: no such file or directory
and a configuration-precedence problem that caused the container to start in stdio mode even though the Dockerfile said HTTP. Those were real failures in the build and are exactly the kinds of problems worth understanding before we move anywhere near Azure.
Once the server works reliably inside Docker, we’ll finally be ready to take it off the workstation and into Azure.
Part 2: Containerizing a Meraki MCP Server with Docker → next.