Compare commits
4 Commits
v1.2.0-alp
...
v1.2.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
| d540105d09 | |||
| d40ae0b238 | |||
| 2a60de1bf1 | |||
| ba45de4582 |
@@ -150,6 +150,7 @@ Add to your MCP client configuration (e.g., Claude Desktop):
|
|||||||
| `GRIST_MCP_TOKEN` | Agent authentication token (required) | - |
|
| `GRIST_MCP_TOKEN` | Agent authentication token (required) | - |
|
||||||
| `CONFIG_PATH` | Path to config file inside container | `/app/config.yaml` |
|
| `CONFIG_PATH` | Path to config file inside container | `/app/config.yaml` |
|
||||||
| `LOG_LEVEL` | Logging verbosity (`DEBUG`, `INFO`, `WARNING`, `ERROR`) | `INFO` |
|
| `LOG_LEVEL` | Logging verbosity (`DEBUG`, `INFO`, `WARNING`, `ERROR`) | `INFO` |
|
||||||
|
| `GRIST_MCP_URL` | Public URL of this server (for session proxy tokens) | - |
|
||||||
|
|
||||||
### config.yaml Structure
|
### config.yaml Structure
|
||||||
|
|
||||||
|
|||||||
@@ -127,6 +127,7 @@ def create_app(config: Config):
|
|||||||
"""Create the ASGI application."""
|
"""Create the ASGI application."""
|
||||||
auth = Authenticator(config)
|
auth = Authenticator(config)
|
||||||
token_manager = SessionTokenManager()
|
token_manager = SessionTokenManager()
|
||||||
|
proxy_base_url = os.environ.get("GRIST_MCP_URL")
|
||||||
|
|
||||||
sse = SseServerTransport("/messages")
|
sse = SseServerTransport("/messages")
|
||||||
|
|
||||||
@@ -144,7 +145,7 @@ def create_app(config: Config):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Create a server instance for this authenticated connection
|
# 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:
|
async with sse.connect_sse(scope, receive, send) as streams:
|
||||||
await server.run(
|
await server.run(
|
||||||
@@ -251,11 +252,18 @@ def create_app(config: Config):
|
|||||||
|
|
||||||
def _print_mcp_config(external_port: int, tokens: list) -> None:
|
def _print_mcp_config(external_port: int, tokens: list) -> None:
|
||||||
"""Print Claude Code MCP configuration."""
|
"""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()
|
||||||
print("Claude Code MCP configuration (copy-paste to add):")
|
print("Claude Code MCP configuration (copy-paste to add):")
|
||||||
for t in tokens:
|
for t in tokens:
|
||||||
config = (
|
config = (
|
||||||
f'{{"type": "sse", "url": "http://localhost:{external_port}/sse", '
|
f'{{"type": "sse", "url": "{sse_url}", '
|
||||||
f'"headers": {{"Authorization": "Bearer {t.token}"}}}}'
|
f'"headers": {{"Authorization": "Bearer {t.token}"}}}}'
|
||||||
)
|
)
|
||||||
print(f" claude mcp add-json grist-{t.name} '{config}'")
|
print(f" claude mcp add-json grist-{t.name} '{config}'")
|
||||||
|
|||||||
@@ -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
|
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.
|
"""Create and configure the MCP server for an authenticated agent.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
auth: Authenticator instance for permission checks.
|
auth: Authenticator instance for permission checks.
|
||||||
agent: The authenticated agent for this server instance.
|
agent: The authenticated agent for this server instance.
|
||||||
token_manager: Optional session token manager for HTTP proxy access.
|
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:
|
Returns:
|
||||||
Configured MCP Server instance.
|
Configured MCP Server instance.
|
||||||
"""
|
"""
|
||||||
server = Server("grist-mcp")
|
server = Server("grist-mcp")
|
||||||
_current_agent = agent
|
_current_agent = agent
|
||||||
|
_proxy_base_url = proxy_base_url
|
||||||
|
|
||||||
@server.list_tools()
|
@server.list_tools()
|
||||||
async def list_tools() -> list[Tool]:
|
async def list_tools() -> list[Tool]:
|
||||||
@@ -327,6 +334,7 @@ def create_server(auth: Authenticator, agent: Agent, token_manager: SessionToken
|
|||||||
arguments["document"],
|
arguments["document"],
|
||||||
arguments["permissions"],
|
arguments["permissions"],
|
||||||
ttl_seconds=arguments.get("ttl_seconds", 300),
|
ttl_seconds=arguments.get("ttl_seconds", 300),
|
||||||
|
proxy_base_url=_proxy_base_url,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return [TextContent(type="text", text=f"Unknown tool: {name}")]
|
return [TextContent(type="text", text=f"Unknown tool: {name}")]
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from grist_mcp.session import SessionTokenManager
|
|||||||
PROXY_DOCUMENTATION = {
|
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.",
|
"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": "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",
|
"authentication": "Bearer token in Authorization header",
|
||||||
"request_format": {
|
"request_format": {
|
||||||
"method": "Operation name (required)",
|
"method": "Operation name (required)",
|
||||||
@@ -88,11 +89,12 @@ PROXY_DOCUMENTATION = {
|
|||||||
import requests
|
import requests
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
# Use token and proxy_url from request_session_token response
|
||||||
token = sys.argv[1]
|
token = sys.argv[1]
|
||||||
host = sys.argv[2]
|
proxy_url = sys.argv[2]
|
||||||
|
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
f'{host}/api/v1/proxy',
|
proxy_url,
|
||||||
headers={'Authorization': f'Bearer {token}'},
|
headers={'Authorization': f'Bearer {token}'},
|
||||||
json={
|
json={
|
||||||
'method': 'add_records',
|
'method': 'add_records',
|
||||||
@@ -117,6 +119,7 @@ async def request_session_token(
|
|||||||
document: str,
|
document: str,
|
||||||
permissions: list[str],
|
permissions: list[str],
|
||||||
ttl_seconds: int = 300,
|
ttl_seconds: int = 300,
|
||||||
|
proxy_base_url: str | None = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Request a short-lived session token for HTTP proxy access.
|
"""Request a short-lived session token for HTTP proxy access.
|
||||||
|
|
||||||
@@ -139,10 +142,17 @@ async def request_session_token(
|
|||||||
ttl_seconds=ttl_seconds,
|
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 {
|
return {
|
||||||
"token": session.token,
|
"token": session.token,
|
||||||
"document": session.document,
|
"document": session.document,
|
||||||
"permissions": session.permissions,
|
"permissions": session.permissions,
|
||||||
"expires_at": session.expires_at.isoformat(),
|
"expires_at": session.expires_at.isoformat(),
|
||||||
"proxy_url": "/api/v1/proxy",
|
"proxy_url": proxy_url,
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user