Metadata-Version: 2.4
Name: infoblox-ddi-mcp
Version: 2.2.1
Summary: Infoblox DDI MCP Server — 26 intent-level workflow tools for agentic AI integration
Author: Igor Racic
License-Expression: MIT OR Apache-2.0
License-File: LICENSE
Keywords: ai,ddi,dhcp,dns,infoblox,ipam,llm,mcp
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: System Administrators
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: System :: Networking
Requires-Python: >=3.10
Requires-Dist: cachetools>=5.3.0
Requires-Dist: fastmcp>=3.1.1
Requires-Dist: pybreaker>=1.0.0
Requires-Dist: python-dotenv>=1.0.0
Requires-Dist: requests>=2.31.0
Requires-Dist: structlog>=24.1.0
Provides-Extra: otel
Requires-Dist: opentelemetry-api>=1.20; extra == 'otel'
Requires-Dist: opentelemetry-exporter-otlp>=1.20; extra == 'otel'
Requires-Dist: opentelemetry-sdk>=1.20; extra == 'otel'
Description-Content-Type: text/markdown

[![CI](https://github.com/iracic82/infoblox-ddi-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/iracic82/infoblox-ddi-mcp/actions/workflows/ci.yml)
[![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
[![License: MIT OR Apache-2.0](https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-green.svg)](LICENSE)
[![MCP](https://img.shields.io/badge/MCP-compatible-purple.svg)](https://modelcontextprotocol.io)
[![PyPI](https://img.shields.io/pypi/v/infoblox-ddi-mcp.svg)](https://pypi.org/project/infoblox-ddi-mcp/)

# Infoblox DDI — MCP Server

> **26 intent-level workflow tools** for managing Infoblox Universal DDI via the Model Context Protocol.

Any MCP-compatible AI agent can manage your entire DDI infrastructure — DNS, DHCP, IPAM, security, and federation — without being an Infoblox expert.

---

## Why Intent-Level Tools (Not 1:1 API Mapping)

The Infoblox Universal DDI platform has **300+ REST API endpoints** across DDI, Security, and Insights services. A naive MCP implementation would expose each endpoint as a separate tool. This server takes a fundamentally different approach: **26 intent-level workflow tools** backed by **303 API methods** that abstract multi-step operations into single calls.

**The problem with 1:1 mapping:**

```
# What an agent must do to provision a host with 1:1 tools (7 API calls):
1. list_ip_spaces(filter="name==prod")          → resolve space name to ID
2. list_subnets(filter="space==ipam/ip_space/1") → find subnets
3. get_next_available_ip(subnet_id)              → allocate IP
4. list_auth_zones(filter="fqdn==example.com")   → resolve zone
5. create_ipam_host(name, addresses, ...)        → create host
6. create_dns_record(type="A", ...)              → create A record
7. create_dns_record(type="PTR", ...)            → create PTR record
```

```
# Same operation with intent-level tool (1 call):
provision_host(hostname="web-01", space="prod", zone="example.com")
```

| Concern | 1:1 Mapping (300+ tools) | Intent Layer (26 tools) |
|---------|----------------------|------------------------|
| **LLM tool selection** | Agent must choose from 300+ tools — high hallucination rate | 26 tools with `USE THIS for X / For Y use Z` disambiguation |
| **Token efficiency** | 5-7 API calls per workflow, each consuming context window | Single call, single response |
| **Error handling** | Agent must implement rollback, partial-success, retry | Server-side orchestration with `steps[]` tracking |
| **Domain knowledge** | Agent needs to know Infoblox resource IDs, filter syntax, API paths | Agent speaks business intent: "provision host", "diagnose DNS" |
| **Safety** | Every destructive call is directly exposed | `dry_run=True` by default, input validation, filter injection protection |
| **Consistency** | Each agent builds its own workflow logic | Standardized response envelope (`status`, `summary`, `steps`, `result`, `warnings`, `next_actions`) |

**Key design principles:**
- **One tool per user intent** — "provision a host", "diagnose DNS", "investigate a threat"
- **Resolvers handle name→ID mapping** — agents pass human-readable names, not resource IDs
- **Dry-run by default** on all mutating operations — agents must explicitly opt in
- **Guided next actions** — every response suggests what to do next, reducing multi-turn back-and-forth

---

## Quick Start

### Option A: uv (recommended)

```bash
cd infoblox-ddi-mcp

# Install dependencies
uv pip install -r requirements.txt

# Configure credentials
cp .env.example .env
# Edit .env — add INFOBLOX_API_KEY

# Run (stdio)
uv run python mcp_intent.py

# Run (HTTP)
uv run python mcp_intent.py --http
```

### Option B: Docker (one command)

```bash
docker build -t infoblox-ddi-mcp .
docker run -p 4005:4005 -e INFOBLOX_API_KEY=your_key infoblox-ddi-mcp
```

Or with docker compose (reads `.env` automatically):

```bash
cp .env.example .env   # add your INFOBLOX_API_KEY
docker compose up -d
```

### Option C: pip install

```bash
cd infoblox-ddi-mcp
pip install .

# Now available as a CLI command:
infoblox-ddi-mcp --http
```

## Transport Modes

| Mode | Command | Use Case |
|------|---------|----------|
| **stdio** (default) | `python mcp_intent.py` | Claude Desktop, Cursor, Windsurf, Claude Code |
| **HTTP streamable** | `python mcp_intent.py --http` | HCL AEX, LangChain, OpenAI SDK, remote clients |
| **Docker** | `docker run -p 4005:4005 ...` | Production, Kubernetes, HCL evaluation |

Stdio transport communicates via stdin/stdout JSON-RPC. HTTP transport runs a spec-compliant MCP server on port 4005 (configurable via `MCP_PORT`).

### Configuration

| Environment Variable | Default | Description |
|---------------------|---------|-------------|
| `INFOBLOX_API_KEY` | (required) | Infoblox CSP API key |
| `INFOBLOX_BASE_URL` | `https://csp.infoblox.com` | CSP portal URL |
| `MCP_HOST` | `0.0.0.0` | HTTP bind address |
| `MCP_PORT` | `4005` | HTTP port |
| `MCP_PATH` | `/mcp` | HTTP endpoint path |
| `MCP_AUTH_TOKEN` | (optional) | Bearer token for HTTP transport authentication |
| `OTEL_EXPORTER_OTLP_ENDPOINT` | (optional) | OTLP endpoint to enable tracing (requires `[otel]` extra) |

When `MCP_AUTH_TOKEN` is set, all HTTP requests must include `Authorization: Bearer <token>`. Stdio transport is unaffected (authentication is handled by the host process).

---

## Connect to AI Frameworks

### Claude Desktop

Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):

```json
{
  "mcpServers": {
    "infoblox-ddi": {
      "command": "python",
      "args": ["/absolute/path/to/infoblox-ddi-mcp/mcp_intent.py"],
      "env": {
        "INFOBLOX_API_KEY": "your_api_key_here",
        "INFOBLOX_BASE_URL": "https://csp.infoblox.com"
      }
    }
  }
}
```

Restart Claude Desktop — the 26 tools appear in the tool picker.

### Claude Code (CLI)

```bash
# Add the MCP server (stdio — Claude Code launches the process)
claude mcp add infoblox-ddi -e INFOBLOX_API_KEY=your_api_key_here -- python /absolute/path/to/infoblox-ddi-mcp/mcp_intent.py

# Or connect to a running HTTP server
claude mcp add --transport http infoblox-ddi http://localhost:4005/mcp
```

### Anthropic Python SDK

```python
import anthropic

client = anthropic.Anthropic()

response = client.beta.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    mcp_servers=[
        {
            "type": "url",
            "url": "https://your-gateway.example.com/mcp",  # must be HTTPS
            "name": "infoblox-ddi",
            "authorization_token": "your_mcp_auth_token",   # optional, if MCP_AUTH_TOKEN is set
        }
    ],
    tools=[
        {
            "type": "mcp_toolset",
            "mcp_server_name": "infoblox-ddi",
        }
    ],
    messages=[{"role": "user", "content": "Show me all IP spaces and their utilization"}],
    betas=["mcp-client-2025-11-20"],
)
```

> **Note:** The Anthropic MCP connector requires the server to be reachable via HTTPS. For local testing, use Claude Desktop (stdio) instead.

### LangChain / LangGraph

```python
from langchain_mcp_adapters.client import MultiServerMCPClient

client = MultiServerMCPClient(
    {
        "infoblox-ddi-stdio": {
            "command": "python",
            "args": ["/path/to/infoblox-ddi-mcp/mcp_intent.py"],
            "transport": "stdio",
        },
        # Or use HTTP (streamable_http is recommended over sse):
        # "infoblox-ddi-http": {
        #     "url": "http://127.0.0.1:4005/mcp",
        #     "transport": "streamable_http",
        # },
    }
)

tools = await client.get_tools()
# Use with any LangChain agent or LangGraph workflow
```

### OpenAI Agents SDK

```python
from agents import Agent, Runner
from agents.mcp import MCPServerStdio, MCPServerStreamableHttp

# Option A: stdio transport
async with MCPServerStdio(
    name="infoblox-ddi",
    params={
        "command": "python",
        "args": ["/path/to/infoblox-ddi-mcp/mcp_intent.py"],
    },
) as server:
    agent = Agent(name="ddi-agent", mcp_servers=[server])
    result = await Runner.run(agent, "Show me all IP spaces")
    print(result.final_output)

# Option B: HTTP streamable transport (start server first with --http)
async with MCPServerStreamableHttp(
    name="infoblox-ddi",
    params={"url": "http://127.0.0.1:4005/mcp"},
) as server:
    agent = Agent(name="ddi-agent", mcp_servers=[server])
    result = await Runner.run(agent, "List all DNS zones")
    print(result.final_output)
```

### Cursor IDE

Add to `.cursor/mcp.json` in your project root:

```json
{
  "mcpServers": {
    "infoblox-ddi": {
      "command": "python",
      "args": ["/absolute/path/to/infoblox-ddi-mcp/mcp_intent.py"],
      "env": {
        "INFOBLOX_API_KEY": "your_api_key_here"
      }
    }
  }
}
```

### Windsurf IDE

Add to `~/.codeium/windsurf/mcp_config.json`:

```json
{
  "mcpServers": {
    "infoblox-ddi": {
      "command": "python",
      "args": ["/absolute/path/to/infoblox-ddi-mcp/mcp_intent.py"],
      "env": {
        "INFOBLOX_API_KEY": "your_api_key_here"
      }
    }
  }
}
```

### HCL BigFix AEX

AEX has native MCP client support. In **Admin Console → Agent Studio**:

1. Add an MCP Server tool source
2. Set the endpoint to `http://<host>:4005/mcp`
3. Start the server with `python mcp_intent.py --http`
4. The 26 tools are auto-discovered and available to AEX agents

### Any HTTP Client

```bash
# Step 1: Initialize session (capture the Mcp-Session-Id header from the response)
curl -v -X POST http://127.0.0.1:4005/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "initialize",
    "params": {
      "protocolVersion": "2024-11-05",
      "capabilities": {},
      "clientInfo": {"name": "curl", "version": "1.0"}
    }
  }'
# Look for the response header: Mcp-Session-Id: <session-id>

# Step 2: List available tools (pass the session ID from step 1)
curl -X POST http://127.0.0.1:4005/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "Mcp-Session-Id: <session-id>" \
  -d '{"jsonrpc": "2.0", "id": 2, "method": "tools/list"}'

# Step 3: Call a tool
curl -X POST http://127.0.0.1:4005/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "Mcp-Session-Id: <session-id>" \
  -d '{
    "jsonrpc": "2.0",
    "id": 3,
    "method": "tools/call",
    "params": {
      "name": "explore_network",
      "arguments": {"depth": "summary"}
    }
  }'
```

### Remote Access (HTTP Transport)

Any MCP-compatible client can connect remotely over HTTP. Start the server with `--http` and point clients to the endpoint:

```
http://<host>:4005/mcp
```

**Local network:**
```bash
# Start the server
python mcp_intent.py --http

# Any client on the network connects to:
# http://192.168.1.100:4005/mcp
```

**Docker (remote host):**
```bash
docker run -p 4005:4005 -e INFOBLOX_API_KEY=your_key infoblox-ddi-mcp

# Clients connect to:
# http://your-docker-host:4005/mcp
```

**With authentication:**
```bash
# Start with auth token
MCP_AUTH_TOKEN=my-secret-token python mcp_intent.py --http

# Clients must include the header:
# Authorization: Bearer my-secret-token
```

**Production (TLS):** For internet-facing deployments, place the server behind a reverse proxy (nginx, API gateway) that handles TLS. See [Production Deployment](#production-deployment) below.

> **Summary:** `stdio` = client launches the server locally. `HTTP` = server runs independently, clients connect to `http://host:4005/mcp`. Use `MCP_AUTH_TOKEN` to secure HTTP access.

---

## Available Tools

### Discovery & Exploration (Read-only)

| Tool | Description |
|------|-------------|
| `explore_network` | Browse the IP hierarchy tree (Spaces → Blocks → Subnets) with utilization. Use for navigating network structure |
| `search_infrastructure` | Find resources by keyword across all DDI domains (IP, hostname, domain, comment) |
| `get_network_summary` | Executive dashboard with counts and health across all DDI infrastructure |

### Provisioning (Write)

| Tool | Description |
|------|-------------|
| `provision_host` | Create host + IP + DNS in one call. Supports auto-IP from subnet and auto-DNS (atomic A/PTR via API) or manual DNS creation |
| `provision_dns` | Create a new DNS record with automatic zone discovery and validation |
| `decommission_host` | Reverse provisioning with dry-run safety — detects auto-generated DNS (system records) vs manual DNS and handles each correctly |

### Troubleshooting (Read-only)

| Tool | Description |
|------|-------------|
| `diagnose_dns` | Diagnose DNS resolution problems: zone, records, security policies, and optional cache flush |
| `diagnose_ip_conflict` | Detect overlapping subnets, duplicate reservations, DHCP usage, and host associations |
| `check_api_health` | Verify Infoblox API connectivity for all three service clients (DDI, Insights, ATCFW) with response latency |
| `check_infrastructure_health` | HA groups, DHCP hosts, DNS zones, DNS views, IP spaces, on-prem appliance and service health |

### Security (Read + Write)

| Tool | Description |
|------|-------------|
| `investigate_threat` | SOC insights with threat indicators, affected assets, and timeline events |
| `assess_security_posture` | Security policies, category filters, compliance, and analytics scorecard |
| `manage_security_policy` | CRUD for named lists (with partial add/remove items), app filters, internal domains, access codes |
| `triage_security_insight` | Update status, bulk triage by priority, get comment history |

### IPAM Management (CRUD)

| Tool | Description |
|------|-------------|
| `manage_network` | Create, update, delete, get, or list IP spaces, address blocks, subnets, and ranges |
| `manage_ip_reservation` | Reserve/release fixed IPs and DHCP static leases |

### DNS Configuration (CRUD)

| Tool | Description |
|------|-------------|
| `manage_dns_zone` | Create, delete, list, or get authoritative and forward zones |
| `manage_dns_record` | Update, delete, list, or get DNS records (smart lookup by name+zone+type) |

### DHCP Configuration (CRUD)

| Tool | Description |
|------|-------------|
| `manage_dhcp` | CRUD for HA groups, option codes, hardware/option filters, hardware entries |
| `manage_dhcp_lease` | List/search active leases, clear (wipe) leases, or resend DDNS updates |

### DNS Traffic Control (CRUD)

| Tool | Description |
|------|-------------|
| `manage_dtc` | Manage DTC/GSLB: LBDNs, pools, servers, and policies for global server load balancing and traffic steering |

### Federation (CRUD)

| Tool | Description |
|------|-------------|
| `manage_federation` | Manage realms, blocks, delegations, pools, overlapping/reserved blocks |

### Reporting (Read-only)

| Tool | Description |
|------|-------------|
| `get_ip_utilization` | Capacity planning — utilization by space, block, and subnet |

---

## Response Format

Every tool returns a standard envelope:

```json
{
  "status": "success | partial | failed",
  "summary": "Human-readable one-liner",
  "steps": [
    {"step": "Resolve IP space", "status": "success", "result": {"space_id": "ipam/ip_space/abc"}},
    {"step": "Create subnet", "status": "success", "result": {"id": "ipam/subnet/xyz"}}
  ],
  "result": { "..." : "..." },
  "warnings": ["Optional warnings"],
  "next_actions": ["Suggested follow-up tool calls"]
}
```

This makes it easy for any LLM to:
- **Check `status`** to know if the operation succeeded
- **Read `summary`** for a one-line answer to show the user
- **Inspect `steps`** to understand the multi-step workflow
- **Follow `next_actions`** for intelligent follow-up suggestions

---

## Example Conversations

**"Show me what's in our network"**
```
→ explore_network(depth="full")
→ Returns hierarchical tree: IP spaces → address blocks → subnets with utilization %
```

**"Create a /24 subnet in the prod space for web servers"**
```
→ manage_network(resource_type="subnet", action="create", address="10.20.3.0/24", space="prod", comment="Web servers")
→ Resolves space name → ID, validates CIDR, creates subnet
```

**"Set up a new host called web-prod-01 in the prod space"**
```
→ provision_host(hostname="web-prod-01", space="prod", subnet="10.20.3.0/24", zone="example.com", view="default")
→ Auto-assigns next available IP (10.20.3.50), creates IPAM host + DNS A/PTR atomically
```

**"Provision web-prod-02 but I want to manage DNS records separately"**
```
→ provision_host(hostname="web-prod-02", ip="10.20.3.51", space="prod", zone="example.com", auto_dns=False)
→ Creates IPAM host, then A and PTR records as separate API calls
```

**"DNS isn't working for api.example.com"**
```
→ diagnose_dns(domain="api.example.com")
→ Returns zone status, records found, security blocks, and fix recommendations
```

**"Reserve 10.20.3.100 for the new database server"**
```
→ manage_ip_reservation(action="reserve", address="10.20.3.100", space="prod", hostname="db-01", mac="AA:BB:CC:DD:EE:FF")
→ Checks availability, validates MAC, creates fixed address reservation
```

**"Close all low-priority security insights"**
```
→ triage_security_insight(action="bulk_triage", priority_filter="low", status="CLOSED", dry_run=True)
→ DRY RUN: Shows 15 insights that would be closed
→ triage_security_insight(action="bulk_triage", priority_filter="low", status="CLOSED", dry_run=False)
→ Bulk closes 15 insights
```

**"What would happen if I decommissioned web-prod-01?"**
```
→ decommission_host(identifier="web-prod-01", dry_run=True)
→ "Would delete: 1 host, 1 A record, 1 PTR, release IP 10.20.3.50"
```

---

## Docker Deployment

```bash
# Build
make docker-build        # or: docker build -t infoblox-ddi-mcp .

# Run standalone
make docker-run          # or: docker run --rm -p 4005:4005 -e INFOBLOX_API_KEY=... infoblox-ddi-mcp

# Run with compose (reads .env)
make docker-up           # or: docker compose up -d
make docker-down         # or: docker compose down
```

The Docker image:
- Uses **multi-stage build** (small final image)
- Runs as **non-root** user
- Has a **health check** built in
- Binds to `0.0.0.0:4005` by default
- Accepts all config via environment variables

## OpenTelemetry (Optional)

Distributed tracing is available as an optional extra:

```bash
pip install infoblox-ddi-mcp[otel]
```

Enable by setting `OTEL_EXPORTER_OTLP_ENDPOINT`:

```bash
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
python mcp_intent.py --http
```

All MCP tool calls are auto-traced with service name `infoblox-ddi-mcp`. Works with Jaeger, Grafana Tempo, Datadog, or any OTLP-compatible backend. If the packages aren't installed, the server runs normally without tracing.

---

## Production Deployment

### Behind an API Gateway (Recommended)

For production environments, run the MCP server behind an API gateway for TLS termination, rate limiting, and centralized authentication.

![Production Deployment](docs/deployment.png)

The MCP server runs plain HTTP internally. The gateway handles TLS and external auth. Set `MCP_AUTH_TOKEN` as a shared secret between the gateway and the server for an additional layer of security.

#### Kubernetes / Docker Compose

```yaml
# docker-compose.prod.yml
services:
  infoblox-mcp:
    image: infoblox-ddi-mcp:latest
    restart: always
    environment:
      - INFOBLOX_API_KEY=${INFOBLOX_API_KEY}
      - INFOBLOX_BASE_URL=${INFOBLOX_BASE_URL:-https://csp.infoblox.com}
      - MCP_HOST=0.0.0.0
      - MCP_PORT=4005
      - MCP_AUTH_TOKEN=${MCP_AUTH_TOKEN}
    ports:
      - "127.0.0.1:4005:4005"   # bind to localhost only — gateway handles external traffic
    healthcheck:
      test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:4005/mcp')"]
      interval: 30s
      timeout: 5s
      retries: 3
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: "0.5"
```

#### Nginx Reverse Proxy Example

```nginx
upstream mcp_backend {
    server 127.0.0.1:4005;
}

server {
    listen 443 ssl;
    server_name mcp.example.com;

    ssl_certificate     /etc/ssl/certs/mcp.crt;
    ssl_certificate_key /etc/ssl/private/mcp.key;

    location /mcp {
        proxy_pass http://mcp_backend/mcp;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Authorization "Bearer ${MCP_AUTH_TOKEN}";

        # Rate limiting
        limit_req zone=mcp burst=20 nodelay;
    }
}
```

#### AWS API Gateway

1. Create an HTTP API in API Gateway
2. Add a route: `POST /mcp` → integration to your ECS/EKS service on port 4005
3. Attach a Lambda authorizer or Cognito user pool for auth
4. Enable CloudWatch logging for audit trail

#### Kubernetes Deployment

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: infoblox-mcp
spec:
  replicas: 2
  selector:
    matchLabels:
      app: infoblox-mcp
  template:
    metadata:
      labels:
        app: infoblox-mcp
    spec:
      containers:
        - name: mcp
          image: infoblox-ddi-mcp:latest
          ports:
            - containerPort: 4005
          env:
            - name: INFOBLOX_API_KEY
              valueFrom:
                secretKeyRef:
                  name: infoblox-secrets
                  key: api-key
            - name: MCP_AUTH_TOKEN
              valueFrom:
                secretKeyRef:
                  name: infoblox-secrets
                  key: mcp-token
          livenessProbe:
            httpGet:
              path: /mcp
              port: 4005
            initialDelaySeconds: 10
            periodSeconds: 30
          resources:
            limits:
              memory: "512Mi"
              cpu: "500m"
---
apiVersion: v1
kind: Service
metadata:
  name: infoblox-mcp
spec:
  selector:
    app: infoblox-mcp
  ports:
    - port: 4005
      targetPort: 4005
```

### Deployment Checklist

| Step | Action |
|------|--------|
| 1 | Set `INFOBLOX_API_KEY` via secrets manager (never in plain text) |
| 2 | Set `MCP_AUTH_TOKEN` for server-to-gateway authentication |
| 3 | Bind to `127.0.0.1` or internal network only (gateway handles external) |
| 4 | Enable TLS on the gateway (never expose plain HTTP externally) |
| 5 | Configure rate limiting (recommended: 60 req/min per client) |
| 6 | Enable access logging on the gateway for audit |
| 7 | Set resource limits (512MB RAM, 0.5 CPU is sufficient) |
| 8 | Monitor health check endpoint |

---

## Makefile Targets

```
make install        Install dependencies with uv
make dev            Install in editable mode
make run            Run MCP server (stdio)
make run-http       Run MCP server (HTTP)
make lint           Run ruff linter
make format         Run ruff formatter
make test           Run test suite (163 tests)
make docker-build   Build Docker image
make docker-run     Run Docker container
make docker-up      Start with docker compose
make docker-down    Stop docker compose
make check          Verify syntax
make clean          Remove build artifacts
```

---

## Architecture

![Architecture](docs/architecture.png)

---

## Project Structure

```
infoblox-ddi-mcp/
├── mcp_intent.py              ← MCP server entry point (run this)
├── services/
│   ├── infoblox_client.py     ← Infoblox DDI API client (90 methods)
│   ├── insights_client.py     ← SOC Insights API client (13 methods)
│   ├── atcfw_client.py        ← DNS Security API client (12 methods)
│   └── metrics.py             ← Internal metrics collection
├── tests/                     ← 163 tests (validators, resolvers, tools, resources)
│   ├── conftest.py
│   ├── test_validation.py
│   ├── test_resolvers.py
│   ├── test_tools.py
│   └── test_resources.py
├── examples/                  ← Integration examples
│   ├── anthropic_sdk.py
│   ├── openai_agents.py
│   ├── langchain_example.py
│   └── curl_test.sh
├── .github/workflows/
│   ├── ci.yml                 ← Lint + test (3.10-3.13) + Docker
│   └── publish.yml            ← PyPI publishing on v* tags
├── pyproject.toml             ← Package metadata (uv/pip install)
├── requirements.txt           ← Pinned dependencies
├── Dockerfile                 ← Production container image
├── docker-compose.yml         ← One-command deployment
├── Makefile                   ← Developer shortcuts
├── .pre-commit-config.yaml    ← Ruff + pre-commit hooks
├── CHANGELOG.md
├── SECURITY.md
├── .env.example
└── README.md
```

---

## Troubleshooting

**"Unexpected non-whitespace character after JSON"**
→ Something is writing to stdout. This server routes all logging to stderr. If you added custom print statements, use `print(..., file=sys.stderr)`.

**"Infoblox client not initialized"**
→ `INFOBLOX_API_KEY` is missing or invalid. Check your `.env` file or environment variables.

**"IP space 'prod' not found"**
→ The space name doesn't match exactly. Use `explore_network()` to see available space names.

**"DNS zone 'example.com' not found"**
→ The zone doesn't exist in Infoblox. Use `manage_dns_zone(action="list")` to see available zones, or `manage_dns_zone(action="create", fqdn="example.com")` to create one.

**Tools not appearing in Claude Desktop**
→ Restart Claude Desktop after editing `claude_desktop_config.json`. Check the path to `mcp_intent.py` is absolute.

**HTTP server not responding**
→ Start with `python mcp_intent.py --http`. Test with: `curl -X POST http://127.0.0.1:4005/mcp -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'`

**Dry run confusion**
→ All destructive operations (delete, release, bulk triage) default to `dry_run=True`. They show what *would* happen without making changes. Set `dry_run=False` to execute.

**Token overflow / response too large**
→ Use `limit` parameters to reduce result sizes. The intent layer already truncates large results, but specific queries return less data.
