Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `CONTRIBUTING.md` with Contributor License Agreement (CLA) requirement
- `benchmarks/semantic_search_bench.py` — latency benchmarks for semantic search across scale tiers (500–10K entries)
- **PR label automation** (`pr-labels.yml`): GitHub Actions workflow that automates label transitions — resets to "Awaiting CI" on push, promotes to "Ready for QA" when CI passes, cleans up stale labels when actors pick up tasks
- **Favicon route**: `/favicon.ico` served from both `SecretPathMiddleware` and `HealthMiddleware` so Anthropic's Connectors UI (and other services using Google's favicon service) display the awareness logo instead of a generic globe. Served publicly — no secret path required.

## [0.12.0] - 2026-03-26

Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ packages = ["src/mcp_awareness"]

[tool.hatch.build.targets.wheel.force-include]
"src/mcp_awareness/instructions.md" = "mcp_awareness/instructions.md"
"src/mcp_awareness/favicon.ico" = "mcp_awareness/favicon.ico"

[tool.pytest.ini_options]
testpaths = ["tests"]
Expand Down
Binary file added src/mcp_awareness/favicon.ico
Binary file not shown.
27 changes: 22 additions & 5 deletions src/mcp_awareness/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

"""ASGI middleware for health checks and secret-path routing."""
"""ASGI middleware for health checks, favicon, and secret-path routing."""

from __future__ import annotations

import pathlib
from collections.abc import Callable
from typing import Any

Expand All @@ -27,6 +28,10 @@
# The health response builder is injected so middleware doesn't depend on server globals.
HealthBuilder = Callable[[], dict[str, Any]]

# Favicon bytes loaded once at import time (~15 KB).
_FAVICON_PATH = pathlib.Path(__file__).parent / "favicon.ico"
_FAVICON_BYTES: bytes | None = _FAVICON_PATH.read_bytes() if _FAVICON_PATH.exists() else None


class SecretPathMiddleware:
"""Rewrite /SECRET/mcp -> /mcp, serve /SECRET/health, reject everything else."""
Expand All @@ -44,6 +49,12 @@ def __init__(
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
if scope["type"] in ("http", "websocket"):
path: str = scope.get("path", "")
# Favicon — served publicly (no secret path required) so external
# services like Google's favicon crawler can fetch it.
if path == "/favicon.ico" and _FAVICON_BYTES is not None:
resp = Response(_FAVICON_BYTES, media_type="image/x-icon")
await resp(scope, receive, send)
return
# Health endpoint — served at /SECRET/health
if path == f"{self.prefix}/health":
health_resp = JSONResponse(self.health_builder())
Expand All @@ -69,8 +80,14 @@ def __init__(self, app: ASGIApp, health_builder: HealthBuilder) -> None:
self.health_builder = health_builder

async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
if scope["type"] == "http" and scope.get("path") == "/health":
health_resp = JSONResponse(self.health_builder())
await health_resp(scope, receive, send)
return
if scope["type"] == "http":
path = scope.get("path", "")
if path == "/health":
health_resp = JSONResponse(self.health_builder())
await health_resp(scope, receive, send)
return
if path == "/favicon.ico" and _FAVICON_BYTES is not None:
resp = Response(_FAVICON_BYTES, media_type="image/x-icon")
await resp(scope, receive, send)
return
await self.app(scope, receive, send)
24 changes: 23 additions & 1 deletion tests/test_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@
import pytest

from mcp_awareness import server as server_mod
from mcp_awareness.middleware import HealthMiddleware, SecretPathMiddleware
from mcp_awareness.middleware import (
_FAVICON_BYTES,
HealthMiddleware,
SecretPathMiddleware,
)


def _health_builder() -> dict[str, Any]:
Expand Down Expand Up @@ -112,6 +116,15 @@ async def test_non_secret_path_returns_404(self) -> None:
status, _body = await _collect_response(app, scope)
assert status == 404

@pytest.mark.anyio
async def test_favicon_served_without_secret_path(self) -> None:
"""/favicon.ico is served publicly without requiring the secret prefix."""
app = self._make_app()
scope = {"type": "http", "path": "/favicon.ico", "method": "GET"}
status, body = await _collect_response(app, scope)
assert status == 200
assert body == _FAVICON_BYTES

@pytest.mark.anyio
async def test_non_http_scope_passes_through(self) -> None:
"""Non-HTTP scope (e.g. lifespan) passes through to wrapped app."""
Expand Down Expand Up @@ -191,6 +204,15 @@ async def test_other_paths_pass_through(self) -> None:
data = json.loads(body)
assert data["path"] == "/mcp"

@pytest.mark.anyio
async def test_favicon_served(self) -> None:
"""/favicon.ico returns the favicon with correct content type."""
app = self._make_app()
scope = {"type": "http", "path": "/favicon.ico", "method": "GET"}
status, body = await _collect_response(app, scope)
assert status == 200
assert body == _FAVICON_BYTES

@pytest.mark.anyio
async def test_non_http_scope_passes_through(self) -> None:
"""Non-HTTP scope passes through to wrapped app."""
Expand Down
Loading