mirror of
https://github.com/jmagar/unraid-mcp.git
synced 2026-03-23 12:39:24 -07:00
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:
7
.gitignore
vendored
7
.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
""",
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user