trelix-mcp 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,58 @@
1
+ # Python
2
+ __pycache__/
3
+ *.pyc
4
+ *.pyo
5
+ *.pyd
6
+ .Python
7
+ *.egg-info/
8
+ *.egg
9
+ build/
10
+ .eggs/
11
+ pip-wheel-metadata/
12
+
13
+ # dist/ — ignore generated packaging artefacts; binaries are attached via GitHub Releases.
14
+ dist/
15
+
16
+ # Virtual environments
17
+ .venv/
18
+ venv/
19
+ env/
20
+ ENV/
21
+
22
+ # Trelix index data — never commit these
23
+ .trelix/
24
+
25
+ # Test / coverage artefacts
26
+ .pytest_cache/
27
+ .coverage
28
+ htmlcov/
29
+ .tox/
30
+
31
+ # Ruff / Mypy caches
32
+ .ruff_cache/
33
+ .mypy_cache/
34
+
35
+ # Environment files — secrets must not be committed
36
+ .env
37
+ .env.local
38
+ .env.*.local
39
+
40
+ # Claude Code internal data
41
+ .claude/
42
+
43
+ # uv lockfile (not committed — project uses pip/hatchling)
44
+ uv.lock
45
+
46
+ # Editor / OS
47
+ .DS_Store
48
+ .idea/
49
+ .vscode/
50
+ *.swp
51
+ *.swo
52
+ Thumbs.db
53
+
54
+ # Distribution / packaging
55
+ *.tar.gz
56
+ *.whl
57
+ MANIFEST
58
+ .superpowers/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Trelix Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,93 @@
1
+ Metadata-Version: 2.4
2
+ Name: trelix-mcp
3
+ Version: 0.5.0
4
+ Summary: MCP server for trelix — semantic code search for Claude Code, Cursor, Windsurf, Continue.dev
5
+ Project-URL: Homepage, https://github.com/sairam0424/trelix
6
+ Author: Trelix Contributors
7
+ License: MIT License
8
+
9
+ Copyright (c) 2025 Trelix Contributors
10
+
11
+ Permission is hereby granted, free of charge, to any person obtaining a copy
12
+ of this software and associated documentation files (the "Software"), to deal
13
+ in the Software without restriction, including without limitation the rights
14
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15
+ copies of the Software, and to permit persons to whom the Software is
16
+ furnished to do so, subject to the following conditions:
17
+
18
+ The above copyright notice and this permission notice shall be included in all
19
+ copies or substantial portions of the Software.
20
+
21
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27
+ SOFTWARE.
28
+ License-File: LICENSE
29
+ Keywords: ai-agent,claude-code,code-intelligence,code-search,cursor,llm,mcp,model-context-protocol
30
+ Classifier: Development Status :: 4 - Beta
31
+ Classifier: Intended Audience :: Developers
32
+ Classifier: License :: OSI Approved :: MIT License
33
+ Classifier: Programming Language :: Python :: 3.11
34
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
35
+ Requires-Python: >=3.11
36
+ Requires-Dist: fastmcp>=2.0.0
37
+ Requires-Dist: mcp>=1.0.0
38
+ Requires-Dist: trelix>=0.4.0
39
+ Description-Content-Type: text/markdown
40
+
41
+ # trelix-mcp
42
+
43
+ MCP server for [trelix](https://github.com/sairam0424/trelix) — semantic code search for Claude Code, Cursor, Windsurf, and Continue.dev.
44
+
45
+ ## Install
46
+
47
+ ```bash
48
+ pip install trelix-mcp
49
+ ```
50
+
51
+ ## Usage
52
+
53
+ ### Claude Code
54
+
55
+ ```bash
56
+ claude mcp add trelix -- trelix-mcp
57
+ ```
58
+
59
+ ### Cursor (`~/.cursor/mcp.json`)
60
+
61
+ ```json
62
+ {
63
+ "mcpServers": {
64
+ "trelix": {
65
+ "command": "trelix-mcp",
66
+ "args": []
67
+ }
68
+ }
69
+ }
70
+ ```
71
+
72
+ ### Continue.dev (`.continue/config.json`)
73
+
74
+ ```json
75
+ {
76
+ "mcpServers": [
77
+ {
78
+ "name": "trelix",
79
+ "command": "trelix-mcp",
80
+ "args": []
81
+ }
82
+ ]
83
+ }
84
+ ```
85
+
86
+ ## Tools
87
+
88
+ | Tool | Description |
89
+ |------|-------------|
90
+ | `search_code` | Semantic hybrid search over an indexed codebase |
91
+ | `index_codebase` | Index a repository so it can be searched |
92
+ | `get_symbol` | Look up a symbol by qualified name |
93
+ | `blast_radius` | Find all files that depend on a symbol |
@@ -0,0 +1,53 @@
1
+ # trelix-mcp
2
+
3
+ MCP server for [trelix](https://github.com/sairam0424/trelix) — semantic code search for Claude Code, Cursor, Windsurf, and Continue.dev.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install trelix-mcp
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### Claude Code
14
+
15
+ ```bash
16
+ claude mcp add trelix -- trelix-mcp
17
+ ```
18
+
19
+ ### Cursor (`~/.cursor/mcp.json`)
20
+
21
+ ```json
22
+ {
23
+ "mcpServers": {
24
+ "trelix": {
25
+ "command": "trelix-mcp",
26
+ "args": []
27
+ }
28
+ }
29
+ }
30
+ ```
31
+
32
+ ### Continue.dev (`.continue/config.json`)
33
+
34
+ ```json
35
+ {
36
+ "mcpServers": [
37
+ {
38
+ "name": "trelix",
39
+ "command": "trelix-mcp",
40
+ "args": []
41
+ }
42
+ ]
43
+ }
44
+ ```
45
+
46
+ ## Tools
47
+
48
+ | Tool | Description |
49
+ |------|-------------|
50
+ | `search_code` | Semantic hybrid search over an indexed codebase |
51
+ | `index_codebase` | Index a repository so it can be searched |
52
+ | `get_symbol` | Look up a symbol by qualified name |
53
+ | `blast_radius` | Find all files that depend on a symbol |
@@ -0,0 +1,34 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [tool.hatch.build.targets.wheel]
6
+ packages = ["src/trelix_mcp"]
7
+
8
+ [project]
9
+ name = "trelix-mcp"
10
+ version = "0.5.0"
11
+ description = "MCP server for trelix — semantic code search for Claude Code, Cursor, Windsurf, Continue.dev"
12
+ readme = "README.md"
13
+ license = { file = "LICENSE" }
14
+ authors = [{ name = "Trelix Contributors" }]
15
+ requires-python = ">=3.11"
16
+ keywords = ["mcp", "model-context-protocol", "code-search", "claude-code", "cursor", "code-intelligence", "llm", "ai-agent"]
17
+ classifiers = [
18
+ "Development Status :: 4 - Beta",
19
+ "Intended Audience :: Developers",
20
+ "License :: OSI Approved :: MIT License",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
23
+ ]
24
+ dependencies = ["trelix>=0.4.0", "mcp>=1.0.0", "fastmcp>=2.0.0"]
25
+
26
+ [project.scripts]
27
+ trelix-mcp = "trelix_mcp.server:main"
28
+
29
+ [project.urls]
30
+ Homepage = "https://github.com/sairam0424/trelix"
31
+
32
+ [tool.pytest.ini_options]
33
+ testpaths = ["tests"]
34
+ asyncio_mode = "auto"
@@ -0,0 +1,4 @@
1
+ """trelix MCP server — expose trelix code intelligence as MCP tools."""
2
+
3
+ __version__ = "0.5.0"
4
+ __all__ = ["__version__"]
@@ -0,0 +1,175 @@
1
+ import logging
2
+ import sys
3
+
4
+ logging.basicConfig(
5
+ stream=sys.stderr,
6
+ level=logging.INFO,
7
+ format="[trelix-mcp] %(levelname)s %(message)s",
8
+ )
9
+
10
+ import signal # noqa: E402
11
+ from typing import Any, Literal # noqa: E402
12
+
13
+ from fastmcp import FastMCP # noqa: E402
14
+
15
+ from trelix.core.config import EmbedderConfig, IndexConfig # noqa: E402
16
+ from trelix.indexing.indexer import Indexer # noqa: E402
17
+ from trelix.retrieval.retriever import Retriever # noqa: E402
18
+ from trelix.store.db import Database # noqa: E402
19
+
20
+ mcp = FastMCP("trelix")
21
+ _log = logging.getLogger("trelix_mcp")
22
+
23
+
24
+ @mcp.tool()
25
+ def search_code(query: str, repo_path: str, k: int = 10) -> list[dict[str, Any]]:
26
+ """Search a codebase for symbols semantically relevant to *query*.
27
+
28
+ Args:
29
+ query: Natural-language or keyword search query.
30
+ repo_path: Absolute path to the repository root (must already be indexed).
31
+ k: Maximum number of results to return (default 10).
32
+
33
+ Returns:
34
+ List of result dicts with keys: file, symbol, kind, lines, score, source,
35
+ body, language.
36
+ """
37
+ _log.info("search_code query=%r repo_path=%r k=%d", query, repo_path, k)
38
+ config = IndexConfig(repo_path=repo_path)
39
+ retriever = Retriever(config)
40
+ context = retriever.retrieve(query)
41
+ results = context.results[:k]
42
+ return [
43
+ {
44
+ "file": r.file.rel_path,
45
+ "symbol": r.symbol.qualified_name,
46
+ "kind": r.symbol.kind,
47
+ "lines": [r.symbol.line_start, r.symbol.line_end],
48
+ "score": round(r.score, 4),
49
+ "source": r.source,
50
+ "body": r.symbol.body,
51
+ "language": r.file.language,
52
+ }
53
+ for r in results
54
+ ]
55
+
56
+
57
+ @mcp.tool()
58
+ def index_codebase(
59
+ repo_path: str,
60
+ provider: Literal["local", "openai", "azure", "voyage", "local-code"] = "local",
61
+ ) -> dict[str, Any]:
62
+ """Index a codebase so it can be searched with search_code.
63
+
64
+ Args:
65
+ repo_path: Absolute path to the repository root.
66
+ provider: Embedding provider — "local" requires no API key.
67
+
68
+ Returns:
69
+ Indexing statistics dict with keys: files_found, files_indexed,
70
+ files_skipped, symbols_extracted, chunks_total, chunks_embedded,
71
+ errors, elapsed_seconds.
72
+ """
73
+ _log.info("index_codebase repo_path=%r provider=%r", repo_path, provider)
74
+ embedder_config = EmbedderConfig(provider=provider) # type: ignore[call-arg]
75
+ config = IndexConfig(repo_path=repo_path, embedder=embedder_config)
76
+ stats = Indexer(config, quiet=True).index()
77
+ return stats
78
+
79
+
80
+ @mcp.tool()
81
+ def get_symbol(qualified_name: str, repo_path: str) -> dict[str, Any] | None:
82
+ """Look up a symbol by its fully-qualified name.
83
+
84
+ Args:
85
+ qualified_name: e.g. "MyClass.my_method" or "my_function".
86
+ repo_path: Absolute path to the repository root.
87
+
88
+ Returns:
89
+ Symbol dict or None if not found. Keys: name, qualified_name, kind,
90
+ file, line_start, line_end, signature, docstring, body, language.
91
+ """
92
+ _log.info("get_symbol qualified_name=%r repo_path=%r", qualified_name, repo_path)
93
+ config = IndexConfig(repo_path=repo_path)
94
+ db = Database(config.db_path_absolute)
95
+ rows = db.get_symbol_by_name(qualified_name)
96
+ if not rows:
97
+ # Fall back to bare name lookup
98
+ name_only = qualified_name.split(".")[-1]
99
+ rows = db.get_symbol_by_name(name_only)
100
+ # Filter to exact qualified_name match when possible
101
+ exact = [s for s in rows if s.qualified_name == qualified_name]
102
+ if exact:
103
+ rows = exact
104
+
105
+ if not rows:
106
+ return None
107
+
108
+ sym = rows[0]
109
+ sym_file = db.get_symbol_with_file(sym.id) # type: ignore[arg-type]
110
+ if sym_file is None:
111
+ return None
112
+ symbol, file = sym_file
113
+ return {
114
+ "name": symbol.name,
115
+ "qualified_name": symbol.qualified_name,
116
+ "kind": symbol.kind,
117
+ "file": file.rel_path,
118
+ "line_start": symbol.line_start,
119
+ "line_end": symbol.line_end,
120
+ "signature": symbol.signature,
121
+ "docstring": symbol.docstring,
122
+ "body": symbol.body,
123
+ "language": file.language,
124
+ }
125
+
126
+
127
+ @mcp.tool()
128
+ def blast_radius(symbol_name: str, repo_path: str) -> list[dict[str, Any]]:
129
+ """Find all symbols that depend on (call or import) a given symbol.
130
+
131
+ Useful for impact analysis: "if I change X, what else might break?"
132
+
133
+ Args:
134
+ symbol_name: Name or qualified name of the symbol to analyse.
135
+ repo_path: Absolute path to the repository root.
136
+
137
+ Returns:
138
+ Deduplicated list of dependent-symbol dicts with keys: file, symbol,
139
+ kind, line_start, language.
140
+ """
141
+ _log.info("blast_radius symbol_name=%r repo_path=%r", symbol_name, repo_path)
142
+ query = f"blast radius dependencies of {symbol_name}"
143
+ config = IndexConfig(repo_path=repo_path)
144
+ retriever = Retriever(config)
145
+ context = retriever.retrieve(query)
146
+
147
+ seen_files: set[str] = set()
148
+ output: list[dict[str, Any]] = []
149
+ for r in context.results:
150
+ file_key = r.file.rel_path
151
+ if file_key in seen_files:
152
+ continue
153
+ seen_files.add(file_key)
154
+ output.append(
155
+ {
156
+ "file": r.file.rel_path,
157
+ "symbol": r.symbol.qualified_name,
158
+ "kind": r.symbol.kind,
159
+ "line_start": r.symbol.line_start,
160
+ "language": r.file.language,
161
+ }
162
+ )
163
+ return output
164
+
165
+
166
+ def main() -> None:
167
+ """Entry point for the trelix-mcp server (stdio transport)."""
168
+
169
+ def _handle_sigterm(signum: int, frame: Any) -> None:
170
+ _log.info("Received SIGTERM — shutting down")
171
+ sys.exit(0)
172
+
173
+ signal.signal(signal.SIGTERM, _handle_sigterm)
174
+ _log.info("trelix-mcp starting (transport=stdio)")
175
+ mcp.run(transport="stdio")
File without changes
@@ -0,0 +1,215 @@
1
+ """Tests for trelix_mcp.server.
2
+
3
+ Uses unittest.mock.patch to avoid touching real files or embedding models.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import sys
9
+ from typing import Any
10
+ from unittest.mock import MagicMock, patch
11
+
12
+ import pytest
13
+
14
+ # ---------------------------------------------------------------------------
15
+ # Helpers
16
+ # ---------------------------------------------------------------------------
17
+
18
+
19
+ def _make_mock_result(
20
+ file: str = "src/foo.py",
21
+ symbol: str = "foo.bar",
22
+ kind: str = "function",
23
+ line_start: int = 1,
24
+ line_end: int = 10,
25
+ score: float = 0.9,
26
+ source: str = "vector",
27
+ body: str = "def bar(): pass",
28
+ language: str = "python",
29
+ ) -> MagicMock:
30
+ """Return a mock SearchResult compatible with server.py expectations."""
31
+ r = MagicMock()
32
+ r.file.rel_path = file
33
+ r.file.language = language
34
+ r.symbol.qualified_name = symbol
35
+ r.symbol.kind = kind
36
+ r.symbol.line_start = line_start
37
+ r.symbol.line_end = line_end
38
+ r.symbol.body = body
39
+ r.score = score
40
+ r.source = source
41
+ return r
42
+
43
+
44
+ def _make_mock_context(results: list[MagicMock]) -> MagicMock:
45
+ ctx = MagicMock()
46
+ ctx.results = results
47
+ return ctx
48
+
49
+
50
+ # ---------------------------------------------------------------------------
51
+ # Module import + basic structure
52
+ # ---------------------------------------------------------------------------
53
+
54
+
55
+ def test_server_importable() -> None:
56
+ import trelix_mcp.server as srv # noqa: F401
57
+
58
+
59
+ def test_mcp_attribute_exists() -> None:
60
+ import trelix_mcp.server as srv
61
+
62
+ assert hasattr(srv, "mcp"), "server.py must expose a top-level `mcp` object"
63
+
64
+
65
+ def test_main_callable() -> None:
66
+ import trelix_mcp.server as srv
67
+
68
+ assert callable(srv.main)
69
+
70
+
71
+ # ---------------------------------------------------------------------------
72
+ # 4 tools registered
73
+ # ---------------------------------------------------------------------------
74
+
75
+
76
+ @pytest.mark.asyncio
77
+ async def test_four_tools_registered() -> None:
78
+ import trelix_mcp.server as srv
79
+
80
+ tools = await srv.mcp.list_tools()
81
+ names = {t.name for t in tools}
82
+ assert {"search_code", "index_codebase", "get_symbol", "blast_radius"} == names, (
83
+ f"Expected exactly 4 tools, got: {names}"
84
+ )
85
+
86
+
87
+ # ---------------------------------------------------------------------------
88
+ # search_code
89
+ # ---------------------------------------------------------------------------
90
+
91
+
92
+ def test_search_code_returns_list_of_dicts() -> None:
93
+ import trelix_mcp.server as srv
94
+
95
+ mock_results = [_make_mock_result()]
96
+ mock_ctx = _make_mock_context(mock_results)
97
+
98
+ with (
99
+ patch("trelix_mcp.server.IndexConfig"),
100
+ patch("trelix_mcp.server.Retriever") as MockRetriever,
101
+ ):
102
+ MockRetriever.return_value.retrieve.return_value = mock_ctx
103
+ results = srv.search_code("authentication", "/fake/repo", k=10)
104
+
105
+ assert isinstance(results, list)
106
+ assert len(results) == 1
107
+ item = results[0]
108
+ assert set(item.keys()) >= {
109
+ "file",
110
+ "symbol",
111
+ "kind",
112
+ "lines",
113
+ "score",
114
+ "source",
115
+ "body",
116
+ "language",
117
+ }
118
+
119
+
120
+ def test_search_code_respects_k_limit() -> None:
121
+ """k=3 must truncate 20 mock results to 3."""
122
+ import trelix_mcp.server as srv
123
+
124
+ mock_results = [_make_mock_result(symbol=f"sym.{i}") for i in range(20)]
125
+ mock_ctx = _make_mock_context(mock_results)
126
+
127
+ with (
128
+ patch("trelix_mcp.server.IndexConfig"),
129
+ patch("trelix_mcp.server.Retriever") as MockRetriever,
130
+ ):
131
+ MockRetriever.return_value.retrieve.return_value = mock_ctx
132
+ results = srv.search_code("auth", "/fake/repo", k=3)
133
+
134
+ assert len(results) == 3
135
+
136
+
137
+ # ---------------------------------------------------------------------------
138
+ # index_codebase
139
+ # ---------------------------------------------------------------------------
140
+
141
+
142
+ def test_index_codebase_returns_dict() -> None:
143
+ import trelix_mcp.server as srv
144
+
145
+ fake_stats: dict[str, Any] = {
146
+ "files_found": 10,
147
+ "files_indexed": 8,
148
+ "files_skipped": 2,
149
+ "symbols_extracted": 50,
150
+ "chunks_total": 50,
151
+ "chunks_embedded": 50,
152
+ "errors": 0,
153
+ "elapsed_seconds": 1.23,
154
+ }
155
+
156
+ with (
157
+ patch("trelix_mcp.server.IndexConfig"),
158
+ patch("trelix_mcp.server.EmbedderConfig"),
159
+ patch("trelix_mcp.server.Indexer") as MockIndexer,
160
+ ):
161
+ MockIndexer.return_value.index.return_value = fake_stats
162
+ result = srv.index_codebase("/fake/repo", provider="local")
163
+
164
+ assert isinstance(result, dict)
165
+ assert result["files_found"] == 10
166
+
167
+
168
+ # ---------------------------------------------------------------------------
169
+ # blast_radius deduplication
170
+ # ---------------------------------------------------------------------------
171
+
172
+
173
+ def test_blast_radius_deduplicates_files() -> None:
174
+ """Two results sharing the same file should produce only one output entry."""
175
+ import trelix_mcp.server as srv
176
+
177
+ r1 = _make_mock_result(file="src/auth.py", symbol="auth.login")
178
+ r2 = _make_mock_result(file="src/auth.py", symbol="auth.logout") # same file
179
+ r3 = _make_mock_result(file="src/db.py", symbol="db.connect")
180
+
181
+ mock_ctx = _make_mock_context([r1, r2, r3])
182
+
183
+ with (
184
+ patch("trelix_mcp.server.IndexConfig"),
185
+ patch("trelix_mcp.server.Retriever") as MockRetriever,
186
+ ):
187
+ MockRetriever.return_value.retrieve.return_value = mock_ctx
188
+ results = srv.blast_radius("auth", "/fake/repo")
189
+
190
+ files = [r["file"] for r in results]
191
+ assert len(files) == 2, f"Expected 2 unique files, got {files}"
192
+ assert "src/auth.py" in files
193
+ assert "src/db.py" in files
194
+
195
+
196
+ # ---------------------------------------------------------------------------
197
+ # CRITICAL: no stdout bytes on import
198
+ # ---------------------------------------------------------------------------
199
+
200
+
201
+ def test_server_import_produces_no_stdout(capsys: pytest.CaptureFixture[str]) -> None:
202
+ """Importing server.py must not write anything to stdout.
203
+
204
+ stdout is the MCP JSON protocol pipe — any spurious bytes corrupt the stream.
205
+ """
206
+ # Force a fresh import to catch any module-level print/write
207
+ if "trelix_mcp.server" in sys.modules:
208
+ del sys.modules["trelix_mcp.server"]
209
+
210
+ import trelix_mcp.server # noqa: F401
211
+
212
+ captured = capsys.readouterr()
213
+ assert captured.out == "", (
214
+ f"server.py wrote {len(captured.out)} bytes to stdout on import: {captured.out!r}"
215
+ )