.. _user_guide_architecture:
============
Architecture
============
This page describes the architecture of PyAnsys Common MCP and explains how components work together.
Overview
========
PyAnsys Common MCP uses a layered architecture with a clear separation of concerns:
.. mermaid::
flowchart TD
A["AI client
(Claude, ChatGPT)"]
B["Your product MCP server
• Custom context
• Product startup/cleanup
• MCP tools"]
C["PyAnsysBaseMCP
(Base class)
• Lifecycle orchestration
• Python session management
• Context creation and injection
• Error handling and logging"]
D["FastMCP
(MCP protocol library)
• MCP protocol implementation
• Tool registration and execution
• Transport layer (stdio)"]
A -->|"MCP protocol (stdio)"| B
B -.->|extends| C
C -.->|uses| D
style A fill:#e1f5ff
style B fill:#fff4e1
style C fill:#e8f5e9
style D fill:#f3e5f5
Core components
===============
PyAnsysBaseMCP
--------------
``PyAnsysBaseMCP`` is the base class for all PyAnsys product-specific MCP servers.
**Responsibilities:** It orchestrates the lifecycle, manages Python sessions, injects context, and
handles errors.
.. important::
**Methods you must implement:**
- ``product_startup()``: Initialize your product connection.
- ``product_cleanup()``: Clean up your product connection.
These methods are abstract and must be implemented in your subclass.
If you fail to implement them, the system raises a ``TypeError`` at instantiation.
**Methods you can optionally override:**
- ``create_context()``: Override this method only if you use a custom context class.
It returns the ``PyAnsysBaseAppContext`` dataclass by default.
**Methods already implemented:**
- ``start_python_session()``: Start a persistent Python subprocess.
- ``cleanup_python_session()``: Stop the Python session.
- ``product_lifespan()``: Manage the complete server lifecycle.
PyAnsysBaseAppContext
---------------------
``PyAnsysBaseAppContent`` is the dataclass that holds the shared state accessible from all MCP tools.
**Built-in fields:**
.. code-block:: python
@dataclass
class PyAnsysBaseAppContext:
product_instance: Optional[Any] = None
python_executable: Optional[Any] = None
python_session: Optional[Any] = None # PersistentPythonSession
metadata: dict = field(default_factory=dict)
command_history: list = field(default_factory=list)
**Extending the context:**
Product-specific servers can extend this class to add custom fields:
.. code-block:: python
from dataclasses import dataclass
from typing import Optional
from ansys.common.mcp import PyAnsysBaseAppContext
@dataclass
class MyProductContext(PyAnsysBaseAppContext):
"""Extended dataclass for MyProduct MCP context."""
custom_field: Optional[str] = None
.. tip::
If you need a custom field, you can either extend this class or use the
``metadata`` dictionary to store arbitrary key-value pairs.
Context injection
=================
The system injects context into tools using FastMCP's dependency system. You can inject context in
two ways:
.. _function_parameter:
Function parameter
------------------
The recommended way to inject context is to declare ``ctx: Context`` as a parameter. FastMCP then
automatically injects it.
- Always include ``ctx: Context`` as the first parameter to ensure proper injection.
This also enforces implementation of critical methods like ``product_startup()``
and ``product_cleanup()`` in your server class.
- Do not attempt to pass ``ctx`` manually when calling the tool. The framework handles it
automatically.
This code shows how to declare ``ctx: Context`` as a parameter:
.. code-block:: python
from mcp.server.fastmcp import Context
@mcp.tool()
def my_tool(ctx: Context, param: str) -> str:
"""Execute something.
Parameters
----------
ctx : Context
MCP context (automatically injected). Do not pass manually.
param : str
Your parameter.
"""
# Access application context
app_context = ctx.request_context.lifespan_context
# Access product instance
result = app_context.product_instance.run(param)
# Add to command history if successful
if result["success"]:
app_context.command_history.append(param)
return result
``get_context()`` function
--------------------------
The other way to inject context is to import and call the ``get_context()`` function
inside the tool function. This function retrieves the current context instance.
.. code-block:: python
from fastmcp.server.dependencies import get_context
@mcp.tool()
def my_tool(param: str) -> str:
"""Execute something."""
ctx = get_context()
app_context = ctx.fastmcp._lifespan_result
result = app_context.product_instance.do_something(param)
app_context.command_history.append(f"my_tool({param})")
return result
Tools
=====
This library provides a set of built-in tools for common operations, such as executing Python code
and creating custom plots. You can explore the :py:mod:`ansys.common.mcp.tools` module to use the
available functions directly in your server or extend them with additional logic.
If you identify a missing function that could benefit multiple repositories, consider opening an
issue or submitting a pull request (PR) to add it. The tools module serves as a shared utility belt
for all PyAnsys product MCP servers.
Lifecycle management
====================
The ``product_lifespan()`` method automatically manages the server lifecycle:
**Phases:**
1. Create context.
2. Start Python session.
3. **Start product.** ← Add your code here.
4. Server runs (handles requests).
5. **Clean up product.** ← Add your code here.
6. Stop Python session.
Using the abstract base class ensures that product-specific servers implement the
``product_startup()`` and ``product_cleanup()`` methods. If these methods are not
implemented, the system raises runtime errors:
.. code-block:: python
# This raises TypeError if methods are not implemented
server = MyProductMCP()
# TypeError: Can't instantiate abstract class MyProductMCP with abstract methods
# product_cleanup() and product_startup()
**Asynchronous lifecycle:**
FastMCP uses async/await for all operations because the MCP protocol is inherently asynchronous.
The ``product_lifespan()`` method is an async context manager that integrates with FastMCP's event
loop. This allows the framework to handle all asynchronous complexity internally.
**Note:** Your ``product_startup()`` and ``product_cleanup()`` methods are regular
(synchronous) functions. The framework handles the async part.
Logging
=======
Logs are written to **stderr** (not ``stdout``) to avoid interfering with the MCP protocol. To
configure logging, use this code:
.. code-block:: python
from ansys.common.mcp.logging_config import setup_logging, get_logger
setup_logging(level="INFO") # or use LOGLEVEL env variable
logger = get_logger(__name__)
logger.info("Starting...")
logger.error("Error occurred")