Skip to content

Commit 23ff566

Browse files
committed
refactor(docs): improve overall docstrings
1 parent 758c00f commit 23ff566

19 files changed

Lines changed: 1568 additions & 150 deletions

File tree

src/MCPStack/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
__version__ = "0.0.1"
2-
__all__ = ["__version__"]
2+
__all__ = ["__version__"]

src/MCPStack/cli.py

Lines changed: 425 additions & 63 deletions
Large diffs are not rendered by default.

src/MCPStack/core/config.py

Lines changed: 163 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,226 @@
11
import logging
22
import os
33
from pathlib import Path
4+
45
from beartype import beartype
56
from beartype.typing import Any, Dict, List, Optional
6-
from .utils.logging import setup_logging
7+
78
from .utils.exceptions import MCPStackConfigError
9+
from .utils.logging import setup_logging
810

911
logger = logging.getLogger(__name__)
1012

13+
1114
@beartype
1215
class StackConfig:
13-
def __init__(self, log_level: str = "INFO", env_vars: Optional[Dict[str, str]] = None) -> None:
16+
"""Configuration container for MCPStack.
17+
18+
Holds logging configuration, environment variables, and computed paths used
19+
by tools and the MCP server.
20+
21+
!!! note "Scope"
22+
Stores env vars and I/O paths used by tools and the MCP server.
23+
24+
Args:
25+
log_level: Logging level name (e.g., `"INFO"`, `"DEBUG"`).
26+
env_vars: Mapping of environment variables to set/merge.
27+
28+
Attributes:
29+
log_level (str): Active logging level.
30+
env_vars (dict[str, str]): Environment variables tracked by the stack.
31+
project_root (Path): Detected project root (see `_get_project_root()`).
32+
data_dir (Path): Base data directory (see `_get_data_dir()`).
33+
databases_dir (Path): `data_dir / "databases"`.
34+
raw_files_dir (Path): `data_dir / "raw_files"`.
35+
36+
!!! tip "When is logging applied?"
37+
Logging is initialized and `env_vars` exported to `os.environ` during
38+
construction via :meth:`_apply_config`.
39+
"""
40+
41+
def __init__(
42+
self, log_level: str = "INFO", env_vars: Optional[Dict[str, str]] = None
43+
) -> None:
1444
self.log_level = log_level
1545
self.env_vars = env_vars or {}
1646
self._set_paths()
1747
self._apply_config()
1848

1949
def to_dict(self) -> Dict[str, Any]:
50+
"""Serialize the configuration to a plain dictionary.
51+
52+
Returns:
53+
dict: A shallow copy with `log_level` and `env_vars`.
54+
55+
!!! example
56+
```python
57+
cfg = StackConfig()
58+
payload = cfg.to_dict()
59+
```
60+
"""
2061
return {"log_level": self.log_level, "env_vars": self.env_vars.copy()}
2162

2263
@classmethod
2364
def from_dict(cls, data: Dict[str, Any]) -> "StackConfig":
24-
return cls(log_level=data.get("log_level", "INFO"), env_vars=data.get("env_vars", {}))
65+
"""Construct a :class:`StackConfig` from a mapping.
66+
67+
Args:
68+
data: A mapping containing optional keys `log_level` and `env_vars`.
69+
70+
Returns:
71+
StackConfig: New instance populated from `data`.
72+
73+
!!! tip
74+
Missing keys default to `log_level="INFO"` and an empty `env_vars`.
75+
"""
76+
return cls(
77+
log_level=data.get("log_level", "INFO"), env_vars=data.get("env_vars", {})
78+
)
79+
80+
def get_env_var(
81+
self, key: str, default: Optional[Any] = None, raise_if_missing: bool = False
82+
) -> Any:
83+
"""Retrieve an environment variable with fallback and validation.
84+
85+
Lookup order: `self.env_vars[key]` → `os.getenv(key)` → `default`.
86+
87+
Args:
88+
key: Environment variable name.
89+
default: Value to return if not found in config or process env.
90+
raise_if_missing: If `True`, raise when the final value is `None`.
2591
26-
def get_env_var(self, key: str, default: Optional[Any] = None, raise_if_missing: bool = False) -> Any:
92+
Returns:
93+
Any: The resolved value, or `""` if the resolved value is falsy.
94+
95+
Raises:
96+
MCPStackConfigError: If `raise_if_missing=True` and no value found.
97+
98+
!!! note
99+
A debug log is emitted indicating whether the key was set or unset.
100+
"""
27101
value = self.env_vars.get(key, os.getenv(key, default))
28102
if value is None and raise_if_missing:
29103
raise MCPStackConfigError(f"Missing required env var: {key}")
30104
logger.debug(f"Accessed env var '{key}': {'[set]' if value else '[unset]'}")
31105
return value or ""
32106

33107
def validate_for_tools(self, tools: List) -> None:
108+
"""Ensure all tools' required environment variables are present.
109+
110+
Inspects each tool's `required_env_vars` (a mapping of `name -> default`)
111+
and verifies that values are available via :meth:`get_env_var`. When a
112+
default is `None`, the key is considered **required**.
113+
114+
Args:
115+
tools: Iterable of tool instances to validate against this config.
116+
117+
Raises:
118+
MCPStackConfigError: Aggregated errors if any requirement is missing.
119+
120+
!!! failure "Common pitfalls"
121+
* No value provided for a required key (`default=None`).
122+
* Typos in environment variable names.
123+
* Forgot to merge preset/tool-provided env.
124+
"""
34125
errors = []
35126
for tool in tools:
36127
for req_key, req_default in getattr(tool, "required_env_vars", {}).items():
37128
try:
38-
self.get_env_var(req_key, default=req_default, raise_if_missing=req_default is None)
129+
self.get_env_var(
130+
req_key,
131+
default=req_default,
132+
raise_if_missing=req_default is None,
133+
)
39134
except Exception as e:
40135
errors.append(f"{tool.__class__.__name__}: {e}")
41136
if errors:
42137
raise MCPStackConfigError("\n".join(errors))
43138
logger.info(f"Validated config for {len(tools)} tools.")
44139

45140
def merge_env(self, new_env: Dict[str, str], prefix: str = "") -> None:
141+
"""Merge environment variables with optional key prefix and conflict checks.
142+
143+
Args:
144+
new_env: Mapping to merge into `env_vars`.
145+
prefix: String to preprend to each key (namespacing).
146+
147+
Raises:
148+
MCPStackConfigError: If a key exists with a **different** value.
149+
150+
!!! tip "Namespacing"
151+
Use `prefix` (e.g., `"MYTOOL_"`) to avoid collisions between tools.
152+
"""
46153
for key, value in new_env.items():
47154
prefixed_key = f"{prefix}{key}" if prefix else key
48155
if prefixed_key in self.env_vars and self.env_vars[prefixed_key] != value:
49-
raise MCPStackConfigError(f"Env conflict: {prefixed_key} ({self.env_vars[prefixed_key]} vs {value})")
156+
raise MCPStackConfigError(
157+
f"Env conflict: {prefixed_key} ({self.env_vars[prefixed_key]} vs {value})"
158+
)
50159
self.env_vars[prefixed_key] = value
51160

52161
def _set_paths(self) -> None:
162+
"""Compute and cache commonly used directories on disk.
163+
164+
Side effects:
165+
Sets `project_root`, `data_dir`, `databases_dir`, and `raw_files_dir`.
166+
167+
!!! note
168+
Paths are derived once at initialization; adjust env and rebuild the
169+
config if your directory layout changes at runtime.
170+
"""
53171
self.project_root = self._get_project_root()
54172
self.data_dir = self._get_data_dir()
55173
self.databases_dir = self.data_dir / "databases"
56174
self.raw_files_dir = self.data_dir / "raw_files"
57175

58176
def _get_project_root(self) -> Path:
177+
"""Infer the project root.
178+
179+
Returns:
180+
Path: Directory containing `pyproject.toml` if found by traversing
181+
up from this file; otherwise the user's home directory.
182+
183+
!!! tip
184+
Useful for resolving default data directories during local dev.
185+
"""
59186
package_root = Path(__file__).resolve().parents[3]
60-
return package_root if (package_root / "pyproject.toml").exists() else Path.home()
187+
return (
188+
package_root if (package_root / "pyproject.toml").exists() else Path.home()
189+
)
61190

62191
def _get_data_dir(self) -> Path:
192+
"""Resolve the base data directory.
193+
194+
Resolution order:
195+
1. `MCPSTACK_DATA_DIR` from config/env (if set)
196+
2. `project_root / "mcpstack_data"`
197+
198+
Returns:
199+
Path: The resolved data directory.
200+
"""
63201
data_dir_str = self.get_env_var("MCPSTACK_DATA_DIR")
64-
return Path(data_dir_str) if data_dir_str else self.project_root / "mcpstack_data"
202+
return (
203+
Path(data_dir_str) if data_dir_str else self.project_root / "mcpstack_data"
204+
)
65205

66206
def _apply_config(self) -> None:
207+
"""Apply logging configuration and export env vars to the process.
208+
209+
Side effects:
210+
* Initializes logging via :func:`setup_logging` using `log_level`.
211+
* Writes keys from `env_vars` into `os.environ`.
212+
213+
Raises:
214+
MCPStackConfigError: If the logging level is invalid.
215+
216+
!!! warning "Global process state"
217+
Exporting `env_vars` updates `os.environ` for the current process
218+
and its children. Avoid unintentional overrides by using distinct
219+
prefixes when merging from multiple sources.
220+
"""
67221
try:
68222
setup_logging(level=self.log_level)
69223
except Exception as e:
70-
raise MCPStackConfigError("Invalid log level", details=str(e))
224+
raise MCPStackConfigError("Invalid log level", details=str(e)) from e
71225
for k, v in self.env_vars.items():
72226
os.environ[k] = v

src/MCPStack/core/mcp_config_generator/mcp_config_generators/claude_mcp_config.py

Lines changed: 63 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,86 @@
1-
import json, logging, os, shutil
1+
import json
2+
import logging
3+
import os
4+
import shutil
25
from pathlib import Path
6+
37
from beartype import beartype
48
from beartype.typing import Any, Dict, List, Optional
9+
510
from MCPStack.core.utils.exceptions import MCPStackValidationError
611

712
logger = logging.getLogger(__name__)
813

14+
915
@beartype
1016
class ClaudeConfigGenerator:
17+
"""Factory for producing an MCP host configuration JSON from a stack dedicated to ClaudeDesktop.
18+
19+
!!! note "Deterministic"
20+
21+
Reads from environment and `StackConfig`; does not mutate the stack.
22+
"""
23+
1124
@classmethod
1225
def generate(
13-
cls, stack,
26+
cls,
27+
stack,
1428
command: Optional[str] = None,
1529
args: Optional[List[str]] = None,
1630
cwd: Optional[str] = None,
1731
module_name: Optional[str] = None,
1832
pipeline_config_path: Optional[str] = None,
1933
save_path: Optional[str] = None,
2034
) -> Dict[str, Any]:
35+
"""Create the configuration mapping and optionally persist it to disk.
36+
37+
!!! tip "Use with CLI"
38+
39+
The `mcpstack build` command calls into this method.
40+
41+
Args:
42+
stack: An `MCPStackCore` instance.
43+
command (str | None): Executable used to launch the server; defaults to the active Python.
44+
args (List[str] | None): Arguments for the command; defaults to `['-m', module_name]`.
45+
cwd (str | None): Working directory for the server process.
46+
module_name (str | None): Python module to run when using `-m`.
47+
pipeline_config_path (str | None): Path to the pipeline JSON produced by `stack.save()`.
48+
save_path (str | None): If set, write the config JSON here.
49+
50+
Returns:
51+
dict: Configuration mapping suitable for MCP-compatible hosts.
52+
53+
Raises:
54+
MCPStackValidationError: If `command` or `cwd` are invalid (FastMCP variant).
55+
"""
2156
_command = cls._get_command(command, stack)
2257
_module_name = cls._get_module_name(module_name, stack)
2358
_args = cls._get_args(args, stack, _module_name)
2459
_cwd = cls._get_cwd(cwd, stack)
2560

2661
if not shutil.which(_command):
27-
raise MCPStackValidationError(f"Invalid command '{_command}': Not found on PATH.")
62+
raise MCPStackValidationError(
63+
f"Invalid command '{_command}': Not found on PATH."
64+
)
2865
if not os.path.isdir(_cwd):
29-
raise MCPStackValidationError(f"Invalid cwd '{_cwd}': Directory does not exist.")
66+
raise MCPStackValidationError(
67+
f"Invalid cwd '{_cwd}': Directory does not exist."
68+
)
3069

3170
env = stack.config.env_vars.copy()
3271
if pipeline_config_path:
3372
env["MCPSTACK_CONFIG_PATH"] = pipeline_config_path
3473

35-
config = {"mcpServers": {"mcpstack": {"command": _command, "args": _args, "cwd": _cwd, "env": env}}}
74+
config = {
75+
"mcpServers": {
76+
"mcpstack": {
77+
"command": _command,
78+
"args": _args,
79+
"cwd": _cwd,
80+
"env": env,
81+
}
82+
}
83+
}
3684
if save_path:
3785
with open(save_path, "w") as f:
3886
json.dump(config, f, indent=2)
@@ -52,6 +100,7 @@ def generate(
52100

53101
@staticmethod
54102
def _get_command(command, stack) -> str:
103+
"""Resolve `command` from explicit args, env, or sensible defaults."""
55104
if command is not None:
56105
return command
57106
if "VIRTUAL_ENV" in os.environ:
@@ -63,12 +112,14 @@ def _get_command(command, stack) -> str:
63112

64113
@staticmethod
65114
def _get_module_name(module_name, stack) -> str:
115+
"""Resolve `module_name` from explicit args, env, or sensible defaults."""
66116
if module_name is not None:
67117
return module_name
68118
return stack.config.get_env_var("MCPSTACK_MODULE", "MCPStack.core.server")
69119

70120
@staticmethod
71121
def _get_args(args, stack, module_name: str):
122+
"""Resolve `args` from explicit args, env, or sensible defaults."""
72123
if args is not None:
73124
return args
74125
default = ["-m", module_name]
@@ -77,15 +128,21 @@ def _get_args(args, stack, module_name: str):
77128

78129
@staticmethod
79130
def _get_cwd(cwd, stack) -> str:
131+
"""Resolve `cwd` from explicit args, env, or sensible defaults."""
80132
if cwd is not None:
81133
return cwd
82134
return stack.config.get_env_var("MCPSTACK_CWD", os.getcwd())
83135

84136
@staticmethod
85137
def _get_claude_config_path():
138+
"""_get_claude_config_path function."""
86139
home = Path.home()
87140
paths = [
88-
home / "Library" / "Application Support" / "Claude" / "claude_desktop_config.json",
141+
home
142+
/ "Library"
143+
/ "Application Support"
144+
/ "Claude"
145+
/ "claude_desktop_config.json",
89146
home / "AppData" / "Roaming" / "Claude" / "claude_desktop_config.json",
90147
home / ".config" / "Claude" / "claude_desktop_config.json",
91148
]

0 commit comments

Comments
 (0)