diff --git a/docs/api.md b/docs/api.md index ced8eb5b3..da5784bcb 100644 --- a/docs/api.md +++ b/docs/api.md @@ -60,6 +60,20 @@ The REST api provides five different forms of authentication. [here](advanced_usage.md#openid-connect-and-social-authentication) for more information on social accounts. +## Model Context Protocol (MCP) + +Paperless-ngx exposes an MCP endpoint powered by `django-mcp-server` so MCP +clients can query data collections, run full-text document search, and invoke +DRF-backed CRUD tools. + +- Endpoint: `/mcp/` +- Authentication: identical to the REST API (Basic, Session, Token, or Remote + User depending on your configuration). + +The MCP server uses existing DRF viewsets and permissions. It also exposes a +`query_data_collections` tool for structured querying across published models +and a `search_documents` tool for full-text search. + ## Searching for documents Full text searching is available on the `/api/documents/` endpoint. Two diff --git a/pyproject.toml b/pyproject.toml index 500461199..524b55ad8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,7 @@ dependencies = [ "django-extensions~=4.1", "django-filter~=25.1", "django-guardian~=3.2.0", + "django-mcp-server~=0.5.7", "django-multiselectfield~=1.0.1", "django-soft-delete~=1.0.18", "django-treenode>=0.23.2", diff --git a/src/documents/mcp.py b/src/documents/mcp.py new file mode 100644 index 000000000..65630c573 --- /dev/null +++ b/src/documents/mcp.py @@ -0,0 +1,481 @@ +from __future__ import annotations + +from django.db.models import Q +from django.http import QueryDict +from mcp_server import MCPToolset +from mcp_server import ModelQueryToolset +from mcp_server import drf_publish_create_mcp_tool +from mcp_server import drf_publish_destroy_mcp_tool +from mcp_server import drf_publish_list_mcp_tool +from mcp_server import drf_publish_update_mcp_tool +from rest_framework.response import Response + +from documents.models import Correspondent +from documents.models import CustomField +from documents.models import Document +from documents.models import DocumentType +from documents.models import Note +from documents.models import SavedView +from documents.models import ShareLink +from documents.models import StoragePath +from documents.models import Tag +from documents.models import Workflow +from documents.models import WorkflowAction +from documents.models import WorkflowTrigger +from documents.permissions import get_objects_for_user_owner_aware +from documents.views import CorrespondentViewSet +from documents.views import CustomFieldViewSet +from documents.views import DocumentTypeViewSet +from documents.views import SavedViewViewSet +from documents.views import ShareLinkViewSet +from documents.views import StoragePathViewSet +from documents.views import TagViewSet +from documents.views import TasksViewSet +from documents.views import UnifiedSearchViewSet +from documents.views import WorkflowActionViewSet +from documents.views import WorkflowTriggerViewSet +from documents.views import WorkflowViewSet + +VIEWSET_ACTIONS = { + "create": {"post": "create"}, + "list": {"get": "list"}, + "update": {"put": "update"}, + "destroy": {"delete": "destroy"}, +} + +BODY_SCHEMA = {"type": "object", "additionalProperties": True} + +VIEWSET_INSTRUCTIONS = { + CorrespondentViewSet: "Manage correspondents.", + TagViewSet: "Manage tags.", + UnifiedSearchViewSet: "Search and manage documents.", + DocumentTypeViewSet: "Manage document types.", + StoragePathViewSet: "Manage storage paths.", + SavedViewViewSet: "Manage saved views.", + ShareLinkViewSet: "Manage share links.", + WorkflowTriggerViewSet: "Manage workflow triggers.", + WorkflowActionViewSet: "Manage workflow actions.", + WorkflowViewSet: "Manage workflows.", + CustomFieldViewSet: "Manage custom fields.", + TasksViewSet: "List background tasks.", +} + + +class OwnerAwareQueryToolsetMixin: + permission: str + + def get_queryset(self): + user = getattr(self.request, "user", None) + if not user or not user.is_authenticated: + return self.model.objects.none() + if user.is_superuser: + return self.model._default_manager.all() + return get_objects_for_user_owner_aware(user, self.permission, self.model) + + +class DocumentQueryToolset(ModelQueryToolset): + model = Document + search_fields = ["title", "content"] + + def get_queryset(self): + user = getattr(self.request, "user", None) + if not user or not user.is_authenticated: + return Document.objects.none() + if user.is_superuser: + return Document.objects.all() + return get_objects_for_user_owner_aware( + user, + "documents.view_document", + Document, + ) + + +class CorrespondentQueryToolset(OwnerAwareQueryToolsetMixin, ModelQueryToolset): + model = Correspondent + permission = "documents.view_correspondent" + + +class TagQueryToolset(OwnerAwareQueryToolsetMixin, ModelQueryToolset): + model = Tag + permission = "documents.view_tag" + + +class DocumentTypeQueryToolset(OwnerAwareQueryToolsetMixin, ModelQueryToolset): + model = DocumentType + permission = "documents.view_documenttype" + + +class StoragePathQueryToolset(OwnerAwareQueryToolsetMixin, ModelQueryToolset): + model = StoragePath + permission = "documents.view_storagepath" + + +class SavedViewQueryToolset(OwnerAwareQueryToolsetMixin, ModelQueryToolset): + model = SavedView + permission = "documents.view_savedview" + + +class ShareLinkQueryToolset(OwnerAwareQueryToolsetMixin, ModelQueryToolset): + model = ShareLink + permission = "documents.view_sharelink" + + +class WorkflowTriggerQueryToolset(OwnerAwareQueryToolsetMixin, ModelQueryToolset): + model = WorkflowTrigger + permission = "documents.view_workflowtrigger" + + +class WorkflowActionQueryToolset(OwnerAwareQueryToolsetMixin, ModelQueryToolset): + model = WorkflowAction + permission = "documents.view_workflowaction" + + +class WorkflowQueryToolset(OwnerAwareQueryToolsetMixin, ModelQueryToolset): + model = Workflow + permission = "documents.view_workflow" + + +class NoteQueryToolset(ModelQueryToolset): + model = Note + + def get_queryset(self): + user = getattr(self.request, "user", None) + if not user or not user.is_authenticated: + return Note.objects.none() + if user.is_superuser: + return Note.objects.all() + return Note.objects.filter( + document__in=get_objects_for_user_owner_aware( + user, + "documents.view_document", + Document, + ), + ) + + +class CustomFieldQueryToolset(ModelQueryToolset): + model = CustomField + + def get_queryset(self): + user = getattr(self.request, "user", None) + base = CustomField.objects.all() + if not user or not user.is_authenticated: + return base.none() + if user.is_superuser: + return base + return base.filter( + Q( + fields__document__id__in=get_objects_for_user_owner_aware( + user, + "documents.view_document", + Document, + ), + ) + | Q(fields__document__isnull=True), + ).distinct() + + +class DocumentSearchTools(MCPToolset): + def search_documents( + self, + query: str | None = None, + more_like_id: int | None = None, + fields: list[str] | None = None, + page: int | None = None, + page_size: int | None = None, + *, + full_perms: bool | None = None, + ) -> dict: + """Search documents using the full-text index.""" + if not query and not more_like_id: + raise ValueError("Provide either query or more_like_id.") + + request = self.request + if request is None: + raise ValueError("Request context is required.") + + viewset = UnifiedSearchViewSet() + viewset.request = request + viewset.args = () + viewset.kwargs = {} + viewset.action = "list" + viewset.format_kwarg = None + viewset.check_permissions(request) + + query_params = QueryDict(mutable=True) + if query: + query_params["query"] = query + if more_like_id: + query_params["more_like_id"] = str(more_like_id) + if full_perms is not None: + query_params["full_perms"] = str(full_perms).lower() + if page: + query_params["page"] = str(page) + if page_size: + query_params["page_size"] = str(page_size) + if fields: + query_params.setlist("fields", fields) + + request._request.GET = query_params + response = viewset.list(request) + if isinstance(response, Response): + return response.data + if hasattr(response, "data"): + return response.data + return { + "detail": getattr(response, "content", b"").decode() or "Search failed.", + } + + +drf_publish_create_mcp_tool( + CorrespondentViewSet, + actions=VIEWSET_ACTIONS["create"], + instructions=VIEWSET_INSTRUCTIONS[CorrespondentViewSet], + body_schema=BODY_SCHEMA, +) +drf_publish_list_mcp_tool( + CorrespondentViewSet, + actions=VIEWSET_ACTIONS["list"], + instructions=VIEWSET_INSTRUCTIONS[CorrespondentViewSet], +) +drf_publish_update_mcp_tool( + CorrespondentViewSet, + actions=VIEWSET_ACTIONS["update"], + instructions=VIEWSET_INSTRUCTIONS[CorrespondentViewSet], + body_schema=BODY_SCHEMA, +) +drf_publish_destroy_mcp_tool( + CorrespondentViewSet, + actions=VIEWSET_ACTIONS["destroy"], + instructions=VIEWSET_INSTRUCTIONS[CorrespondentViewSet], +) + +drf_publish_create_mcp_tool( + TagViewSet, + actions=VIEWSET_ACTIONS["create"], + instructions=VIEWSET_INSTRUCTIONS[TagViewSet], + body_schema=BODY_SCHEMA, +) +drf_publish_list_mcp_tool( + TagViewSet, + actions=VIEWSET_ACTIONS["list"], + instructions=VIEWSET_INSTRUCTIONS[TagViewSet], +) +drf_publish_update_mcp_tool( + TagViewSet, + actions=VIEWSET_ACTIONS["update"], + instructions=VIEWSET_INSTRUCTIONS[TagViewSet], + body_schema=BODY_SCHEMA, +) +drf_publish_destroy_mcp_tool( + TagViewSet, + actions=VIEWSET_ACTIONS["destroy"], + instructions=VIEWSET_INSTRUCTIONS[TagViewSet], +) + +drf_publish_list_mcp_tool( + UnifiedSearchViewSet, + actions=VIEWSET_ACTIONS["list"], + instructions=VIEWSET_INSTRUCTIONS[UnifiedSearchViewSet], +) +drf_publish_update_mcp_tool( + UnifiedSearchViewSet, + actions=VIEWSET_ACTIONS["update"], + instructions=VIEWSET_INSTRUCTIONS[UnifiedSearchViewSet], + body_schema=BODY_SCHEMA, +) +drf_publish_destroy_mcp_tool( + UnifiedSearchViewSet, + actions=VIEWSET_ACTIONS["destroy"], + instructions=VIEWSET_INSTRUCTIONS[UnifiedSearchViewSet], +) + +drf_publish_create_mcp_tool( + DocumentTypeViewSet, + actions=VIEWSET_ACTIONS["create"], + instructions=VIEWSET_INSTRUCTIONS[DocumentTypeViewSet], + body_schema=BODY_SCHEMA, +) +drf_publish_list_mcp_tool( + DocumentTypeViewSet, + actions=VIEWSET_ACTIONS["list"], + instructions=VIEWSET_INSTRUCTIONS[DocumentTypeViewSet], +) +drf_publish_update_mcp_tool( + DocumentTypeViewSet, + actions=VIEWSET_ACTIONS["update"], + instructions=VIEWSET_INSTRUCTIONS[DocumentTypeViewSet], + body_schema=BODY_SCHEMA, +) +drf_publish_destroy_mcp_tool( + DocumentTypeViewSet, + actions=VIEWSET_ACTIONS["destroy"], + instructions=VIEWSET_INSTRUCTIONS[DocumentTypeViewSet], +) + +drf_publish_create_mcp_tool( + StoragePathViewSet, + actions=VIEWSET_ACTIONS["create"], + instructions=VIEWSET_INSTRUCTIONS[StoragePathViewSet], + body_schema=BODY_SCHEMA, +) +drf_publish_list_mcp_tool( + StoragePathViewSet, + actions=VIEWSET_ACTIONS["list"], + instructions=VIEWSET_INSTRUCTIONS[StoragePathViewSet], +) +drf_publish_update_mcp_tool( + StoragePathViewSet, + actions=VIEWSET_ACTIONS["update"], + instructions=VIEWSET_INSTRUCTIONS[StoragePathViewSet], + body_schema=BODY_SCHEMA, +) +drf_publish_destroy_mcp_tool( + StoragePathViewSet, + actions=VIEWSET_ACTIONS["destroy"], + instructions=VIEWSET_INSTRUCTIONS[StoragePathViewSet], +) + +drf_publish_create_mcp_tool( + SavedViewViewSet, + actions=VIEWSET_ACTIONS["create"], + instructions=VIEWSET_INSTRUCTIONS[SavedViewViewSet], + body_schema=BODY_SCHEMA, +) +drf_publish_list_mcp_tool( + SavedViewViewSet, + actions=VIEWSET_ACTIONS["list"], + instructions=VIEWSET_INSTRUCTIONS[SavedViewViewSet], +) +drf_publish_update_mcp_tool( + SavedViewViewSet, + actions=VIEWSET_ACTIONS["update"], + instructions=VIEWSET_INSTRUCTIONS[SavedViewViewSet], + body_schema=BODY_SCHEMA, +) +drf_publish_destroy_mcp_tool( + SavedViewViewSet, + actions=VIEWSET_ACTIONS["destroy"], + instructions=VIEWSET_INSTRUCTIONS[SavedViewViewSet], +) + +drf_publish_create_mcp_tool( + ShareLinkViewSet, + actions=VIEWSET_ACTIONS["create"], + instructions=VIEWSET_INSTRUCTIONS[ShareLinkViewSet], + body_schema=BODY_SCHEMA, +) +drf_publish_list_mcp_tool( + ShareLinkViewSet, + actions=VIEWSET_ACTIONS["list"], + instructions=VIEWSET_INSTRUCTIONS[ShareLinkViewSet], +) +drf_publish_update_mcp_tool( + ShareLinkViewSet, + actions=VIEWSET_ACTIONS["update"], + instructions=VIEWSET_INSTRUCTIONS[ShareLinkViewSet], + body_schema=BODY_SCHEMA, +) +drf_publish_destroy_mcp_tool( + ShareLinkViewSet, + actions=VIEWSET_ACTIONS["destroy"], + instructions=VIEWSET_INSTRUCTIONS[ShareLinkViewSet], +) + +drf_publish_create_mcp_tool( + WorkflowTriggerViewSet, + actions=VIEWSET_ACTIONS["create"], + instructions=VIEWSET_INSTRUCTIONS[WorkflowTriggerViewSet], + body_schema=BODY_SCHEMA, +) +drf_publish_list_mcp_tool( + WorkflowTriggerViewSet, + actions=VIEWSET_ACTIONS["list"], + instructions=VIEWSET_INSTRUCTIONS[WorkflowTriggerViewSet], +) +drf_publish_update_mcp_tool( + WorkflowTriggerViewSet, + actions=VIEWSET_ACTIONS["update"], + instructions=VIEWSET_INSTRUCTIONS[WorkflowTriggerViewSet], + body_schema=BODY_SCHEMA, +) +drf_publish_destroy_mcp_tool( + WorkflowTriggerViewSet, + actions=VIEWSET_ACTIONS["destroy"], + instructions=VIEWSET_INSTRUCTIONS[WorkflowTriggerViewSet], +) + +drf_publish_create_mcp_tool( + WorkflowActionViewSet, + actions=VIEWSET_ACTIONS["create"], + instructions=VIEWSET_INSTRUCTIONS[WorkflowActionViewSet], + body_schema=BODY_SCHEMA, +) +drf_publish_list_mcp_tool( + WorkflowActionViewSet, + actions=VIEWSET_ACTIONS["list"], + instructions=VIEWSET_INSTRUCTIONS[WorkflowActionViewSet], +) +drf_publish_update_mcp_tool( + WorkflowActionViewSet, + actions=VIEWSET_ACTIONS["update"], + instructions=VIEWSET_INSTRUCTIONS[WorkflowActionViewSet], + body_schema=BODY_SCHEMA, +) +drf_publish_destroy_mcp_tool( + WorkflowActionViewSet, + actions=VIEWSET_ACTIONS["destroy"], + instructions=VIEWSET_INSTRUCTIONS[WorkflowActionViewSet], +) + +drf_publish_create_mcp_tool( + WorkflowViewSet, + actions=VIEWSET_ACTIONS["create"], + instructions=VIEWSET_INSTRUCTIONS[WorkflowViewSet], + body_schema=BODY_SCHEMA, +) +drf_publish_list_mcp_tool( + WorkflowViewSet, + actions=VIEWSET_ACTIONS["list"], + instructions=VIEWSET_INSTRUCTIONS[WorkflowViewSet], +) +drf_publish_update_mcp_tool( + WorkflowViewSet, + actions=VIEWSET_ACTIONS["update"], + instructions=VIEWSET_INSTRUCTIONS[WorkflowViewSet], + body_schema=BODY_SCHEMA, +) +drf_publish_destroy_mcp_tool( + WorkflowViewSet, + actions=VIEWSET_ACTIONS["destroy"], + instructions=VIEWSET_INSTRUCTIONS[WorkflowViewSet], +) + +drf_publish_create_mcp_tool( + CustomFieldViewSet, + actions=VIEWSET_ACTIONS["create"], + instructions=VIEWSET_INSTRUCTIONS[CustomFieldViewSet], + body_schema=BODY_SCHEMA, +) +drf_publish_list_mcp_tool( + CustomFieldViewSet, + actions=VIEWSET_ACTIONS["list"], + instructions=VIEWSET_INSTRUCTIONS[CustomFieldViewSet], +) +drf_publish_update_mcp_tool( + CustomFieldViewSet, + actions=VIEWSET_ACTIONS["update"], + instructions=VIEWSET_INSTRUCTIONS[CustomFieldViewSet], + body_schema=BODY_SCHEMA, +) +drf_publish_destroy_mcp_tool( + CustomFieldViewSet, + actions=VIEWSET_ACTIONS["destroy"], + instructions=VIEWSET_INSTRUCTIONS[CustomFieldViewSet], +) + +drf_publish_list_mcp_tool( + TasksViewSet, + actions=VIEWSET_ACTIONS["list"], + instructions=VIEWSET_INSTRUCTIONS[TasksViewSet], +) diff --git a/src/paperless/mcp.py b/src/paperless/mcp.py new file mode 100644 index 000000000..472cf4cc6 --- /dev/null +++ b/src/paperless/mcp.py @@ -0,0 +1,82 @@ +from mcp_server import drf_publish_create_mcp_tool +from mcp_server import drf_publish_destroy_mcp_tool +from mcp_server import drf_publish_list_mcp_tool +from mcp_server import drf_publish_update_mcp_tool + +from paperless.views import ApplicationConfigurationViewSet +from paperless.views import GroupViewSet +from paperless.views import UserViewSet + +VIEWSET_ACTIONS = { + "create": {"post": "create"}, + "list": {"get": "list"}, + "update": {"put": "update"}, + "destroy": {"delete": "destroy"}, +} + +BODY_SCHEMA = {"type": "object", "additionalProperties": True} + +VIEWSET_INSTRUCTIONS = { + UserViewSet: "Manage Paperless users.", + GroupViewSet: "Manage Paperless groups.", + ApplicationConfigurationViewSet: "Manage application configuration.", +} + + +drf_publish_create_mcp_tool( + UserViewSet, + actions=VIEWSET_ACTIONS["create"], + instructions=VIEWSET_INSTRUCTIONS[UserViewSet], + body_schema=BODY_SCHEMA, +) +drf_publish_list_mcp_tool( + UserViewSet, + actions=VIEWSET_ACTIONS["list"], + instructions=VIEWSET_INSTRUCTIONS[UserViewSet], +) +drf_publish_update_mcp_tool( + UserViewSet, + actions=VIEWSET_ACTIONS["update"], + instructions=VIEWSET_INSTRUCTIONS[UserViewSet], + body_schema=BODY_SCHEMA, +) +drf_publish_destroy_mcp_tool( + UserViewSet, + actions=VIEWSET_ACTIONS["destroy"], + instructions=VIEWSET_INSTRUCTIONS[UserViewSet], +) + +drf_publish_create_mcp_tool( + GroupViewSet, + actions=VIEWSET_ACTIONS["create"], + instructions=VIEWSET_INSTRUCTIONS[GroupViewSet], + body_schema=BODY_SCHEMA, +) +drf_publish_list_mcp_tool( + GroupViewSet, + actions=VIEWSET_ACTIONS["list"], + instructions=VIEWSET_INSTRUCTIONS[GroupViewSet], +) +drf_publish_update_mcp_tool( + GroupViewSet, + actions=VIEWSET_ACTIONS["update"], + instructions=VIEWSET_INSTRUCTIONS[GroupViewSet], + body_schema=BODY_SCHEMA, +) +drf_publish_destroy_mcp_tool( + GroupViewSet, + actions=VIEWSET_ACTIONS["destroy"], + instructions=VIEWSET_INSTRUCTIONS[GroupViewSet], +) + +drf_publish_list_mcp_tool( + ApplicationConfigurationViewSet, + actions=VIEWSET_ACTIONS["list"], + instructions=VIEWSET_INSTRUCTIONS[ApplicationConfigurationViewSet], +) +drf_publish_update_mcp_tool( + ApplicationConfigurationViewSet, + actions=VIEWSET_ACTIONS["update"], + instructions=VIEWSET_INSTRUCTIONS[ApplicationConfigurationViewSet], + body_schema=BODY_SCHEMA, +) diff --git a/src/paperless/settings.py b/src/paperless/settings.py index 9ad0fea4d..5538b2313 100644 --- a/src/paperless/settings.py +++ b/src/paperless/settings.py @@ -348,6 +348,7 @@ INSTALLED_APPS = [ "allauth.headless", "drf_spectacular", "drf_spectacular_sidecar", + "mcp_server", "treenode", *env_apps, ] @@ -612,6 +613,17 @@ def _parse_remote_user_settings() -> str: HTTP_REMOTE_USER_HEADER_NAME = _parse_remote_user_settings() +DJANGO_MCP_AUTHENTICATION_CLASSES = REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"] +DJANGO_MCP_GLOBAL_SERVER_CONFIG = { + "name": "paperless-ngx", + "instructions": ( + "Use the MCP tools to search, query, and manage Paperless-ngx data. " + "Use `search_documents` for full-text search, and `query_data_collections` " + "for structured queries against available collections. " + "Write operations are exposed via DRF-backed tools for create/update/delete." + ), +} + # X-Frame options for embedded PDF display: X_FRAME_OPTIONS = "SAMEORIGIN" diff --git a/src/paperless/urls.py b/src/paperless/urls.py index ce5c68494..1282c5711 100644 --- a/src/paperless/urls.py +++ b/src/paperless/urls.py @@ -356,6 +356,7 @@ urlpatterns = [ ], ), ), + path("", include("mcp_server.urls")), # Root of the Frontend re_path( r".*", diff --git a/src/paperless_mail/mcp.py b/src/paperless_mail/mcp.py new file mode 100644 index 000000000..1255da795 --- /dev/null +++ b/src/paperless_mail/mcp.py @@ -0,0 +1,129 @@ +from mcp_server import ModelQueryToolset +from mcp_server import drf_publish_create_mcp_tool +from mcp_server import drf_publish_destroy_mcp_tool +from mcp_server import drf_publish_list_mcp_tool +from mcp_server import drf_publish_update_mcp_tool + +from documents.permissions import get_objects_for_user_owner_aware +from paperless_mail.models import MailAccount +from paperless_mail.models import MailRule +from paperless_mail.models import ProcessedMail +from paperless_mail.views import MailAccountViewSet +from paperless_mail.views import MailRuleViewSet +from paperless_mail.views import ProcessedMailViewSet + +VIEWSET_ACTIONS = { + "create": {"post": "create"}, + "list": {"get": "list"}, + "update": {"put": "update"}, + "destroy": {"delete": "destroy"}, +} + +BODY_SCHEMA = {"type": "object", "additionalProperties": True} + +VIEWSET_INSTRUCTIONS = { + MailAccountViewSet: "Manage mail accounts.", + MailRuleViewSet: "Manage mail rules.", + ProcessedMailViewSet: "List processed mail.", +} + + +class MailAccountQueryToolset(ModelQueryToolset): + model = MailAccount + + def get_queryset(self): + user = getattr(self.request, "user", None) + if not user or not user.is_authenticated: + return MailAccount.objects.none() + if user.is_superuser: + return MailAccount.objects.all() + return get_objects_for_user_owner_aware( + user, + "paperless_mail.view_mailaccount", + MailAccount, + ) + + +class MailRuleQueryToolset(ModelQueryToolset): + model = MailRule + + def get_queryset(self): + user = getattr(self.request, "user", None) + if not user or not user.is_authenticated: + return MailRule.objects.none() + if user.is_superuser: + return MailRule.objects.all() + return get_objects_for_user_owner_aware( + user, + "paperless_mail.view_mailrule", + MailRule, + ) + + +class ProcessedMailQueryToolset(ModelQueryToolset): + model = ProcessedMail + + def get_queryset(self): + user = getattr(self.request, "user", None) + if not user or not user.is_authenticated: + return ProcessedMail.objects.none() + if user.is_superuser: + return ProcessedMail.objects.all() + return get_objects_for_user_owner_aware( + user, + "paperless_mail.view_processedmail", + ProcessedMail, + ) + + +drf_publish_create_mcp_tool( + MailAccountViewSet, + actions=VIEWSET_ACTIONS["create"], + instructions=VIEWSET_INSTRUCTIONS[MailAccountViewSet], + body_schema=BODY_SCHEMA, +) +drf_publish_list_mcp_tool( + MailAccountViewSet, + actions=VIEWSET_ACTIONS["list"], + instructions=VIEWSET_INSTRUCTIONS[MailAccountViewSet], +) +drf_publish_update_mcp_tool( + MailAccountViewSet, + actions=VIEWSET_ACTIONS["update"], + instructions=VIEWSET_INSTRUCTIONS[MailAccountViewSet], + body_schema=BODY_SCHEMA, +) +drf_publish_destroy_mcp_tool( + MailAccountViewSet, + actions=VIEWSET_ACTIONS["destroy"], + instructions=VIEWSET_INSTRUCTIONS[MailAccountViewSet], +) + +drf_publish_create_mcp_tool( + MailRuleViewSet, + actions=VIEWSET_ACTIONS["create"], + instructions=VIEWSET_INSTRUCTIONS[MailRuleViewSet], + body_schema=BODY_SCHEMA, +) +drf_publish_list_mcp_tool( + MailRuleViewSet, + actions=VIEWSET_ACTIONS["list"], + instructions=VIEWSET_INSTRUCTIONS[MailRuleViewSet], +) +drf_publish_update_mcp_tool( + MailRuleViewSet, + actions=VIEWSET_ACTIONS["update"], + instructions=VIEWSET_INSTRUCTIONS[MailRuleViewSet], + body_schema=BODY_SCHEMA, +) +drf_publish_destroy_mcp_tool( + MailRuleViewSet, + actions=VIEWSET_ACTIONS["destroy"], + instructions=VIEWSET_INSTRUCTIONS[MailRuleViewSet], +) + +drf_publish_list_mcp_tool( + ProcessedMailViewSet, + actions=VIEWSET_ACTIONS["list"], + instructions=VIEWSET_INSTRUCTIONS[ProcessedMailViewSet], +) diff --git a/uv.lock b/uv.lock index da7c721f5..acca40654 100644 --- a/uv.lock +++ b/uv.lock @@ -1038,6 +1038,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2f/23/63a7d868373a73d25c4a5c2dd3cce3aaeb22fbee82560d42b6e93ba01403/django_guardian-3.2.0-py3-none-any.whl", hash = "sha256:0768565a057988a93fc4a1d93649c4a794abfd7473a8408a079cfbf83c559d77", size = 134674, upload-time = "2025-09-16T10:35:51.69Z" }, ] +[[package]] +name = "django-mcp-server" +version = "0.5.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "djangorestframework", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "inflection", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "mcp", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "uritemplate", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/70/e2cf268b77d0aa171b72763325279284561dbbd9b80ed4fd6975b4b7bd9c/django_mcp_server-0.5.7.tar.gz", hash = "sha256:5077f8fabf5fb621b5ce490afd0db60f21e57b3a451ed14a9f44aef545ea4eee", size = 23910, upload-time = "2025-10-10T17:13:34.681Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/01/f78a11f51437f70b4ff2d9f131d47acf82c2a4cf78d63e9cf291e3727054/django_mcp_server-0.5.7-py3-none-any.whl", hash = "sha256:04b58bf02623aaee59708c3661ffe17981acd4532587c38b6cfe2c9e7090c6d3", size = 26389, upload-time = "2025-10-10T17:13:33.56Z" }, +] + [[package]] name = "django-multiselectfield" version = "1.0.1" @@ -1706,6 +1722,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/45/4b/2b81e876abf77b4af3372aff731f4f6722840ebc7dcfd85778eaba271733/httpx_oauth-0.16.1-py3-none-any.whl", hash = "sha256:2fcad82f80f28d0473a0fc4b4eda223dc952050af7e3a8c8781342d850f09fb5", size = 38056, upload-time = "2024-12-20T07:23:00.394Z" }, ] +[[package]] +name = "httpx-sse" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, +] + [[package]] name = "huggingface-hub" version = "0.30.2" @@ -2378,6 +2403,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/be/2f/5108cb3ee4ba6501748c4908b908e55f42a5b66245b4cfe0c99326e1ef6e/marshmallow-3.26.2-py3-none-any.whl", hash = "sha256:013fa8a3c4c276c24d26d84ce934dc964e2aa794345a0f8c7e5a7191482c8a73", size = 50964, upload-time = "2025-12-22T06:53:51.801Z" }, ] +[[package]] +name = "mcp" +version = "1.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "httpx", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "httpx-sse", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "jsonschema", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pydantic-settings", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pyjwt", extra = ["crypto"], marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "python-multipart", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "sse-starlette", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "starlette", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "typing-inspection", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "uvicorn", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/6d/62e76bbb8144d6ed86e202b5edd8a4cb631e7c8130f3f4893c3f90262b10/mcp-1.26.0.tar.gz", hash = "sha256:db6e2ef491eecc1a0d93711a76f28dec2e05999f93afd48795da1c1137142c66", size = 608005, upload-time = "2026-01-24T19:40:32.468Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/d9/eaa1f80170d2b7c5ba23f3b59f766f3a0bb41155fbc32a69adfa1adaaef9/mcp-1.26.0-py3-none-any.whl", hash = "sha256:904a21c33c25aa98ddbeb47273033c435e595bbacfdb177f4bd87f6dceebe1ca", size = 233615, upload-time = "2026-01-24T19:40:30.652Z" }, +] + [[package]] name = "mdurl" version = "0.1.2" @@ -2937,6 +2986,7 @@ dependencies = [ { name = "django-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "django-filter", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "django-guardian", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "django-mcp-server", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "django-multiselectfield", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "django-soft-delete", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "django-treenode", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, @@ -3085,6 +3135,7 @@ requires-dist = [ { name = "django-extensions", specifier = "~=4.1" }, { name = "django-filter", specifier = "~=25.1" }, { name = "django-guardian", specifier = "~=3.2.0" }, + { name = "django-mcp-server", specifier = "~=0.5.7" }, { name = "django-multiselectfield", specifier = "~=1.0.1" }, { name = "django-soft-delete", specifier = "~=1.0.18" }, { name = "django-treenode", specifier = ">=0.23.2" }, @@ -3790,6 +3841,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, ] +[[package]] +name = "pydantic-settings" +version = "2.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "python-dotenv", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "typing-inspection", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/4b/ac7e0aae12027748076d72a8764ff1c9d82ca75a7a52622e67ed3f765c54/pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0", size = 194184, upload-time = "2025-11-10T14:25:47.013Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880, upload-time = "2025-11-10T14:25:45.546Z" }, +] + [[package]] name = "pygments" version = "2.19.2" @@ -4007,6 +4072,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6c/73/9f872cb81fc5c3bb48f7227872c28975f998f3e7c2b1c16e95e6432bbb90/python_magic-0.4.27-py2.py3-none-any.whl", hash = "sha256:c212960ad306f700aa0d01e5d7a325d20548ff97eb9920dcd29513174f0294d3", size = 13840, upload-time = "2022-06-07T20:16:57.763Z" }, ] +[[package]] +name = "python-multipart" +version = "0.0.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, +] + [[package]] name = "pytz" version = "2025.2" @@ -4948,6 +5022,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a9/5c/bfd6bd0bf979426d405cc6e71eceb8701b148b16c21d2dc3c261efc61c7b/sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca", size = 44415, upload-time = "2024-12-10T12:05:27.824Z" }, ] +[[package]] +name = "sse-starlette" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "starlette", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/8d/00d280c03ffd39aaee0e86ec81e2d3b9253036a0f93f51d10503adef0e65/sse_starlette-3.2.0.tar.gz", hash = "sha256:8127594edfb51abe44eac9c49e59b0b01f1039d0c7461c6fd91d4e03b70da422", size = 27253, upload-time = "2026-01-17T13:11:05.62Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/7f/832f015020844a8b8f7a9cbc103dd76ba8e3875004c41e08440ea3a2b41a/sse_starlette-3.2.0-py3-none-any.whl", hash = "sha256:5876954bd51920fc2cd51baee47a080eb88a37b5b784e615abb0b283f801cdbf", size = 12763, upload-time = "2026-01-17T13:11:03.775Z" }, +] + +[[package]] +name = "starlette" +version = "0.52.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "(python_full_version < '3.13' and sys_platform == 'darwin') or (python_full_version < '3.13' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/68/79977123bb7be889ad680d79a40f339082c1978b5cfcf62c2d8d196873ac/starlette-0.52.1.tar.gz", hash = "sha256:834edd1b0a23167694292e94f597773bc3f89f362be6effee198165a35d62933", size = 2653702, upload-time = "2026-01-18T13:34:11.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272, upload-time = "2026-01-18T13:34:09.188Z" }, +] + [[package]] name = "sympy" version = "1.13.3" @@ -5108,13 +5208,13 @@ dependencies = [ { name = "typing-extensions", marker = "sys_platform == 'darwin'" }, ] wheels = [ - { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1-cp310-none-macosx_11_0_arm64.whl" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1-cp311-none-macosx_11_0_arm64.whl" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1-cp312-none-macosx_11_0_arm64.whl" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1-cp313-cp313t-macosx_11_0_arm64.whl" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1-cp313-none-macosx_11_0_arm64.whl" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1-cp314-cp314-macosx_11_0_arm64.whl" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1-cp314-cp314t-macosx_11_0_arm64.whl" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:bf1e68cfb935ae2046374ff02a7aa73dda70351b46342846f557055b3a540bf0" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:a52952a8c90a422c14627ea99b9826b7557203b46b4d0772d3ca5c7699692425" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:287242dd1f830846098b5eca847f817aa5c6015ea57ab4c1287809efea7b77eb" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8924d10d36eac8fe0652a060a03fc2ae52980841850b9a1a2ddb0f27a4f181cd" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:bcee64ae7aa65876ceeae6dcaebe75109485b213528c74939602208a20706e3f" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:defadbeb055cfcf5def58f70937145aecbd7a4bc295238ded1d0e85ae2cf0e1d" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:886f84b181f766f53265ba0a1d503011e60f53fff9d569563ef94f24160e1072" }, ] [[package]] @@ -5138,20 +5238,20 @@ dependencies = [ { name = "typing-extensions", marker = "sys_platform == 'linux'" }, ] wheels = [ - { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp310-cp310-manylinux_2_28_aarch64.whl" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp310-cp310-manylinux_2_28_x86_64.whl" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp311-cp311-manylinux_2_28_aarch64.whl" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp311-cp311-manylinux_2_28_x86_64.whl" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp312-cp312-manylinux_2_28_aarch64.whl" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp312-cp312-manylinux_2_28_x86_64.whl" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp313-cp313-manylinux_2_28_aarch64.whl" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp313-cp313-manylinux_2_28_x86_64.whl" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp313-cp313t-manylinux_2_28_aarch64.whl" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp313-cp313t-manylinux_2_28_x86_64.whl" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp314-cp314-manylinux_2_28_aarch64.whl" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp314-cp314-manylinux_2_28_x86_64.whl" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp314-cp314t-manylinux_2_28_aarch64.whl" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp314-cp314t-manylinux_2_28_x86_64.whl" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:10866c8a48c4aa5ae3f48538dc8a055b99c57d9c6af2bf5dd715374d9d6ddca3" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:7210713b66943fdbfcc237b2e782871b649123ac5d29f548ce8c85be4223ab38" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:0e611cfb16724e62252b67d31073bc5c490cb83e92ecdc1192762535e0e44487" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:3de2adb9b4443dc9210ef1f1b16da3647ace53553166d6360bbbd7edd6f16e4d" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3bf9b442a51a2948e41216a76d7ab00f0694cfcaaa51b6f9bcab57b7f89843e6" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:7417d8c565f219d3455654cb431c6d892a3eb40246055e14d645422de13b9ea1" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:3e532e553b37ee859205a9b2d1c7977fd6922f53bbb1b9bfdd5bdc00d1a60ed4" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:39b3dff6d8fba240ae0d1bede4ca11c2531ae3b47329206512d99e17907ff74b" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:01b1884f724977a20c7da2f640f1c7b37f4a2c117a7f4a6c1c0424d14cb86322" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:031a597147fa81b1e6d79ccf1ad3ccc7fafa27941d6cf26ff5caaa384fb20e92" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:65010ab4aacce6c9a1ddfc935f986c003ca8638ded04348fd326c3e74346237c" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:88adf5157db5da1d54b1c9fe4a6c1d20ceef00e75d854e206a87dbf69e3037dc" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:3ac2b8df2c55430e836dcda31940d47f1f5f94b8731057b6f20300ebea394dd9" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.9.1%2Bcpu-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:5b688445f928f13563b7418b17c57e97bf955ab559cf73cd8f2b961f8572dbb3" }, ] [[package]] @@ -5495,6 +5595,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5d/34/257747253ad446fd155e39f0c30afda4597b3b9e28f44a9de5dee76a6509/uv-0.9.6-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:b31377ebf2d0499afc5abe3fe1abded5ca843f3a1161b432fe26eb0ce15bab8e", size = 21597889, upload-time = "2025-10-29T19:40:36.963Z" }, ] +[[package]] +name = "uvicorn" +version = "0.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "h11", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e6689dd3b1d345c815f10f86acd044ee1fb9a4dcd0b8c5/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761, upload-time = "2025-12-21T14:16:22.45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502, upload-time = "2025-12-21T14:16:21.041Z" }, +] + [[package]] name = "uvloop" version = "0.21.0"