| name | ref-mcp-module-organization |
| description | Module organization and design patterns for MCP tool development in this project — invoke when creating or modifying MCP tool modules |
| user-invocable | false |
MCP Tools Module Organization Guidelines
Scope. This skill applies only to MCP server tool modules under src/deephaven_mcp/mcp_systems_server/_tools/. For commands in the local dhcli CLI under src/deephaven_mcp/cli/_commands/, apply the cli-command-add skill instead — the CLI uses a different framework (click), a different async pattern (@run_async from cli/_async.py), and a different error contract (CliError from cli/_errors.py). Do not transplant patterns between the two.
File Organization Principles
-
Module Cohesion: Each file in _tools/ should contain MCP tools and helpers for a single, well-defined domain (e.g., table operations, session lifecycle, script execution).
-
Helper Function Placement:
- If used by 1 module → keep it in that module (private with
_ prefix).
- If used by 2 modules → duplicate is acceptable. Promote to
_tools/shared.py when a third consumer appears (rule below).
- If used by 3+ modules → move to
_tools/shared.py.
-
Constant Placement:
- Module-specific constants → keep in that module after imports
- Shared constants → place in
_tools/shared.py or the most relevant module
- Always include a docstring explaining the constant's purpose
- Operator-tunable values (timeouts, defaults, thresholds) are not module constants — see the
ref-configuration-conventions skill for where they live and how to read them.
-
New MCP Tool Placement:
- Before creating a new file, check if the tool fits an existing domain
- Only create a new module if the tool represents a distinct new domain
- Module size is a consequence of one-domain-per-module, not a target to hit
-
Registering New Tool Modules:
- CRITICAL: Every tool module must define a
register_tools(server: FastMCP) -> None function
- This function calls
server.tool()(tool_fn) for each tool in the module
- After creating a new module, add a
module.register_tools(server) call to _register_tools() in src/deephaven_mcp/mcp_systems_server/_fastmcp.py. Registration is not gated by configuration — every tool module registers unconditionally, so the tool surface is stable and self-describing across all deployment shapes (and matches the static dhcli CLI command tree). A tool that needs a configuration section the deployment lacks self-reports applicability when invoked: it returns an empty result where one is meaningful (e.g. enterprise_systems_status → {"systems": []}) or a structured user-facing error otherwise. The section accessors enforce this — get_enterprise_settings raises EnterpriseNotConfiguredError and get_community_settings/get_community_registry raise CommunityNotConfiguredError (both ConfigurationError subclasses), which each tool's except handler converts to a success=False response. Do not gate registration on multi_config.community/multi_config.enterprise. See _register_tools in mcp_systems_server/_fastmcp.py.
Required Pattern for MCP Tool Modules
All tool modules must follow this pattern — no @decorator on tool functions:
from mcp.server.fastmcp import Context, FastMCP
async def my_tool(context: Context, ...) -> dict:
"""Tool docstring (consumed by AI agents)."""
...
def register_tools(server: FastMCP) -> None:
"""Register all tools in this module with the given FastMCP server."""
server.tool()(my_tool)
Common shared utilities (import only what you need):
from deephaven_mcp.mcp_systems_server._tools.shared import (
error_response,
format_partial_result,
get_lifespan_context,
get_registry,
get_multi_config,
get_community_registry,
get_enterprise_registry,
get_session_from_context,
get_enterprise_session,
check_response_size,
format_schema_result,
build_table_data_response,
redact_json_sensitive_fields,
)
Session acquisition: a tool that works with any session type calls get_session_from_context; an enterprise-only tool calls get_enterprise_session, which validates the type up front and returns the CorePlusSession directly. On a non-enterprise session it raises UnsupportedOperationError; the tool's except handler converts that to the error dict. Do not acquire a generic session and rely on a downstream queries helper to reject the wrong type: the resulting error names an internal function instead of the tool the agent called. Canonical implementations: all four tools in _tools/catalog.py.
Helper failure contract: a _tools helper that produces a value the tool then uses (acquisition, resolution, parsing) raises a named exception on failure, which the calling tool's except handler converts to the error dict. Never mirror a failure through a (value, error) tuple and cast() past the None — cast is unchecked, so mypy cannot catch a broken invariant. Guard helpers are exempt: a helper whose entire product is a ready-made error dict (check_response_size in _tools/shared.py, _check_session_limit in _tools/session_community.py) returns dict | None, and the tool returns the dict verbatim when present — there is no separate value to mirror. Canonical implementations: get_enterprise_session in _tools/shared.py; _setup_batch_pq_operation in _tools/pq.py (raises, returns a frozen dataclass with non-optional fields).
Closed-vocabulary parameters: a tool parameter that accepts a fixed set of string values is typed as a Literal alias, never bare str — the values then surface in the tool's MCP inputSchema, so agents pick valid values without a trial-and-error round trip. Pair the alias with its runtime collection per ref-python-coding-practices rule 18: derive the collection from the Literal via typing.get_args. Canonical implementation: TableFormat in formatters/__init__.py (VALID_FORMATS = set(get_args(TableFormat))), used by session_table_data and catalog_table_sample.
Id parsing: call QualifiedSessionId.from_str (from deephaven_mcp.resource_manager) to parse and validate a fully qualified id — it raises InvalidSessionNameError rather than substituting a default. For PQ ids, parse_pq_id / make_pq_id in shared.py add the enterprise-scope and integer-serial refinement on top.
Error strings: an except handler that embeds the caught exception in a payload error field renders it with exception_summary(e) — f"Failed to <action> '{id}': {exception_summary(e)}". Apply ref-python-coding-practices rule 20 for the renderer choice and the logging boundary.
Naming Conventions
MCP Tool Functions (Public API)
- Pattern:
{domain}_{action} (e.g., session_table_data, pq_create, catalog_tables_list)
- No underscore prefix: These are the public MCP tools exposed to AI agents
- Descriptive and specific: Name states what the tool does in
{domain}_{action} form
- Registered explicitly: via
server.tool()(fn) inside register_tools()
Helper Functions (Internal Use Only)
- Always private: Use underscore prefix (e.g.,
_validate_launch_method, _build_response)
- Purpose: Support MCP tools within the same module or shared utilities
- Not exported: Never include in
__all__ (if present)
- Local scope: Keep in the module where used, unless used by 3+ modules
Module-Level Objects
- Constants: ALLCAPS with docstring (e.g.,
MAX_RESPONSE_SIZE, DEFAULT_TIMEOUT)
- Logger:
_LOGGER = logging.getLogger(__name__) (private, standard pattern)
- Type variables: Follow typing conventions (e.g.,
T = TypeVar("T"))
Module Independence
- Avoid circular dependencies between
_tools/ modules
_tools/shared.py should not import from other _tools/ modules
- Cross-module communication should go through the shared utilities or MCP context