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
2 changes: 2 additions & 0 deletions financedatabase/mcp_server/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
# Server name and the instructions sent to the client on initialisation.
server:
name: "Finance Database"
website_url: "https://www.jeroenbouma.com/projects/financedatabase/mcp"
icon_url: "https://financedatabase.jeroenbouma.com/icon.png"
instructions: >-
The Finance Database categorises financial symbols: equities, ETFs, indices,
currencies and cryptocurrencies. It holds classifications (sector, industry,
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added financedatabase/mcp_server/icons/favicon-32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added financedatabase/mcp_server/icons/favicon.ico
Binary file not shown.
11 changes: 9 additions & 2 deletions financedatabase/mcp_server/mcp_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import uvicorn
import yaml
from mcp.server.fastmcp import FastMCP
from mcp.types import Icon
from starlette.middleware.cors import CORSMiddleware
from starlette.requests import Request
from starlette.responses import JSONResponse
Expand All @@ -26,6 +27,7 @@
DatabaseProvider,
)
from financedatabase.mcp_server.registry_model import AssetToolRegistry
from financedatabase.mcp_server.site_model import register_site_routes
from financedatabase.mcp_server.tools_model import UtilityToolRegistry
from financedatabase.utilities.logger_model import get_logger

Expand Down Expand Up @@ -70,9 +72,12 @@ def _build_mcp_app() -> tuple[FastMCP, DatabaseProvider]:
synonyms=configuration.get("synonyms", {}),
)

server = configuration["server"]
mcp = FastMCP(
name=configuration["server"]["name"],
instructions=configuration["server"]["instructions"],
name=server["name"],
instructions=server["instructions"],
website_url=server["website_url"],
icons=[Icon(src=server["icon_url"], mimeType="image/png", sizes=["512x512"])],
log_level="CRITICAL",
# All interfaces for Docker, which also leaves the DNS-rebinding guard off.
host="0.0.0.0", # noqa: S104
Expand Down Expand Up @@ -105,6 +110,8 @@ def _build_mcp_app() -> tuple[FastMCP, DatabaseProvider]:
if analytics is not None:
analytics.register_route(mcp)

register_site_routes(mcp, server["website_url"])

@mcp.custom_route("/health", methods=["GET"])
async def check_health(request: Request) -> JSONResponse: # noqa: ARG001
return JSONResponse({"status": "ok"})
Expand Down
67 changes: 67 additions & 0 deletions financedatabase/mcp_server/site_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
"""Site Model"""

__docformat__ = "google"

from pathlib import Path
from typing import Any

from starlette.requests import Request
from starlette.responses import HTMLResponse, Response

ICON_DIRECTORY = Path(__file__).parent / "icons"
FULL_ICON = Path(__file__).parent / "mcpb" / "finance_database_icon.png"
ICONS = {
"/favicon.ico": (ICON_DIRECTORY / "favicon.ico", "image/x-icon"),
"/favicon-32.png": (ICON_DIRECTORY / "favicon-32.png", "image/png"),
"/apple-touch-icon.png": (ICON_DIRECTORY / "apple-touch-icon.png", "image/png"),
"/icon.png": (FULL_ICON, "image/png"),
}
PAGE = """<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Finance Database MCP</title>
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png">
<link rel="icon" type="image/png" sizes="512x512" href="/icon.png">
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
</head>
<body>
<p>This is the Finance Database MCP server; MCP clients connect at <code>/mcp</code>.
See <a href="{website}">{website}</a> for how to add it.</p>
</body>
</html>
"""


def register_site_routes(mcp: Any, website: str) -> None:
"""
Serve the icons and a short page at the root, so browsers and favicon lookups find the
Finance Database icon.

Args:
mcp (Any): The FastMCP server.
website (str): The documentation page the root page links to.
"""
if not hasattr(mcp, "custom_route"):
return

for path, (file, media_type) in ICONS.items():
content = file.read_bytes()

@mcp.custom_route(path, methods=["GET"])
async def show_icon(
request: Request, # noqa: ARG001
content: bytes = content,
media_type: str = media_type,
) -> Response:
return Response(
content,
media_type=media_type,
headers={"Cache-Control": "public, max-age=86400"},
)

page = PAGE.format(website=website)

@mcp.custom_route("/", methods=["GET"])
async def show_page(request: Request) -> HTMLResponse: # noqa: ARG001
return HTMLResponse(page)
27 changes: 27 additions & 0 deletions tests/mcp_server/test_site_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""Site Model Tests"""

from mcp.server.fastmcp import FastMCP
from starlette.testclient import TestClient

from financedatabase.mcp_server.site_model import register_site_routes


def test_icons_and_root_page_are_served() -> None:
"""Test that the icons and the root page are served for browsers and favicon lookups."""
mcp = FastMCP(name="Finance Database")
register_site_routes(mcp, "https://example.com/mcp")
client = TestClient(mcp.streamable_http_app())

icon = client.get("/favicon.ico")
assert icon.status_code == 200
assert icon.headers["content-type"] == "image/x-icon"
assert icon.content[:4] == b"\x00\x00\x01\x00" # ICO header
for path in ("/favicon-32.png", "/apple-touch-icon.png", "/icon.png"):
response = client.get(path)
assert response.status_code == 200 and response.content[:4] == b"\x89PNG"

page = client.get("/", follow_redirects=False)
assert page.status_code == 200
assert "<title>Finance Database MCP</title>" in page.text
assert 'href="/favicon-32.png"' in page.text
assert "https://example.com/mcp" in page.text
Loading