init: vaultmesh mcp server
This commit is contained in:
610
packages/vaultmesh_mcp/server.py
Normal file
610
packages/vaultmesh_mcp/server.py
Normal file
@@ -0,0 +1,610 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user