From d0a49d0a0dbfb381f5e928f93788d39c2cb830d3 Mon Sep 17 00:00:00 2001 From: Lucas Van Vonderen Date: Thu, 23 Apr 2026 15:32:50 -0400 Subject: [PATCH 1/6] fix(mcp): mount session manager via Starlette Mount to avoid double response.start MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit StreamableHTTPSessionManager is an ASGI app — it sends its own http.response.start and http.response.body messages via the scope/receive/send triple. Wrapping it inside a @app.api_route FastAPI handler causes FastAPI to try to finalize the response after the handler returns, emitting a second http.response.start. uvicorn rejects this with: RuntimeError: Unexpected ASGI message 'http.response.start' sent, after response already completed. Every POST /mcp raises, making the server unreachable from any MCP client (tested with Claude Code 2.x against mcp==1.27.0). Fix: mount the session manager as a Starlette Mount so it owns the response cycle directly. Auth middleware still applies because add_middleware attaches at the app level, wrapping all mounted sub-apps. --- mcp/app/main.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/mcp/app/main.py b/mcp/app/main.py index ca40953..7beafa2 100644 --- a/mcp/app/main.py +++ b/mcp/app/main.py @@ -1,7 +1,8 @@ from contextlib import asynccontextmanager -from fastapi import FastAPI, Request +from fastapi import FastAPI from mcp.server import Server from mcp.server.streamable_http_manager import StreamableHTTPSessionManager +from starlette.routing import Mount from .auth import ApiKeyMiddleware from .backend_client import backend @@ -28,15 +29,20 @@ async def lifespan(app: FastAPI): await backend.stop() -app = FastAPI(title="Homelable MCP", lifespan=lifespan) +# Mount the session manager as an ASGI sub-app instead of wrapping it in a +# FastAPI @app.api_route handler. Wrapping it in a route handler causes +# FastAPI to send http.response.start after the session manager has already +# started the response, raising `RuntimeError: Unexpected ASGI message +# 'http.response.start' sent, after response already completed` on every +# POST /mcp — which makes the server unreachable from any MCP client. +app = FastAPI( + title="Homelable MCP", + lifespan=lifespan, + routes=[Mount("/mcp", app=session_manager.handle_request)], +) app.add_middleware(ApiKeyMiddleware) -@app.api_route("/mcp", methods=["GET", "POST", "DELETE"]) -async def mcp_endpoint(request: Request): - await session_manager.handle_request(request.scope, request.receive, request._send) - - @app.get("/health") async def health(): return {"status": "ok"} From 3ccdde0beafdf3a57c372171bc7aa7ca2c2f10e1 Mon Sep 17 00:00:00 2001 From: Brett Ferrante <83841899+findthelorax@users.noreply.github.com> Date: Mon, 20 Apr 2026 09:35:53 -0400 Subject: [PATCH 2/6] Update Docker image references to use repository owner --- .github/workflows/docker-publish.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 600ae67..fe0a4e8 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -15,13 +15,13 @@ jobs: strategy: matrix: include: - - image: ghcr.io/pouzor/homelable-backend + - image: ghcr.io/${{ github.repository_owner }}/homelable-backend dockerfile: Dockerfile.backend build_args: "" - - image: ghcr.io/pouzor/homelable-frontend + - image: ghcr.io/${{ github.repository_owner }}/homelable-frontend dockerfile: Dockerfile.frontend build_args: "" - - image: ghcr.io/pouzor/homelable-frontend-standalone + - image: ghcr.io/${{ github.repository_owner }}/homelable-frontend-standalone dockerfile: Dockerfile.frontend build_args: "VITE_STANDALONE=true" From cd0e08fb91fbbe498f1a85e9b78a1ef04adba3a2 Mon Sep 17 00:00:00 2001 From: findthelorax Date: Mon, 20 Apr 2026 23:48:24 -0400 Subject: [PATCH 3/6] fixed canvas style options to fit better and be max 50vw --- frontend/src/components/modals/ThemeModal.tsx | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/frontend/src/components/modals/ThemeModal.tsx b/frontend/src/components/modals/ThemeModal.tsx index 23f1d6c..62901e7 100644 --- a/frontend/src/components/modals/ThemeModal.tsx +++ b/frontend/src/components/modals/ThemeModal.tsx @@ -70,13 +70,13 @@ function ThemeCard({ themeId, selected, onClick }: ThemeCardProps) { {/* Label */}
{preset.label}
{preset.description} @@ -121,19 +121,20 @@ export function ThemeModal({ open, onClose }: ThemeModalProps) { return ( { if (!o) handleCancel() }}> - + Choose Canvas Style -
+
{THEME_ORDER.map((id) => ( - handleSelect(id)} - /> +
+ handleSelect(id)} + /> +
))}
From adb208875258d539aa8e6aabef6a1fc7ce843635 Mon Sep 17 00:00:00 2001 From: findthelorax Date: Mon, 20 Apr 2026 23:49:17 -0400 Subject: [PATCH 4/6] revert: restore workflow to upstream version --- .github/workflows/docker-publish.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index fe0a4e8..600ae67 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -15,13 +15,13 @@ jobs: strategy: matrix: include: - - image: ghcr.io/${{ github.repository_owner }}/homelable-backend + - image: ghcr.io/pouzor/homelable-backend dockerfile: Dockerfile.backend build_args: "" - - image: ghcr.io/${{ github.repository_owner }}/homelable-frontend + - image: ghcr.io/pouzor/homelable-frontend dockerfile: Dockerfile.frontend build_args: "" - - image: ghcr.io/${{ github.repository_owner }}/homelable-frontend-standalone + - image: ghcr.io/pouzor/homelable-frontend-standalone dockerfile: Dockerfile.frontend build_args: "VITE_STANDALONE=true" From da287d459ce85f1b2a28a2be75380348b2eeb122 Mon Sep 17 00:00:00 2001 From: findthelorax Date: Mon, 20 Apr 2026 23:57:28 -0400 Subject: [PATCH 5/6] allow for keyboard navigation and adjusted card height to match regardless of text inside --- frontend/src/components/modals/ThemeModal.tsx | 41 +++++++++++++++---- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/frontend/src/components/modals/ThemeModal.tsx b/frontend/src/components/modals/ThemeModal.tsx index 62901e7..5b50979 100644 --- a/frontend/src/components/modals/ThemeModal.tsx +++ b/frontend/src/components/modals/ThemeModal.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react' +import { useRef, useState, type KeyboardEvent } from 'react' import { toast } from 'sonner' import { Check } from 'lucide-react' import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' @@ -14,17 +14,21 @@ interface ThemeCardProps { themeId: ThemeId selected: boolean onClick: () => void + onKeyDown?: (event: KeyboardEvent) => void + buttonRef?: (element: HTMLButtonElement | null) => void } -function ThemeCard({ themeId, selected, onClick }: ThemeCardProps) { +function ThemeCard({ themeId, selected, onClick, onKeyDown, buttonRef }: ThemeCardProps) { const preset = THEMES[themeId] const c = preset.colors return (