Skip to content

Commit 326db29

Browse files
📝 Add docstrings to mcp_server (#62)
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Gabriel Barberini <[email protected]>
1 parent 6500475 commit 326db29

File tree

3 files changed

+70
-5
lines changed

3 files changed

+70
-5
lines changed

src/mcp/server.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,13 @@
88

99
def build_mcp(app: FastAPI) -> FastMCP:
1010
"""
11-
Create (or return cached) FastMCP server
12-
that mirrors the FastAPI app.
11+
Create or return a cached FastMCP server that mirrors the given FastAPI app.
12+
13+
Parameters:
14+
app (FastAPI): FastAPI application to mirror; the created FastMCP instance is cached on `app.state.mcp`.
15+
16+
Returns:
17+
FastMCP: The FastMCP instance corresponding to the provided FastAPI app.
1318
"""
1419

1520
if hasattr(app.state, 'mcp'):
@@ -18,4 +23,4 @@ def build_mcp(app: FastAPI) -> FastMCP:
1823
settings.experimental.enable_new_openapi_parser = True
1924
mcp = FastMCP.from_fastapi(app, name=app.title)
2025
app.state.mcp = mcp # type: ignore[attr-defined]
21-
return mcp
26+
return mcp

src/repositories/interface.py

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,19 @@ def __init__(self):
3535

3636

3737
def repository_exception_handler(method):
38+
"""
39+
Decorator that standardizes error handling and logging for repository coroutine methods.
40+
41+
Parameters:
42+
method (Callable): The asynchronous repository method to wrap.
43+
44+
Returns:
45+
wrapper (Callable): An async wrapper that:
46+
- re-raises PyMongoError after logging the exception,
47+
- re-raises RepositoryNotInitializedException after logging the exception,
48+
- logs any other exception and raises an HTTPException with status 500 and detail 'Unexpected error ocurred',
49+
- always logs completion of the repository method call with the repository name, method name, and kwargs.
50+
"""
3851
@functools.wraps(method)
3952
async def wrapper(self, *args, **kwargs):
4053
try:
@@ -81,13 +94,31 @@ class RepositoryInterface:
8194
_global_thread_lock = threading.Lock()
8295

8396
def __new__(cls, *args, **kwargs):
97+
"""
98+
Ensure a single thread-safe instance exists for the subclass.
99+
100+
Creates and returns the singleton instance for this subclass, creating it if absent while holding a global thread lock to prevent concurrent instantiation.
101+
102+
Returns:
103+
The singleton instance of the subclass.
104+
"""
84105
with cls._global_thread_lock:
85106
if cls not in cls._global_instances:
86107
instance = super().__new__(cls)
87108
cls._global_instances[cls] = instance
88109
return cls._global_instances[cls]
89110

90111
def __init__(self, model: ApiBaseModel, *, max_pool_size: int = 3):
112+
"""
113+
Initialize the repository instance for a specific API model and configure its connection pool.
114+
115+
Parameters:
116+
model (ApiBaseModel): The API model used for validation and to determine the repository's collection.
117+
max_pool_size (int, optional): Maximum size of the MongoDB connection pool. Defaults to 3.
118+
119+
Notes:
120+
If the instance is already initialized, this constructor will not reconfigure it. Initialization of the underlying connection is started asynchronously.
121+
"""
91122
if not getattr(self, '_initialized', False):
92123
self.model = model
93124
self._max_pool_size = max_pool_size
@@ -96,6 +127,11 @@ def __init__(self, model: ApiBaseModel, *, max_pool_size: int = 3):
96127

97128
@retry(stop=stop_after_attempt(5), wait=wait_fixed(0.2))
98129
async def _async_init(self):
130+
"""
131+
Perform idempotent, retry-safe asynchronous initialization of the repository instance.
132+
133+
Ensures a per-instance asyncio.Lock exists and acquires it to run initialization exactly once; on success it marks the instance as initialized and sets the internal _initialized_event so awaiters can proceed. If initialization fails, the original exception from _initialize_connection is propagated after logging.
134+
"""
99135
if getattr(self, '_initialized', False):
100136
return
101137

@@ -117,6 +153,11 @@ async def _async_init(self):
117153
self._initialized_event.set()
118154

119155
def _initialize(self):
156+
"""
157+
Ensure the repository's asynchronous initializer is executed: run it immediately if no event loop is active, otherwise schedule it on the running loop.
158+
159+
If there is no running asyncio event loop, this method runs self._async_init() to completion on the current thread, blocking until it finishes. If an event loop is running, it schedules self._async_init() as a background task on that loop and returns immediately.
160+
"""
120161
try:
121162
loop = asyncio.get_running_loop()
122163
except RuntimeError:
@@ -125,13 +166,27 @@ def _initialize(self):
125166
loop.create_task(self._async_init())
126167

127168
async def __aenter__(self):
169+
"""
170+
Waits for repository initialization to complete and returns the repository instance.
171+
172+
Returns:
173+
RepositoryInterface: The initialized repository instance.
174+
"""
128175
await self._initialized_event.wait() # Ensure initialization is complete
129176
return self
130177

131178
async def __aexit__(self, exc_type, exc_value, traceback):
132179
await self._initialized_event.wait()
133180

134181
def _initialize_connection(self):
182+
"""
183+
Initialize the MongoDB async client, store the connection string, and bind the collection for this repository instance.
184+
185+
This method fetches the MongoDB connection string from secrets, creates an AsyncMongoClient configured with pool and timeout settings, and sets self._collection to the repository's collection named by the model. On success it logs the initialized client; on failure it raises a ConnectionError.
186+
187+
Raises:
188+
ConnectionError: If the client or collection cannot be initialized.
189+
"""
135190
try:
136191
self._connection_string = Secrets.get_secret(
137192
"MONGODB_CONNECTION_STRING"
@@ -229,4 +284,4 @@ async def find_by_query(self, query: dict):
229284
parsed_model = self.model.model_validate(read_data)
230285
parsed_model.set_id(str(read_data["_id"]))
231286
parsed_models.append(parsed_model)
232-
return parsed_models
287+
return parsed_models

tests/unit/test_mcp/test_mcp_server.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@
1212

1313
@pytest.fixture(autouse=True)
1414
def reset_mcp_state():
15+
"""
16+
Ensure the FastAPI app has no lingering MCP state before and after a test.
17+
18+
This fixture deletes app.state.mcp if it exists, yields control to the test, and then deletes app.state.mcp again to guarantee the MCP state is cleared between tests.
19+
"""
1520
if hasattr(rest_app.state, 'mcp'):
1621
delattr(rest_app.state, 'mcp')
1722
yield
@@ -76,4 +81,4 @@ async def test_combined_app_serves_rest_and_mcp(monkeypatch):
7681
resp_rest = await client.get('/health')
7782
assert resp_rest.status_code == 200
7883
resp_docs = await client.get('/docs')
79-
assert resp_docs.status_code == 200
84+
assert resp_docs.status_code == 200

0 commit comments

Comments
 (0)