diff --git a/fasta2a/applications.py b/fasta2a/applications.py
index 89a3cda..b835869 100644
--- a/fasta2a/applications.py
+++ b/fasta2a/applications.py
@@ -1,5 +1,7 @@
from __future__ import annotations as _annotations
+import html
+import json
from collections.abc import AsyncIterator, Sequence
from contextlib import asynccontextmanager
from pathlib import Path
@@ -8,7 +10,7 @@
from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.requests import Request
-from starlette.responses import FileResponse, Response, StreamingResponse
+from starlette.responses import Response, StreamingResponse
from starlette.routing import Route
from starlette.types import ExceptionHandler, Lifespan, Receive, Scope, Send
@@ -112,7 +114,11 @@ async def _agent_card_endpoint(self, request: Request) -> Response:
async def _docs_endpoint(self, request: Request) -> Response:
"""Serve the documentation interface."""
docs_path = Path(__file__).parent / 'static' / 'docs.html'
- return FileResponse(docs_path, media_type='text/html')
+ root_path = request.scope.get('root_path', '').rstrip('/')
+ content = docs_path.read_text()
+ content = content.replace('__FASTA2A_API_ROOT_JSON__', json.dumps(root_path))
+ content = content.replace('__FASTA2A_API_ROOT__', html.escape(root_path, quote=True))
+ return Response(content=content, media_type='text/html')
async def _agent_run_endpoint(self, request: Request) -> Response:
"""This is the main endpoint for the A2A server.
diff --git a/fasta2a/static/docs.html b/fasta2a/static/docs.html
index a1862a2..5e320e2 100644
--- a/fasta2a/static/docs.html
+++ b/fasta2a/static/docs.html
@@ -489,7 +489,7 @@
📋 Agent Information
Loading agent information...
@@ -526,11 +526,16 @@ 💬 Chat Interface
let agentCard = null;
let currentTaskId = null;
let contextId = null;
+ const apiRoot = __FASTA2A_API_ROOT_JSON__;
+
+ function apiUrl(path) {
+ return `${apiRoot}${path}`;
+ }
// Load agent card information
async function loadAgentCard() {
try {
- const response = await fetch('/.well-known/agent-card.json');
+ const response = await fetch(apiUrl('/.well-known/agent-card.json'));
if (!response.ok) throw new Error('Failed to load agent card');
agentCard = await response.json();
@@ -596,7 +601,7 @@ Skills:
try {
// Send message to A2A endpoint
- const response = await fetch('/', {
+ const response = await fetch(apiUrl('/'), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -660,7 +665,7 @@ Skills:
if (!currentTaskId) return;
try {
- const response = await fetch('/', {
+ const response = await fetch(apiUrl('/'), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
diff --git a/tests/test_applications.py b/tests/test_applications.py
index 2385138..18a0fc7 100644
--- a/tests/test_applications.py
+++ b/tests/test_applications.py
@@ -6,6 +6,7 @@
import pytest
from asgi_lifespan import LifespanManager
from inline_snapshot import snapshot
+from starlette.applications import Starlette
from fasta2a.applications import FastA2A
from fasta2a.broker import InMemoryBroker
@@ -56,6 +57,8 @@ async def test_docs_endpoint_default(self):
async with create_test_client(app) as client:
response = await client.get('/docs')
assert response.status_code == 200
+ assert '__FASTA2A_API_ROOT__' not in response.text
+ assert 'const apiRoot = "";' in response.text
async def test_docs_endpoint_custom_url(self):
app = FastA2A(storage=InMemoryStorage(), broker=InMemoryBroker(), docs_url='/custom-docs')
@@ -63,6 +66,29 @@ async def test_docs_endpoint_custom_url(self):
response = await client.get('/custom-docs')
assert response.status_code == 200
+ async def test_docs_endpoint_mounted_app_uses_root_path(self):
+ a2a_app = FastA2A(storage=InMemoryStorage(), broker=InMemoryBroker())
+
+ @asynccontextmanager
+ async def lifespan(_app: Starlette):
+ async with a2a_app.router.lifespan_context(a2a_app):
+ yield
+
+ app = Starlette(lifespan=lifespan)
+ app.mount('/agent', a2a_app)
+
+ async with LifespanManager(app=app) as manager:
+ transport = httpx.ASGITransport(app=manager.app)
+ async with httpx.AsyncClient(transport=transport, base_url='http://testclient') as client:
+ response = await client.get('/agent/docs')
+ assert response.status_code == 200
+ assert '__FASTA2A_API_ROOT__' not in response.text
+ assert 'const apiRoot = "/agent";' in response.text
+ assert 'href="/agent/.well-known/agent-card.json"' in response.text
+
+ response = await client.get('/agent/.well-known/agent-card.json')
+ assert response.status_code == 200
+
async def test_docs_endpoint_disabled(self):
app = FastA2A(storage=InMemoryStorage(), broker=InMemoryBroker(), docs_url=None)
async with create_test_client(app) as client: