Skip to main content
Glama
agileandy
by agileandy

MCP Secure Local Server

A production-ready, security-first Model Context Protocol (MCP) server that runs locally with strict security controls while allowing controlled external network access for specific use cases like web search.

Features

  • Security-First Design: All operations are validated against a configurable security policy

  • Network Firewall: Block all external network access except explicitly allowlisted endpoints

  • Input Validation: JSON Schema validation, path traversal protection, command sanitization

  • Rate Limiting: Per-tool rate limits to prevent abuse

  • Audit Logging: JSON Lines format logging with sensitive data redaction

  • Plugin System: Extensible architecture for adding new tools

  • MCP Protocol Compliant: Full JSON-RPC 2.0 over STDIO transport

Quick Start

Installation

# Clone the repository git clone <repository-url> cd mcp-server # Install dependencies with uv uv sync

Running the Server

# Run with default policy uv run python main.py # Run with custom policy file uv run python main.py --policy /path/to/policy.yaml # Show version uv run python main.py --version

Integration with MCP Clients

This server works with any MCP-compatible client. Add the following to your client's MCP configuration:

{ "mcpServers": { "secure-local": { "command": "uv", "args": ["run", "python", "/path/to/mcp-server/main.py"], "env": {} } } }

Example client configuration locations:

  • Claude Desktop: ~/Library/Application Support/Claude/claude_desktop_config.json (macOS)

  • Other MCP clients: Refer to your client's documentation for the configuration file location

Architecture

mcp-server/ ├── main.py # CLI entry point ├── config/ │ └── policy.yaml # Security policy configuration ├── src/ │ ├── server.py # Main MCP server │ ├── protocol/ │ │ ├── jsonrpc.py # JSON-RPC 2.0 parsing │ │ ├── transport.py # STDIO transport │ │ ├── lifecycle.py # MCP lifecycle management │ │ └── tools.py # tools/list & tools/call handlers │ ├── plugins/ │ │ ├── base.py # Plugin base class │ │ ├── loader.py # Plugin discovery │ │ ├── dispatcher.py # Tool call routing │ │ ├── discovery.py # Built-in: Progressive disclosure tools │ │ ├── websearch.py # Example: DuckDuckGo search plugin │ │ └── bugtracker.py # Example: Bug tracking plugin │ └── security/ │ ├── policy.py # Policy loader │ ├── firewall.py # Network access control │ ├── validator.py # Input validation │ ├── engine.py # Integrated security engine │ └── audit.py # Audit logging └── tests/ # Test suite (343 tests, 96%+ coverage)

Security Policy

The security policy is defined in YAML format. See config/policy.yaml for a complete example.

Network Security

network: # Allowed local network ranges allowed_ranges: - "127.0.0.0/8" - "10.0.0.0/8" - "192.168.0.0/16" # Explicitly allowed external endpoints allowed_endpoints: - host: "lite.duckduckgo.com" ports: [443] description: "DuckDuckGo search" # Blocked ports (even on local network) blocked_ports: - 22 # SSH # DNS settings allow_dns: true dns_allowlist: - "lite.duckduckgo.com"

Filesystem Security

filesystem: # Allowed paths (supports globs and env vars) allowed_paths: - "${HOME}/projects/**" - "/tmp/mcp-workspace/**" # Denied paths (takes precedence) denied_paths: - "**/.ssh/**" - "**/.aws/**" - "**/*.pem" - "**/.env"

Tool Configuration

tools: # Rate limits (requests per minute) rate_limits: default: 60 web_search: 20 # Execution timeout timeout: 30

Audit Logging

audit: log_file: "${HOME}/.mcp-secure/audit.log" log_level: "INFO"

Built-in Tools

The server automatically registers discovery tools for progressive disclosure, enabling agents to efficiently find and load only the tools they need.

search_tools

Search for available tools by keyword or category. Use detail_level to control context usage.

Input Schema:

{ "type": "object", "properties": { "query": { "type": "string", "description": "Keyword to search in tool names and descriptions" }, "category": { "type": "string", "description": "Filter by plugin category (e.g., 'bugtracker')" }, "detail_level": { "type": "string", "enum": ["name", "summary", "full"], "description": "Level of detail: 'name' (just names), 'summary' (names + descriptions), 'full' (complete schemas)" } } }

Example - Find bug-related tools with minimal context:

{ "name": "search_tools", "arguments": { "query": "bug", "detail_level": "name" } } // Returns: ["add_bug", "get_bug", "update_bug", "close_bug", "list_bugs", "search_bugs_global"]

Example - Get full schema for a specific category:

{ "name": "search_tools", "arguments": { "category": "websearch", "detail_level": "full" } }

list_categories

List all available tool categories (plugins) with tool counts. Use this to discover capabilities before searching.

Input Schema:

{ "type": "object", "properties": {} }

Example Response:

[ { "category": "discovery", "version": "1.0.0", "tool_count": 2, "tools": ["search_tools", "list_categories"] }, { "category": "websearch", "version": "1.0.0", "tool_count": 1, "tools": ["web_search"] }, { "category": "bugtracker", "version": "1.0.0", "tool_count": 7, "tools": ["init_bugtracker", "add_bug", "get_bug", "update_bug", "close_bug", "list_bugs", "search_bugs_global"] } ]

Example Plugins

The server includes example plugins to demonstrate the plugin architecture. These are provided as reference implementations showing how to build your own plugins for any use case.

web_search (Example Plugin)

An example plugin that searches the web using DuckDuckGo. Demonstrates how to build plugins that make external network requests within the security policy.

Input Schema:

{ "type": "object", "properties": { "query": { "type": "string", "description": "The search query" }, "max_results": { "type": "integer", "description": "Maximum results to return (default: 5)" } }, "required": ["query"] }

Example:

{ "name": "web_search", "arguments": { "query": "Python asyncio tutorial", "max_results": 3 } }

Bug Tracker (Example Plugin)

An example plugin implementing a local bug tracking system with a centralized SQLite database. Demonstrates how to build plugins that manage local state, support multiple projects, and perform complex queries.

init_bugtracker

Initialize bug tracking for a project.

Input Schema:

{ "type": "object", "properties": { "project_path": { "type": "string", "description": "Path to project directory (defaults to cwd)" } } }

add_bug

Add a new bug to the tracker.

Input Schema:

{ "type": "object", "properties": { "title": { "type": "string", "description": "Brief title for the bug" }, "description": { "type": "string", "description": "Detailed description" }, "priority": { "type": "string", "enum": ["low", "medium", "high", "critical"], "description": "Bug priority (default: medium)" }, "tags": { "type": "array", "items": {"type": "string"}, "description": "Tags for categorization" }, "project_path": { "type": "string", "description": "Path to project directory (defaults to cwd)" } }, "required": ["title"] }

get_bug

Retrieve a bug by ID.

Input Schema:

{ "type": "object", "properties": { "bug_id": { "type": "string", "description": "The bug ID to retrieve" }, "project_path": { "type": "string", "description": "Path to project directory (defaults to cwd)" } }, "required": ["bug_id"] }

update_bug

Update an existing bug's status, priority, tags, or related bugs. Supports note-only updates for progress tracking.

Input Schema:

{ "type": "object", "properties": { "bug_id": { "type": "string", "description": "The bug ID to update" }, "status": { "type": "string", "enum": ["open", "in_progress", "closed"] }, "priority": { "type": "string", "enum": ["low", "medium", "high", "critical"] }, "tags": { "type": "array", "items": {"type": "string"}, "description": "New tags (replaces existing)" }, "related_bugs": { "type": "array", "description": "Related bugs with relationship type" }, "note": { "type": "string", "description": "Note for the history entry" }, "project_path": { "type": "string" } }, "required": ["bug_id"] }

close_bug

Close a bug with a resolution note.

Input Schema:

{ "type": "object", "properties": { "bug_id": { "type": "string", "description": "The bug ID to close" }, "resolution": { "type": "string", "description": "Resolution note explaining how the bug was fixed" }, "project_path": { "type": "string" } }, "required": ["bug_id"] }

list_bugs

List bugs with optional filtering.

Input Schema:

{ "type": "object", "properties": { "status": { "type": "string", "enum": ["open", "in_progress", "closed"] }, "priority": { "type": "string", "enum": ["low", "medium", "high", "critical"] }, "tags": { "type": "array", "items": {"type": "string"}, "description": "Filter by tags (must have ALL specified tags)" }, "project_path": { "type": "string" } } }

search_bugs_global

Search bugs across all indexed projects.

Input Schema:

{ "type": "object", "properties": { "status": { "type": "string", "enum": ["open", "in_progress", "closed"] }, "priority": { "type": "string", "enum": ["low", "medium", "high", "critical"] }, "tags": { "type": "array", "items": {"type": "string"} } } }

Example - Create and track a bug:

// Add a bug { "name": "add_bug", "arguments": { "title": "Login button not responding", "description": "The login button on the home page doesn't trigger the auth flow", "priority": "high", "tags": ["ui", "auth"] } } // Update with progress { "name": "update_bug", "arguments": { "bug_id": "BUG-001", "status": "in_progress", "note": "Identified missing onClick handler" } } // Close with resolution { "name": "close_bug", "arguments": { "bug_id": "BUG-001", "resolution": "Added onClick handler to LoginButton component" } }

Creating Custom Plugins

Python Plugins

Plugins must inherit from PluginBase and implement the required methods:

from src.plugins.base import PluginBase, ToolDefinition, ToolResult class MyPlugin(PluginBase): @property def name(self) -> str: return "my_plugin" @property def version(self) -> str: return "1.0.0" def get_tools(self) -> list[ToolDefinition]: return [ ToolDefinition( name="my_tool", description="Does something useful", input_schema={ "type": "object", "properties": { "input": {"type": "string"} }, "required": ["input"] }, ) ] def execute(self, tool_name: str, arguments: dict) -> ToolResult: if tool_name == "my_tool": result = do_something(arguments["input"]) return ToolResult( content=[{"type": "text", "text": result}] ) return ToolResult( content=[{"type": "text", "text": "Unknown tool"}], is_error=True )

Register the plugin in main.py:

from my_plugin import MyPlugin server.register_plugin(MyPlugin())

External Plugins (Non-Python)

The plugin system can support tools written in any language (Rust, JavaScript, TypeScript, Go, etc.) through a subprocess wrapper approach. This is a planned feature - contributions welcome.

Architecture Overview

External plugins run as separate processes, communicating with the Python wrapper via JSON over stdin/stdout:

┌─────────────────────────────────────────────────────────────┐ │ MCP Server (Python) │ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ WebSearch │ │ BugTracker │ │ External │ │ │ │ (Python) │ │ (Python) │ │ Plugin │ │ │ └──────────────┘ └──────────────┘ │ (Wrapper) │ │ │ └──────┬───────┘ │ │ │ │ └─────────────────────────────────────────────────┼────────────┘ │ JSON/stdin/stdout ▼ ┌──────────────┐ │ my-rust-tool │ │ (subprocess) │ └──────────────┘

How It Works

  1. Python Wrapper: A thin ExternalPlugin class inherits from PluginBase and handles the subprocess lifecycle

  2. Manifest: A manifest.yaml declares the tool definitions and points to the executable

  3. Contract: The external tool receives JSON on stdin and writes JSON to stdout

Manifest Format

name: my-rust-tools version: "1.0.0" type: external executable: ./target/release/my-rust-tool tools: - name: calculate_hash description: Calculate cryptographic hash of input input_schema: type: object properties: algorithm: type: string enum: [sha256, sha512, blake3] input: type: string required: [algorithm, input]

External Tool Contract

The external executable must:

  1. Accept a JSON object on stdin:

{ "tool": "calculate_hash", "arguments": { "algorithm": "sha256", "input": "hello world" } }
  1. Return a JSON object on stdout:

{ "content": [ {"type": "text", "text": "sha256: b94d27b9934d3e08..."} ], "isError": false }
  1. Exit with code 0 on success, non-zero on failure

Example: Rust Tool

use serde::{Deserialize, Serialize}; use std::io::{self, BufRead, Write}; #[derive(Deserialize)] struct Request { tool: String, arguments: serde_json::Value, } #[derive(Serialize)] struct Response { content: Vec<Content>, #[serde(rename = "isError")] is_error: bool, } #[derive(Serialize)] struct Content { #[serde(rename = "type")] content_type: String, text: String, } fn main() { let stdin = io::stdin(); let line = stdin.lock().lines().next().unwrap().unwrap(); let request: Request = serde_json::from_str(&line).unwrap(); let result = match request.tool.as_str() { "calculate_hash" => calculate_hash(request.arguments), _ => Err(format!("Unknown tool: {}", request.tool)), }; let response = match result { Ok(text) => Response { content: vec![Content { content_type: "text".into(), text }], is_error: false, }, Err(e) => Response { content: vec![Content { content_type: "text".into(), text: e }], is_error: true, }, }; println!("{}", serde_json::to_string(&response).unwrap()); }

Example: Node.js Tool

const readline = require('readline'); const rl = readline.createInterface({ input: process.stdin }); rl.on('line', (line) => { const request = JSON.parse(line); let response; try { const result = handleTool(request.tool, request.arguments); response = { content: [{ type: 'text', text: result }], isError: false }; } catch (e) { response = { content: [{ type: 'text', text: e.message }], isError: true }; } console.log(JSON.stringify(response)); process.exit(0); }); function handleTool(tool, args) { switch (tool) { case 'format_json': return JSON.stringify(JSON.parse(args.input), null, 2); default: throw new Error(`Unknown tool: ${tool}`); } }

Security Considerations for External Plugins

  1. Process Isolation: External tools run in separate processes with their own memory space

  2. Timeout Enforcement: The wrapper kills subprocesses that exceed the configured timeout

  3. No Network Inheritance: Subprocess network access is governed by OS-level controls

  4. Executable Allowlist: Only executables listed in registered manifests can be invoked

  5. Input Validation: JSON schemas are validated before passing to the subprocess

Trade-offs

Aspect

Python Plugin

External Plugin

Startup latency

None

~10-50ms per call

Memory

Shared with server

Separate process

Language

Python only

Any language

Debugging

Easy

Harder (separate process)

Security

Shared memory space

Process isolation

When to Use External Plugins

  • Performance-critical tools: Rust/Go for CPU-intensive operations

  • Existing CLI tools: Wrap existing binaries without rewriting

  • Language-specific libraries: Use npm packages, Cargo crates, etc.

  • Team expertise: Let teams use their preferred language

Development

Running Tests

# Run all tests uv run pytest # Run with coverage report uv run pytest --cov=src --cov-report=term-missing # Run specific test file uv run pytest tests/test_server.py -v

Linting

# Check for issues uv run ruff check . # Auto-fix issues uv run ruff check --fix . # Format code uv run ruff format .

Project Structure

Directory

Purpose

src/protocol/

MCP protocol implementation (JSON-RPC, STDIO, lifecycle)

src/plugins/

Plugin system and built-in plugins

src/security/

Security layer (firewall, validation, audit)

tests/

Test suite

config/

Configuration files

MCP Protocol Support

This server implements MCP protocol version 2025-11-25 with support for:

Method

Description

initialize

Initialize the connection

notifications/initialized

Confirm initialization complete

tools/list

List available tools

tools/call

Execute a tool

Security Considerations

  1. Network Isolation: By default, all external network access is blocked. Only explicitly allowlisted endpoints can be reached.

  2. Path Traversal Protection: All file paths are validated against allowed/denied patterns to prevent accessing sensitive files.

  3. Command Injection Prevention: Commands are sanitized to block dangerous patterns like shell operators.

  4. Rate Limiting: Per-tool rate limits prevent abuse and resource exhaustion.

  5. Audit Trail: All operations are logged with timestamps, request IDs, and sanitized arguments.

License

MIT License - see LICENSE file for details.

-
security - not tested
A
license - permissive license
-
quality - not tested

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/agileandy/mcp-secure-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server