xsecuremcp / src /fastmcp /server /contract_routes.py
svkrishna
🤖 Implement Story 2: Inter-Agent Contract Module
4d56236
Raw
History Blame Contribute Delete
13 kB
"""Contract management HTTP routes."""
from typing import Any, Dict, List, Optional
from uuid import UUID
from starlette.requests import Request
from starlette.responses import JSONResponse
from starlette.routing import Route
from fastmcp.contracts import ContractEngine, ContractState
from fastmcp.contracts.contract import (
ContractCreateRequest, ContractProposeRequest, ContractSignRequest,
ContractRevokeRequest, ContractResponse
)
from fastmcp.utilities.logging import get_logger
logger = get_logger(__name__)
async def create_contract_endpoint(request: Request) -> JSONResponse:
"""HTTP endpoint for creating contracts.
Expected JSON body:
{
"title": "Contract Title",
"description": "Contract Description",
"clauses": [...],
"parties": [...],
"is_hipaa_compliant": false,
"expires_at": "2024-12-31T23:59:59Z"
}
"""
try:
# Parse request body
body = await request.json()
# Get contract engine from request state
contract_engine: ContractEngine = request.app.state.contract_engine
# Get creator from headers or body
created_by = request.headers.get("X-User-ID") or body.get("created_by", "anonymous")
# Create contract request
contract_request = ContractCreateRequest(**body)
# Create contract
contract = await contract_engine.create_contract(contract_request, created_by)
# Return contract response
response = ContractResponse.from_contract(contract)
return JSONResponse(
status_code=201,
content=response.model_dump()
)
except Exception as e:
logger.error(f"Contract creation error: {e}")
return JSONResponse(
status_code=400,
content={
"error": "Contract creation failed",
"reason": str(e)
}
)
async def get_contract_endpoint(request: Request) -> JSONResponse:
"""HTTP endpoint for getting a contract by ID."""
try:
# Get contract ID from path
contract_id = UUID(request.path_params["id"])
# Get contract engine from request state
contract_engine: ContractEngine = request.app.state.contract_engine
# Get contract
contract = await contract_engine.get_contract(contract_id)
if not contract:
return JSONResponse(
status_code=404,
content={
"error": "Contract not found",
"contract_id": str(contract_id)
}
)
# Return contract response
response = ContractResponse.from_contract(contract)
return JSONResponse(
status_code=200,
content=response.model_dump()
)
except ValueError as e:
return JSONResponse(
status_code=400,
content={
"error": "Invalid contract ID",
"reason": str(e)
}
)
except Exception as e:
logger.error(f"Contract retrieval error: {e}")
return JSONResponse(
status_code=500,
content={
"error": "Contract retrieval failed",
"reason": str(e)
}
)
async def list_contracts_endpoint(request: Request) -> JSONResponse:
"""HTTP endpoint for listing contracts."""
try:
# Get query parameters
state = request.query_params.get("state")
created_by = request.query_params.get("created_by")
# Get contract engine from request state
contract_engine: ContractEngine = request.app.state.contract_engine
# Parse state if provided
contract_state = None
if state:
try:
contract_state = ContractState(state)
except ValueError:
return JSONResponse(
status_code=400,
content={
"error": "Invalid state",
"valid_states": [s.value for s in ContractState]
}
)
# List contracts
contracts = await contract_engine.list_contracts(contract_state, created_by)
# Convert to response format
responses = [ContractResponse.from_contract(contract).model_dump() for contract in contracts]
return JSONResponse(
status_code=200,
content={
"contracts": responses,
"count": len(responses)
}
)
except Exception as e:
logger.error(f"Contract listing error: {e}")
return JSONResponse(
status_code=500,
content={
"error": "Contract listing failed",
"reason": str(e)
}
)
async def propose_contract_endpoint(request: Request) -> JSONResponse:
"""HTTP endpoint for proposing contracts.
Expected JSON body:
{
"proposed_to": ["party1", "party2"],
"message": "Please review and sign this contract"
}
"""
try:
# Get contract ID from path
contract_id = UUID(request.path_params["id"])
# Parse request body
body = await request.json()
# Get contract engine from request state
contract_engine: ContractEngine = request.app.state.contract_engine
# Get proposer from headers or body
proposed_by = request.headers.get("X-User-ID") or body.get("proposed_by", "anonymous")
# Create proposal request
proposal_request = ContractProposeRequest(**body)
# Propose contract
contract = await contract_engine.propose_contract(contract_id, proposal_request, proposed_by)
if not contract:
return JSONResponse(
status_code=404,
content={
"error": "Contract not found",
"contract_id": str(contract_id)
}
)
# Return contract response
response = ContractResponse.from_contract(contract)
return JSONResponse(
status_code=200,
content=response.model_dump()
)
except ValueError as e:
return JSONResponse(
status_code=400,
content={
"error": "Invalid request",
"reason": str(e)
}
)
except Exception as e:
logger.error(f"Contract proposal error: {e}")
return JSONResponse(
status_code=500,
content={
"error": "Contract proposal failed",
"reason": str(e)
}
)
async def sign_contract_endpoint(request: Request) -> JSONResponse:
"""HTTP endpoint for signing contracts.
Expected JSON body:
{
"signer_id": "party1",
"signer_type": "provider",
"public_key": "base64_public_key",
"signature": "base64_signature"
}
"""
try:
# Get contract ID from path
contract_id = UUID(request.path_params["id"])
# Parse request body
body = await request.json()
# Get contract engine from request state
contract_engine: ContractEngine = request.app.state.contract_engine
# Create signing request
sign_request = ContractSignRequest(**body)
# Sign contract
contract = await contract_engine.sign_contract(contract_id, sign_request)
if not contract:
return JSONResponse(
status_code=404,
content={
"error": "Contract not found",
"contract_id": str(contract_id)
}
)
# Return contract response
response = ContractResponse.from_contract(contract)
return JSONResponse(
status_code=200,
content=response.model_dump()
)
except ValueError as e:
return JSONResponse(
status_code=400,
content={
"error": "Invalid request",
"reason": str(e)
}
)
except Exception as e:
logger.error(f"Contract signing error: {e}")
return JSONResponse(
status_code=500,
content={
"error": "Contract signing failed",
"reason": str(e)
}
)
async def revoke_contract_endpoint(request: Request) -> JSONResponse:
"""HTTP endpoint for revoking contracts.
Expected JSON body:
{
"reason": "Contract terms violated",
"revoked_by": "party1"
}
"""
try:
# Get contract ID from path
contract_id = UUID(request.path_params["id"])
# Parse request body
body = await request.json()
# Get contract engine from request state
contract_engine: ContractEngine = request.app.state.contract_engine
# Create revocation request
revoke_request = ContractRevokeRequest(**body)
# Revoke contract
contract = await contract_engine.revoke_contract(contract_id, revoke_request)
if not contract:
return JSONResponse(
status_code=404,
content={
"error": "Contract not found",
"contract_id": str(contract_id)
}
)
# Return contract response
response = ContractResponse.from_contract(contract)
return JSONResponse(
status_code=200,
content=response.model_dump()
)
except ValueError as e:
return JSONResponse(
status_code=400,
content={
"error": "Invalid request",
"reason": str(e)
}
)
except Exception as e:
logger.error(f"Contract revocation error: {e}")
return JSONResponse(
status_code=500,
content={
"error": "Contract revocation failed",
"reason": str(e)
}
)
async def get_contract_statistics_endpoint(request: Request) -> JSONResponse:
"""HTTP endpoint for getting contract statistics."""
try:
# Get contract engine from request state
contract_engine: ContractEngine = request.app.state.contract_engine
# Get statistics
stats = await contract_engine.get_contract_statistics()
return JSONResponse(
status_code=200,
content=stats
)
except Exception as e:
logger.error(f"Contract statistics error: {e}")
return JSONResponse(
status_code=500,
content={
"error": "Contract statistics failed",
"reason": str(e)
}
)
def create_contract_routes(contract_engine: ContractEngine) -> List[Route]:
"""Create contract management routes.
Args:
contract_engine: The contract engine instance
Returns:
List of Starlette Route objects for contract management
"""
def endpoint_with_engine(endpoint_func):
async def wrapper(request: Request) -> JSONResponse:
# Store contract engine in app state for access in endpoint
request.app.state.contract_engine = contract_engine
return await endpoint_func(request)
return wrapper
return [
Route(
path="/contracts",
endpoint=endpoint_with_engine(create_contract_endpoint),
methods=["POST"]
),
Route(
path="/contracts",
endpoint=endpoint_with_engine(list_contracts_endpoint),
methods=["GET"]
),
Route(
path="/contracts/statistics",
endpoint=endpoint_with_engine(get_contract_statistics_endpoint),
methods=["GET"]
),
Route(
path="/contracts/{id}",
endpoint=endpoint_with_engine(get_contract_endpoint),
methods=["GET"]
),
Route(
path="/contracts/{id}/propose",
endpoint=endpoint_with_engine(propose_contract_endpoint),
methods=["POST"]
),
Route(
path="/contracts/{id}/sign",
endpoint=endpoint_with_engine(sign_contract_endpoint),
methods=["POST"]
),
Route(
path="/contracts/{id}/revoke",
endpoint=endpoint_with_engine(revoke_contract_endpoint),
methods=["POST"]
)
]