feat(main): add /api/v1/proxy HTTP endpoint

This commit is contained in:
2026-01-02 14:14:15 -05:00
parent 80e93ab3d9
commit ed1d14a4d4

View File

@@ -11,6 +11,8 @@ from mcp.server.sse import SseServerTransport
from grist_mcp.server import create_server
from grist_mcp.config import Config, load_config
from grist_mcp.auth import Authenticator, AuthError
from grist_mcp.session import SessionTokenManager
from grist_mcp.proxy import parse_proxy_request, dispatch_proxy_request, ProxyError
Scope = dict[str, Any]
@@ -41,6 +43,20 @@ async def send_error(send: Send, status: int, message: str) -> None:
})
async def send_json_response(send: Send, status: int, data: dict) -> None:
"""Send a JSON response."""
body = json.dumps(data).encode()
await send({
"type": "http.response.start",
"status": status,
"headers": [[b"content-type", b"application/json"]],
})
await send({
"type": "http.response.body",
"body": body,
})
CONFIG_TEMPLATE = """\
# grist-mcp configuration
#
@@ -108,6 +124,7 @@ def _ensure_config(config_path: str) -> bool:
def create_app(config: Config):
"""Create the ASGI application."""
auth = Authenticator(config)
token_manager = SessionTokenManager()
sse = SseServerTransport("/messages")
@@ -125,7 +142,7 @@ def create_app(config: Config):
return
# Create a server instance for this authenticated connection
server = create_server(auth, agent)
server = create_server(auth, agent, token_manager)
async with sse.connect_sse(scope, receive, send) as streams:
await server.run(
@@ -157,6 +174,58 @@ def create_app(config: Config):
"body": b'{"error":"Not found"}',
})
async def handle_proxy(scope: Scope, receive: Receive, send: Send) -> None:
# Extract token
token = _get_bearer_token(scope)
if not token:
await send_json_response(send, 401, {
"success": False,
"error": "Missing Authorization header",
"code": "INVALID_TOKEN",
})
return
# Validate session token
session = token_manager.validate_token(token)
if session is None:
await send_json_response(send, 401, {
"success": False,
"error": "Invalid or expired token",
"code": "TOKEN_EXPIRED",
})
return
# Read request body
body = b""
while True:
message = await receive()
body += message.get("body", b"")
if not message.get("more_body", False):
break
try:
request_data = json.loads(body)
except json.JSONDecodeError:
await send_json_response(send, 400, {
"success": False,
"error": "Invalid JSON",
"code": "INVALID_REQUEST",
})
return
# Parse and dispatch
try:
request = parse_proxy_request(request_data)
result = await dispatch_proxy_request(request, session, auth)
await send_json_response(send, 200, result)
except ProxyError as e:
status = 403 if e.code == "UNAUTHORIZED" else 400
await send_json_response(send, status, {
"success": False,
"error": e.message,
"code": e.code,
})
async def app(scope: Scope, receive: Receive, send: Send) -> None:
if scope["type"] != "http":
return
@@ -170,6 +239,8 @@ def create_app(config: Config):
await handle_sse(scope, receive, send)
elif path == "/messages" and method == "POST":
await handle_messages(scope, receive, send)
elif path == "/api/v1/proxy" and method == "POST":
await handle_proxy(scope, receive, send)
else:
await handle_not_found(scope, receive, send)