SentinelOps
SentinelOps is an MCP server that enables AI-powered security operations with human-in-the-loop safety. It allows agents to investigate threats, propose actions, and execute security responses only after explicit human approval.
README
SentinelOps
AI-Powered Security Operations with Human-in-the-Loop Safety
An intelligent security operations platform demonstrating safe LLM agent design through multi-layered guardrails. Built with the Model Context Protocol (MCP), SentinelOps shows how to give AI agents access to destructive operations while maintaining rigorous human control.
What It Does
SentinelOps is a conversational security copilot that can investigate threats, recommend actions, and execute security responses—but only after explicit human approval for any destructive operation.
Core workflow:
- User describes a security concern in natural language
- Agent retrieves context (IP reputation, incident history, logs)
- Agent proposes actions (block IP, quarantine file, create alert)
- Human reviews preview and explicitly confirms or declines
- Confirmed actions execute with full audit trail
Example session:
You: check if 185.220.101.1 is malicious
Agent: [calls check_ip_reputation(185.220.101.1)]
AbuseIPDB reports this IP has a 100% abuse confidence score
with 172 reports. It's associated with malware distribution.
Would you like me to block it?
You: yes, block that IP
Agent: [calls propose_block_ip(185.220.101.1, reason="malware distribution")]
PREVIEW: Would block IP 185.220.101.1
Reason: Malware distribution, 100% abuse score
Token: a3f7c491
[Confirm] [Decline]
[You click Confirm]
Agent: [execute_pending_action(a3f7c491) called by Flask /confirm route]
SUCCESS: Blocked IP 185.220.101.1
Firewall rule created: data/firewall_sandbox/block_185_220_101_1.rule
Architecture
The Challenge: Safe Tool Access for LLMs
LLMs are powerful but unreliable. Giving an agent tools like "block this IP" or "quarantine this file" creates real risk:
- The model might hallucinate threats that don't exist
- Parameter injection could trick the model into dangerous actions
- A simple
confirmed=trueparameter can be bypassed by a clever adversarial prompt
SentinelOps addresses this through layered guardrails rather than trusting the model to make safe choices.
Layer 1: Evidence Chain Integrity
Problem: Model says "this IP is malicious" — how do you know it actually checked?
Solution: Chain-integrity tokens.
When the agent calls check_ip_reputation(ip), the server returns:
{
"ip": "185.220.101.1",
"abuse_score": 100,
"is_malicious": true,
"result_token": "f5bce9c4"
}
If the agent then tries to create a high-severity incident about that IP, it must provide the result_token:
create_incident(
summary="Malicious IP detected: 185.220.101.1",
severity="high",
evidence_token="f5bce9c4" # REQUIRED for high/critical severity
)
The server validates:
- Token exists in recent reputation checks
- Token's IP matches the incident claim
- Token's data actually shows
is_malicious: true
Result: The model cannot fabricate high-severity incidents. It must provide evidence it actually retrieved.
Layer 2: Human-in-the-Loop Execution Gate
Problem: Even with evidence, we don't want the LLM to execute destructive actions directly. A confirmed=true parameter is too easy for the model to add on its own.
Solution: Propose/execute split enforced by separate processes.
Propose phase (agent has access):
result = propose_block_ip(
ip="185.220.101.1",
reason="Malware distribution"
)
# Returns: "PREVIEW: Would block IP... Token: a3f7c491"
# NO actual execution happens here
Execute phase (agent does NOT have access):
# This tool exists, but agent is never given access to it
execute_pending_action(token="a3f7c491")
Instead, the Flask web UI provides /confirm/<token> and /decline/<token> routes that humans trigger by clicking buttons. The agent sees the proposal result but cannot proceed without the human's explicit POST request to Flask.
Why this works:
- The agent cannot call
execute_pending_actionbecause it's never in its tool list - Even if the agent tries to manipulate its own context, the execution happens in a separate process (Flask) that only responds to HTTP requests from the user's browser session
- Tokens are session-scoped—even if the agent somehow got a valid token, it couldn't forge the session cookie
Layer 3: Multi-Gate Validation
Even with human confirmation, Flask validates three gates before executing:
# Gate 1: Token exists
if token not in pending_actions:
return 404
# Gate 2: Token belongs to your session
if action['session_id'] != current_session:
return 403
# Gate 3: Token hasn't been used already
if action['status'] != 'pending':
return 400
Result: No replay attacks, no cross-session hijacking, no double-execution.
Architecture Diagram
┌─────────────────────────────────────────────────────┐
│ User (Browser) │
│ │ │
│ ├─ Sends query: "block 1.2.3.4" │
│ ├─ Views proposal preview │
│ └─ Clicks [Confirm] → POST /confirm/a3f7c491 │
└─────────────┬───────────────────────────────────────┘
│
┌─────▼──────┐
│ Flask │ Step 2: Propose phase
│ (web_ui) │ - Spawns MCP subprocess
│ │ - Agent gets tool list (NO execute_pending_action)
└─────┬──────┘ - Returns proposal with token
│
┌─────▼──────┐
│ Groq LLM │ Step 3: Agent reasoning
│ (LLaMA 3.3)│ - Calls check_ip_reputation (evidence)
│ │ - Calls propose_block_ip (preview)
└─────┬──────┘ - Returns "Token: a3f7c491"
│
┌─────▼──────┐
│ MCP Server │ Step 4: Tool execution
│ (server.py)│ - propose_block_ip: writes pending_actions.json
│ │ - execute_pending_action: NOT in agent's tool list
└─────┬──────┘
│
┌─────▼────────────────┐
│ File-Based Storage │ Step 5: Single source of truth
│ pending_actions.json │ - Both Flask and MCP read/write this file
└──────────────────────┘ - Prevents desynch bugs
│
┌─────▼──────┐
│ Flask │ Step 6: Execute phase (human-triggered)
│ /confirm │ - Validates 3 gates
│ │ - Spawns NEW MCP subprocess
└─────┬──────┘ - Calls execute_pending_action(token)
│
┌─────▼──────┐
│ MCP Server │ Step 7: Actual execution
│ (server.py)│ - Creates firewall rule in sandbox
│ │ - Marks action as 'executed'
└────────────┘ - Returns success
Key insight: The agent process that proposes actions and the Flask process that executes them are separate. The agent cannot execute. Flask will not execute without human confirmation. This separation is enforced at the process level, not by trusting the model.
Setup
Prerequisites
- Python 3.11+
- Docker (optional, for containerized deployment)
- API keys (all free tiers available):
Quick Start (Docker)
- Clone and configure:
git clone https://github.com/yourusername/sentinelops.git
cd sentinelops
cp .env.example .env
# Edit .env with your API keys
- Start:
docker compose up -d
- Access:
Open http://localhost:5000
Local Development
- Install dependencies:
python -m venv venv
source venv/bin/activate # Linux/Mac
venv\Scripts\activate # Windows
pip install -r requirements.txt
- Configure environment:
cp .env.example .env
# Edit .env with your API keys
- Initialize sandbox (first-time setup):
python setup_sandbox.py
python ingest_sample_logs.py # Optional: seed ChromaDB
- Run:
python web_ui.py # Web interface at http://localhost:5000
# OR
python cli.py # Terminal interface
Configuration
Required in .env:
GROQ_API_KEY=gsk_... # Get from console.groq.com
ABUSEIPDB_API_KEY=... # Get from abuseipdb.com/api
TELEGRAM_BOT_TOKEN=... # From @BotFather (optional)
TELEGRAM_CHAT_ID=... # Your chat ID (optional)
# Production settings
USE_REAL_APIS=true # false = stub mode for testing
ENABLE_DEBUG_ENDPOINTS=false # NEVER true in production
Design Decisions
Why Not Just a confirmed Parameter?
Rejected approach:
@mcp.tool()
def block_ip(ip: str, confirmed: bool = False) -> str:
if not confirmed:
return "Please confirm this action"
# Execute block
Problem: The model can trivially set confirmed=True itself:
Agent: [calls block_ip("1.2.3.4", confirmed=True)]
Even if you try to hide it:
def block_ip(ip: str) -> str:
"""Block an IP. Returns a confirmation token."""
# Show preview, return token
The model learns from examples and documentation that confirmed=True is the pattern, and will attempt it. Adversarial prompts can manipulate this further.
The fix: Execution is not a tool parameter. It's a separate HTTP endpoint in a different process that the agent cannot access.
Why Evidence Tokens?
Problem observed: In early versions, the agent would write incident reports claiming "this IP is malicious" without actually calling check_ip_reputation first. The model "knew" certain IP ranges were bad and fabricated justifications.
The fix: High-severity incidents require an evidence_token from a prior API call. The server validates the chain:
- Was
check_ip_reputationcalled? - Did it return a token?
- Does that token's data support the severity claim?
Now the agent must actually retrieve evidence, not fabricate it.
Why File-Based Storage?
Problem: Flask (web UI) and MCP (agent tools) run in separate processes. Early versions used in-memory dicts, causing desynch:
- Agent proposes action → writes to MCP's in-memory dict
- User clicks Confirm → Flask reads from its own empty in-memory dict → 404
The fix: pending_actions.json is the single source of truth. Both processes read and write to the same file. No desynch possible.
Why Sandbox-Only Execution?
All destructive operations target sandboxed directories:
- Firewall rules →
data/firewall_sandbox/(not actual iptables) - Quarantined files →
data/sentinelops_sandbox/quarantine/(path-validated)
Rationale: This is a demonstration/development platform. In production, you'd replace the sandbox with real integrations (actual firewall API, real quarantine system), but the confirmation flow remains the same.
Testing
Unit Tests (Fast)
python -m pytest tests/test_unit.py -v
# 7 tests, <5 seconds
Integration Tests (Requires APIs)
python -m pytest tests/test_integration.py -v -m integration
# Full MCP stack, ~30 seconds
Security Gate Tests
# Flask must be running on localhost:5000
python tests/test_security_gates.py
Validates:
- ✅ Valid token + correct session → executes
- ✅ Fake token → 404
- ✅ Reused token → 400
- ✅ Cross-session token → 403
- ✅ Declined action → no execution
Docker Validation
# Before building
python validate_docker.py
# After docker compose up
python tests/test_docker_deployment.py
Tech Stack
- LLM: Groq (LLaMA 3.3 70B) - Fast inference, function calling
- MCP: Model Context Protocol - Standardized tool interface
- Web: Flask - Lightweight web framework
- Vector DB: ChromaDB - Semantic log search (future feature)
- APIs: AbuseIPDB (IP reputation), Telegram (alerts)
- Deployment: Docker + docker-compose
Project Structure
sentinelops/
├── server.py # MCP server - defines tools
├── agent_shared.py # Agent loop logic (reused by CLI and web)
├── cli.py # Terminal interface
├── web_ui.py # Flask web interface + confirmation routes
├── templates/
│ └── index.html # Web UI
├── tests/
│ ├── test_unit.py # Fast unit tests
│ ├── test_integration.py # Full stack tests
│ ├── test_security_gates.py # Security validation
│ └── ... # Additional test files
├── data/ # Runtime (gitignored)
│ ├── pending_actions.json
│ ├── firewall_sandbox/
│ └── chromadb_sentinelops/
├── Dockerfile
├── docker-compose.yml
├── requirements.txt
└── README.md
Production Considerations
This is a demonstration platform. For production:
Security:
- [ ] Add HTTPS (nginx reverse proxy)
- [ ] Use secrets management (Docker secrets, Vault)
- [ ] Enable rate limiting
- [ ] Add authentication (OAuth, JWT)
- [ ] Audit logging to immutable storage
Reliability:
- [ ] Replace sandbox with real integrations
- [ ] Add monitoring (Prometheus + Grafana)
- [ ] Set up log aggregation
- [ ] Configure resource limits
- [ ] Implement health checks
Compliance:
- [ ] SOC 2 controls for change management
- [ ] Audit trail for all executed actions
- [ ] Role-based access control
See ARCHITECTURE.md for detailed design rationale and Makefile for common operations.
Troubleshooting
Port 5000 in use:
# Use different port in docker-compose.yml
ports:
- "8080:5000"
Groq rate limit (free tier: 100k tokens/day):
- Wait for reset (error shows time)
- Upgrade tier at console.groq.com
- Use
USE_REAL_APIS=falsefor testing without API calls
ChromaDB slow first request (~30s):
- This is normal (model loading)
- Subsequent requests are fast
- Happens once per container start
Docker build fails:
docker system prune -a # Clear cache
docker compose build --no-cache
License
MIT License - see LICENSE file
Acknowledgments
Built to demonstrate safe agent design patterns:
- Evidence chains - Prevent AI hallucination in critical decisions
- Propose/execute split - Separate planning from execution
- Multi-gate validation - Defense in depth
- Process isolation - Agent cannot bypass human confirmation
Inspired by real security operations workflows where humans remain accountable for destructive actions, even when AI suggests them.
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。