opsinsight-mcp 0.1.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.
Files changed (111) hide show
  1. opsinsight_mcp-0.1.0/.dockerignore +18 -0
  2. opsinsight_mcp-0.1.0/.env.example +40 -0
  3. opsinsight_mcp-0.1.0/.github/workflows/ci.yml +43 -0
  4. opsinsight_mcp-0.1.0/.github/workflows/deploy.yml +52 -0
  5. opsinsight_mcp-0.1.0/.github/workflows/publish.yml +66 -0
  6. opsinsight_mcp-0.1.0/.gitignore +20 -0
  7. opsinsight_mcp-0.1.0/.python-version +1 -0
  8. opsinsight_mcp-0.1.0/CLAUDE.md +161 -0
  9. opsinsight_mcp-0.1.0/CONTRIBUTING.md +69 -0
  10. opsinsight_mcp-0.1.0/Dockerfile +20 -0
  11. opsinsight_mcp-0.1.0/LICENSE +21 -0
  12. opsinsight_mcp-0.1.0/PKG-INFO +279 -0
  13. opsinsight_mcp-0.1.0/README.md +255 -0
  14. opsinsight_mcp-0.1.0/docs/agensi-skill/LISTING.md +33 -0
  15. opsinsight_mcp-0.1.0/docs/agensi-skill/support-ticket-investigation/SKILL.md +70 -0
  16. opsinsight_mcp-0.1.0/docs/agensi-skill/support-ticket-investigation/reference/query-cookbook.md +77 -0
  17. opsinsight_mcp-0.1.0/docs/agensi-skill/support-ticket-investigation/reference/setup.md +38 -0
  18. opsinsight_mcp-0.1.0/docs/agensi-skill/support-ticket-investigation/reference/tools.md +34 -0
  19. opsinsight_mcp-0.1.0/docs/agensi-skill/support-ticket-investigation/scripts/env.example +26 -0
  20. opsinsight_mcp-0.1.0/docs/agensi-skill/support-ticket-investigation/scripts/mcp-config.example.json +8 -0
  21. opsinsight_mcp-0.1.0/docs/architecture.svg +62 -0
  22. opsinsight_mcp-0.1.0/docs/design.md +446 -0
  23. opsinsight_mcp-0.1.0/docs/logo-512.png +0 -0
  24. opsinsight_mcp-0.1.0/docs/logo-64.png +0 -0
  25. opsinsight_mcp-0.1.0/docs/logo.jpg +0 -0
  26. opsinsight_mcp-0.1.0/docs/logo.png +0 -0
  27. opsinsight_mcp-0.1.0/docs/logo.svg +153 -0
  28. opsinsight_mcp-0.1.0/docs/packaging-plan.md +153 -0
  29. opsinsight_mcp-0.1.0/docs/tool-reference.md +882 -0
  30. opsinsight_mcp-0.1.0/pyproject.toml +53 -0
  31. opsinsight_mcp-0.1.0/src/core/__init__.py +0 -0
  32. opsinsight_mcp-0.1.0/src/core/app.py +18 -0
  33. opsinsight_mcp-0.1.0/src/core/cache.py +84 -0
  34. opsinsight_mcp-0.1.0/src/core/config.py +146 -0
  35. opsinsight_mcp-0.1.0/src/core/container.py +85 -0
  36. opsinsight_mcp-0.1.0/src/core/errors.py +42 -0
  37. opsinsight_mcp-0.1.0/src/core/field_heuristics.py +8 -0
  38. opsinsight_mcp-0.1.0/src/core/logging_config.py +71 -0
  39. opsinsight_mcp-0.1.0/src/core/models.py +12 -0
  40. opsinsight_mcp-0.1.0/src/core/redaction.py +101 -0
  41. opsinsight_mcp-0.1.0/src/data/query_cookbook.yaml +205 -0
  42. opsinsight_mcp-0.1.0/src/integrations/__init__.py +0 -0
  43. opsinsight_mcp-0.1.0/src/integrations/cloudwatch/__init__.py +0 -0
  44. opsinsight_mcp-0.1.0/src/integrations/cloudwatch/client.py +70 -0
  45. opsinsight_mcp-0.1.0/src/integrations/cloudwatch/constants.py +17 -0
  46. opsinsight_mcp-0.1.0/src/integrations/cloudwatch/guardrail.py +126 -0
  47. opsinsight_mcp-0.1.0/src/integrations/cloudwatch/models.py +83 -0
  48. opsinsight_mcp-0.1.0/src/integrations/cloudwatch/queries.py +7 -0
  49. opsinsight_mcp-0.1.0/src/integrations/database/__init__.py +0 -0
  50. opsinsight_mcp-0.1.0/src/integrations/database/constants.py +42 -0
  51. opsinsight_mcp-0.1.0/src/integrations/database/engine.py +73 -0
  52. opsinsight_mcp-0.1.0/src/integrations/database/guardrail.py +152 -0
  53. opsinsight_mcp-0.1.0/src/integrations/database/introspect.py +477 -0
  54. opsinsight_mcp-0.1.0/src/integrations/database/models.py +111 -0
  55. opsinsight_mcp-0.1.0/src/integrations/database/queries.py +76 -0
  56. opsinsight_mcp-0.1.0/src/integrations/newrelic/__init__.py +0 -0
  57. opsinsight_mcp-0.1.0/src/integrations/newrelic/client.py +57 -0
  58. opsinsight_mcp-0.1.0/src/integrations/newrelic/constants.py +16 -0
  59. opsinsight_mcp-0.1.0/src/integrations/newrelic/guardrail.py +46 -0
  60. opsinsight_mcp-0.1.0/src/integrations/newrelic/models.py +40 -0
  61. opsinsight_mcp-0.1.0/src/integrations/newrelic/queries.py +23 -0
  62. opsinsight_mcp-0.1.0/src/resources/__init__.py +1 -0
  63. opsinsight_mcp-0.1.0/src/resources/cookbook.py +27 -0
  64. opsinsight_mcp-0.1.0/src/resources/logs.py +11 -0
  65. opsinsight_mcp-0.1.0/src/resources/schema.py +21 -0
  66. opsinsight_mcp-0.1.0/src/server.py +50 -0
  67. opsinsight_mcp-0.1.0/src/service/__init__.py +5 -0
  68. opsinsight_mcp-0.1.0/src/service/base.py +31 -0
  69. opsinsight_mcp-0.1.0/src/service/cloudwatch_service.py +427 -0
  70. opsinsight_mcp-0.1.0/src/service/database_service.py +132 -0
  71. opsinsight_mcp-0.1.0/src/service/newrelic_service.py +130 -0
  72. opsinsight_mcp-0.1.0/src/tools/__init__.py +5 -0
  73. opsinsight_mcp-0.1.0/src/tools/cloudwatch/__init__.py +15 -0
  74. opsinsight_mcp-0.1.0/src/tools/cloudwatch/cw_describe_log_fields.py +13 -0
  75. opsinsight_mcp-0.1.0/src/tools/cloudwatch/cw_filter_events.py +9 -0
  76. opsinsight_mcp-0.1.0/src/tools/cloudwatch/cw_get_metric_stats.py +18 -0
  77. opsinsight_mcp-0.1.0/src/tools/cloudwatch/cw_get_trace_events.py +22 -0
  78. opsinsight_mcp-0.1.0/src/tools/cloudwatch/cw_list_log_groups.py +8 -0
  79. opsinsight_mcp-0.1.0/src/tools/cloudwatch/cw_run_insights_query.py +27 -0
  80. opsinsight_mcp-0.1.0/src/tools/database/__init__.py +17 -0
  81. opsinsight_mcp-0.1.0/src/tools/database/db_describe_table.py +9 -0
  82. opsinsight_mcp-0.1.0/src/tools/database/db_explain_query.py +8 -0
  83. opsinsight_mcp-0.1.0/src/tools/database/db_list_tables.py +8 -0
  84. opsinsight_mcp-0.1.0/src/tools/database/db_resolve_store.py +15 -0
  85. opsinsight_mcp-0.1.0/src/tools/database/db_run_query.py +8 -0
  86. opsinsight_mcp-0.1.0/src/tools/database/db_sample_rows.py +8 -0
  87. opsinsight_mcp-0.1.0/src/tools/database/db_search_by_identifier.py +17 -0
  88. opsinsight_mcp-0.1.0/src/tools/newrelic/__init__.py +9 -0
  89. opsinsight_mcp-0.1.0/src/tools/newrelic/nr_describe_log_fields.py +14 -0
  90. opsinsight_mcp-0.1.0/src/tools/newrelic/nr_list_event_types.py +12 -0
  91. opsinsight_mcp-0.1.0/src/tools/newrelic/nr_run_nrql_query.py +9 -0
  92. opsinsight_mcp-0.1.0/tests/__init__.py +0 -0
  93. opsinsight_mcp-0.1.0/tests/conftest.py +71 -0
  94. opsinsight_mcp-0.1.0/tests/integration/__init__.py +0 -0
  95. opsinsight_mcp-0.1.0/tests/integration/test_db_tools.py +69 -0
  96. opsinsight_mcp-0.1.0/tests/unit/__init__.py +0 -0
  97. opsinsight_mcp-0.1.0/tests/unit/test_cache.py +20 -0
  98. opsinsight_mcp-0.1.0/tests/unit/test_cloudwatch_tools.py +206 -0
  99. opsinsight_mcp-0.1.0/tests/unit/test_config.py +49 -0
  100. opsinsight_mcp-0.1.0/tests/unit/test_container.py +59 -0
  101. opsinsight_mcp-0.1.0/tests/unit/test_cw_guardrail.py +139 -0
  102. opsinsight_mcp-0.1.0/tests/unit/test_database_engine.py +21 -0
  103. opsinsight_mcp-0.1.0/tests/unit/test_database_queries.py +24 -0
  104. opsinsight_mcp-0.1.0/tests/unit/test_db_guardrail.py +122 -0
  105. opsinsight_mcp-0.1.0/tests/unit/test_db_introspect.py +32 -0
  106. opsinsight_mcp-0.1.0/tests/unit/test_logging.py +53 -0
  107. opsinsight_mcp-0.1.0/tests/unit/test_newrelic_guardrail.py +67 -0
  108. opsinsight_mcp-0.1.0/tests/unit/test_newrelic_tools.py +83 -0
  109. opsinsight_mcp-0.1.0/tests/unit/test_redaction.py +81 -0
  110. opsinsight_mcp-0.1.0/tests/unit/test_schema_resources.py +37 -0
  111. opsinsight_mcp-0.1.0/uv.lock +1900 -0
@@ -0,0 +1,18 @@
1
+ .venv
2
+ .uv-cache
3
+ __pycache__
4
+ *.pyc
5
+ .pytest_cache
6
+ .ruff_cache
7
+ .git
8
+ .github
9
+ .env
10
+ .env.example
11
+ tests
12
+ docs
13
+ dist
14
+ dist_review
15
+ .vscode
16
+ .cursor
17
+ .agents
18
+ .claude
@@ -0,0 +1,40 @@
1
+ # Copy to .env and fill in your PostgreSQL connection details.
2
+
3
+ # Database — provide your full PostgreSQL connection URL (asyncpg driver)
4
+ DB_URL=postgresql+asyncpg://user:password@host:5432/dbname
5
+ DB_MAX_ROWS=500
6
+ DB_STATEMENT_TIMEOUT_MS=15000
7
+ DB_IDLE_TXN_TIMEOUT_MS=30000
8
+ DB_MAX_CELL_BYTES=4096
9
+ DB_SCHEMA_CACHE_TTL_S=600
10
+
11
+ # AWS / CloudWatch — CLOUDWATCH_REGION is required (there is no AWS_REGION
12
+ # fallback; plain AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY in .env are NOT
13
+ # read by this app either). Authenticate via either AWS_PROFILE (a boto3
14
+ # profile name) or CLOUDWATCH_ACCESS_KEY_ID + CLOUDWATCH_SECRET_ACCESS_KEY.
15
+ CLOUDWATCH_REGION=ap-south-1
16
+ AWS_PROFILE=mcp-readonly
17
+ # CLOUDWATCH_ACCESS_KEY_ID=
18
+ # CLOUDWATCH_SECRET_ACCESS_KEY=
19
+ # CLOUDWATCH_ALLOWED_LOG_GROUP= # comma-separated list of allowed log group name(s)
20
+ CW_MAX_WINDOW_HOURS=168
21
+ CW_DEFAULT_WINDOW_HOURS=24
22
+ CW_MAX_BYTES_SCANNED=5000000000
23
+ CW_POLL_MAX_WAIT_S=60
24
+
25
+ # New Relic (NerdGraph / NRQL) — User API key (NRAK-...), not an ingest/license key
26
+ NEW_RELIC_API_KEY=
27
+ NEW_RELIC_ACCOUNT_ID=
28
+ NEW_RELIC_REGION=us
29
+ NR_MAX_WINDOW_HOURS=168
30
+ NR_DEFAULT_WINDOW_HOURS=24
31
+ NR_MAX_ROWS=500
32
+
33
+ # Server
34
+ # Loopback by default because the MCP HTTP transport has no built-in authentication.
35
+ # Only bind externally when access is protected by an authenticated proxy or firewall.
36
+ SERVER_HOST=127.0.0.1
37
+ SERVER_PORT=8001 # ignored if $PORT is set by the host platform (Render/Railway/Fly) and
38
+ # this var is left unset — see core/config.py
39
+ SERVER_PATH=/mcp
40
+ PII_REDACTION=true
@@ -0,0 +1,43 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main, dev]
6
+ pull_request:
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ lint-and-test:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+
17
+ - name: Install uv
18
+ uses: astral-sh/setup-uv@v3
19
+ with:
20
+ enable-cache: true
21
+
22
+ - name: Set up Python
23
+ run: uv python install 3.13
24
+
25
+ - name: Install dependencies
26
+ run: uv sync --all-extras --dev
27
+
28
+ - name: Lint (ruff)
29
+ run: uv run ruff check .
30
+
31
+ - name: Unit tests
32
+ run: uv run pytest tests/unit -q
33
+
34
+ - name: Integration tests (auto-skip if no reachable Postgres)
35
+ run: uv run pytest tests/integration -m integration -q
36
+
37
+ docker-build:
38
+ runs-on: ubuntu-latest
39
+ steps:
40
+ - uses: actions/checkout@v4
41
+
42
+ - name: Build Docker image
43
+ run: docker build -t investigation-mcp .
@@ -0,0 +1,52 @@
1
+ name: Deploy
2
+
3
+ # Publishes the Docker image to GitHub Container Registry on every merge to main.
4
+ on:
5
+ push:
6
+ branches: [main]
7
+ workflow_dispatch:
8
+
9
+ concurrency:
10
+ group: deploy-${{ github.ref }}
11
+ cancel-in-progress: false
12
+
13
+ env:
14
+ REGISTRY: ghcr.io
15
+ IMAGE_NAME: ${{ github.repository }}
16
+
17
+ jobs:
18
+ publish-image:
19
+ runs-on: ubuntu-latest
20
+ permissions:
21
+ contents: read
22
+ packages: write
23
+ steps:
24
+ - uses: actions/checkout@v4
25
+
26
+ - uses: docker/setup-buildx-action@v3
27
+
28
+ - name: Log in to GHCR
29
+ uses: docker/login-action@v3
30
+ with:
31
+ registry: ${{ env.REGISTRY }}
32
+ username: ${{ github.actor }}
33
+ password: ${{ secrets.GITHUB_TOKEN }}
34
+
35
+ - name: Image metadata
36
+ id: meta
37
+ uses: docker/metadata-action@v5
38
+ with:
39
+ images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
40
+ tags: |
41
+ type=raw,value=latest
42
+ type=sha,prefix=sha-
43
+
44
+ - name: Build and push
45
+ uses: docker/build-push-action@v6
46
+ with:
47
+ context: .
48
+ push: true
49
+ tags: ${{ steps.meta.outputs.tags }}
50
+ labels: ${{ steps.meta.outputs.labels }}
51
+ cache-from: type=gha
52
+ cache-to: type=gha,mode=max
@@ -0,0 +1,66 @@
1
+ name: Publish to PyPI
2
+
3
+ # Publishes the `opsinsight-mcp` package to PyPI when a version tag (v*) is pushed.
4
+ # Uses PyPI trusted publishing (OIDC) — no API token is stored in this repo. The
5
+ # trusted publisher must be registered on PyPI for this repo, this workflow file
6
+ # (`publish.yml`) and the `pypi` environment before the first run.
7
+ on:
8
+ push:
9
+ tags: ["v*"]
10
+
11
+ concurrency:
12
+ group: publish-${{ github.ref }}
13
+ cancel-in-progress: false
14
+
15
+ jobs:
16
+ build:
17
+ runs-on: ubuntu-latest
18
+ permissions:
19
+ contents: read
20
+ steps:
21
+ - uses: actions/checkout@v4
22
+
23
+ - name: Install uv
24
+ uses: astral-sh/setup-uv@v3
25
+
26
+ - name: Set up Python
27
+ run: uv python install 3.13
28
+
29
+ - name: Check tag matches package version
30
+ run: |
31
+ version=$(uv run --no-project python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
32
+ if [ "v${version}" != "${GITHUB_REF_NAME}" ]; then
33
+ echo "Tag ${GITHUB_REF_NAME} does not match pyproject.toml version ${version}" >&2
34
+ exit 1
35
+ fi
36
+
37
+ - name: Lint and unit tests
38
+ run: |
39
+ uv sync --all-extras --dev
40
+ uv run ruff check .
41
+ uv run pytest tests/unit -q
42
+
43
+ - name: Build sdist and wheel
44
+ run: uv build
45
+
46
+ - uses: actions/upload-artifact@v4
47
+ with:
48
+ name: dist
49
+ path: dist/
50
+
51
+ publish:
52
+ needs: build
53
+ runs-on: ubuntu-latest
54
+ environment:
55
+ name: pypi
56
+ url: https://pypi.org/project/opsinsight-mcp/
57
+ permissions:
58
+ id-token: write
59
+ steps:
60
+ - uses: actions/download-artifact@v4
61
+ with:
62
+ name: dist
63
+ path: dist/
64
+
65
+ - name: Publish to PyPI
66
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,20 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+
12
+ # Local secrets / runtime output
13
+ .env
14
+ audit.jsonl
15
+
16
+ # Editor / AI-tool local config
17
+ .claude/
18
+ .cursor/
19
+ .vscode/
20
+
@@ -0,0 +1 @@
1
+ 3.14
@@ -0,0 +1,161 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## What this is
6
+
7
+ A [FastMCP](https://gofastmcp.com) server exposing safe, **read-only** tools for investigating
8
+ support tickets: schema discovery + query execution against Postgres, log/metric access
9
+ against AWS CloudWatch, and log/event access against New Relic (NRQL). It's meant to be
10
+ driven by a client model (Claude Desktop, Cursor) that writes queries in natural-language
11
+ conversation — the server's job is to supply enough grounding context (schema, field
12
+ discovery, a query cookbook) that the model generates correct queries, and to validate +
13
+ execute them safely. See `docs/design.md` for the as-built design (§3.1 explains why
14
+ query generation is client-side, not server-side; §4.5 lists items from the original draft
15
+ that are still not implemented).
16
+
17
+ Cross-source correlation (DB + CloudWatch + New Relic) is currently entirely manual — the
18
+ client model fans out to each source's tools and merges results itself, with no server-side
19
+ guidance beyond the query cookbook. Composite tools `investigation_start` / `trace_request`
20
+ and MCP prompts (`investigate_incident` / `trace_user_journey` / `slow_endpoint_rca`) are
21
+ **not implemented**; don't assume they exist.
22
+
23
+ ## Commands
24
+
25
+ ```powershell
26
+ uv sync # install deps
27
+ copy .env.example .env # then fill in DB_URL, AWS_*, NEW_RELIC_*
28
+ uv run pytest tests/unit # guardrail/redaction tests, no external deps
29
+ uv run pytest tests/unit/test_db_guardrail.py -q # run a single test file
30
+ uv run pytest tests/unit -k test_limit_clamped # run a single test by name
31
+ uv run pytest tests/integration -m integration # requires a reachable Postgres; auto-skips otherwise
32
+ uv run ruff check . # lint
33
+ uv run investigation-server # start the MCP server (streamable HTTP, :8000/mcp by default)
34
+ ```
35
+
36
+ `asyncio_mode = "auto"` is set in `pyproject.toml` — async test functions run without needing
37
+ `@pytest.mark.asyncio`.
38
+
39
+ ## Architecture
40
+
41
+ **Layering is strict and one-directional**: `tools/*` (thin `@mcp.tool()`-decorated wrappers)
42
+ → `service/*` (business logic, one class per source) → `integrations/<source>/guardrail.py`
43
+ (validate/reject, pure functions, no I/O) → `integrations/<source>/client.py` or `engine.py`
44
+ (actual network/DB I/O). Guardrail functions never touch the network; service methods never
45
+ build a query without passing it through the guardrail first.
46
+
47
+ **`src/service/` holds one class per source** — `DatabaseService`, `CloudWatchService`,
48
+ `NewRelicService` (`service/database_service.py`, `service/cloudwatch_service.py`,
49
+ `service/newrelic_service.py`), all inheriting `BaseService` (`service/base.py`) for the
50
+ shared `Settings` handle and the `ok(data, meta)` success-envelope helper. This is where a
51
+ tool's actual logic (validate → execute → redact → structured response) lives; error-response
52
+ translation for that source's exception family (`SQLAlchemyError` /
53
+ `BotoCoreError`+`ClientError` / `httpx.HTTPError`) is a private method on the same class
54
+ (`_sqlalchemy_error_response`, `_aws_error_response`, `_httpx_error_response`) rather than a
55
+ shared abstract method, since the three sources don't share an exception type — forcing one
56
+ would violate interface segregation for no benefit. Cross-tool logic that one source's several
57
+ tools share (e.g. CloudWatch's `run_insights_query`, used by both `cw_run_insights_query` and
58
+ `cw_get_trace_events`) is just another method on that source's service class, called from the
59
+ other public methods — composition over duplicating the polling/cost-ceiling logic per tool.
60
+ Network/DB clients are **constructor-injected** on `CloudWatchService`
61
+ (`logs_client_factory`/`metrics_client_factory`, default the real boto3 factories) and
62
+ `NewRelicService` (`run_nrql_fn`, defaults the real NerdGraph client) — dependency inversion,
63
+ so unit tests pass a fake client into the constructor instead of monkeypatching module state;
64
+ `DatabaseService` has no such seam because its only unit-level tests are integration tests
65
+ against a real Postgres (see `tests/integration/test_db_tools.py`).
66
+
67
+ **`tools/*` are thin MCP wrappers, nothing else**: each source is its own package
68
+ (`tools/database/`, `tools/cloudwatch/`, `tools/newrelic/`) with **one file per tool**, named
69
+ after the tool itself (e.g. `tools/database/db_resolve_store.py`). A tool file's only job is
70
+ the `@mcp.tool()` decorator, the docstring the client model reads, and the parameter schema —
71
+ the body is one line pulling that source's service off the container and calling the matching
72
+ method (see `tools/newrelic/nr_list_event_types.py` for the smallest complete example). Do not
73
+ put logic in a tool file; if you're tempted to, it belongs on the service class instead. The
74
+ one deliberate exception is inventory tools (`db_list_tables`, `cw_list_log_groups`,
75
+ `nr_list_event_types`): their service methods send a hardcoded, parameter-free (or
76
+ window-clamped) query straight to the client, bypassing the query guardrail, since there's no
77
+ user-supplied query text to validate — `nr_list_event_types` in particular sends `SHOW EVENT
78
+ TYPES`, which isn't a `SELECT` and would otherwise be rejected by `validate_nrql`.
79
+
80
+ **`core/container.py` is the composition root** — the one place that constructs services and
81
+ wires their dependencies. `Container` lazily builds and caches (singleton-scoped, per
82
+ container instance) `database_service`/`cloudwatch_service`/`newrelic_service`, each backed by
83
+ `self.settings` (which resolves through `get_settings()` unless a `Settings` override was
84
+ passed to the constructor). Every dependency — `settings`, or a fully pre-built service — can
85
+ also be injected via `Container(...)` keyword arguments; tests that want an isolated object
86
+ graph build their own `Container` rather than reaching for the process-wide one.
87
+ `get_container()` returns the process-wide singleton that `tools/*` call
88
+ (`get_container().database_service.list_tables(...)`); `reset_container()` drops it so the
89
+ next `get_container()` rebuilds against current `Settings` — call it after
90
+ `get_settings.cache_clear()` in any test that routes through the container, the same way
91
+ `reset_engine()` is called for the DB engine (`tests/conftest.py`'s `settings_override` does
92
+ both). This container is *not* the seam individual service unit tests use — those construct
93
+ the service directly with an injected fake client (see below) since that's simpler when you
94
+ only need one service; the container exists for wiring the full tools → services graph in
95
+ `server.py` and for tests that want that whole graph pre-assembled.
96
+
97
+ **Three independent data sources, one shared pattern**: `database` (SQLAlchemy/asyncpg +
98
+ `sqlglot` AST validation), `cloudwatch` (boto3 Logs Insights + a log-group allowlist + a
99
+ bytes-scanned cost ceiling), `newrelic` (NerdGraph GraphQL + a regex-based NRQL guardrail,
100
+ since NRQL has no AST parser like sqlglot). Each source has a `*_list_*` inventory tool
101
+ (`db_list_tables`, `cw_list_log_groups`, `nr_list_event_types`) and a `*_describe_*`
102
+ grounding tool (`db_describe_table`, `cw_describe_log_fields`, `nr_describe_log_fields`, which
103
+ uses NRQL's `keyset()` and separately flags likely trace/correlation-id attributes) that
104
+ should be called before writing a query against unfamiliar fields. Skipping the
105
+ grounding step is the main cause of queries that silently return zero rows because a field
106
+ name was guessed wrong.
107
+
108
+ **`core/app.py` holds the single shared `mcp = FastMCP(...)` instance in its own module**,
109
+ imported by every tool/resource/prompt module as `from core.app import mcp`. This exists
110
+ specifically so `server.py` (which is also the entry point run via `-m`/`investigation-server`)
111
+ never gets circularly imported — see the comment in `core/app.py` if touching this.
112
+
113
+ **Registration is import-side-effect based**: `src/tools/__init__.py` imports the three
114
+ source packages (`cloudwatch`, `database`, `newrelic`); each package's own `__init__.py` in
115
+ turn imports every tool file in it purely for the `@mcp.tool()` decorator to run, and
116
+ re-exports the tool function so it can also be imported as `from tools.<source> import
117
+ <tool_name>`. `src/resources/__init__.py` does the same for `@mcp.resource()`. `server.py`
118
+ imports both top-level `__init__` modules before calling `mcp.run()`. A new tool file must be
119
+ added to its source package's `__init__.py` import list (and, transitively, the source
120
+ package must already be listed in `src/tools/__init__.py`) or it will never register.
121
+
122
+ **Errors are structured and self-correcting by design**: every guardrail raises a `ToolError`
123
+ subclass (`GuardrailError`, `CWGuardrailError`, `NewRelicGuardrailError` in `core/errors.py`)
124
+ carrying a machine-readable `rule` plus a `detail` string explaining what to change — never a
125
+ bare "denied". Every `@mcp.tool()` function catches its own `ToolError`s and converts them via
126
+ `.to_response()` into `{"ok": False, "error": {...}}`; successful results always follow
127
+ `{"ok": True, "data": {...}, "meta": {...}}`. Preserve this shape in any new tool.
128
+
129
+ **Settings are one `pydantic-settings` object** (`core/config.py`), loaded once via
130
+ `@lru_cache def get_settings()`. Tests that change env vars must call
131
+ `get_settings.cache_clear()` (and, for DB tests, `await reset_engine()` from
132
+ `integrations/database/engine.py`) — see `tests/conftest.py`'s `settings_override` fixture
133
+ and `tests/unit/test_newrelic_tools.py` for the pattern. Unit tests construct the service
134
+ class directly (`CloudWatchService(get_settings(), logs_client_factory=lambda s: fake_client)`,
135
+ `NewRelicService(get_settings(), run_nrql_fn=fake_run_nrql)`) rather than monkeypatching —
136
+ prefer that constructor-injection seam over `monkeypatch.setattr` when a new service method
137
+ needs a fake client/network call in tests.
138
+
139
+ **Redaction is centralized and applied at the tool boundary**, not deeper in the stack:
140
+ `core/redaction.py` masks PII by column name (`PII_COLUMN_NAMES`) and by regex pattern
141
+ (email/card/Aadhaar/PAN/phone) applied to both DB row values and raw log message text.
142
+ Every tool that returns rows/log content calls `redact_rows`/`redact_log_event` right before
143
+ building its response — do the same in new tools rather than relying on upstream masking.
144
+
145
+ **The query cookbook (`src/data/query_cookbook.yaml`, exposed as the `docs://query-cookbook`
146
+ resource) is a deliberate accuracy lever**, not just documentation — per design doc §4.4,
147
+ giving the client model curated example queries measurably improves generation accuracy over
148
+ schema alone. When a query pattern proves useful or a source's field-naming quirk causes
149
+ repeated bad queries, add/update a cookbook entry rather than only fixing it in prose.
150
+
151
+ **CloudWatch cost/window safety**: `cw_run_insights_query` enforces a time-window cap
152
+ (`clamp_window`), polls with exponential backoff (`poll_query_with_backoff`), and aborts +
153
+ reports if `bytesScanned` exceeds `CW_MAX_BYTES_SCANNED` — bytes scanned is always logged and
154
+ returned in `meta`, since Logs Insights bills per GB scanned.
155
+
156
+ **DB safety is defense-in-depth**: `sqlglot`-based AST validation (single `SELECT`/`WITH...SELECT`
157
+ only, blocked node types, blocked dangerous functions, LIMIT injection/clamping) in
158
+ `integrations/database/guardrail.py`, *plus* session-level hardening applied on every new
159
+ physical connection in `integrations/database/engine.py` (`SET default_transaction_read_only
160
+ = on`, statement timeout, idle-in-transaction timeout) — the DB role itself is meant to be the
161
+ last line of defense, not the application layer alone.
@@ -0,0 +1,69 @@
1
+ # Contributing
2
+
3
+ Thanks for considering a contribution to the Investigation MCP Server.
4
+
5
+ ## Before you start
6
+
7
+ This is a **read-only, guardrail-first** MCP server — every change should
8
+ preserve that property. If a change would let a tool write to the database,
9
+ bypass a guardrail, or return unredacted data, it needs an explicit
10
+ discussion first (open an issue), not just a PR.
11
+
12
+ ## Setup
13
+
14
+ ```powershell
15
+ uv sync
16
+ copy .env.example .env
17
+ ```
18
+
19
+ You don't need every backend configured to contribute — Postgres, CloudWatch,
20
+ and New Relic are independent (see the README's
21
+ [What each source needs](README.md#what-each-source-needs)). Unit tests don't
22
+ require any of them; only `tests/integration/` needs a reachable Postgres,
23
+ and it auto-skips otherwise.
24
+
25
+ ## Before opening a PR
26
+
27
+ ```powershell
28
+ uv run ruff check . # lint — must be clean
29
+ uv run pytest tests/unit -q # must pass
30
+ uv run pytest tests/integration -m integration # if you touched DB code and have Postgres available
31
+ ```
32
+
33
+ CI (`.github/workflows/ci.yml`) runs the same checks on every push/PR — a red
34
+ check means the PR isn't mergeable as-is.
35
+
36
+ ## Architecture and conventions
37
+
38
+ Read [`CLAUDE.md`](CLAUDE.md) first — it's the authoritative description of
39
+ this repo's layering (`tools/* → service/* → integrations/<source>/guardrail.py
40
+ → client.py|engine.py`) and the conventions each layer follows. Highlights:
41
+
42
+ - **One file per tool** in `tools/<source>/`, named after the tool. The file
43
+ is just the `@mcp.tool()` decorator, the docstring, and one call into the
44
+ service layer — no logic there.
45
+ - **Guardrails are pure functions, no I/O.** A service method never executes
46
+ a user-supplied query without passing it through the guardrail first.
47
+ - **A new tool file must be registered** in its source package's `__init__.py`
48
+ (see `src/tools/database/__init__.py` etc.) or it silently never loads.
49
+ - **Errors are structured**, not bare rejections — every guardrail raises a
50
+ `ToolError` subclass with a machine-readable `rule` and a `detail` string
51
+ explaining what to change, so the calling model can self-correct.
52
+
53
+ For the step-by-step of adding a new tool, see
54
+ [`docs/tool-reference.md`](docs/tool-reference.md) → **How To Update A Tool**
55
+ and **Where To Add Tests**.
56
+
57
+ ## Commit style
58
+
59
+ Recent history mostly follows [Conventional Commits](https://www.conventionalcommits.org/)
60
+ (`feat(scope): ...`, `fix(scope): ...`, `test: ...`, `docs: ...`) — please
61
+ follow the same pattern for new commits.
62
+
63
+ ## What to add to the query cookbook
64
+
65
+ If a query pattern proves useful, or a source's field-naming quirk causes
66
+ repeated bad queries from the client model, add/update an entry in
67
+ [`src/data/query_cookbook.yaml`](src/data/query_cookbook.yaml) rather than
68
+ only fixing it in prose — see `CLAUDE.md` for why this matters more than it
69
+ looks like it should.
@@ -0,0 +1,20 @@
1
+ FROM python:3.13-slim
2
+
3
+ COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
4
+
5
+ WORKDIR /app
6
+
7
+ # Install dependencies first (separate layer, cached unless deps change)
8
+ COPY pyproject.toml uv.lock ./
9
+ RUN uv sync --frozen --no-dev --no-install-project
10
+
11
+ # Then install the project itself
12
+ COPY src ./src
13
+ COPY README.md LICENSE ./
14
+ RUN uv sync --frozen --no-dev
15
+
16
+ ENV PATH="/app/.venv/bin:$PATH"
17
+
18
+ EXPOSE 8000
19
+
20
+ CMD ["investigation-server"]
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vignesh
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.