Skip to content

Commit 7433b67

Browse files
committed
feat(mcp): support configurable MCP endpoints and enhance CORS handling
- Introduce `DJANGO_MCP_ENDPOINT` for custom REST route prefixes. - Update CORS middleware to dynamically apply allowed origins based on the configured endpoint. - Refactor and consolidate path prefix logic for improved clarity. - Add corresponding tests for custom MCP endpoint behavior. - Bump version to 2.2.0.
1 parent eea28ea commit 7433b67

4 files changed

Lines changed: 43 additions & 6 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "django-smartbase-admin"
3-
version = "2.1.1"
3+
version = "2.2.0"
44
description = ""
55
authors = ["SmartBase <info@smartbase.sk>"]
66
readme = "README.md"

src/django_smartbase_admin/engine/configuration.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ def __init__(
233233
mcp_whoami_sbadmin=None,
234234
) -> None:
235235
super().__init__()
236-
self.default_view = default_view or self.default_view or []
236+
self.default_view = default_view or self.default_view
237237
self.registered_views = registered_views or self.registered_views or []
238238
self.menu_items = menu_items or self.menu_items or []
239239
self.global_filter_form = global_filter_form or self.global_filter_form

src/django_smartbase_admin/mcp/middleware.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
33
Browser-hosted MCP clients (claude.ai's Cowork integration, the various
44
ChatGPT bridges, Cursor's web client) and custom browser dashboards
5-
cross-origin POST to ``/mcp/`` and send an OAuth preflight to
5+
cross-origin POST to the configured MCP endpoint and send an OAuth preflight to
66
``/.well-known/oauth-protected-resource``. Without
77
``Access-Control-Allow-Origin`` + matching ``-Methods`` / ``-Headers``,
88
the browser blocks every request before it reaches Django.
@@ -41,8 +41,7 @@
4141
"https://cursor.com",
4242
)
4343

44-
_MCP_PATH_PREFIXES: tuple[str, ...] = (
45-
"/mcp",
44+
_OAUTH_PATH_PREFIXES: tuple[str, ...] = (
4645
"/.well-known/oauth-authorization-server",
4746
"/.well-known/oauth-protected-resource",
4847
"/oauth/",
@@ -76,8 +75,23 @@ def _allowed_request_headers() -> str:
7675
return ", ".join(headers)
7776

7877

78+
def _mcp_path_prefix() -> str | None:
79+
endpoint = getattr(settings, "DJANGO_MCP_ENDPOINT", "mcp/")
80+
endpoint = endpoint.strip("/")
81+
if not endpoint:
82+
return None
83+
return f"/{endpoint}"
84+
85+
86+
def _path_matches_prefix(path: str, prefix: str) -> bool:
87+
return path == prefix or path.startswith(f"{prefix}/")
88+
89+
7990
def _path_needs_cors(path: str) -> bool:
80-
return any(path.startswith(prefix) for prefix in _MCP_PATH_PREFIXES)
91+
mcp_prefix = _mcp_path_prefix()
92+
if mcp_prefix and _path_matches_prefix(path, mcp_prefix):
93+
return True
94+
return any(path.startswith(prefix) for prefix in _OAUTH_PATH_PREFIXES)
8195

8296

8397
def _apply_cors_headers(response: HttpResponse, origin: str) -> None:

src/django_smartbase_admin/mcp/tests/test_cors_middleware.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,21 @@
88

99
from __future__ import annotations
1010

11+
from django.conf import settings
1112
from django.http import HttpResponse
1213
from django.test import RequestFactory, SimpleTestCase, override_settings
1314

1415
from django_smartbase_admin.mcp.middleware import SBAdminMCPCorsMiddleware
1516

17+
if not settings.configured:
18+
settings.configure(
19+
DEFAULT_CHARSET="utf-8",
20+
SECRET_KEY="test-secret-key",
21+
DATABASES={
22+
"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}
23+
},
24+
)
25+
1626
ORIGIN = "http://localhost:8010"
1727

1828

@@ -51,6 +61,19 @@ def test_mcp_rest_and_oauth_paths_are_decorated(self):
5161
self._send(path, method="get")["Access-Control-Allow-Origin"], ORIGIN
5262
)
5363

64+
@override_settings(DJANGO_MCP_ENDPOINT="api/mcp/")
65+
def test_configured_mcp_endpoint_is_used_for_cors(self):
66+
resp = self._send("/api/mcp/rest/tools/list_rows/", preflight=True)
67+
68+
self.assertEqual(resp.status_code, 204)
69+
self.assertEqual(resp["Access-Control-Allow-Origin"], ORIGIN)
70+
71+
@override_settings(DJANGO_MCP_ENDPOINT="api/mcp/")
72+
def test_default_mcp_endpoint_is_not_decorated_when_endpoint_moves(self):
73+
resp = self._send("/mcp/rest/tools/list_rows/", method="get")
74+
75+
self.assertNotIn("Access-Control-Allow-Origin", resp)
76+
5477
def test_preflight_uses_default_allowed_headers(self):
5578
resp = self._send("/mcp/rest/tools/list_rows/", preflight=True)
5679

0 commit comments

Comments
 (0)