Skip to content

refactor(server): Split lifespan into composable browser + auth lifespans#199

Merged
stickerdaniel merged 2 commits intomainfrom
03-05-refactor_server_split_lifespan_into_composable_browser_auth_lifespans
Mar 5, 2026
Merged

refactor(server): Split lifespan into composable browser + auth lifespans#199
stickerdaniel merged 2 commits intomainfrom
03-05-refactor_server_split_lifespan_into_composable_browser_auth_lifespans

Conversation

@stickerdaniel
Copy link
Owner

@stickerdaniel stickerdaniel commented Mar 5, 2026

Greptile Summary

This PR refactors the single monolithic lifespan context manager into two focused, composable lifespans — auth_lifespan (validates authentication at startup) and browser_lifespan (manages browser setup and teardown) — composed via fastmcp's | operator as auth_lifespan | browser_lifespan. It also cleans up the legacy Dict import in favour of the built-in dict.

Key changes:

  • Removes @asynccontextmanager in favour of fastmcp's @lifespan decorator, aligning with the framework's native composition API.
  • auth_lifespan provides a fail-fast authentication check before the browser is ever started; if credentials are missing, startup aborts without initialising any browser resources.
  • browser_lifespan retains the original startup/shutdown log messages and calls close_browser() on teardown.
  • Shutdown order is correct: browser is torn down before auth lifespan completes (right-to-left for | composition), and since auth_lifespan has no cleanup code this is benign.
  • Two minor style points: auth_lifespan does not log when get_authentication_source() raises an exception (making failure diagnosis harder), and the synchronous filesystem call inside the async generator could briefly block the event loop on startup.

Confidence Score: 4/5

  • Safe to merge — the refactoring is functionally correct with only minor style concerns around error logging and a blocking I/O call in an async context.
  • The logic is sound: auth_lifespan correctly fails fast before browser resources are allocated, and browser_lifespan preserves existing teardown behaviour. The two style concerns (no error log on auth failure, sync I/O in async context) do not affect correctness or safety in practice, only observability and strict async hygiene.
  • No files require special attention.

Important Files Changed

Filename Overview
linkedin_mcp_server/server.py Refactors the single monolithic lifespan into two composable lifespans (auth_lifespan and browser_lifespan) using fastmcp's `

Sequence Diagram

sequenceDiagram
    participant F as FastMCP Framework
    participant AL as auth_lifespan
    participant BL as browser_lifespan
    participant AS as get_authentication_source()
    participant CB as close_browser()

    Note over F,CB: Startup Phase
    F->>AL: enter lifespan
    AL->>AL: log "Validating LinkedIn authentication..."
    AL->>AS: get_authentication_source()
    alt credentials missing
        AS-->>AL: raise CredentialsNotFoundError
        AL-->>F: propagate exception (server aborts)
    else credentials found
        AS-->>AL: return True
        AL->>AL: yield {} (startup complete)
        F->>BL: enter lifespan
        BL->>BL: log "LinkedIn MCP Server starting..."
        BL->>BL: yield {} (startup complete)
    end

    Note over F,CB: Server Running...

    Note over F,CB: Shutdown Phase
    F->>BL: resume after yield (shutdown)
    BL->>BL: log "LinkedIn MCP Server shutting down..."
    BL->>CB: close_browser()
    CB-->>BL: done
    F->>AL: resume after yield (shutdown)
    Note over AL: no cleanup needed
Loading

Last reviewed commit: 64ac267

Greptile also left 2 inline comments on this PR.

@stickerdaniel stickerdaniel marked this pull request as ready for review March 5, 2026 01:19
Copilot AI review requested due to automatic review settings March 5, 2026 01:19
Copy link
Owner Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Refactors the MCP server startup/shutdown handling by splitting the prior single lifespan context into composable auth + browser lifespans, aiming to validate auth at startup and ensure browser cleanup on shutdown.

Changes:

  • Replace the local asynccontextmanager lifespan with fastmcp.server.lifespan.lifespan-decorated lifespans.
  • Add auth_lifespan to validate an authentication profile exists at startup.
  • Compose auth_lifespan and browser_lifespan via auth_lifespan | browser_lifespan when constructing FastMCP.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Use lowercase dict instead of Dict, add auth validation log line
@stickerdaniel
Copy link
Owner Author

@greptileai review

Comment on lines +33 to +38
@lifespan
async def auth_lifespan(app: FastMCP) -> AsyncIterator[dict[str, Any]]:
"""Validate authentication profile exists at startup."""
logger.info("Validating LinkedIn authentication...")
get_authentication_source()
yield {}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No error logging on auth failure

If get_authentication_source() raises a CredentialsNotFoundError (or any other unexpected exception), the exception propagates out of the lifespan silently with no log line at the auth_lifespan level. Depending on how fastmcp surfaces lifespan errors, a user may see only the raw exception text with no surrounding context. Consider wrapping the call to log the failure before re-raising:

Suggested change
@lifespan
async def auth_lifespan(app: FastMCP) -> AsyncIterator[dict[str, Any]]:
"""Validate authentication profile exists at startup."""
logger.info("Validating LinkedIn authentication...")
get_authentication_source()
yield {}
@lifespan
async def auth_lifespan(app: FastMCP) -> AsyncIterator[dict[str, Any]]:
"""Validate authentication profile exists at startup."""
logger.info("Validating LinkedIn authentication...")
try:
get_authentication_source()
except Exception:
logger.error("LinkedIn authentication validation failed", exc_info=True)
raise
yield {}
Prompt To Fix With AI
This is a comment left during a code review.
Path: linkedin_mcp_server/server.py
Line: 33-38

Comment:
**No error logging on auth failure**

If `get_authentication_source()` raises a `CredentialsNotFoundError` (or any other unexpected exception), the exception propagates out of the lifespan silently with no log line at the `auth_lifespan` level. Depending on how fastmcp surfaces lifespan errors, a user may see only the raw exception text with no surrounding context. Consider wrapping the call to log the failure before re-raising:

```suggestion
@lifespan
async def auth_lifespan(app: FastMCP) -> AsyncIterator[dict[str, Any]]:
    """Validate authentication profile exists at startup."""
    logger.info("Validating LinkedIn authentication...")
    try:
        get_authentication_source()
    except Exception:
        logger.error("LinkedIn authentication validation failed", exc_info=True)
        raise
    yield {}
```

How can I resolve this? If you propose a fix, please make it concise.

async def auth_lifespan(app: FastMCP) -> AsyncIterator[dict[str, Any]]:
"""Validate authentication profile exists at startup."""
logger.info("Validating LinkedIn authentication...")
get_authentication_source()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Synchronous blocking I/O inside async lifespan

get_authentication_source() is a synchronous function that performs filesystem I/O (checking profile directory existence via os.path.exists). Calling it directly inside an async generator blocks the event loop for the duration of the filesystem call. While this is unlikely to cause practical problems at startup (one-time, fast path), it is a best-practice violation in an async context.

Consider offloading it to a thread pool:

Suggested change
get_authentication_source()
await asyncio.get_event_loop().run_in_executor(None, get_authentication_source)

(Don't forget to add import asyncio at the top of the file.)

Prompt To Fix With AI
This is a comment left during a code review.
Path: linkedin_mcp_server/server.py
Line: 37

Comment:
**Synchronous blocking I/O inside async lifespan**

`get_authentication_source()` is a synchronous function that performs filesystem I/O (checking profile directory existence via `os.path.exists`). Calling it directly inside an async generator blocks the event loop for the duration of the filesystem call. While this is unlikely to cause practical problems at startup (one-time, fast path), it is a best-practice violation in an `async` context.

Consider offloading it to a thread pool:

```suggestion
    await asyncio.get_event_loop().run_in_executor(None, get_authentication_source)
```

(Don't forget to add `import asyncio` at the top of the file.)

How can I resolve this? If you propose a fix, please make it concise.

@stickerdaniel stickerdaniel merged commit 2fb8e25 into main Mar 5, 2026
8 checks passed
@stickerdaniel stickerdaniel deleted the 03-05-refactor_server_split_lifespan_into_composable_browser_auth_lifespans branch March 5, 2026 01:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants