fix(base): reference-count session lifecycle for safe concurrent use - #9
Merged
Merged
Conversation
The MCP server shares one global FMPBaseClient across all in-flight
requests, and every tool wraps its work in `async with client:`, which
calls start() on entry and close() on exit. Under concurrent requests the
tools shared a single aiohttp session, so the first request to exit closed
it out from under the others:
- `await session.close()` tore down the connector mid-request
-> "Connector is closed" (then retried)
- `self._session = None`, so the retry hit `self._session.get(...)`
on None -> "'NoneType' object has no attribute 'get'"
Reference-count start()/close() under an asyncio.Lock so overlapping
scopes share one session, torn down only when the last active scope exits.
This fixes all 22 tool categories at once since they funnel through
FMPBaseClient.start/close; no tool code changes required.
Add tests/test_base.py covering the overlapping-scope invariant.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Under concurrent MCP requests, tools were failing with:
This was not an FMP subscription/tier issue — a single sequential call to
company.profile("AAPL")andcompany.shares_float("AAPL")returns valid data on the current plan.Root cause
The MCP server hands every tool the same global singleton client (
get_fmp_client()), and every one of the 22*_tools.pycategories wraps its work inasync with client:— which callsstart()on entry andclose()on exit. Under concurrent requests the tools share one aiohttp session, so the first request to finish closed the session out from under the others:await session.close()tears down the connector while another request is mid-flight →Connector is closed(retried, "attempt 2/4").self._session = None, so the retry hitsself._session.get(...)onNone→'NoneType' object has no attribute 'get'.Reproduced deterministically: one shared client + 12 concurrent
async withscopes → 11/12 failed with exactly these errors.Fix
Reference-count the session lifecycle in
FMPBaseClient.start()/close()under anasyncio.Lock: overlapping scopes share a single session, and it is only torn down when the last active scope exits. The reference is counted only after the session is guaranteed to exist, so a failed session creation can't leak a reference.Because every tool funnels through
FMPBaseClient.start/close, this one change fixes all 22 tool categories — no tool code changes.Verification
tests/test_base.py)ruff checkget_shares_float/get_company_profile🤖 Generated with Claude Code