4 Commits

Author SHA1 Message Date
d540105d09 docs(proxy): clarify proxy_url usage in documentation
All checks were successful
Build and Push Docker Image / build (push) Successful in 21s
2026-01-02 15:01:33 -05:00
d40ae0b238 feat(main): use GRIST_MCP_URL in startup config output 2026-01-02 14:58:55 -05:00
2a60de1bf1 docs: add GRIST_MCP_URL to environment variables 2026-01-02 14:56:02 -05:00
ba45de4582 fix(session): include full proxy URL from GRIST_MCP_URL env var 2026-01-02 14:54:25 -05:00
5 changed files with 34 additions and 7 deletions

View File

@@ -150,6 +150,7 @@ Add to your MCP client configuration (e.g., Claude Desktop):
| `GRIST_MCP_TOKEN` | Agent authentication token (required) | - |
| `CONFIG_PATH` | Path to config file inside container | `/app/config.yaml` |
| `LOG_LEVEL` | Logging verbosity (`DEBUG`, `INFO`, `WARNING`, `ERROR`) | `INFO` |
| `GRIST_MCP_URL` | Public URL of this server (for session proxy tokens) | - |
### config.yaml Structure

View File

@@ -127,6 +127,7 @@ def create_app(config: Config):
"""Create the ASGI application."""
auth = Authenticator(config)
token_manager = SessionTokenManager()
proxy_base_url = os.environ.get("GRIST_MCP_URL")
sse = SseServerTransport("/messages")
@@ -144,7 +145,7 @@ def create_app(config: Config):
return
# Create a server instance for this authenticated connection
server = create_server(auth, agent, token_manager)
server = create_server(auth, agent, token_manager, proxy_base_url)
async with sse.connect_sse(scope, receive, send) as streams:
await server.run(
@@ -251,11 +252,18 @@ def create_app(config: Config):
def _print_mcp_config(external_port: int, tokens: list) -> None:
"""Print Claude Code MCP configuration."""
# Use GRIST_MCP_URL if set, otherwise fall back to localhost
base_url = os.environ.get("GRIST_MCP_URL")
if base_url:
sse_url = f"{base_url.rstrip('/')}/sse"
else:
sse_url = f"http://localhost:{external_port}/sse"
print()
print("Claude Code MCP configuration (copy-paste to add):")
for t in tokens:
config = (
f'{{"type": "sse", "url": "http://localhost:{external_port}/sse", '
f'{{"type": "sse", "url": "{sse_url}", '
f'"headers": {{"Authorization": "Bearer {t.token}"}}}}'
)
print(f" claude mcp add-json grist-{t.name} '{config}'")

View File

@@ -28,19 +28,26 @@ from grist_mcp.tools.schema import modify_column as _modify_column
from grist_mcp.tools.schema import delete_column as _delete_column
def create_server(auth: Authenticator, agent: Agent, token_manager: SessionTokenManager | None = None) -> Server:
def create_server(
auth: Authenticator,
agent: Agent,
token_manager: SessionTokenManager | None = None,
proxy_base_url: str | None = None,
) -> Server:
"""Create and configure the MCP server for an authenticated agent.
Args:
auth: Authenticator instance for permission checks.
agent: The authenticated agent for this server instance.
token_manager: Optional session token manager for HTTP proxy access.
proxy_base_url: Base URL for the proxy endpoint (e.g., "https://example.com").
Returns:
Configured MCP Server instance.
"""
server = Server("grist-mcp")
_current_agent = agent
_proxy_base_url = proxy_base_url
@server.list_tools()
async def list_tools() -> list[Tool]:
@@ -327,6 +334,7 @@ def create_server(auth: Authenticator, agent: Agent, token_manager: SessionToken
arguments["document"],
arguments["permissions"],
ttl_seconds=arguments.get("ttl_seconds", 300),
proxy_base_url=_proxy_base_url,
)
else:
return [TextContent(type="text", text=f"Unknown tool: {name}")]

View File

@@ -7,6 +7,7 @@ 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",
"endpoint_note": "The full URL is returned in the 'proxy_url' field of request_session_token response",
"authentication": "Bearer token in Authorization header",
"request_format": {
"method": "Operation name (required)",
@@ -88,11 +89,12 @@ PROXY_DOCUMENTATION = {
import requests
import sys
# Use token and proxy_url from request_session_token response
token = sys.argv[1]
host = sys.argv[2]
proxy_url = sys.argv[2]
response = requests.post(
f'{host}/api/v1/proxy',
proxy_url,
headers={'Authorization': f'Bearer {token}'},
json={
'method': 'add_records',
@@ -117,6 +119,7 @@ async def request_session_token(
document: str,
permissions: list[str],
ttl_seconds: int = 300,
proxy_base_url: str | None = None,
) -> dict:
"""Request a short-lived session token for HTTP proxy access.
@@ -139,10 +142,17 @@ async def request_session_token(
ttl_seconds=ttl_seconds,
)
# Build proxy URL - use base URL if provided, otherwise just path
proxy_path = "/api/v1/proxy"
if proxy_base_url:
proxy_url = f"{proxy_base_url.rstrip('/')}{proxy_path}"
else:
proxy_url = proxy_path
return {
"token": session.token,
"document": session.document,
"permissions": session.permissions,
"expires_at": session.expires_at.isoformat(),
"proxy_url": "/api/v1/proxy",
"proxy_url": proxy_url,
}

2
uv.lock generated
View File

@@ -153,7 +153,7 @@ wheels = [
[[package]]
name = "grist-mcp"
version = "1.1.0"
version = "1.2.0"
source = { editable = "." }
dependencies = [
{ name = "httpx" },