A production-grade, policy-as-code middleware for FastMCP servers that uses macaroons for fine-grained, dynamic, and capability-based authorization.
Project description
MCP Macaroon Middleware
A production-grade, macaroon-based authorization middleware for Python applications, offering fine-grained, progressive, and policy-driven access control.
Overview
mcp_macaroon_middleware provides cryptographic, capability-based authorization using macaroons. Unlike traditional OAuth/JWT tokens, macaroons enable advanced security patterns like decentralized attenuation (adding restrictions without re-signing), offline verification, and progressive authorization, where permissions can be dynamically requested from users in real-time.
This middleware is designed to solve common and complex authorization challenges, moving beyond simple "all-or-nothing" access. It allows you to protect sensitive functions, redact specific data fields, and enforce complex, context-aware rules with minimal boilerplate code. By externalizing authorization logic into a simple YAML file and extensible Python functions, it helps you build more secure, maintainable, and user-friendly applications.
Key Benefits:
- Solve Over-Privileged Tokens: Replace static API keys with short-lived, narrowly-scoped credentials that have fine-grained permissions.
- Decentralized Authorization: Verify tokens anywhere, without a network call to an authorization server, reducing latency and single points of failure.
- Progressive Permissions: Instead of requesting all permissions upfront, prompt users for approval only when they try to access a sensitive feature or data field.
- Fine-Grained Control: Enforce rules before a function executes (e.g., rate limiting, access control) and after it returns (e.g., redacting sensitive fields from a response).
- Policy-as-Code: Define authorization rules in a simple, auditable YAML file, separating policy from application logic.
Core Concepts
mcp_macaroon_middleware is built on a few powerful concepts that enable flexible and secure authorization.
1. Policy-as-Code in YAML
Instead of hardcoding business rules in your application, you define them in a simple, human-readable YAML file. This separates authorization logic from your application code, making it easier to manage, audit, and update policies without redeploying your application.
policies:
# Deny access to a specific function before it's called
- "bf:admin_tool:tool_access:deny"
# Redact a sensitive field from an object after it's returned
- "af:get_user_profile:field_access:deny:user_pii.social_security_number"
# Ask the user for permission before accessing their attachments
- "af:read_emails:field_access:elicit:attachments"
2. Two-Phase Policy Enforcement
Policies are enforced in two distinct phases, giving you complete control over your functions' execution lifecycle:
bf(Before-Call): Executed before your function is called. Ideal for coarse-grained access control, rate limiting, or input validation. If adenypolicy is triggered here, the function is never executed.af(After-Call): Executed after your function returns a result. Perfect for fine-grained response filtering, redacting sensitive data fields, or triggering side-effects based on the output.
3. Progressive Authorization via Elicitation
For actions that are sensitive but not always disallowed, you can use the elicit action. This will automatically prompt the user for real-time approval when they attempt the action. If the user grants permission, the middleware adds a temporary, time-bound caveat to their macaroon, allowing them to perform the action for a limited period without being prompted again. This provides a seamless user experience while maintaining a "least privilege" security posture.
4. Extensible Custom Enforcers
While the middleware comes with built-in policies for common use cases (like tool access and field redaction), its real power lies in its extensibility. You can easily create your own complex, context-aware authorization rules using a simple Python decorator.
Custom enforcers can inspect anything in the request context (like user roles, IP addresses, or request parameters) to make dynamic authorization decisions.
# A custom enforcer that only allows access during business hours
@policy_enforcer("business_hours")
def enforce_business_hours(caveat: Caveat, context: Context, **kwargs) -> List[Caveat]:
hour = datetime.now().hour
if not (9 <= hour < 17):
raise PolicyViolationError("Access only allowed during business hours")
return [] # Success
You can then use this custom enforcer directly in your policies.yaml:
policies:
- "bf:sensitive_tool:business_hours:allow"
How It Solves Common Security Problems
| Security Problem | How mcp-macaroon-middleware Solves It |
|---|---|
| Over-Privileged API Keys | Replaces static, long-lived API keys with short-lived, narrowly-scoped macaroons. Permissions can be attenuated (restricted) for specific tasks without needing a new token. |
| Data Leakage / Accidental Exposure | The after-call enforcement phase allows you to dynamically redact sensitive fields from API responses based on the requesting user's permissions, ensuring that clients only see the data they are authorized to see. |
| Hardcoded Authorization Logic | Moves authorization logic out of your application code and into a separate, auditable policy layer. This makes your codebase cleaner and your security rules easier to manage and reason about. |
| Poor User Experience vs. Security | The progressive authorization (elicit) feature provides a powerful middle ground between denying access and always allowing it. It improves security by default while giving users a frictionless way to grant permissions when needed. |
| Centralized Bottlenecks | Macaroons can be verified cryptographically without calling back to a central authorization server. This is ideal for distributed systems and reduces latency and single points of failure. |
Installation
pip install mcp-macaroon-middleware
For development:
pip install -e '.[dev]'
Quick Start
This guide provides a conceptual overview. For a runnable example, see the examples/ directory.
1. Define Your Policies
Create a policies.yaml file to define your authorization rules.
config:
secret_key: "your-secret-key-here"
elicit_expiry: 3600 # Elicited permissions are valid for 1 hour
policies:
# Allow access to 'read_emails' but deny access to 'admin_tool'
- "bf:read_emails:tool_access:allow"
- "bf:admin_tool:tool_access:deny"
# After 'read_emails' is called, deny access to the 'body' field
- "af:read_emails:field_access:deny:body"
# For the 'read_emails' function, prompt the user for permission to access 'attachments'
- "af:read_emails:field_access:elicit:attachments"
2. Write a Custom Enforcer (Optional)
For more complex rules, create a custom enforcer function in your application and register it with the @policy_enforcer decorator.
from mcp_macaroon_middleware import policy_enforcer, PolicyViolationError
from datetime import datetime
@policy_enforcer("business_hours")
def enforce_business_hours(caveat, context, **kwargs):
"""Only allow access between 9 AM and 5 PM."""
hour = datetime.now().hour
if not (9 <= hour < 17):
raise PolicyViolationError("Access only allowed during business hours")
return []
Then, reference it in your policies.yaml:
- "bf:sensitive_tool:business_hours:allow"
3. Integrate the Middleware
In your application's request lifecycle, initialize MacaroonMiddleware and PolicyEngine. Then, invoke the policy engine at the appropriate stages.
from mcp_macaroon_middleware import MacaroonMiddleware, PolicyEngine
from mcp_macaroon_middleware.core.policy_engine import RequestContext
# 1. Initialize the middleware (typically once at startup)
# This will load your policies from the YAML file.
middleware = MacaroonMiddleware(config_path="./policies.yaml")
# In your request handler / middleware layer:
async def handle_request(request):
# 2. Get or create a macaroon for the user
macaroon = await middleware._get_or_create_macaroon(request.user_id)
# 3. Define the request context
# This context is available to all policy enforcers
context = RequestContext(
user_id=request.user_id,
# ... other relevant data like user roles, IP address, etc.
)
# 4. Enforce "before-call" policies
# This happens before your main business logic
modified_macaroon = await middleware.policy_engine.enforce_policies(
macaroon,
'bf', # ExecutionPhase.BEFORE
tool_name="my_protected_function",
context=context
)
# If no PolicyViolationError was raised, proceed.
# 5. Execute your main business logic
result = my_protected_function(request.params)
# 6. Enforce "after-call" policies
# This happens after your function returns, allowing you to filter the result
final_result, modified_macaroon = await middleware.policy_engine.enforce_policies(
macaroon,
'af', # ExecutionPhase.AFTER
tool_name="my_protected_function",
context=context,
result=result
)
# 7. The user receives the potentially redacted 'final_result'
return final_result
Note: The example above is a simplified, conceptual guide. The current MacaroonMiddleware class in mcp_macaroon_middleware provides a direct integration for the FastMCP framework.
Architecture
The middleware's architecture is composed of a few key components that work together to enforce authorization policies.
┌──────────────────┐
│ Initial Request │
│ (e.g., HTTP API) │
└────────┬─────────┘
│
▼
┌──────────────────┐ ┌─────────────────┐
│ Get or Create ├──────► In-Memory Cache │
│ User Macaroon │ │ (Per-User State)│
└────────┬─────────┘ └─────────────────┘
│
▼
┌──────────────────┐ ┌─────────────────┐
│ Policy Engine ├──────► Policy & Custom│
│ │ │ Enforcers │
└────────┬─────────┘ └─────────────────┘
│
┌────┴────┐
▼ ▼
┌───────┐ ┌───────┐
│ BEFORE│ │ AFTER │
│ Phase │ │ Phase │
└───────┘ └───────┘
│ │
▼ ▼
Function Result
Execution Redaction
Framework and Developer Notes
Framework Integration
The core logic of this middleware is framework-agnostic. The PolicyEngine is the central component that takes a macaroon, a set of policies, and a request context, and returns an authorization decision.
The MacaroonMiddleware class currently provides a convenient, out-of-the-box integration for the FastMCP framework by implementing its on_call_tool hook.
Adapting to Other Frameworks (e.g., FastAPI, Flask): To use this middleware with another web framework, you would create a new middleware class or decorator that:
- Initializes the
PolicyEnginewith your policies. - Extracts user identity from the incoming request.
- Manages the lifecycle of the user's macaroon (e.g., retrieving it from a cache or creating a new one).
- Calls
policy_engine.enforce_policiesbefore and after your endpoint/controller logic is executed. - Handles
PolicyViolationErrorexceptions by returning an appropriate HTTP error (e.g.,403 Forbidden). - Manages the user prompt-and-response flow required for the
elicitaction.
Production Considerations
- Caching: The default in-memory cache is for demonstration purposes and is tied to a single process. For production, multi-instance deployments, this should be replaced with a distributed cache like Redis to ensure that a user's macaroon state is consistent across all application servers.
- Secret Management: The
secret_keyused for signing macaroons should be treated as a sensitive secret. Load it from a secure source like an environment variable or a secret management service (e.g., HashiCorp Vault, AWS Secrets Manager).
Configuration
YAML Configuration
config:
secret_key: "..." # Macaroon signing key
elicit_expiry: 3600 # Permission grant TTL (seconds)
policies:
- "bf:tool:policy:action:params..."
Examples
See the examples/ directory for a complete, runnable server using the FastMCP framework.
server.py- Complete server with GitHub OAuth and custom enforcers.policies.yaml- Sample policy configuration for the server.
Run the example:
cd examples/github_example
pip install -r requirements.txt
python server.py
API Reference
MacaroonMiddleware
MacaroonMiddleware(config_path: str)
Parameters:
config_path: Path to policies.yaml configuration file
Methods:
on_call_tool(context, call_next): Main middleware hook for FastMCP.
Policy Enforcer Decorator
@policy_enforcer(policy_name: str)
def my_enforcer(
caveat: Caveat,
context: Context,
result: ToolResult,
macaroon: Macaroon,
*params
) -> List[Caveat]:
"""
Args:
caveat: Parsed caveat object
context: Request context containing user info
result: Tool execution result (None for pre-call)
macaroon: Current macaroon instance
*params: Additional parameters from the caveat string
Returns:
A list of new caveats to add to the macaroon upon success.
Raises:
PolicyViolationError: If the authorization check fails.
"""
pass
Caveat Model
@dataclass
class Caveat:
raw: str # Original caveat string
execution_phase: ExecutionPhase # BEFORE or AFTER
tool_name: str # Target tool
policy_name: str # Policy enforcer name
action: ActionType # ALLOW, DENY, or ELICIT
params: Tuple[str, ...] # Policy parameters
expiry: Optional[datetime] # Expiration time
Development
Running Tests
pytest
Building
python -m build
Version Bump
bump2version patch # or minor, major
git push --tags
Contributing
See CONTRIBUTING.md for guidelines.
Security Considerations
- Secret Key: Store
secret_keysecurely (use environment variables or secrets management). - Transport Security: Use HTTPS in production to protect macaroons from being intercepted.
- Elicitation Expiry: Set appropriate
elicit_expiryvalues. Short expiries are safer but may require users to grant permission more often. - Audit Custom Enforcers: Carefully review and test custom policy enforcers for security vulnerabilities, as they are a critical part of your security model.
- Input Validation: While this middleware provides authorization, always validate and sanitize user-provided input in your main application logic.
License
MIT License - see LICENSE file.
Citation
@software{mcp_macaroon_middleware,
author = {Indresh Pradeepkumar, Neil Grover, Ravi Gadgil},
title = {MCP Macaroon Middleware},
year = {2025},
url = {https://github.com/indreshp135/CSE-227-OPENMCP}
}
Links
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file mcp_macaroon_middleware-2.0.0.tar.gz.
File metadata
- Download URL: mcp_macaroon_middleware-2.0.0.tar.gz
- Upload date:
- Size: 23.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a34bf028e3a327f529a14b7ea26697b3774fc0d82a123f9ebcc186691fe85149
|
|
| MD5 |
d09f2dc6e3ae97562e3fb76bb9783a09
|
|
| BLAKE2b-256 |
68a60005d34fc2f2d65c222bd559d35b5fd5d467b99486a33d5b8495e690ca98
|
Provenance
The following attestation bundles were made for mcp_macaroon_middleware-2.0.0.tar.gz:
Publisher:
release.yaml on indreshp135/CSE-227-OPENMCP
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mcp_macaroon_middleware-2.0.0.tar.gz -
Subject digest:
a34bf028e3a327f529a14b7ea26697b3774fc0d82a123f9ebcc186691fe85149 - Sigstore transparency entry: 735397722
- Sigstore integration time:
-
Permalink:
indreshp135/CSE-227-OPENMCP@9408527e9464f9f61d88547460e3062909009ff3 -
Branch / Tag:
refs/tags/v2.0.0 - Owner: https://github.com/indreshp135
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yaml@9408527e9464f9f61d88547460e3062909009ff3 -
Trigger Event:
release
-
Statement type:
File details
Details for the file mcp_macaroon_middleware-2.0.0-py3-none-any.whl.
File metadata
- Download URL: mcp_macaroon_middleware-2.0.0-py3-none-any.whl
- Upload date:
- Size: 21.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
045f1671a18aa3178256c09866d3a8f4ba5481c811b790b0cfe04da823f98671
|
|
| MD5 |
8e3203d3c34555cb21f5c96946278c18
|
|
| BLAKE2b-256 |
03eac6fa8b6a6813de7d2bddbff6fda8da33df96f8eaeb3fb385a769ccae67cb
|
Provenance
The following attestation bundles were made for mcp_macaroon_middleware-2.0.0-py3-none-any.whl:
Publisher:
release.yaml on indreshp135/CSE-227-OPENMCP
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mcp_macaroon_middleware-2.0.0-py3-none-any.whl -
Subject digest:
045f1671a18aa3178256c09866d3a8f4ba5481c811b790b0cfe04da823f98671 - Sigstore transparency entry: 735397742
- Sigstore integration time:
-
Permalink:
indreshp135/CSE-227-OPENMCP@9408527e9464f9f61d88547460e3062909009ff3 -
Branch / Tag:
refs/tags/v2.0.0 - Owner: https://github.com/indreshp135
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yaml@9408527e9464f9f61d88547460e3062909009ff3 -
Trigger Event:
release
-
Statement type: