stackdoctor 0.1.0__tar.gz → 0.1.2__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.
- {stackdoctor-0.1.0 → stackdoctor-0.1.2}/.github/workflows/release.yml +1 -1
- {stackdoctor-0.1.0 → stackdoctor-0.1.2}/PKG-INFO +14 -8
- {stackdoctor-0.1.0 → stackdoctor-0.1.2}/README.md +13 -7
- {stackdoctor-0.1.0 → stackdoctor-0.1.2}/pyproject.toml +1 -1
- stackdoctor-0.1.2/stackdoctor/__init__.py +9 -0
- {stackdoctor-0.1.0 → stackdoctor-0.1.2}/stackdoctor/server.py +30 -1
- stackdoctor-0.1.2/tests/test_cli.py +47 -0
- {stackdoctor-0.1.0 → stackdoctor-0.1.2}/uv.lock +1 -1
- stackdoctor-0.1.0/stackdoctor/__init__.py +0 -0
- {stackdoctor-0.1.0 → stackdoctor-0.1.2}/.gitattributes +0 -0
- {stackdoctor-0.1.0 → stackdoctor-0.1.2}/.github/workflows/demo.yml +0 -0
- {stackdoctor-0.1.0 → stackdoctor-0.1.2}/.github/workflows/tests.yml +0 -0
- {stackdoctor-0.1.0 → stackdoctor-0.1.2}/.gitignore +0 -0
- {stackdoctor-0.1.0 → stackdoctor-0.1.2}/DEMO.md +0 -0
- {stackdoctor-0.1.0 → stackdoctor-0.1.2}/LICENSE +0 -0
- {stackdoctor-0.1.0 → stackdoctor-0.1.2}/demo/.env.example +0 -0
- {stackdoctor-0.1.0 → stackdoctor-0.1.2}/demo/app/Dockerfile +0 -0
- {stackdoctor-0.1.0 → stackdoctor-0.1.2}/demo/app/api.py +0 -0
- {stackdoctor-0.1.0 → stackdoctor-0.1.2}/demo/app/enqueue.py +0 -0
- {stackdoctor-0.1.0 → stackdoctor-0.1.2}/demo/app/requirements.txt +0 -0
- {stackdoctor-0.1.0 → stackdoctor-0.1.2}/demo/app/tasks.py +0 -0
- {stackdoctor-0.1.0 → stackdoctor-0.1.2}/demo/break_worker.ps1 +0 -0
- {stackdoctor-0.1.0 → stackdoctor-0.1.2}/demo/break_worker.sh +0 -0
- {stackdoctor-0.1.0 → stackdoctor-0.1.2}/demo/docker-compose.yml +0 -0
- {stackdoctor-0.1.0 → stackdoctor-0.1.2}/demo/e2e.py +0 -0
- {stackdoctor-0.1.0 → stackdoctor-0.1.2}/demo/lock_table.ps1 +0 -0
- {stackdoctor-0.1.0 → stackdoctor-0.1.2}/demo/lock_table.sh +0 -0
- {stackdoctor-0.1.0 → stackdoctor-0.1.2}/demo/postgres/init.sql +0 -0
- {stackdoctor-0.1.0 → stackdoctor-0.1.2}/demo/reset.ps1 +0 -0
- {stackdoctor-0.1.0 → stackdoctor-0.1.2}/demo/reset.sh +0 -0
- {stackdoctor-0.1.0 → stackdoctor-0.1.2}/demo/slow_query.ps1 +0 -0
- {stackdoctor-0.1.0 → stackdoctor-0.1.2}/demo/slow_query.sh +0 -0
- {stackdoctor-0.1.0 → stackdoctor-0.1.2}/docs/images/diagnosis.png +0 -0
- {stackdoctor-0.1.0 → stackdoctor-0.1.2}/stackdoctor/__main__.py +0 -0
- {stackdoctor-0.1.0 → stackdoctor-0.1.2}/stackdoctor/checks/__init__.py +0 -0
- {stackdoctor-0.1.0 → stackdoctor-0.1.2}/stackdoctor/checks/celery.py +0 -0
- {stackdoctor-0.1.0 → stackdoctor-0.1.2}/stackdoctor/checks/logs.py +0 -0
- {stackdoctor-0.1.0 → stackdoctor-0.1.2}/stackdoctor/checks/postgres.py +0 -0
- {stackdoctor-0.1.0 → stackdoctor-0.1.2}/stackdoctor/checks/redis.py +0 -0
- {stackdoctor-0.1.0 → stackdoctor-0.1.2}/stackdoctor/config.py +0 -0
- {stackdoctor-0.1.0 → stackdoctor-0.1.2}/stackdoctor/diagnose.py +0 -0
- {stackdoctor-0.1.0 → stackdoctor-0.1.2}/stackdoctor/safety.py +0 -0
- {stackdoctor-0.1.0 → stackdoctor-0.1.2}/tests/test_diagnose.py +0 -0
- {stackdoctor-0.1.0 → stackdoctor-0.1.2}/tests/test_integration.py +0 -0
- {stackdoctor-0.1.0 → stackdoctor-0.1.2}/tests/test_logs.py +0 -0
- {stackdoctor-0.1.0 → stackdoctor-0.1.2}/tests/test_safety.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
name: release
|
|
2
2
|
|
|
3
3
|
# Publishes to PyPI with trusted publishing (OIDC), no API token.
|
|
4
|
-
# Triggered by pushing a version tag that matches pyproject.toml
|
|
4
|
+
# Triggered by pushing a version tag that matches pyproject.toml: git tag "v$(uv version --short)" && git push origin --tags
|
|
5
5
|
|
|
6
6
|
on:
|
|
7
7
|
push:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: stackdoctor
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Read-only MCP server that diagnoses a Python backend stack (Postgres, Celery, Redis, logs) with cross-system correlation.
|
|
5
5
|
Project-URL: Homepage, https://github.com/lepri89/stackdoctor
|
|
6
6
|
Project-URL: Issues, https://github.com/lepri89/stackdoctor/issues
|
|
@@ -20,6 +20,7 @@ Description-Content-Type: text/markdown
|
|
|
20
20
|
|
|
21
21
|
[](https://github.com/lepri89/stackdoctor/actions/workflows/tests.yml)
|
|
22
22
|
[](https://github.com/lepri89/stackdoctor/actions/workflows/demo.yml)
|
|
23
|
+
[](https://pypi.org/project/stackdoctor/)
|
|
23
24
|

|
|
24
25
|

|
|
25
26
|
|
|
@@ -60,20 +61,23 @@ curl -LsSf https://astral.sh/uv/install.sh | sh # macOS
|
|
|
60
61
|
powershell -c "irm https://astral.sh/uv/install.ps1 | iex" # Windows
|
|
61
62
|
```
|
|
62
63
|
|
|
63
|
-
Then
|
|
64
|
+
Then either run it straight from [PyPI](https://pypi.org/project/stackdoctor/) or install it once:
|
|
64
65
|
|
|
65
66
|
```sh
|
|
66
|
-
uvx stackdoctor
|
|
67
|
+
uvx stackdoctor # runs the latest release, nothing to install
|
|
68
|
+
uv tool install stackdoctor # or install it; the command is then `stackdoctor`
|
|
67
69
|
```
|
|
68
70
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
+
You don't normally run it by hand: your MCP client starts it (see [Configure](#configure)).
|
|
72
|
+
To check that it works, run `uvx stackdoctor --help`, which prints usage and the configuration
|
|
73
|
+
variables. To upgrade, use `uvx stackdoctor@latest` or `uv tool upgrade stackdoctor`.
|
|
71
74
|
|
|
72
75
|
> **macOS 12 (Monterey):** `uvx stackdoctor` fails with `realpath: command not found`, because uv's
|
|
73
76
|
> launcher script needs `realpath`, which only ships with macOS 13+. Use
|
|
74
77
|
> `uvx --from stackdoctor python -m stackdoctor` instead. In client configs that means
|
|
75
|
-
> `"args": ["--from", "stackdoctor", "python", "-m", "stackdoctor"]`. Alternatively,
|
|
76
|
-
> `uv tool install stackdoctor`
|
|
78
|
+
> `"args": ["--from", "stackdoctor", "python", "-m", "stackdoctor"]`. Alternatively, use
|
|
79
|
+
> `uv tool install stackdoctor` and set the command to the full path of the installed tool,
|
|
80
|
+
> `/Users/<username>/.local/bin/stackdoctor`. Its launcher doesn't need `realpath`.
|
|
77
81
|
|
|
78
82
|
## Configure
|
|
79
83
|
|
|
@@ -317,7 +321,9 @@ in WSL2 and run the `.ps1` scripts from PowerShell.
|
|
|
317
321
|
## Development
|
|
318
322
|
|
|
319
323
|
```sh
|
|
324
|
+
git clone https://github.com/lepri89/stackdoctor && cd stackdoctor
|
|
320
325
|
uv sync
|
|
326
|
+
uv run stackdoctor --help # run from the checkout
|
|
321
327
|
uv run pytest # unit tests (safety layer, diagnose, logs)
|
|
322
328
|
STACKDOCTOR_TEST_DATABASE_URL=postgresql://shop:shop@localhost:55432/shop \
|
|
323
329
|
STACKDOCTOR_TEST_REDIS_URL=redis://localhost:56379/0 \
|
|
@@ -333,7 +339,7 @@ Releases go to PyPI from GitHub Actions with
|
|
|
333
339
|
`lepri89`, repository `stackdoctor`, workflow `release.yml` and environment `pypi`. In GitHub, create
|
|
334
340
|
an environment named `pypi` (Settings → Environments); adding yourself as a required reviewer is a good idea.
|
|
335
341
|
2. Bump `version` in `pyproject.toml`, commit, then tag and push:
|
|
336
|
-
`git tag
|
|
342
|
+
`git tag "v$(uv version --short)" && git push origin "v$(uv version --short)"`.
|
|
337
343
|
The workflow checks that the tag matches the version, runs the tests, builds, and publishes.
|
|
338
344
|
|
|
339
345
|
## License
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/lepri89/stackdoctor/actions/workflows/tests.yml)
|
|
4
4
|
[](https://github.com/lepri89/stackdoctor/actions/workflows/demo.yml)
|
|
5
|
+
[](https://pypi.org/project/stackdoctor/)
|
|
5
6
|

|
|
6
7
|

|
|
7
8
|
|
|
@@ -42,20 +43,23 @@ curl -LsSf https://astral.sh/uv/install.sh | sh # macOS
|
|
|
42
43
|
powershell -c "irm https://astral.sh/uv/install.ps1 | iex" # Windows
|
|
43
44
|
```
|
|
44
45
|
|
|
45
|
-
Then
|
|
46
|
+
Then either run it straight from [PyPI](https://pypi.org/project/stackdoctor/) or install it once:
|
|
46
47
|
|
|
47
48
|
```sh
|
|
48
|
-
uvx stackdoctor
|
|
49
|
+
uvx stackdoctor # runs the latest release, nothing to install
|
|
50
|
+
uv tool install stackdoctor # or install it; the command is then `stackdoctor`
|
|
49
51
|
```
|
|
50
52
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
+
You don't normally run it by hand: your MCP client starts it (see [Configure](#configure)).
|
|
54
|
+
To check that it works, run `uvx stackdoctor --help`, which prints usage and the configuration
|
|
55
|
+
variables. To upgrade, use `uvx stackdoctor@latest` or `uv tool upgrade stackdoctor`.
|
|
53
56
|
|
|
54
57
|
> **macOS 12 (Monterey):** `uvx stackdoctor` fails with `realpath: command not found`, because uv's
|
|
55
58
|
> launcher script needs `realpath`, which only ships with macOS 13+. Use
|
|
56
59
|
> `uvx --from stackdoctor python -m stackdoctor` instead. In client configs that means
|
|
57
|
-
> `"args": ["--from", "stackdoctor", "python", "-m", "stackdoctor"]`. Alternatively,
|
|
58
|
-
> `uv tool install stackdoctor`
|
|
60
|
+
> `"args": ["--from", "stackdoctor", "python", "-m", "stackdoctor"]`. Alternatively, use
|
|
61
|
+
> `uv tool install stackdoctor` and set the command to the full path of the installed tool,
|
|
62
|
+
> `/Users/<username>/.local/bin/stackdoctor`. Its launcher doesn't need `realpath`.
|
|
59
63
|
|
|
60
64
|
## Configure
|
|
61
65
|
|
|
@@ -299,7 +303,9 @@ in WSL2 and run the `.ps1` scripts from PowerShell.
|
|
|
299
303
|
## Development
|
|
300
304
|
|
|
301
305
|
```sh
|
|
306
|
+
git clone https://github.com/lepri89/stackdoctor && cd stackdoctor
|
|
302
307
|
uv sync
|
|
308
|
+
uv run stackdoctor --help # run from the checkout
|
|
303
309
|
uv run pytest # unit tests (safety layer, diagnose, logs)
|
|
304
310
|
STACKDOCTOR_TEST_DATABASE_URL=postgresql://shop:shop@localhost:55432/shop \
|
|
305
311
|
STACKDOCTOR_TEST_REDIS_URL=redis://localhost:56379/0 \
|
|
@@ -315,7 +321,7 @@ Releases go to PyPI from GitHub Actions with
|
|
|
315
321
|
`lepri89`, repository `stackdoctor`, workflow `release.yml` and environment `pypi`. In GitHub, create
|
|
316
322
|
an environment named `pypi` (Settings → Environments); adding yourself as a required reviewer is a good idea.
|
|
317
323
|
2. Bump `version` in `pyproject.toml`, commit, then tag and push:
|
|
318
|
-
`git tag
|
|
324
|
+
`git tag "v$(uv version --short)" && git push origin "v$(uv version --short)"`.
|
|
319
325
|
The workflow checks that the tag matches the version, runs the tests, builds, and publishes.
|
|
320
326
|
|
|
321
327
|
## License
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""stackdoctor: read-only MCP server that diagnoses a Python backend stack."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
# pyproject.toml is the single source of truth; this reads the installed package's metadata.
|
|
7
|
+
__version__ = version("stackdoctor")
|
|
8
|
+
except PackageNotFoundError: # running from a source tree that isn't installed
|
|
9
|
+
__version__ = "0.0.0+unknown"
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import argparse
|
|
5
6
|
import functools
|
|
6
7
|
import inspect
|
|
7
8
|
import logging
|
|
@@ -10,6 +11,7 @@ import sys
|
|
|
10
11
|
from mcp.server.mcpserver import MCPServer
|
|
11
12
|
from mcp.types import ToolAnnotations
|
|
12
13
|
|
|
14
|
+
from . import __version__
|
|
13
15
|
from . import diagnose as diag
|
|
14
16
|
from .checks import celery, logs, postgres, redis
|
|
15
17
|
from .config import get_config
|
|
@@ -154,7 +156,34 @@ def search_logs(pattern: str, since_minutes: int = 30, source: str | None = None
|
|
|
154
156
|
return logs.search_logs(pattern, since_minutes, source)
|
|
155
157
|
|
|
156
158
|
|
|
157
|
-
|
|
159
|
+
USAGE_EPILOG = """\
|
|
160
|
+
MCP clients (Claude Desktop, Claude Code, Cursor, VS Code) start this server themselves.
|
|
161
|
+
Running it by hand starts an MCP server on stdin/stdout that waits for a client.
|
|
162
|
+
|
|
163
|
+
configuration (environment variables or a .env file; unset sources are skipped):
|
|
164
|
+
DATABASE_URL Postgres, e.g. postgresql://stackdoctor_ro:...@localhost:5432/app
|
|
165
|
+
REDIS_URL Redis, e.g. redis://localhost:6379/0
|
|
166
|
+
CELERY_BROKER_URL Celery broker (Redis; RabbitMQ is experimental)
|
|
167
|
+
CELERY_RESULT_BACKEND Redis result backend, for failed tasks
|
|
168
|
+
CELERY_APP optional import path of your Celery app, e.g. myproject.celery:app
|
|
169
|
+
LOG_SOURCES comma-separated log files and/or docker:<container>
|
|
170
|
+
STACKDOCTOR_ENV_FILE path to a .env file (default: .env in the working directory)
|
|
171
|
+
|
|
172
|
+
docs: https://github.com/lepri89/stackdoctor
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def main(argv: list[str] | None = None) -> None:
|
|
177
|
+
parser = argparse.ArgumentParser(
|
|
178
|
+
prog="stackdoctor",
|
|
179
|
+
description="Read-only MCP server that diagnoses a Python backend stack "
|
|
180
|
+
"(Postgres, Celery, Redis, logs).",
|
|
181
|
+
epilog=USAGE_EPILOG,
|
|
182
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
183
|
+
)
|
|
184
|
+
parser.add_argument("--version", action="version", version=f"stackdoctor {__version__}")
|
|
185
|
+
parser.parse_args(argv) # --help / --version print and exit here
|
|
186
|
+
|
|
158
187
|
logging.basicConfig(level=logging.WARNING, stream=sys.stderr) # stdout is the MCP channel
|
|
159
188
|
get_config()
|
|
160
189
|
mcp.run("stdio")
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from stackdoctor import server
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@pytest.mark.parametrize("flag,expected", [("--help", "usage: stackdoctor"), ("--version", "stackdoctor ")])
|
|
7
|
+
def test_flags_print_and_exit_without_starting_server(flag, expected, capsys, monkeypatch):
|
|
8
|
+
monkeypatch.setattr(server.mcp, "run", lambda *a, **k: pytest.fail("server must not start"))
|
|
9
|
+
with pytest.raises(SystemExit) as exit_info:
|
|
10
|
+
server.main([flag])
|
|
11
|
+
assert exit_info.value.code == 0
|
|
12
|
+
assert expected in capsys.readouterr().out
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_unknown_flag_is_an_error(monkeypatch):
|
|
16
|
+
monkeypatch.setattr(server.mcp, "run", lambda *a, **k: pytest.fail("server must not start"))
|
|
17
|
+
with pytest.raises(SystemExit) as exit_info:
|
|
18
|
+
server.main(["--nope"])
|
|
19
|
+
assert exit_info.value.code == 2
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_version_matches_pyproject(capsys):
|
|
23
|
+
"""pyproject.toml is the single source of truth for the version."""
|
|
24
|
+
import tomllib
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
pyproject = tomllib.loads((Path(__file__).parents[1] / "pyproject.toml").read_text())
|
|
28
|
+
with pytest.raises(SystemExit):
|
|
29
|
+
server.main(["--version"])
|
|
30
|
+
assert capsys.readouterr().out.strip() == f"stackdoctor {pyproject['project']['version']}"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_version_fallback_when_not_installed(monkeypatch):
|
|
34
|
+
import importlib
|
|
35
|
+
import importlib.metadata
|
|
36
|
+
|
|
37
|
+
import stackdoctor
|
|
38
|
+
|
|
39
|
+
def missing(name):
|
|
40
|
+
raise importlib.metadata.PackageNotFoundError(name)
|
|
41
|
+
|
|
42
|
+
monkeypatch.setattr(importlib.metadata, "version", missing)
|
|
43
|
+
try:
|
|
44
|
+
assert importlib.reload(stackdoctor).__version__ == "0.0.0+unknown"
|
|
45
|
+
finally:
|
|
46
|
+
monkeypatch.undo()
|
|
47
|
+
importlib.reload(stackdoctor)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|