611 lines
21 KiB
Python
611 lines
21 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
VaultMesh MCP Server
|
|
|
|
Model Context Protocol server exposing VaultMesh Guardian, Treasury,
|
|
Cognitive, and Auth tools. This enables Claude to operate as the
|
|
7th Organ of VaultMesh - the Cognitive Ψ-Layer.
|
|
"""
|
|
|
|
import asyncio
|
|
import json
|
|
import logging
|
|
import os
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import blake3
|
|
|
|
# Try to import mcp, fallback gracefully if not available
|
|
try:
|
|
from mcp.server import Server
|
|
from mcp.server.stdio import stdio_server
|
|
from mcp.types import Tool, TextContent
|
|
MCP_AVAILABLE = True
|
|
except ImportError:
|
|
MCP_AVAILABLE = False
|
|
|
|
from .tools import (
|
|
# Guardian
|
|
guardian_anchor_now,
|
|
guardian_verify_receipt,
|
|
guardian_status,
|
|
# Treasury
|
|
treasury_balance,
|
|
treasury_debit,
|
|
treasury_credit,
|
|
treasury_create_budget,
|
|
# Cognitive
|
|
cognitive_context,
|
|
cognitive_decide,
|
|
cognitive_invoke_tem,
|
|
cognitive_memory_get,
|
|
cognitive_memory_set,
|
|
cognitive_attest,
|
|
cognitive_audit_trail,
|
|
cognitive_oracle_chain,
|
|
# Auth
|
|
auth_challenge,
|
|
auth_verify,
|
|
auth_validate_token,
|
|
auth_check_permission,
|
|
check_profile_permission,
|
|
get_profile_for_scope,
|
|
auth_revoke,
|
|
auth_list_sessions,
|
|
auth_create_dev_session,
|
|
auth_revoke,
|
|
auth_list_sessions,
|
|
)
|
|
|
|
# Setup logging
|
|
logging.basicConfig(level=logging.INFO)
|
|
logger = logging.getLogger("vaultmesh-mcp")
|
|
|
|
# VaultMesh root
|
|
VAULTMESH_ROOT = Path(os.environ.get("VAULTMESH_ROOT", Path(__file__).parents[2])).resolve()
|
|
MCP_RECEIPTS = VAULTMESH_ROOT / "receipts/mcp/mcp_calls.jsonl"
|
|
|
|
# Tools that must remain callable without an authenticated session token.
|
|
# These are the bootstrap endpoints required to obtain/check a session.
|
|
OPEN_TOOLS = {
|
|
"auth_challenge",
|
|
"auth_verify",
|
|
"auth_create_dev_session",
|
|
"auth_check_permission",
|
|
}
|
|
|
|
|
|
def _vmhash_blake3(data: bytes) -> str:
|
|
"""VaultMesh hash: blake3:<hex>."""
|
|
return f"blake3:{blake3.blake3(data).hexdigest()}"
|
|
|
|
|
|
def _redact_call_arguments(arguments: dict) -> dict:
|
|
# Never persist session tokens in receipts.
|
|
if not arguments:
|
|
return {}
|
|
redacted = dict(arguments)
|
|
redacted.pop("session_token", None)
|
|
return redacted
|
|
|
|
|
|
def _emit_mcp_receipt(tool_name: str, arguments: dict, result: dict, caller: str = "did:vm:mcp:client") -> None:
|
|
"""Emit a receipt for every MCP tool call."""
|
|
MCP_RECEIPTS.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
body = {
|
|
"tool": tool_name,
|
|
"arguments": _redact_call_arguments(arguments),
|
|
"result_hash": _vmhash_blake3(json.dumps(result, sort_keys=True).encode()),
|
|
"caller": caller,
|
|
"success": "error" not in result,
|
|
}
|
|
|
|
receipt = {
|
|
"schema_version": "2.0.0",
|
|
"type": "mcp_tool_call",
|
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
"scroll": "mcp",
|
|
"tags": ["mcp", "tool-call", tool_name],
|
|
"root_hash": _vmhash_blake3(json.dumps(body, sort_keys=True).encode()),
|
|
"body": body,
|
|
}
|
|
|
|
with open(MCP_RECEIPTS, "a") as f:
|
|
f.write(json.dumps(receipt) + "\n")
|
|
|
|
|
|
def require_session_and_permission(name: str, arguments: dict) -> tuple[bool, dict, str, dict | None]:
|
|
"""Fail-closed session + profile enforcement ahead of tool handlers.
|
|
|
|
Returns (allowed, safe_args, caller, denial_result).
|
|
- safe_args strips session_token so downstream handlers never see it.
|
|
- caller is derived from the validated session (operator_did) when available.
|
|
- denial_result is a structured error payload when denied.
|
|
"""
|
|
|
|
safe_args = dict(arguments or {})
|
|
caller = "did:vm:mcp:client"
|
|
|
|
if name in OPEN_TOOLS:
|
|
return True, safe_args, caller, None
|
|
|
|
session_token = safe_args.pop("session_token", None)
|
|
if not session_token:
|
|
return False, safe_args, caller, {
|
|
"error": "Missing session_token",
|
|
"allowed": False,
|
|
"reason": "Session required for non-auth tools",
|
|
}
|
|
|
|
validation = auth_validate_token(session_token)
|
|
if not validation.get("valid"):
|
|
return False, safe_args, caller, {
|
|
"error": "Invalid session",
|
|
"allowed": False,
|
|
"reason": validation.get("error", "invalid_session"),
|
|
}
|
|
|
|
caller = validation.get("operator_did") or caller
|
|
profile = get_profile_for_scope(str(validation.get("scope", "read")))
|
|
perm = check_profile_permission(profile, name)
|
|
if not perm.get("allowed"):
|
|
return False, safe_args, caller, {
|
|
"error": "Permission denied",
|
|
"allowed": False,
|
|
"profile": perm.get("profile"),
|
|
"reason": perm.get("reason", "denied"),
|
|
}
|
|
|
|
return True, safe_args, caller, None
|
|
|
|
|
|
# =============================================================================
|
|
# TOOL DEFINITIONS
|
|
# =============================================================================
|
|
|
|
TOOLS = [
|
|
# -------------------------------------------------------------------------
|
|
# GUARDIAN TOOLS
|
|
# -------------------------------------------------------------------------
|
|
{
|
|
"name": "guardian_anchor_now",
|
|
"description": "Anchor all or specified scrolls to compute a Merkle root snapshot. Emits a guardian receipt.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"scrolls": {
|
|
"type": "array",
|
|
"items": {"type": "string"},
|
|
"description": "List of scroll names to anchor. Omit for all scrolls.",
|
|
},
|
|
"guardian_did": {
|
|
"type": "string",
|
|
"default": "did:vm:guardian:mcp",
|
|
},
|
|
"backend": {
|
|
"type": "string",
|
|
"default": "local",
|
|
"enum": ["local", "ethereum", "stellar"],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
"name": "guardian_verify_receipt",
|
|
"description": "Verify a receipt exists in a scroll's JSONL by its hash.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"receipt_hash": {"type": "string"},
|
|
"scroll": {"type": "string", "default": "guardian"},
|
|
},
|
|
"required": ["receipt_hash"],
|
|
},
|
|
},
|
|
{
|
|
"name": "guardian_status",
|
|
"description": "Get current status of all scrolls including Merkle roots and leaf counts.",
|
|
"inputSchema": {"type": "object", "properties": {}},
|
|
},
|
|
# -------------------------------------------------------------------------
|
|
# TREASURY TOOLS
|
|
# -------------------------------------------------------------------------
|
|
{
|
|
"name": "treasury_create_budget",
|
|
"description": "Create a new budget for tracking expenditures.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"budget_id": {"type": "string"},
|
|
"name": {"type": "string"},
|
|
"allocated": {"type": "integer"},
|
|
"currency": {"type": "string", "default": "EUR"},
|
|
"created_by": {"type": "string", "default": "did:vm:mcp:treasury"},
|
|
},
|
|
"required": ["budget_id", "name", "allocated"],
|
|
},
|
|
},
|
|
{
|
|
"name": "treasury_balance",
|
|
"description": "Get balance for a specific budget or all budgets.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"budget_id": {"type": "string"},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
"name": "treasury_debit",
|
|
"description": "Debit (spend) from a budget. Fails if insufficient funds.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"budget_id": {"type": "string"},
|
|
"amount": {"type": "integer"},
|
|
"description": {"type": "string"},
|
|
"debited_by": {"type": "string", "default": "did:vm:mcp:treasury"},
|
|
},
|
|
"required": ["budget_id", "amount", "description"],
|
|
},
|
|
},
|
|
{
|
|
"name": "treasury_credit",
|
|
"description": "Credit (add funds) to a budget.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"budget_id": {"type": "string"},
|
|
"amount": {"type": "integer"},
|
|
"description": {"type": "string"},
|
|
"credited_by": {"type": "string", "default": "did:vm:mcp:treasury"},
|
|
},
|
|
"required": ["budget_id", "amount", "description"],
|
|
},
|
|
},
|
|
# -------------------------------------------------------------------------
|
|
# COGNITIVE TOOLS (Claude as 7th Organ)
|
|
# -------------------------------------------------------------------------
|
|
{
|
|
"name": "cognitive_context",
|
|
"description": "Read current VaultMesh context for AI reasoning. Aggregates alerts, health, receipts, threats, treasury, governance, and memory.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"include": {
|
|
"type": "array",
|
|
"items": {"type": "string"},
|
|
"description": "Context types: alerts, health, receipts, threats, treasury, governance, memory",
|
|
},
|
|
"session_id": {"type": "string"},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
"name": "cognitive_decide",
|
|
"description": "Submit a reasoned decision with cryptographic attestation. Every decision is signed and anchored to ProofChain.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"reasoning_chain": {
|
|
"type": "array",
|
|
"items": {"type": "string"},
|
|
"description": "List of reasoning steps leading to decision",
|
|
},
|
|
"decision": {
|
|
"type": "string",
|
|
"description": "Decision type: invoke_tem, alert, remediate, approve, etc.",
|
|
},
|
|
"confidence": {
|
|
"type": "number",
|
|
"minimum": 0,
|
|
"maximum": 1,
|
|
},
|
|
"evidence": {
|
|
"type": "array",
|
|
"items": {"type": "string"},
|
|
},
|
|
"operator_did": {"type": "string", "default": "did:vm:cognitive:claude"},
|
|
"auto_action_threshold": {"type": "number", "default": 0.95},
|
|
},
|
|
"required": ["reasoning_chain", "decision", "confidence"],
|
|
},
|
|
},
|
|
{
|
|
"name": "cognitive_invoke_tem",
|
|
"description": "Invoke Tem (Guardian) with AI-detected threat pattern. Transmutes threats into defensive capabilities.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"threat_type": {
|
|
"type": "string",
|
|
"description": "Category: replay_attack, intrusion, anomaly, credential_stuffing, etc.",
|
|
},
|
|
"threat_id": {"type": "string"},
|
|
"target": {"type": "string"},
|
|
"evidence": {
|
|
"type": "array",
|
|
"items": {"type": "string"},
|
|
},
|
|
"recommended_transmutation": {"type": "string"},
|
|
"operator_did": {"type": "string", "default": "did:vm:cognitive:claude"},
|
|
},
|
|
"required": ["threat_type", "threat_id", "target", "evidence"],
|
|
},
|
|
},
|
|
{
|
|
"name": "cognitive_memory_get",
|
|
"description": "Query conversation/reasoning memory from CRDT realm.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"key": {"type": "string"},
|
|
"session_id": {"type": "string"},
|
|
"realm": {"type": "string", "default": "memory"},
|
|
},
|
|
"required": ["key"],
|
|
},
|
|
},
|
|
{
|
|
"name": "cognitive_memory_set",
|
|
"description": "Store reasoning artifacts for future sessions. Uses CRDT-style merge.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"key": {"type": "string"},
|
|
"value": {"type": "object"},
|
|
"session_id": {"type": "string"},
|
|
"realm": {"type": "string", "default": "memory"},
|
|
"merge": {"type": "boolean", "default": True},
|
|
},
|
|
"required": ["key", "value"],
|
|
},
|
|
},
|
|
{
|
|
"name": "cognitive_attest",
|
|
"description": "Create cryptographic attestation of Claude's reasoning state. Anchors to external chains.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"attestation_type": {"type": "string"},
|
|
"content": {"type": "object"},
|
|
"anchor_to": {
|
|
"type": "array",
|
|
"items": {"type": "string"},
|
|
"description": "Anchor backends: local, rfc3161, eth, btc",
|
|
},
|
|
"operator_did": {"type": "string", "default": "did:vm:cognitive:claude"},
|
|
},
|
|
"required": ["attestation_type", "content"],
|
|
},
|
|
},
|
|
{
|
|
"name": "cognitive_audit_trail",
|
|
"description": "Query historical AI decisions for audit with full provenance.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"filter_type": {"type": "string"},
|
|
"time_range": {
|
|
"type": "object",
|
|
"properties": {
|
|
"start": {"type": "string"},
|
|
"end": {"type": "string"},
|
|
},
|
|
},
|
|
"confidence_min": {"type": "number"},
|
|
"limit": {"type": "integer", "default": 100},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
"name": "cognitive_oracle_chain",
|
|
"description": "Execute oracle chain with cognitive enhancement. Adds memory context and Tem awareness.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"question": {"type": "string"},
|
|
"frameworks": {
|
|
"type": "array",
|
|
"items": {"type": "string"},
|
|
"description": "Compliance frameworks: GDPR, AI_ACT, NIS2, etc.",
|
|
},
|
|
"max_docs": {"type": "integer", "default": 10},
|
|
"include_memory": {"type": "boolean", "default": True},
|
|
"session_id": {"type": "string"},
|
|
},
|
|
"required": ["question"],
|
|
},
|
|
},
|
|
# -------------------------------------------------------------------------
|
|
# AUTH TOOLS
|
|
# -------------------------------------------------------------------------
|
|
{
|
|
"name": "auth_challenge",
|
|
"description": "Generate an authentication challenge for an operator.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"operator_pubkey_b64": {"type": "string"},
|
|
"scope": {
|
|
"type": "string",
|
|
"enum": ["read", "admin", "vault", "anchor", "cognitive"],
|
|
"default": "read",
|
|
},
|
|
"ttl_seconds": {"type": "integer", "default": 300},
|
|
},
|
|
"required": ["operator_pubkey_b64"],
|
|
},
|
|
},
|
|
{
|
|
"name": "auth_verify",
|
|
"description": "Verify a signed challenge and issue session token.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"challenge_id": {"type": "string"},
|
|
"signature_b64": {"type": "string"},
|
|
"ip_hint": {"type": "string"},
|
|
},
|
|
"required": ["challenge_id", "signature_b64"],
|
|
},
|
|
},
|
|
{
|
|
"name": "auth_check_permission",
|
|
"description": "Check if a session has permission to call a tool.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"token": {"type": "string"},
|
|
"tool_name": {"type": "string"},
|
|
},
|
|
"required": ["token", "tool_name"],
|
|
},
|
|
},
|
|
{
|
|
"name": "auth_create_dev_session",
|
|
"description": "Create a development session for testing (DEV ONLY).",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"scope": {"type": "string", "default": "cognitive"},
|
|
"operator_did": {"type": "string", "default": "did:vm:cognitive:claude-dev"},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
"name": "auth_revoke",
|
|
"description": "Revoke a session token.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"token": {"type": "string"},
|
|
},
|
|
"required": ["token"],
|
|
},
|
|
},
|
|
{
|
|
"name": "auth_list_sessions",
|
|
"description": "List all active sessions (admin only).",
|
|
"inputSchema": {"type": "object", "properties": {}},
|
|
},
|
|
|
|
]
|
|
|
|
|
|
def handle_tool_call(name: str, arguments: dict) -> dict[str, Any]:
|
|
"""Dispatch tool call to appropriate handler."""
|
|
handlers = {
|
|
# Guardian
|
|
"guardian_anchor_now": guardian_anchor_now,
|
|
"guardian_verify_receipt": guardian_verify_receipt,
|
|
"guardian_status": guardian_status,
|
|
# Treasury
|
|
"treasury_create_budget": treasury_create_budget,
|
|
"treasury_balance": treasury_balance,
|
|
"treasury_debit": treasury_debit,
|
|
"treasury_credit": treasury_credit,
|
|
# Cognitive
|
|
"cognitive_context": cognitive_context,
|
|
"cognitive_decide": cognitive_decide,
|
|
"cognitive_invoke_tem": cognitive_invoke_tem,
|
|
"cognitive_memory_get": cognitive_memory_get,
|
|
"cognitive_memory_set": cognitive_memory_set,
|
|
"cognitive_attest": cognitive_attest,
|
|
"cognitive_audit_trail": cognitive_audit_trail,
|
|
"cognitive_oracle_chain": cognitive_oracle_chain,
|
|
# Auth
|
|
"auth_challenge": auth_challenge,
|
|
"auth_verify": auth_verify,
|
|
"auth_check_permission": auth_check_permission,
|
|
"auth_create_dev_session": auth_create_dev_session,
|
|
"auth_revoke": auth_revoke,
|
|
"auth_list_sessions": auth_list_sessions,
|
|
}
|
|
|
|
if name not in handlers:
|
|
return {"error": f"Unknown tool: {name}"}
|
|
allowed, safe_args, caller, denial = require_session_and_permission(name, arguments)
|
|
if not allowed:
|
|
_emit_mcp_receipt(name, safe_args, denial, caller=caller)
|
|
return denial
|
|
|
|
result = handlers[name](**safe_args)
|
|
|
|
# Emit receipt for the tool call
|
|
_emit_mcp_receipt(name, safe_args, result, caller=caller)
|
|
|
|
return result
|
|
|
|
|
|
if MCP_AVAILABLE:
|
|
# Create MCP server
|
|
app = Server("vaultmesh-mcp")
|
|
|
|
@app.list_tools()
|
|
async def list_tools() -> list[Tool]:
|
|
"""List available VaultMesh tools."""
|
|
return [Tool(**t) for t in TOOLS]
|
|
|
|
@app.call_tool()
|
|
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
|
|
"""Handle tool invocation."""
|
|
logger.info(f"Tool call: {name} with {arguments}")
|
|
result = handle_tool_call(name, arguments)
|
|
return [TextContent(type="text", text=json.dumps(result, indent=2))]
|
|
|
|
|
|
async def main():
|
|
"""Run the MCP server."""
|
|
if not MCP_AVAILABLE:
|
|
print("MCP library not available. Install with: pip install mcp")
|
|
return
|
|
|
|
logger.info(f"Starting VaultMesh MCP Server (root: {VAULTMESH_ROOT})")
|
|
logger.info(f"Tools registered: {len(TOOLS)}")
|
|
async with stdio_server() as (read_stream, write_stream):
|
|
await app.run(read_stream, write_stream, app.create_initialization_options())
|
|
|
|
|
|
def run_standalone():
|
|
"""Run as standalone CLI for testing without MCP."""
|
|
import sys
|
|
|
|
if len(sys.argv) < 2:
|
|
print("VaultMesh MCP Server - Standalone Mode")
|
|
print(f"\nVaultMesh Root: {VAULTMESH_ROOT}")
|
|
print(f"\nRegistered Tools ({len(TOOLS)}):")
|
|
print("-" * 60)
|
|
for tool in TOOLS:
|
|
print(f" {tool['name']}")
|
|
print(f" {tool['description'][:70]}...")
|
|
print("-" * 60)
|
|
print("\nUsage: python -m vaultmesh_mcp.server <tool> [json_args]")
|
|
print("\nExample:")
|
|
print(' python -m vaultmesh_mcp.server cognitive_context \'{"include": ["health"]}\'')
|
|
return
|
|
|
|
tool_name = sys.argv[1]
|
|
args_str = sys.argv[2] if len(sys.argv) > 2 else "{}"
|
|
|
|
try:
|
|
arguments = json.loads(args_str)
|
|
except json.JSONDecodeError:
|
|
print(f"Invalid JSON arguments: {args_str}")
|
|
return
|
|
|
|
result = handle_tool_call(tool_name, arguments)
|
|
print(json.dumps(result, indent=2))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import sys
|
|
# If any CLI arguments provided (other than module name), run standalone
|
|
if len(sys.argv) > 1 or not MCP_AVAILABLE:
|
|
run_standalone()
|
|
else:
|
|
asyncio.run(main())
|