#!/usr/bin/env python3 # Copyright (c) 2026 Charles R. Strogish. All Rights Reserved. # Dual-licensed under AGPLv3 and Commercial terms. """CrewAI example for free discovery and explicitly authorized paid MCP calls. Default mode only discovers MCP tools and reads public cost metadata. It does not create an agent-provider request, wallet payment, or paid data request. """ from __future__ import annotations import argparse import json import os import re import sys from pathlib import Path from typing import Any, Type try: from .l402_client import ( MCP_URL, BitcoinStratigraphyClient, add_cli_arguments, client_from_args, validate_catalog, ) except ImportError: from l402_client import ( # type: ignore[no-redef] MCP_URL, BitcoinStratigraphyClient, add_cli_arguments, client_from_args, validate_catalog, ) def build_native_discovery_agent(mcp_url: str = MCP_URL) -> Any: """Construct CrewAI's native remote-MCP agent, restricted to free discovery. This demonstrates MCPServerHTTP configuration without invoking an LLM or asking the agent to call a paid tool. Paid L402 calls use the custom BaseTool wrappers below because MCPServerHTTP has no documented httpx-client-factory injection point. """ from crewai import Agent from crewai.mcp import MCPServerHTTP from crewai.mcp.filters import create_static_tool_filter return Agent( role="Bitcoin stratigraphy catalog discoverer", goal="Discover available stratigraphy data without requesting paid data.", backstory="A careful assistant limited to free catalog discovery.", verbose=False, max_iter=4, mcps=[ MCPServerHTTP( url=mcp_url, streamable=True, cache_tools_list=True, tool_filter=create_static_tool_filter( allowed_tool_names=["stratigraphy.list"], ), ) ], ) def _read_proof_arguments(path: str) -> dict[str, Any]: try: value = json.loads(Path(path).read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError) as exc: raise ValueError(f"Could not read proof arguments from {path}: {exc}") from exc if not isinstance(value, dict): raise ValueError( "--proof-file must contain a JSON object of proof.verify arguments" ) if value.get("proofType") == "ots": valid = all(key in value for key in ("otsProof", "targetHash")) elif value.get("circuit") == "zk.drift.v1": valid = all( key in value for key in ( "sessionContextHash", "targetBlockHeight", "entropyThreshold", "proof", ) ) else: public_inputs = value.get("publicInputs") valid = ( isinstance(value.get("proof"), str) and isinstance(public_inputs, dict) and all( key in public_inputs for key in ( "blockHeight", "snapshotDigest", "thermodynamicHash", "timestamp", "valuationSat", ) ) and isinstance(value.get("verificationKeyId"), str) and "data" in value ) if not valid: raise ValueError( "--proof-file must contain an actual proof.verify argument object " "matching one of the server's supported proof schemas" ) return value def _build_paid_tools( client: BitcoinStratigraphyClient, proof_arguments: dict[str, Any], ) -> tuple[list[Any], set[str], list[str]]: from crewai.tools import BaseTool from pydantic import BaseModel, Field class StratigraphyGetArgs(BaseModel): days: int = Field(default=1, ge=1, le=422) class ProofVerifyArgs(BaseModel): confirm: bool = Field( description="Must be true to verify the exact caller-supplied proof payload." ) used_tools: set[str] = set() tool_errors: list[str] = [] class StratigraphyGetTool(BaseTool): name: str = "stratigraphy_get" description: str = ( "Call canonical MCP tool stratigraphy.get for indexed telemetry. " "This is a paid L402 operation; use only when the task explicitly requests it." ) args_schema: Type[BaseModel] = StratigraphyGetArgs def _run(self, days: int = 1) -> str: if "stratigraphy.get" in used_tools: raise RuntimeError( "This example permits only one stratigraphy.get call." ) used_tools.add("stratigraphy.get") try: result = client.call_tool("stratigraphy.get", {"days": days}) return json.dumps(result, ensure_ascii=False) except Exception as exc: tool_errors.append(f"stratigraphy.get failed: {exc}") raise class ProofVerifyTool(BaseTool): name: str = "proof_verify" description: str = ( "Call canonical MCP tool proof.verify on the exact real proof payload " "provided by the caller. This is a paid operation. Do not invent or modify " "proof inputs; invoke only when confirm=true." ) args_schema: Type[BaseModel] = ProofVerifyArgs def _run(self, confirm: bool) -> str: if not confirm: raise ValueError("Proof verification requires explicit confirm=true.") if "proof.verify" in used_tools: raise RuntimeError("This example permits only one proof.verify call.") used_tools.add("proof.verify") try: result = client.call_tool("proof.verify", proof_arguments) return json.dumps(result, ensure_ascii=False) except Exception as exc: tool_errors.append(f"proof.verify failed: {exc}") raise return [StratigraphyGetTool(), ProofVerifyTool()], used_tools, tool_errors def _safe_error_detail(error: Exception) -> str: """Keep useful failures while avoiding common payment/credential leaks.""" detail = str(error) detail = re.sub( r"(?i)(authorization\s*[:=]\s*)(?:L402|Bearer|Nostr)?\s*[^\s,;]+", r"\1[REDACTED]", detail, ) detail = re.sub( r"(?i)\b(?:lnbc|lntb|lnbcrt)[0-9a-z]+\b", "[REDACTED_INVOICE]", detail ) detail = re.sub( r"(?i)(macaroon\s*[:=]\s*)[^\s,;]+", r"\1[REDACTED]", detail, ) detail = re.sub(r"\b[0-9a-fA-F]{64}\b", "[REDACTED_HEX]", detail) return detail def _run_paid_workflow( client: BitcoinStratigraphyClient, proof_arguments: dict[str, Any], ) -> None: if not os.environ.get("OPENAI_API_KEY"): raise ValueError("Set OPENAI_API_KEY before using --run.") from crewai import Agent, Crew, Process, Task (get_tool, verify_tool), used_tools, tool_errors = _build_paid_tools( client, proof_arguments ) data_agent = Agent( role="Bitcoin stratigraphy analyst", goal="Retrieve the requested indexed telemetry using the approved tool.", backstory="A cautious analyst that uses only explicitly authorized data tools.", tools=[get_tool], llm=os.environ.get("OPENAI_MODEL", "gpt-4.1-mini"), verbose=False, max_iter=4, allow_delegation=False, ) verify_agent = Agent( role="ZK proof verifier", goal="Verify only the caller-supplied proof using the approved tool.", backstory="A careful verifier that never fabricates proof inputs.", tools=[verify_tool], llm=os.environ.get("OPENAI_MODEL", "gpt-4.1-mini"), verbose=False, max_iter=4, allow_delegation=False, ) data_task = Task( description=( "Call stratigraphy_get exactly once for the latest indexed day (days=1). " "Report the returned result and do not call any other tool." ), expected_output="The result returned by stratigraphy.get.", agent=data_agent, ) proof_task = Task( description=( "Call proof_verify exactly once with confirm=true. The wrapper supplies " "the exact caller-provided proof.verify arguments from --proof-file. " "Do not create, change, or infer proof fields." ), expected_output="The result returned by proof.verify.", agent=verify_agent, ) crew = Crew( agents=[data_agent, verify_agent], tasks=[data_task, proof_task], process=Process.sequential, ) result = crew.kickoff() if tool_errors: raise RuntimeError("; ".join(tool_errors)) if not {"stratigraphy.get", "proof.verify"}.issubset(used_tools): missing = sorted({"stratigraphy.get", "proof.verify"} - used_tools) raise RuntimeError( "Crew completed without executing required tool(s): " + ", ".join(missing) ) print(result) def _discover(client: BitcoinStratigraphyClient) -> None: tools = validate_catalog(client.list_tools()) print("MCP tool catalog (validated):") print(json.dumps([tool.get("name") for tool in tools], indent=2)) response = client.request("GET", "/api/v1/cost") response.raise_for_status() print("Free REST cost/schema discovery:") print(json.dumps(response.json(), indent=2, ensure_ascii=False)) def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description=__doc__) add_cli_arguments(parser) parser.add_argument( "--run", action="store_true", help="Run paid stratigraphy.get and proof.verify CrewAI tasks.", ) parser.add_argument( "--proof-file", help="JSON file containing real proof.verify arguments (required with --run).", ) parser.add_argument( "--native-discovery", action="store_true", help="Also construct a free-discovery-only native MCPServerHTTP agent.", ) args = parser.parse_args(argv) if args.run: if not args.proof_file: parser.error("--run requires --proof-file with real proof.verify arguments") if not os.environ.get("OPENAI_API_KEY"): parser.error("--run requires OPENAI_API_KEY") if not args.pay_hook: parser.error("--run requires --pay-hook module:function") elif args.pay_hook: parser.error("--pay-hook is only used with --run; default discovery is free") try: proof_arguments = _read_proof_arguments(args.proof_file) if args.run else None except ValueError as exc: parser.error(str(exc)) try: with client_from_args(args) as client: _discover(client) if args.native_discovery: agent = build_native_discovery_agent(args.url) print( "Constructed native CrewAI MCPServerHTTP discovery agent " f"for {args.url}; only stratigraphy.list is allowed." ) del agent if args.run: assert proof_arguments is not None _run_paid_workflow(client, proof_arguments) return 0 except Exception as exc: print(f"CrewAI example failed: {_safe_error_detail(exc)}", file=sys.stderr) return 1 if __name__ == "__main__": raise SystemExit(main())