Add MCP tool for agents to request short-lived session tokens for HTTP proxy access. The tool validates that agents can only request permissions they already have (no privilege escalation). - Validates document access and each requested permission - Creates session token via SessionTokenManager - Returns token metadata including proxy URL and expiration - Includes tests for success case and permission denial scenarios
149 lines
4.6 KiB
Python
149 lines
4.6 KiB
Python
"""Session token tools for HTTP proxy access."""
|
|
|
|
from grist_mcp.auth import Agent, Authenticator, AuthError, Permission
|
|
from grist_mcp.session import SessionTokenManager
|
|
|
|
|
|
PROXY_DOCUMENTATION = {
|
|
"description": "HTTP proxy API for bulk data operations. Use request_session_token to get a short-lived token, then call the proxy endpoint directly from scripts.",
|
|
"endpoint": "POST /api/v1/proxy",
|
|
"authentication": "Bearer token in Authorization header",
|
|
"request_format": {
|
|
"method": "Operation name (required)",
|
|
"table": "Table name (required for most operations)",
|
|
},
|
|
"methods": {
|
|
"get_records": {
|
|
"description": "Fetch records from a table",
|
|
"fields": {
|
|
"table": "string",
|
|
"filter": "object (optional)",
|
|
"sort": "string (optional)",
|
|
"limit": "integer (optional)",
|
|
},
|
|
},
|
|
"sql_query": {
|
|
"description": "Run a read-only SQL query",
|
|
"fields": {"query": "string"},
|
|
},
|
|
"list_tables": {
|
|
"description": "List all tables in the document",
|
|
"fields": {},
|
|
},
|
|
"describe_table": {
|
|
"description": "Get column information for a table",
|
|
"fields": {"table": "string"},
|
|
},
|
|
"add_records": {
|
|
"description": "Add records to a table",
|
|
"fields": {"table": "string", "records": "array of objects"},
|
|
},
|
|
"update_records": {
|
|
"description": "Update existing records",
|
|
"fields": {"table": "string", "records": "array of {id, fields}"},
|
|
},
|
|
"delete_records": {
|
|
"description": "Delete records by ID",
|
|
"fields": {"table": "string", "record_ids": "array of integers"},
|
|
},
|
|
"create_table": {
|
|
"description": "Create a new table",
|
|
"fields": {"table_id": "string", "columns": "array of {id, type}"},
|
|
},
|
|
"add_column": {
|
|
"description": "Add a column to a table",
|
|
"fields": {
|
|
"table": "string",
|
|
"column_id": "string",
|
|
"column_type": "string",
|
|
"formula": "string (optional)",
|
|
},
|
|
},
|
|
"modify_column": {
|
|
"description": "Modify a column's type or formula",
|
|
"fields": {
|
|
"table": "string",
|
|
"column_id": "string",
|
|
"type": "string (optional)",
|
|
"formula": "string (optional)",
|
|
},
|
|
},
|
|
"delete_column": {
|
|
"description": "Delete a column",
|
|
"fields": {"table": "string", "column_id": "string"},
|
|
},
|
|
},
|
|
"response_format": {
|
|
"success": {"success": True, "data": "..."},
|
|
"error": {"success": False, "error": "message", "code": "ERROR_CODE"},
|
|
},
|
|
"error_codes": [
|
|
"UNAUTHORIZED",
|
|
"INVALID_TOKEN",
|
|
"TOKEN_EXPIRED",
|
|
"INVALID_REQUEST",
|
|
"GRIST_ERROR",
|
|
],
|
|
"example_script": """#!/usr/bin/env python3
|
|
import requests
|
|
import sys
|
|
|
|
token = sys.argv[1]
|
|
host = sys.argv[2]
|
|
|
|
response = requests.post(
|
|
f'{host}/api/v1/proxy',
|
|
headers={'Authorization': f'Bearer {token}'},
|
|
json={
|
|
'method': 'add_records',
|
|
'table': 'Orders',
|
|
'records': [{'item': 'Widget', 'qty': 100}]
|
|
}
|
|
)
|
|
print(response.json())
|
|
""",
|
|
}
|
|
|
|
|
|
async def get_proxy_documentation() -> dict:
|
|
"""Return complete documentation for the HTTP proxy API."""
|
|
return PROXY_DOCUMENTATION
|
|
|
|
|
|
async def request_session_token(
|
|
agent: Agent,
|
|
auth: Authenticator,
|
|
token_manager: SessionTokenManager,
|
|
document: str,
|
|
permissions: list[str],
|
|
ttl_seconds: int = 300,
|
|
) -> dict:
|
|
"""Request a short-lived session token for HTTP proxy access.
|
|
|
|
The token can only grant permissions the agent already has.
|
|
"""
|
|
# Verify agent has access to the document
|
|
# Check each requested permission
|
|
for perm_str in permissions:
|
|
try:
|
|
perm = Permission(perm_str)
|
|
except ValueError:
|
|
raise AuthError(f"Invalid permission: {perm_str}")
|
|
auth.authorize(agent, document, perm)
|
|
|
|
# Create the session token
|
|
session = token_manager.create_token(
|
|
agent_name=agent.name,
|
|
document=document,
|
|
permissions=permissions,
|
|
ttl_seconds=ttl_seconds,
|
|
)
|
|
|
|
return {
|
|
"token": session.token,
|
|
"document": session.document,
|
|
"permissions": session.permissions,
|
|
"expires_at": session.expires_at.isoformat(),
|
|
"proxy_url": "/api/v1/proxy",
|
|
}
|