MCP Integration
Tenuo provides full Model Context Protocol (MCP) client integration with cryptographic authorization.
Full Stack: Connect to MCP servers → Discover tools → Auto-protect with warrants → Execute securely.
Prerequisites
pip install tenuo
For the full LangChain + MCP example:
pip install "tenuo[langchain]"
Why MCP + Tenuo?
MCP exposes powerful capabilities (filesystem, database, code execution) to AI agents.
Tenuo ensures those capabilities are constrained with cryptographic warrants.
| Without Tenuo | With Tenuo |
|---|---|
| Agent has full MCP server access | Warrant constrains what MCP tools can do |
| No audit trail | Cryptographic proof of who authorized each action |
| Ambient authority | Task-scoped, time-limited permissions |
Where Tenuo Fits in MCP
MCP’s native auth (OAuth) answers: WHO is calling?
Tenuo answers: WHAT can they do right now?
┌──────────────────────────────────────────────────────────────────┐
│ MCP STACK │
├──────────────────────────────────────────────────────────────────┤
│ │
│ Host App ──► MCP Client ──► [Tenuo Guard] ──► MCP Server │
│ │ │
│ ┌──────┴──────┐ │
│ │ Warrant │ │
│ │ • tools │ │
│ │ • constraints│ │
│ │ • TTL │ │
│ └─────────────┘ │
└──────────────────────────────────────────────────────────────────┘
OAuth vs Warrants Comparison:
| Aspect | OAuth Token | Tenuo Warrant |
|---|---|---|
| Scope granularity | Coarse (files:read) |
Fine (read_file(path=/data/x/*)) |
| Proof-of-Possession | Optional (DPoP) | Mandatory |
| Delegation | No native chaining | Cryptographic attenuation chains |
| Verification | Requires introspection/JWKS | Stateless, self-contained |
OAuth tells you who is authenticated. Warrants constrain what they can do with which arguments. Even if an LLM is prompt-injected mid-task, it can only use what the warrant allows.
Quick Start
Tenuo supports two integration patterns for MCP:
SecureMCPClient(Built-in): Full client with automatic discovery and protection.langchain-mcp-adapters(Official): Secure the official LangChain MCP client.
Pattern 1: SecureMCPClient (Recommended)
Prerequisite: Python 3.10+ (required by MCP SDK)
from tenuo.mcp import SecureMCPClient
from tenuo import configure, mint, Capability, Subpath, SigningKey
# 1. Configure Tenuo
key = SigningKey.generate() # In production: SigningKey.from_env("MY_KEY")
configure(issuer_key=key)
# 2. Connect to MCP server
# Automatically discovers tools and wraps them with authorization
async with SecureMCPClient("python", ["server.py"], register_config=True) as client:
# 3. Call tool with authorization
async with mint(Capability("read_file", path=Subpath("/data"))):
result = await client.tools["read_file"](path="/data/file.txt")
Pattern 2: Securing LangChain Adapters
If you are already using langchain-mcp-adapters, you can protect its tools using guard():
from langchain_mcp_adapters.client import MultiServerMCPClient
from tenuo.langchain import guard_tools
# 1. Connect via official client
client = MultiServerMCPClient({...})
mcp_tools = await client.get_tools()
# 2. Wrap with Tenuo protection
secure_tools = guard(mcp_tools, bound)
# ... use secure_tools in your agent
Advanced: Manual Configuration
For fine-grained control or Python < 3.10, you can manually define constraints and authorize calls.
1. Create MCP Configuration
Define how to extract constraints from MCP tool calls:
# mcp-config.yaml
version: "1"
tools:
filesystem_read:
description: "Read files from the filesystem"
constraints:
path:
from: body
path: "path"
required: true
max_size:
from: body
path: "maxSize"
type: integer
default: 1048576 # 1 MB
2. Authorize MCP Calls (Manual)
If you are not using SecureMCPClient, you must manually authorize extracted arguments.
from tenuo import McpConfig, CompiledMcpConfig, Authorizer, SigningKey, Warrant, Constraints, Pattern, Range
# 1. Load MCP configuration
config = McpConfig.from_file("mcp-config.yaml")
compiled = CompiledMcpConfig.compile(config)
# 2. Create warrant (usually done by control plane)
control_key = SigningKey.generate() # In production: SigningKey.from_env("MY_KEY")
warrant = (Warrant.mint_builder()
.capability("filesystem_read",
path=Subpath("/var/log"),
max_size=Range.max_value(1024 * 1024))
.holder(control_key.public_key)
.ttl(3600)
.mint(control_key))
# 3. Handle MCP tool call
# (Simulated MCP arguments)
mcp_arguments = {
"path": "/var/log/app.log",
"maxSize": 512 * 1024
}
# 4. Extract constraints based on config
result = compiled.extract_constraints("filesystem_read", mcp_arguments)
# 5. Authorize with PoP signature
pop_sig = warrant.sign(control_key, "filesystem_read", dict(result.constraints))
authorizer = Authorizer(trusted_roots=[control_key.public_key])
authorizer.check(warrant, "filesystem_read", dict(result.constraints), bytes(pop_sig))
# ✓ Authorized - proceed to execute tool
LangChain + MCP Integration
Tenuo integrates seamlessly with langchain-mcp-adapters.
Pattern: LangChain MultiServerMCPClient → Tenuo Authorization → MCP Server
Secure Adapter Pattern
The most robust way to use MCP with LangChain is to wrap the official client tools with Tenuo authorization:
from langchain_mcp_adapters.client import MultiServerMCPClient
from tenuo.mcp import SecureMCPClient # Wrapper for official client
# 1. Connect via official client
client = MultiServerMCPClient({
"math": {
"transport": "stdio",
"command": "python",
"args": ["math_server.py"]
}
})
# 2. Get protected tools (Tenuo auto-wraps them)
tools = await client.get_tools()
# ... use tools in LangChain agent
Python Example
from tenuo import McpConfig, CompiledMcpConfig, Authorizer, SigningKey, Warrant, Pattern, Capability
from tenuo import guard, configure, mint_sync
# 1. Configure Tenuo
control_key = SigningKey.generate() # In production: SigningKey.from_env("MY_KEY")
configure(issuer_key=control_key)
# 2. Load MCP configuration
config = McpConfig.from_file("mcp-config.yaml")
compiled = CompiledMcpConfig.compile(config)
# 3. Define MCP tool wrapper
@guard(tool="filesystem_read")
def filesystem_read(path: str, maxSize: int = 1048576):
"""Read file from filesystem (MCP tool)"""
# In production: Call actual MCP server
with open(path, 'r') as f:
content = f.read(maxSize)
return content
# 4. Use with task scoping
with mint_sync(Capability("filesystem_read", path="/var/log/*")):
# Agent calls MCP tool - Tenuo authorizes
content = filesystem_read("/var/log/app.log", maxSize=512 * 1024)
print(content)
End-to-End Flow
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ LangChain │────▶│ Tenuo │────▶│ MCP Server │
│ Agent │ │ Authorizer │ │ │
└─────────────┘ └─────────────┘ └─────────────┘
│ │ │
│ 1. Call tool │ │
│───────────────────▶│ │
│ │ 2. Extract │
│ │ constraints │
│ │ 3. Check warrant │
│ │ 4. Authorize │
│ │◀───────────────────│
│ │ 5. Execute │
│◀───────────────────│───────────────────▶│
│ 6. Return result │ │
MCP Configuration
Define how to extract constraints from MCP tool call arguments.
Remember: This configuration defines extraction, not policy. It tells Tenuo where to find the arguments in the JSON-RPC call. The actual limits (e.g., which paths are allowed) are defined in the Warrant. See Argument Extraction for a deep dive.
Extraction Sources
MCP tool calls provide an arguments JSON object. Use:
from: body- Extract from arguments (recommended)from: literal- Use default value
❌ Don’t use: from: path, from: query, from: header (HTTP-only)
Example: Filesystem Tool
tools:
filesystem_read:
description: "Read files from the filesystem"
constraints:
path:
from: body
path: "path"
description: "File path to read"
required: true
max_size:
from: body
path: "maxSize"
description: "Maximum file size in bytes"
type: integer
default: 1048576
allowed_paths:
from: body
path: "allowedPaths"
description: "List of allowed path prefixes"
Example: Database Tool
tools:
database_query:
description: "Execute database queries"
constraints:
table:
from: body
path: "query.table"
required: true
operation:
from: body
path: "query.operation"
required: true
allowed_values: ["select", "insert", "update", "delete"]
row_limit:
from: body
path: "query.limit"
type: integer
default: 100
Constraint Extraction
Automatic Extraction
Tenuo extracts constraints from MCP arguments using YAML config.
When using SecureMCPClient(config_path="...", register_config=True), extraction happens automatically during tool calls.
Warrant Propagation (Mesh)
To enable end-to-end authorization where the server verifies the warrant, set inject_warrant=True:
async with SecureMCPClient(..., inject_warrant=True) as client:
# Warrants now travel in arguments._tenuo
await client.tools["read_file"](path="/tmp/test.txt")
⚠️ Interoperability Risk: Strict Schemas
When inject_warrant=True, Tenuo injects a _tenuo field into the tool arguments:
# Tenuo modifies the call payload:
{
"path": "/data/file.txt",
"_tenuo": { "warrant": "...", "signature": "..." }
}
If the destination MCP server uses a strict JSON Schema validator (e.g., explicit additionalProperties: false), the call will fail because _tenuo is not in the server’s known input schema.
Mitigation:
- Configure Server: Ensure your MCP servers are configured to allow unknown properties (this is the default in most Pydantic/Zod setups unless explicitly strict).
- Update Schema: If strict validation is required, add
_tenuo(type: object, optional) to your tool schemas. ```
Manual Extraction
If not using SecureMCPClient, you can extract constraints manually:
# Extract constraints
result = compiled.extract_constraints("filesystem_read", arguments)
# Result contains:
# result.constraints: { "path": "/var/log/app.log", "max_size": 524288 }
# result.warrant_base64: "..."
Nested Paths
Use dot notation for nested fields:
constraints:
table:
from: body
path: "query.table" # Extracts arguments.query.table
Wildcard Extraction
Extract lists with wildcards:
constraints:
item_ids:
from: body
path: "items.*.id" # Extracts all item IDs
Note: Wildcard extraction returns a list. Use compatible constraints:
OneOf/NotOneOf- Membership checksCEL- Complex list operations
Authorization Patterns
Pattern 1: Decorator-Based
from tenuo import guard, mint_sync
@guard(tool="filesystem_read")
def filesystem_read(path: str, maxSize: int):
# MCP server call
return read_file_from_mcp_server(path, maxSize)
with mint_sync(Capability("filesystem_read", path="/var/log/*")):
content = filesystem_read("/var/log/app.log", 1024)
Pattern 2: Explicit Authorization
from tenuo import Authorizer, Warrant
# Create warrant
warrant = (Warrant.mint_builder()
.capability("filesystem_read", path=Subpath("/var/log"))
.holder(key.public_key)
.ttl(3600)
.mint(key)
)
# Extract constraints from MCP call
result = compiled.extract_constraints("filesystem_read", arguments)
# Authorize
pop_sig = warrant.sign(key, "filesystem_read", dict(result.constraints))
authorizer.check(warrant, "filesystem_read", dict(result.constraints), bytes(pop_sig))
Pattern 3: Multi-Agent Delegation
# Control plane issues root warrant
root_warrant = (Warrant.mint_builder()
.capability("filesystem_read", path=Subpath("/data"))
.capability("database_query", path=Subpath("/data"))
.holder(orchestrator_key.public_key)
.ttl(3600)
.mint(control_key))
# Orchestrator attenuates for worker
worker_warrant = (root_warrant.grant_builder()
.capability("filesystem_read", path=Subpath("/data/reports"))
.holder(worker_key.public_key)
.grant(orchestrator_key)) # Orchestrator signs (they hold the parent)
# Worker uses attenuated warrant
# (narrower permissions, cryptographic proof of delegation)
Security Best Practices
1. Validate Configuration
compiled = CompiledMcpConfig.compile(config)
warnings = compiled.validate()
for warning in warnings:
print(warning)
Warns about incompatible extraction sources (path, query, header).
2. Use Trusted Roots
# Load control plane public key
control_plane_key = PublicKey.from_bytes(key_bytes)
# Create authorizer with trusted root
authorizer = Authorizer(trusted_roots=[control_plane_key])
Without trusted roots, chain verification only checks internal consistency.
3. Proof-of-Possession
Always require PoP signatures for MCP tool calls:
# Create PoP signature
pop_sig = warrant.sign(keypair, tool, args)
# Authorize with signature
authorizer.check(warrant, tool, args, bytes(pop_sig))
Prevents warrant theft and replay attacks.
4. Constraint Narrowing
Use specific constraints, not wildcards:
# Too broad
constraints = {"path": Wildcard()}
# Specific
constraints = {"path": Subpath("/var/log")}
5. Short TTLs
MCP tools are often high-risk (filesystem, database). Use short TTLs:
warrant = (Warrant.mint_builder()
.tool("filesystem_write")
.holder(key.public_key)
.ttl(300) # 5 minutes
.mint(key)
)
Common MCP Tools
Filesystem
filesystem_read:
constraints:
path:
from: body
path: "path"
required: true
max_size:
from: body
path: "maxSize"
type: integer
default: 1048576
filesystem_write:
constraints:
path:
from: body
path: "path"
required: true
content:
from: body
path: "content"
required: true
max_size:
from: body
path: "maxSize"
type: integer
default: 1048576
Database
database_query:
constraints:
table:
from: body
path: "query.table"
required: true
operation:
from: body
path: "query.operation"
required: true
allowed_values: ["select", "insert", "update", "delete"]
row_limit:
from: body
path: "query.limit"
type: integer
default: 100
Code Execution
execute_code:
constraints:
language:
from: body
path: "code.language"
required: true
allowed_values: ["python", "javascript", "bash"]
timeout:
from: body
path: "code.timeout"
type: integer
default: 30
max_memory:
from: body
path: "code.maxMemory"
type: integer
default: 512
HTTP Requests
http_request:
constraints:
url:
from: body
path: "request.url"
required: true
method:
from: body
path: "request.method"
required: true
allowed_values: ["GET", "POST", "PUT", "DELETE"]
max_response_size:
from: body
path: "request.maxResponseSize"
type: integer
default: 10485760 # 10 MB
Troubleshooting
Extraction Errors
Problem: ExtractionError: field 'path' not found
Solution: Check MCP arguments match config:
# Config expects:
path: "path"
# MCP call must have:
arguments = {"path": "/var/log/app.log"}
Authorization Denied
Problem: AuthorizationDenied: path is not contained in allowed directory
Solution: Check warrant constraints match extracted values:
# Warrant:
constraints = {"path": Subpath("/var/log")}
# MCP call:
arguments = {"path": "/etc/passwd"} # ❌ Not under /var/log
# Fix: Narrow MCP call or broaden warrant
Type Mismatches
Problem: TypeError: expected integer, got string
Solution: Specify type in config:
max_size:
from: body
path: "maxSize"
type: integer # ← Add this
Scope & Boundaries
Tenuo Provides
- Secure Client: A wrapper around the official MCP SDK that adds authorization.
- Tool discovery: Automatic wrapping of discovered tools with
@guard. - Warrant propagation: Injecting warrants into
_tenuofield for server-side verification. - Constraint extraction: Config-driven extraction from MCP arguments.
Tenuo Does NOT Provide
- MCP Server Library: Use
fastmcpor the official SDK to build servers. - MCP Transport: Tenuo relies on standard transports (stdio, SSE, HTTP).
- Prompt Injection Detection: Tenuo assumes injection will happen and makes unauthorized actions impossible.
Examples
See tenuo-python/examples/mcp_integration.py for a complete working example.