Files
grist-mcp-server/src/grist_mcp/server.py
Bill Ballou 33bb464102 feat: add label parameter to add_column and modify_column tools
Allow setting a human-readable display label for columns, separate from
the column_id used in formulas and API calls. The label defaults to the
column_id if not provided.
2026-01-26 15:18:11 -05:00

396 lines
16 KiB
Python

"""MCP server setup and tool registration."""
import json
import time
from mcp.server import Server
from mcp.types import Tool, TextContent
from grist_mcp.auth import Authenticator, Agent, AuthError
from grist_mcp.session import SessionTokenManager
from grist_mcp.tools.session import get_proxy_documentation as _get_proxy_documentation
from grist_mcp.tools.session import request_session_token as _request_session_token
from grist_mcp.logging import get_logger, extract_stats, format_tool_log
logger = get_logger("server")
from grist_mcp.tools.discovery import list_documents as _list_documents
from grist_mcp.tools.read import list_tables as _list_tables
from grist_mcp.tools.read import describe_table as _describe_table
from grist_mcp.tools.read import get_records as _get_records
from grist_mcp.tools.read import sql_query as _sql_query
from grist_mcp.tools.write import add_records as _add_records
from grist_mcp.tools.write import update_records as _update_records
from grist_mcp.tools.write import delete_records as _delete_records
from grist_mcp.tools.schema import create_table as _create_table
from grist_mcp.tools.schema import add_column as _add_column
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,
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]:
return [
Tool(
name="list_documents",
description="List documents this agent can access with their permissions",
inputSchema={"type": "object", "properties": {}, "required": []},
),
Tool(
name="list_tables",
description="List all tables in a document",
inputSchema={
"type": "object",
"properties": {"document": {"type": "string", "description": "Document name"}},
"required": ["document"],
},
),
Tool(
name="describe_table",
description="Get column information for a table",
inputSchema={
"type": "object",
"properties": {
"document": {"type": "string"},
"table": {"type": "string"},
},
"required": ["document", "table"],
},
),
Tool(
name="get_records",
description="Fetch records from a table",
inputSchema={
"type": "object",
"properties": {
"document": {"type": "string"},
"table": {"type": "string"},
"filter": {"type": "object"},
"sort": {"type": "string"},
"limit": {"type": "integer"},
},
"required": ["document", "table"],
},
),
Tool(
name="sql_query",
description="Run a read-only SQL query against a document",
inputSchema={
"type": "object",
"properties": {
"document": {"type": "string"},
"query": {"type": "string"},
},
"required": ["document", "query"],
},
),
Tool(
name="add_records",
description="Add records to a table",
inputSchema={
"type": "object",
"properties": {
"document": {"type": "string"},
"table": {"type": "string"},
"records": {"type": "array", "items": {"type": "object"}},
},
"required": ["document", "table", "records"],
},
),
Tool(
name="update_records",
description="Update existing records",
inputSchema={
"type": "object",
"properties": {
"document": {"type": "string"},
"table": {"type": "string"},
"records": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {"type": "integer"},
"fields": {"type": "object"},
},
},
},
},
"required": ["document", "table", "records"],
},
),
Tool(
name="delete_records",
description="Delete records by ID",
inputSchema={
"type": "object",
"properties": {
"document": {"type": "string"},
"table": {"type": "string"},
"record_ids": {"type": "array", "items": {"type": "integer"}},
},
"required": ["document", "table", "record_ids"],
},
),
Tool(
name="create_table",
description="Create a new table with columns",
inputSchema={
"type": "object",
"properties": {
"document": {"type": "string"},
"table_id": {"type": "string"},
"columns": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {"type": "string"},
"type": {"type": "string"},
},
},
},
},
"required": ["document", "table_id", "columns"],
},
),
Tool(
name="add_column",
description="Add a column to a table",
inputSchema={
"type": "object",
"properties": {
"document": {"type": "string"},
"table": {"type": "string"},
"column_id": {"type": "string"},
"column_type": {"type": "string"},
"formula": {"type": "string"},
"label": {"type": "string", "description": "Display label for the column"},
},
"required": ["document", "table", "column_id", "column_type"],
},
),
Tool(
name="modify_column",
description="Modify a column's type, formula, or label",
inputSchema={
"type": "object",
"properties": {
"document": {"type": "string"},
"table": {"type": "string"},
"column_id": {"type": "string"},
"type": {"type": "string"},
"formula": {"type": "string"},
"label": {"type": "string", "description": "Display label for the column"},
},
"required": ["document", "table", "column_id"],
},
),
Tool(
name="delete_column",
description="Delete a column from a table",
inputSchema={
"type": "object",
"properties": {
"document": {"type": "string"},
"table": {"type": "string"},
"column_id": {"type": "string"},
},
"required": ["document", "table", "column_id"],
},
),
Tool(
name="get_proxy_documentation",
description="Get complete documentation for the HTTP proxy API",
inputSchema={"type": "object", "properties": {}, "required": []},
),
Tool(
name="request_session_token",
description="Request a short-lived token for direct HTTP API access. Use this to delegate bulk data operations to scripts.",
inputSchema={
"type": "object",
"properties": {
"document": {
"type": "string",
"description": "Document name to grant access to",
},
"permissions": {
"type": "array",
"items": {"type": "string", "enum": ["read", "write", "schema"]},
"description": "Permission levels to grant",
},
"ttl_seconds": {
"type": "integer",
"description": "Token lifetime in seconds (max 3600, default 300)",
},
},
"required": ["document", "permissions"],
},
),
]
@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
start_time = time.time()
document = arguments.get("document")
# Log arguments at DEBUG level
logger.debug(
format_tool_log(
agent_name=_current_agent.name,
token=_current_agent.token,
tool=name,
document=document,
stats=f"args: {json.dumps(arguments)}",
status="started",
duration_ms=0,
)
)
try:
if name == "list_documents":
result = await _list_documents(_current_agent)
elif name == "list_tables":
result = await _list_tables(_current_agent, auth, arguments["document"])
elif name == "describe_table":
result = await _describe_table(
_current_agent, auth, arguments["document"], arguments["table"]
)
elif name == "get_records":
result = await _get_records(
_current_agent, auth, arguments["document"], arguments["table"],
filter=arguments.get("filter"),
sort=arguments.get("sort"),
limit=arguments.get("limit"),
)
elif name == "sql_query":
result = await _sql_query(
_current_agent, auth, arguments["document"], arguments["query"]
)
elif name == "add_records":
result = await _add_records(
_current_agent, auth, arguments["document"], arguments["table"],
arguments["records"],
)
elif name == "update_records":
result = await _update_records(
_current_agent, auth, arguments["document"], arguments["table"],
arguments["records"],
)
elif name == "delete_records":
result = await _delete_records(
_current_agent, auth, arguments["document"], arguments["table"],
arguments["record_ids"],
)
elif name == "create_table":
result = await _create_table(
_current_agent, auth, arguments["document"], arguments["table_id"],
arguments["columns"],
)
elif name == "add_column":
result = await _add_column(
_current_agent, auth, arguments["document"], arguments["table"],
arguments["column_id"], arguments["column_type"],
formula=arguments.get("formula"),
label=arguments.get("label"),
)
elif name == "modify_column":
result = await _modify_column(
_current_agent, auth, arguments["document"], arguments["table"],
arguments["column_id"],
type=arguments.get("type"),
formula=arguments.get("formula"),
label=arguments.get("label"),
)
elif name == "delete_column":
result = await _delete_column(
_current_agent, auth, arguments["document"], arguments["table"],
arguments["column_id"],
)
elif name == "get_proxy_documentation":
result = await _get_proxy_documentation()
elif name == "request_session_token":
if token_manager is None:
return [TextContent(type="text", text="Session tokens not enabled")]
result = await _request_session_token(
_current_agent, auth, token_manager,
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}")]
duration_ms = int((time.time() - start_time) * 1000)
stats = extract_stats(name, arguments, result)
logger.info(
format_tool_log(
agent_name=_current_agent.name,
token=_current_agent.token,
tool=name,
document=document,
stats=stats,
status="success",
duration_ms=duration_ms,
)
)
return [TextContent(type="text", text=json.dumps(result))]
except AuthError as e:
duration_ms = int((time.time() - start_time) * 1000)
logger.warning(
format_tool_log(
agent_name=_current_agent.name,
token=_current_agent.token,
tool=name,
document=document,
stats="-",
status="auth_error",
duration_ms=duration_ms,
error_message=str(e),
)
)
return [TextContent(type="text", text=f"Authorization error: {e}")]
except Exception as e:
duration_ms = int((time.time() - start_time) * 1000)
logger.error(
format_tool_log(
agent_name=_current_agent.name,
token=_current_agent.token,
tool=name,
document=document,
stats="-",
status="error",
duration_ms=duration_ms,
error_message=str(e),
)
)
return [TextContent(type="text", text=f"Error: {e}")]
return server