Skip to content

Commit 5413027

Browse files
📝 Add docstrings to mcp_server
Docstrings generation was requested by @GabrielBarberini. * #61 (comment) The following files were modified: * `src/mcp/server.py` * `src/repositories/interface.py` * `tests/unit/test_mcp/test_mcp_server.py`
1 parent f18358b commit 5413027

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,13 +8,18 @@
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'):
1621
return app.state.mcp # type: ignore[attr-defined]
1722

1823
mcp = FastMCP.from_fastapi(app, name=app.title)
1924
app.state.mcp = mcp # type: ignore[attr-defined]
20-
return mcp
25+
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
@@ -14,6 +14,11 @@
1414

1515
@pytest.fixture(autouse=True)
1616
def reset_mcp_state():
17+
"""
18+
Ensure the FastAPI app has no lingering MCP state before and after a test.
19+
20+
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.
21+
"""
1722
if hasattr(app.state, 'mcp'):
1823
delattr(app.state, 'mcp')
1924
yield
@@ -63,4 +68,4 @@ async def test_mcp_tools_cover_registered_routes():
6368
# Path parameters must be represented as required MCP tool arguments
6469
assert path_params.issubset(
6570
required
66-
), f"{tool_name} missing path params {path_params - required}"
71+
), f"{tool_name} missing path params {path_params - required}"

0 commit comments

Comments
 (0)