chore: update gitignore, bump to 0.2.1, apply CodeRabbit fixes

- Add .windsurf/, *.bak*, .1code/, .emdash.json to .gitignore
- Sync standard gitignore entries per project conventions
- Apply final test/tool fixes from CodeRabbit review threads
- Update GraphQL schema to latest introspection snapshot
- Bump version 0.2.0 → 0.2.1

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Jacob Magar
2026-03-13 00:53:51 -04:00
parent 2a5b19c42f
commit 06f18f32fc
16 changed files with 3294 additions and 1138 deletions

7
.gitignore vendored
View File

@@ -34,6 +34,13 @@ logs/
# IDE/Editor
.bivvy
.cursor
.windsurf/
.1code/
.emdash.json
# Backup files
*.bak
*.bak-*
# Claude Code user settings (gitignore local settings)
.claude/settings.local.json

View File

@@ -4,8 +4,8 @@ FROM python:3.12-slim
# Set the working directory in the container
WORKDIR /app
# Install uv
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /usr/local/bin/
# Install uv (pinned tag to avoid mutable latest)
COPY --from=ghcr.io/astral-sh/uv:0.5.4 /uv /uvx /usr/local/bin/
# Create non-root user with home directory and give ownership of /app
RUN groupadd --gid 1000 appuser && \
@@ -42,7 +42,7 @@ ENV UNRAID_MCP_LOG_LEVEL="INFO"
# Health check
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD ["python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:6970/mcp')"]
CMD ["python", "-c", "import os, urllib.request; port = os.getenv('UNRAID_MCP_PORT', '6970'); urllib.request.urlopen(f'http://localhost:{port}/mcp')"]
# Run unraid-mcp-server when the container launches
CMD ["uv", "run", "unraid-mcp-server"]

View File

@@ -10,14 +10,15 @@ services:
- ALL
tmpfs:
- /tmp:noexec,nosuid,size=64m
- /app/logs:noexec,nosuid,size=16m
ports:
# HostPort:ContainerPort (maps to UNRAID_MCP_PORT inside the container, default 6970)
# Change the host port (left side) if 6970 is already in use on your host
- "${UNRAID_MCP_PORT:-6970}:${UNRAID_MCP_PORT:-6970}"
environment:
# Core API Configuration (Required)
- UNRAID_API_URL=${UNRAID_API_URL}
- UNRAID_API_KEY=${UNRAID_API_KEY}
- UNRAID_API_URL=${UNRAID_API_URL:?UNRAID_API_URL is required}
- UNRAID_API_KEY=${UNRAID_API_KEY:?UNRAID_API_KEY is required}
# MCP Server Settings
- UNRAID_MCP_PORT=${UNRAID_MCP_PORT:-6970}

View File

@@ -243,13 +243,13 @@ Every mutation identified across all research documents with their parameters an
| Mutation | Parameters | Returns | Current MCP Coverage |
|----------|------------|---------|---------------------|
| `login(username, password)` | `String!`, `String!` | `String` | **NO** |
| `createApiKey(input)` | `CreateApiKeyInput!` | `ApiKeyWithSecret!` | **NO** |
| `apiKey.create(input)` | `CreateApiKeyInput!` | `ApiKey!` | **NO** |
| `addPermission(input)` | `AddPermissionInput!` | `Boolean!` | **NO** |
| `addRoleForUser(input)` | `AddRoleForUserInput!` | `Boolean!` | **NO** |
| `addRoleForApiKey(input)` | `AddRoleForApiKeyInput!` | `Boolean!` | **NO** |
| `removeRoleFromApiKey(input)` | `RemoveRoleFromApiKeyInput!` | `Boolean!` | **NO** |
| `deleteApiKeys(input)` | API key IDs | `Boolean` | **NO** |
| `updateApiKey(input)` | API key update data | `Boolean` | **NO** |
| `apiKey.delete(input)` | API key IDs | `Boolean!` | **NO** |
| `apiKey.update(input)` | API key update data | `ApiKey!` | **NO** |
| `addUser(input)` | `addUserInput!` | `User` | **NO** |
| `deleteUser(input)` | `deleteUserInput!` | `User` | **NO** |
@@ -417,11 +417,11 @@ GRAPHQL_PUBSUB_CHANNEL {
| Input Type | Used By | Fields |
|-----------|---------|--------|
| `CreateApiKeyInput` | `createApiKey` | `name!`, `description`, `roles[]`, `permissions[]`, `overwrite` |
| `CreateApiKeyInput` | `apiKey.create` | `name!`, `description`, `roles[]`, `permissions[]`, `overwrite` |
| `AddPermissionInput` | `addPermission` | `resource!`, `actions![]` |
| `AddRoleForUserInput` | `addRoleForUser` | User + role assignment |
| `AddRoleForApiKeyInput` | `addRoleForApiKey` | API key + role assignment |
| `RemoveRoleFromApiKeyInput` | `removeRoleFromApiKey` | API key + role removal |
| `AddRoleForApiKeyInput` | `apiKey.addRole` | API key + role assignment |
| `RemoveRoleFromApiKeyInput` | `apiKey.removeRole` | API key + role removal |
| `arrayDiskInput` | `addDiskToArray`, `removeDiskFromArray` | Disk assignment data |
| `ConnectSignInInput` | `connectSignIn` | Connect credentials |
| `EnableDynamicRemoteAccessInput` | `enableDynamicRemoteAccess` | Remote access config |
@@ -619,9 +619,9 @@ The current MCP server has 10 tools (76 actions) after consolidation. The follow
|--------------|---------------|---------------|
| `list_api_keys()` | `apiKeys` query | Key inventory |
| `get_api_key(id)` | `apiKey(id)` query | Key details |
| `create_api_key(input)` | `createApiKey` mutation | Key provisioning |
| `delete_api_keys(input)` | `deleteApiKeys` mutation | Key cleanup |
| `update_api_key(input)` | `updateApiKey` mutation | Key modification |
| `create_api_key(input)` | `apiKey.create` mutation | Key provisioning |
| `delete_api_keys(input)` | `apiKey.delete` mutation | Key cleanup |
| `update_api_key(input)` | `apiKey.update` mutation | Key modification |
#### Remote Access Management (0 tools currently, 1 query + 3 mutations)

View File

@@ -678,11 +678,9 @@ type Query {
```graphql
type Mutation {
createApiKey(input: CreateApiKeyInput!): ApiKeyWithSecret!
apiKey: ApiKeyMutations!
addPermission(input: AddPermissionInput!): Boolean!
addRoleForUser(input: AddRoleForUserInput!): Boolean!
addRoleForApiKey(input: AddRoleForApiKeyInput!): Boolean!
removeRoleFromApiKey(input: RemoveRoleFromApiKeyInput!): Boolean!
startArray: Array
stopArray: Array
addDiskToArray(input: arrayDiskInput): Array

View File

@@ -565,11 +565,11 @@ api/src/unraid-api/graph/resolvers/
| **RClone** | `createRCloneRemote(input)` | Create remote storage | CREATE_ANY:FLASH |
| **RClone** | `deleteRCloneRemote(input)` | Delete remote storage | DELETE_ANY:FLASH |
| **UPS** | `configureUps(config)` | Update UPS configuration | UPDATE_ANY:* |
| **API Keys** | `createApiKey(input)` | Create API key | CREATE_ANY:API_KEY |
| **API Keys** | `addRoleForApiKey(input)` | Add role to key | UPDATE_ANY:API_KEY |
| **API Keys** | `removeRoleFromApiKey(input)` | Remove role from key | UPDATE_ANY:API_KEY |
| **API Keys** | `deleteApiKeys(input)` | Delete API keys | DELETE_ANY:API_KEY |
| **API Keys** | `updateApiKey(input)` | Update API key | UPDATE_ANY:API_KEY |
| **API Keys** | `apiKey.create(input)` | Create API key | CREATE_ANY:API_KEY |
| **API Keys** | `apiKey.addRole(input)` | Add role to key | UPDATE_ANY:API_KEY |
| **API Keys** | `apiKey.removeRole(input)` | Remove role from key | UPDATE_ANY:API_KEY |
| **API Keys** | `apiKey.delete(input)` | Delete API keys | DELETE_ANY:API_KEY |
| **API Keys** | `apiKey.update(input)` | Update API key | UPDATE_ANY:API_KEY |
---

File diff suppressed because it is too large Load Diff

View File

@@ -10,7 +10,7 @@ build-backend = "hatchling.build"
# ============================================================================
[project]
name = "unraid-mcp"
version = "0.2.0"
version = "0.2.1"
description = "MCP Server for Unraid API - provides tools to interact with an Unraid server's GraphQL API"
readme = "README.md"
license = {file = "LICENSE"}
@@ -189,7 +189,7 @@ ignore = [
[tool.ruff.lint.per-file-ignores]
"__init__.py" = ["F401", "D104"]
"tests/**/*.py" = ["D", "S101", "PLR2004"] # Allow asserts and magic values in tests
"tests/**/*.py" = ["D", "S101", "S105", "S106", "S107", "PLR2004"] # Allow test-only patterns
[tool.ruff.lint.pydocstyle]
convention = "google"

View File

@@ -659,9 +659,10 @@ class TestArrayToolRequests:
return_value=_graphql_response({"parityCheck": {"start": True}})
)
tool = self._get_tool()
result = await tool(action="parity_start")
result = await tool(action="parity_start", correct=False)
body = _extract_request_body(route.calls.last.request)
assert "StartParityCheck" in body["query"]
assert body["variables"] == {"correct": False}
assert result["success"] is True
@respx.mock
@@ -858,9 +859,9 @@ class TestNotificationsToolRequests:
async def test_create_sends_input_variables(self) -> None:
route = respx.post(API_URL).mock(
return_value=_graphql_response(
{"notifications": {"createNotification": {
{"createNotification": {
"id": "n1", "title": "Test", "importance": "INFO",
}}}
}}
)
)
tool = self._get_tool()
@@ -882,7 +883,7 @@ class TestNotificationsToolRequests:
async def test_archive_sends_id_variable(self) -> None:
route = respx.post(API_URL).mock(
return_value=_graphql_response(
{"notifications": {"archiveNotification": True}}
{"archiveNotification": {"id": "notif-1"}}
)
)
tool = self._get_tool()
@@ -901,7 +902,7 @@ class TestNotificationsToolRequests:
async def test_delete_sends_id_and_type(self) -> None:
route = respx.post(API_URL).mock(
return_value=_graphql_response(
{"notifications": {"deleteNotification": True}}
{"deleteNotification": {"unread": {"total": 0}}}
)
)
tool = self._get_tool()
@@ -920,7 +921,7 @@ class TestNotificationsToolRequests:
async def test_archive_all_sends_importance_when_provided(self) -> None:
route = respx.post(API_URL).mock(
return_value=_graphql_response(
{"notifications": {"archiveAll": True}}
{"archiveAll": {"archive": {"total": 1}}}
)
)
tool = self._get_tool()
@@ -1087,10 +1088,10 @@ class TestKeysToolRequests:
async def test_create_sends_input_variables(self) -> None:
route = respx.post(API_URL).mock(
return_value=_graphql_response(
{"createApiKey": {
{"apiKey": {"create": {
"id": "k2", "name": "new-key",
"key": "secret", "roles": ["read"],
}}
}}}
)
)
tool = self._get_tool()
@@ -1106,7 +1107,7 @@ class TestKeysToolRequests:
async def test_update_sends_input_variables(self) -> None:
route = respx.post(API_URL).mock(
return_value=_graphql_response(
{"updateApiKey": {"id": "k1", "name": "renamed", "roles": ["admin"]}}
{"apiKey": {"update": {"id": "k1", "name": "renamed", "roles": ["admin"]}}}
)
)
tool = self._get_tool()
@@ -1126,12 +1127,12 @@ class TestKeysToolRequests:
@respx.mock
async def test_delete_sends_ids_when_confirmed(self) -> None:
route = respx.post(API_URL).mock(
return_value=_graphql_response({"deleteApiKeys": True})
return_value=_graphql_response({"apiKey": {"delete": True}})
)
tool = self._get_tool()
result = await tool(action="delete", key_id="k1", confirm=True)
body = _extract_request_body(route.calls.last.request)
assert "DeleteApiKeys" in body["query"]
assert "DeleteApiKey" in body["query"]
assert body["variables"]["input"]["ids"] == ["k1"]
assert result["success"] is True

View File

@@ -305,7 +305,7 @@ class TestConfirmAllowsExecution:
assert result["success"] is True
async def test_notifications_delete_with_confirm(self, _mock_notif_graphql: AsyncMock) -> None:
_mock_notif_graphql.return_value = {"notifications": {"deleteNotification": True}}
_mock_notif_graphql.return_value = {"deleteNotification": {"unread": {"total": 0}}}
tool_fn = make_tool_fn(
"unraid_mcp.tools.notifications", "register_notifications_tool", "unraid_notifications"
)
@@ -318,7 +318,7 @@ class TestConfirmAllowsExecution:
assert result["success"] is True
async def test_notifications_delete_archived_with_confirm(self, _mock_notif_graphql: AsyncMock) -> None:
_mock_notif_graphql.return_value = {"notifications": {"deleteArchivedNotifications": True}}
_mock_notif_graphql.return_value = {"deleteArchivedNotifications": {"archive": {"total": 0}}}
tool_fn = make_tool_fn(
"unraid_mcp.tools.notifications", "register_notifications_tool", "unraid_notifications"
)
@@ -332,7 +332,7 @@ class TestConfirmAllowsExecution:
assert result["success"] is True
async def test_keys_delete_with_confirm(self, _mock_keys_graphql: AsyncMock) -> None:
_mock_keys_graphql.return_value = {"deleteApiKeys": True}
_mock_keys_graphql.return_value = {"apiKey": {"delete": True}}
tool_fn = make_tool_fn("unraid_mcp.tools.keys", "register_keys_tool", "unraid_keys")
result = await tool_fn(action="delete", key_id="key-123", confirm=True)
assert result["success"] is True

View File

@@ -39,12 +39,17 @@ class TestArrayValidation:
with pytest.raises(ToolError, match="Invalid action"):
await tool_fn(action=action)
async def test_parity_start_requires_correct(self, _mock_graphql: AsyncMock) -> None:
tool_fn = _make_tool()
with pytest.raises(ToolError, match="correct is required"):
await tool_fn(action="parity_start")
class TestArrayActions:
async def test_parity_start(self, _mock_graphql: AsyncMock) -> None:
_mock_graphql.return_value = {"parityCheck": {"start": True}}
tool_fn = _make_tool()
result = await tool_fn(action="parity_start")
result = await tool_fn(action="parity_start", correct=False)
assert result["success"] is True
assert result["action"] == "parity_start"
_mock_graphql.assert_called_once()
@@ -94,14 +99,14 @@ class TestArrayMutationFailures:
async def test_parity_start_mutation_returns_false(self, _mock_graphql: AsyncMock) -> None:
_mock_graphql.return_value = {"parityCheck": {"start": False}}
tool_fn = _make_tool()
result = await tool_fn(action="parity_start")
result = await tool_fn(action="parity_start", correct=False)
assert result["success"] is True
assert result["data"] == {"parityCheck": {"start": False}}
async def test_parity_start_mutation_returns_null(self, _mock_graphql: AsyncMock) -> None:
_mock_graphql.return_value = {"parityCheck": {"start": None}}
tool_fn = _make_tool()
result = await tool_fn(action="parity_start")
result = await tool_fn(action="parity_start", correct=False)
assert result["success"] is True
assert result["data"] == {"parityCheck": {"start": None}}
@@ -110,7 +115,7 @@ class TestArrayMutationFailures:
) -> None:
_mock_graphql.return_value = {"parityCheck": {"start": {}}}
tool_fn = _make_tool()
result = await tool_fn(action="parity_start")
result = await tool_fn(action="parity_start", correct=False)
assert result["success"] is True
assert result["data"] == {"parityCheck": {"start": {}}}
@@ -128,7 +133,7 @@ class TestArrayNetworkErrors:
_mock_graphql.side_effect = ToolError("HTTP error 500: Internal Server Error")
tool_fn = _make_tool()
with pytest.raises(ToolError, match="HTTP error 500"):
await tool_fn(action="parity_start")
await tool_fn(action="parity_start", correct=False)
async def test_connection_refused(self, _mock_graphql: AsyncMock) -> None:
_mock_graphql.side_effect = ToolError("Network connection error: Connection refused")

View File

@@ -65,7 +65,9 @@ class TestKeysActions:
async def test_create(self, _mock_graphql: AsyncMock) -> None:
_mock_graphql.return_value = {
"createApiKey": {"id": "k:new", "name": "new-key", "key": "secret123", "roles": []}
"apiKey": {
"create": {"id": "k:new", "name": "new-key", "key": "secret123", "roles": []}
}
}
tool_fn = _make_tool()
result = await tool_fn(action="create", name="new-key")
@@ -74,25 +76,29 @@ class TestKeysActions:
async def test_create_with_roles(self, _mock_graphql: AsyncMock) -> None:
_mock_graphql.return_value = {
"createApiKey": {
"apiKey": {
"create": {
"id": "k:new",
"name": "admin-key",
"key": "secret",
"roles": ["admin"],
}
}
}
tool_fn = _make_tool()
result = await tool_fn(action="create", name="admin-key", roles=["admin"])
assert result["success"] is True
async def test_update(self, _mock_graphql: AsyncMock) -> None:
_mock_graphql.return_value = {"updateApiKey": {"id": "k:1", "name": "renamed", "roles": []}}
_mock_graphql.return_value = {
"apiKey": {"update": {"id": "k:1", "name": "renamed", "roles": []}}
}
tool_fn = _make_tool()
result = await tool_fn(action="update", key_id="k:1", name="renamed")
assert result["success"] is True
async def test_delete(self, _mock_graphql: AsyncMock) -> None:
_mock_graphql.return_value = {"deleteApiKeys": True}
_mock_graphql.return_value = {"apiKey": {"delete": True}}
tool_fn = _make_tool()
result = await tool_fn(action="delete", key_id="k:1", confirm=True)
assert result["success"] is True

View File

@@ -82,10 +82,8 @@ class TestNotificationsActions:
async def test_create(self, _mock_graphql: AsyncMock) -> None:
_mock_graphql.return_value = {
"notifications": {
"createNotification": {"id": "n:new", "title": "Test", "importance": "INFO"}
}
}
tool_fn = _make_tool()
result = await tool_fn(
action="create",
@@ -97,13 +95,13 @@ class TestNotificationsActions:
assert result["success"] is True
async def test_archive_notification(self, _mock_graphql: AsyncMock) -> None:
_mock_graphql.return_value = {"notifications": {"archiveNotification": True}}
_mock_graphql.return_value = {"archiveNotification": {"id": "n:1"}}
tool_fn = _make_tool()
result = await tool_fn(action="archive", notification_id="n:1")
assert result["success"] is True
async def test_delete_with_confirm(self, _mock_graphql: AsyncMock) -> None:
_mock_graphql.return_value = {"notifications": {"deleteNotification": True}}
_mock_graphql.return_value = {"deleteNotification": {"unread": {"total": 0}}}
tool_fn = _make_tool()
result = await tool_fn(
action="delete",
@@ -114,13 +112,13 @@ class TestNotificationsActions:
assert result["success"] is True
async def test_archive_all(self, _mock_graphql: AsyncMock) -> None:
_mock_graphql.return_value = {"notifications": {"archiveAll": True}}
_mock_graphql.return_value = {"archiveAll": {"archive": {"total": 1}}}
tool_fn = _make_tool()
result = await tool_fn(action="archive_all")
assert result["success"] is True
async def test_unread_notification(self, _mock_graphql: AsyncMock) -> None:
_mock_graphql.return_value = {"notifications": {"unreadNotification": True}}
_mock_graphql.return_value = {"unreadNotification": {"id": "n:1"}}
tool_fn = _make_tool()
result = await tool_fn(action="unread", notification_id="n:1")
assert result["success"] is True
@@ -140,7 +138,7 @@ class TestNotificationsActions:
assert filter_var["offset"] == 5
async def test_delete_archived(self, _mock_graphql: AsyncMock) -> None:
_mock_graphql.return_value = {"notifications": {"deleteArchivedNotifications": True}}
_mock_graphql.return_value = {"deleteArchivedNotifications": {"archive": {"total": 0}}}
tool_fn = _make_tool()
result = await tool_fn(action="delete_archived", confirm=True)
assert result["success"] is True
@@ -180,9 +178,7 @@ class TestNotificationsCreateValidation:
)
async def test_alert_importance_accepted(self, _mock_graphql: AsyncMock) -> None:
_mock_graphql.return_value = {
"notifications": {"createNotification": {"id": "n:1", "importance": "ALERT"}}
}
_mock_graphql.return_value = {"createNotification": {"id": "n:1", "importance": "ALERT"}}
tool_fn = _make_tool()
result = await tool_fn(
action="create", title="T", subject="S", description="D", importance="alert"
@@ -223,9 +219,7 @@ class TestNotificationsCreateValidation:
)
async def test_title_at_max_accepted(self, _mock_graphql: AsyncMock) -> None:
_mock_graphql.return_value = {
"notifications": {"createNotification": {"id": "n:1", "importance": "NORMAL"}}
}
_mock_graphql.return_value = {"createNotification": {"id": "n:1", "importance": "NORMAL"}}
tool_fn = _make_tool()
result = await tool_fn(
action="create",

View File

@@ -22,7 +22,7 @@ QUERIES: dict[str, str] = {
MUTATIONS: dict[str, str] = {
"parity_start": """
mutation StartParityCheck($correct: Boolean) {
mutation StartParityCheck($correct: Boolean!) {
parityCheck { start(correct: $correct) }
}
""",
@@ -92,7 +92,9 @@ def register_array_tool(mcp: FastMCP) -> None:
query = MUTATIONS[action]
variables: dict[str, Any] | None = None
if action == "parity_start" and correct is not None:
if action == "parity_start":
if correct is None:
raise ToolError("correct is required for 'parity_start' action")
variables = {"correct": correct}
data = await make_graphql_request(query, variables)

View File

@@ -29,17 +29,17 @@ QUERIES: dict[str, str] = {
MUTATIONS: dict[str, str] = {
"create": """
mutation CreateApiKey($input: CreateApiKeyInput!) {
createApiKey(input: $input) { id name key roles }
apiKey { create(input: $input) { id name key roles } }
}
""",
"update": """
mutation UpdateApiKey($input: UpdateApiKeyInput!) {
updateApiKey(input: $input) { id name roles }
apiKey { update(input: $input) { id name roles } }
}
""",
"delete": """
mutation DeleteApiKeys($input: DeleteApiKeysInput!) {
deleteApiKeys(input: $input)
mutation DeleteApiKey($input: DeleteApiKeyInput!) {
apiKey { delete(input: $input) }
}
""",
}
@@ -116,7 +116,7 @@ def register_keys_tool(mcp: FastMCP) -> None:
data = await make_graphql_request(MUTATIONS["create"], {"input": input_data})
return {
"success": True,
"key": data.get("createApiKey", {}),
"key": (data.get("apiKey") or {}).get("create", {}),
}
if action == "update":
@@ -130,14 +130,14 @@ def register_keys_tool(mcp: FastMCP) -> None:
data = await make_graphql_request(MUTATIONS["update"], {"input": input_data})
return {
"success": True,
"key": data.get("updateApiKey", {}),
"key": (data.get("apiKey") or {}).get("update", {}),
}
if action == "delete":
if not key_id:
raise ToolError("key_id is required for 'delete' action")
data = await make_graphql_request(MUTATIONS["delete"], {"input": {"ids": [key_id]}})
result = data.get("deleteApiKeys")
result = (data.get("apiKey") or {}).get("delete")
if not result:
raise ToolError(
f"Failed to delete API key '{key_id}': no confirmation from server"

View File

@@ -44,33 +44,33 @@ QUERIES: dict[str, str] = {
MUTATIONS: dict[str, str] = {
"create": """
mutation CreateNotification($input: CreateNotificationInput!) {
notifications { createNotification(input: $input) { id title importance } }
mutation CreateNotification($input: NotificationData!) {
createNotification(input: $input) { id title importance }
}
""",
"archive": """
mutation ArchiveNotification($id: PrefixedID!) {
notifications { archiveNotification(id: $id) }
archiveNotification(id: $id)
}
""",
"unread": """
mutation UnreadNotification($id: PrefixedID!) {
notifications { unreadNotification(id: $id) }
unreadNotification(id: $id)
}
""",
"delete": """
mutation DeleteNotification($id: PrefixedID!, $type: NotificationType!) {
notifications { deleteNotification(id: $id, type: $type) }
deleteNotification(id: $id, type: $type)
}
""",
"delete_archived": """
mutation DeleteArchivedNotifications {
notifications { deleteArchivedNotifications }
deleteArchivedNotifications
}
""",
"archive_all": """
mutation ArchiveAllNotifications($importance: NotificationImportance) {
notifications { archiveAll(importance: $importance) }
archiveAll(importance: $importance)
}
""",
}