diff --git a/src/grist_mcp/tools/schema.py b/src/grist_mcp/tools/schema.py new file mode 100644 index 0000000..12e2a91 --- /dev/null +++ b/src/grist_mcp/tools/schema.py @@ -0,0 +1,84 @@ +"""Schema tools - create and modify tables and columns.""" + +from grist_mcp.auth import Agent, Authenticator, Permission +from grist_mcp.grist_client import GristClient + + +async def create_table( + agent: Agent, + auth: Authenticator, + document: str, + table_id: str, + columns: list[dict], + client: GristClient | None = None, +) -> dict: + """Create a new table with columns.""" + auth.authorize(agent, document, Permission.SCHEMA) + + if client is None: + doc = auth.get_document(document) + client = GristClient(doc) + + created_id = await client.create_table(table_id, columns) + return {"table_id": created_id} + + +async def add_column( + agent: Agent, + auth: Authenticator, + document: str, + table: str, + column_id: str, + column_type: str, + formula: str | None = None, + client: GristClient | None = None, +) -> dict: + """Add a column to a table.""" + auth.authorize(agent, document, Permission.SCHEMA) + + if client is None: + doc = auth.get_document(document) + client = GristClient(doc) + + created_id = await client.add_column(table, column_id, column_type, formula=formula) + return {"column_id": created_id} + + +async def modify_column( + agent: Agent, + auth: Authenticator, + document: str, + table: str, + column_id: str, + type: str | None = None, + formula: str | None = None, + client: GristClient | None = None, +) -> dict: + """Modify a column's type or formula.""" + auth.authorize(agent, document, Permission.SCHEMA) + + if client is None: + doc = auth.get_document(document) + client = GristClient(doc) + + await client.modify_column(table, column_id, type=type, formula=formula) + return {"modified": True} + + +async def delete_column( + agent: Agent, + auth: Authenticator, + document: str, + table: str, + column_id: str, + client: GristClient | None = None, +) -> dict: + """Delete a column from a table.""" + auth.authorize(agent, document, Permission.SCHEMA) + + if client is None: + doc = auth.get_document(document) + client = GristClient(doc) + + await client.delete_column(table, column_id) + return {"deleted": True} diff --git a/tests/test_tools_schema.py b/tests/test_tools_schema.py new file mode 100644 index 0000000..cb92b18 --- /dev/null +++ b/tests/test_tools_schema.py @@ -0,0 +1,109 @@ +import pytest +from unittest.mock import AsyncMock + +from grist_mcp.tools.schema import create_table, add_column, modify_column, delete_column +from grist_mcp.auth import Authenticator, AuthError +from grist_mcp.config import Config, Document, Token, TokenScope + + +@pytest.fixture +def config(): + return Config( + documents={ + "budget": Document( + url="https://grist.example.com", + doc_id="abc123", + api_key="key", + ), + }, + tokens=[ + Token( + token="schema-token", + name="schema-agent", + scope=[TokenScope(document="budget", permissions=["read", "write", "schema"])], + ), + Token( + token="write-token", + name="write-agent", + scope=[TokenScope(document="budget", permissions=["read", "write"])], + ), + ], + ) + + +@pytest.fixture +def auth(config): + return Authenticator(config) + + +@pytest.fixture +def mock_client(): + client = AsyncMock() + client.create_table.return_value = "NewTable" + client.add_column.return_value = "NewCol" + client.modify_column.return_value = None + client.delete_column.return_value = None + return client + + +@pytest.mark.asyncio +async def test_create_table(auth, mock_client): + agent = auth.authenticate("schema-token") + + result = await create_table( + agent, auth, "budget", "NewTable", + columns=[{"id": "Name", "type": "Text"}], + client=mock_client, + ) + + assert result == {"table_id": "NewTable"} + + +@pytest.mark.asyncio +async def test_create_table_denied_without_schema(auth, mock_client): + agent = auth.authenticate("write-token") + + with pytest.raises(AuthError, match="Permission denied"): + await create_table( + agent, auth, "budget", "NewTable", + columns=[{"id": "Name", "type": "Text"}], + client=mock_client, + ) + + +@pytest.mark.asyncio +async def test_add_column(auth, mock_client): + agent = auth.authenticate("schema-token") + + result = await add_column( + agent, auth, "budget", "Table1", "NewCol", "Text", + client=mock_client, + ) + + assert result == {"column_id": "NewCol"} + + +@pytest.mark.asyncio +async def test_modify_column(auth, mock_client): + agent = auth.authenticate("schema-token") + + result = await modify_column( + agent, auth, "budget", "Table1", "Col1", + type="Int", + formula="$A + $B", + client=mock_client, + ) + + assert result == {"modified": True} + + +@pytest.mark.asyncio +async def test_delete_column(auth, mock_client): + agent = auth.authenticate("schema-token") + + result = await delete_column( + agent, auth, "budget", "Table1", "OldCol", + client=mock_client, + ) + + assert result == {"deleted": True}