queryview 0.0.2__tar.gz → 0.0.3__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 (141) hide show
  1. {queryview-0.0.2 → queryview-0.0.3}/PKG-INFO +36 -9
  2. {queryview-0.0.2 → queryview-0.0.3}/README.md +34 -8
  3. {queryview-0.0.2 → queryview-0.0.3}/backend/queryview/connect.py +9 -1
  4. {queryview-0.0.2 → queryview-0.0.3}/backend/queryview/main.py +15 -1
  5. queryview-0.0.3/backend/queryview/test_db_path.py +58 -0
  6. queryview-0.0.3/backend/queryview/test_mcp_mount.py +38 -0
  7. {queryview-0.0.2 → queryview-0.0.3}/docker/Dockerfile +4 -3
  8. {queryview-0.0.2 → queryview-0.0.3}/docs/api.md +4 -3
  9. {queryview-0.0.2 → queryview-0.0.3}/docs/remote.md +9 -1
  10. {queryview-0.0.2 → queryview-0.0.3}/pyproject.toml +1 -0
  11. {queryview-0.0.2 → queryview-0.0.3}/uv.lock +2 -0
  12. {queryview-0.0.2 → queryview-0.0.3}/.claude/settings.json +0 -0
  13. {queryview-0.0.2 → queryview-0.0.3}/.github/actions/start-git-daemon/action.yaml +0 -0
  14. {queryview-0.0.2 → queryview-0.0.3}/.github/workflows/ci.yml +0 -0
  15. {queryview-0.0.2 → queryview-0.0.3}/.github/workflows/publish.yaml +0 -0
  16. {queryview-0.0.2 → queryview-0.0.3}/.gitignore +0 -0
  17. {queryview-0.0.2 → queryview-0.0.3}/.mcp.json +0 -0
  18. {queryview-0.0.2 → queryview-0.0.3}/.pre-commit-config.yaml +0 -0
  19. {queryview-0.0.2 → queryview-0.0.3}/CLAUDE.md +0 -0
  20. {queryview-0.0.2 → queryview-0.0.3}/LICENSE +0 -0
  21. {queryview-0.0.2 → queryview-0.0.3}/backend/alembic.ini +0 -0
  22. {queryview-0.0.2 → queryview-0.0.3}/backend/queryview/__init__.py +0 -0
  23. {queryview-0.0.2 → queryview-0.0.3}/backend/queryview/conftest.py +0 -0
  24. {queryview-0.0.2 → queryview-0.0.3}/backend/queryview/dashboard_queries.py +0 -0
  25. {queryview-0.0.2 → queryview-0.0.3}/backend/queryview/dashboards.py +0 -0
  26. {queryview-0.0.2 → queryview-0.0.3}/backend/queryview/drivers/__init__.py +0 -0
  27. {queryview-0.0.2 → queryview-0.0.3}/backend/queryview/drivers/base.py +0 -0
  28. {queryview-0.0.2 → queryview-0.0.3}/backend/queryview/drivers/clickhouse.py +0 -0
  29. {queryview-0.0.2 → queryview-0.0.3}/backend/queryview/drivers/duckdb.py +0 -0
  30. {queryview-0.0.2 → queryview-0.0.3}/backend/queryview/drivers/postgres.py +0 -0
  31. {queryview-0.0.2 → queryview-0.0.3}/backend/queryview/drivers/test_base.py +0 -0
  32. {queryview-0.0.2 → queryview-0.0.3}/backend/queryview/drivers/test_clickhouse.py +0 -0
  33. {queryview-0.0.2 → queryview-0.0.3}/backend/queryview/drivers/test_contract.py +0 -0
  34. {queryview-0.0.2 → queryview-0.0.3}/backend/queryview/drivers/test_duckdb.py +0 -0
  35. {queryview-0.0.2 → queryview-0.0.3}/backend/queryview/drivers/test_postgres.py +0 -0
  36. {queryview-0.0.2 → queryview-0.0.3}/backend/queryview/gitsync.py +0 -0
  37. {queryview-0.0.2 → queryview-0.0.3}/backend/queryview/mcp_server.py +0 -0
  38. {queryview-0.0.2 → queryview-0.0.3}/backend/queryview/migrations/env.py +0 -0
  39. {queryview-0.0.2 → queryview-0.0.3}/backend/queryview/migrations/script.py.mako +0 -0
  40. {queryview-0.0.2 → queryview-0.0.3}/backend/queryview/migrations/versions/9a536b7c0328_initial_schema.py +0 -0
  41. {queryview-0.0.2 → queryview-0.0.3}/backend/queryview/migrations/versions/a1b2c3d4e5f6_connection_config_blob.py +0 -0
  42. {queryview-0.0.2 → queryview-0.0.3}/backend/queryview/migrations/versions/b2c3d4e5f6a7_predefined_presentation.py +0 -0
  43. {queryview-0.0.2 → queryview-0.0.3}/backend/queryview/migrations/versions/c7d8e9f0a1b2_workspaces.py +0 -0
  44. {queryview-0.0.2 → queryview-0.0.3}/backend/queryview/queries.py +0 -0
  45. {queryview-0.0.2 → queryview-0.0.3}/backend/queryview/remote.py +0 -0
  46. {queryview-0.0.2 → queryview-0.0.3}/backend/queryview/static/assets/index-CvnC_D68.js +0 -0
  47. {queryview-0.0.2 → queryview-0.0.3}/backend/queryview/static/assets/index-Qe7bhycG.css +0 -0
  48. {queryview-0.0.2 → queryview-0.0.3}/backend/queryview/static/favicon.svg +0 -0
  49. {queryview-0.0.2 → queryview-0.0.3}/backend/queryview/static/index.html +0 -0
  50. {queryview-0.0.2 → queryview-0.0.3}/backend/queryview/test_api_db.py +0 -0
  51. {queryview-0.0.2 → queryview-0.0.3}/backend/queryview/test_api_export_import.py +0 -0
  52. {queryview-0.0.2 → queryview-0.0.3}/backend/queryview/test_api_gitsync.py +0 -0
  53. {queryview-0.0.2 → queryview-0.0.3}/backend/queryview/test_api_workspaces.py +0 -0
  54. {queryview-0.0.2 → queryview-0.0.3}/backend/queryview/test_connect_flow.py +0 -0
  55. {queryview-0.0.2 → queryview-0.0.3}/backend/queryview/test_connect_store.py +0 -0
  56. {queryview-0.0.2 → queryview-0.0.3}/backend/queryview/test_dashboards.py +0 -0
  57. {queryview-0.0.2 → queryview-0.0.3}/backend/queryview/test_gitsync.py +0 -0
  58. {queryview-0.0.2 → queryview-0.0.3}/backend/queryview/test_main.py +0 -0
  59. {queryview-0.0.2 → queryview-0.0.3}/backend/queryview/test_mcp_gitsync.py +0 -0
  60. {queryview-0.0.2 → queryview-0.0.3}/backend/queryview/test_migrations.py +0 -0
  61. {queryview-0.0.2 → queryview-0.0.3}/backend/queryview/test_queries.py +0 -0
  62. {queryview-0.0.2 → queryview-0.0.3}/backend/queryview/test_remote.py +0 -0
  63. {queryview-0.0.2 → queryview-0.0.3}/backend/queryview/test_validation.py +0 -0
  64. {queryview-0.0.2 → queryview-0.0.3}/backend/queryview/test_workspaces.py +0 -0
  65. {queryview-0.0.2 → queryview-0.0.3}/backend/queryview/test_yamlio.py +0 -0
  66. {queryview-0.0.2 → queryview-0.0.3}/backend/queryview/validation.py +0 -0
  67. {queryview-0.0.2 → queryview-0.0.3}/backend/queryview/workspaces.py +0 -0
  68. {queryview-0.0.2 → queryview-0.0.3}/backend/queryview/yamlio.py +0 -0
  69. {queryview-0.0.2 → queryview-0.0.3}/docs/connect.md +0 -0
  70. {queryview-0.0.2 → queryview-0.0.3}/docs/dashboard.md +0 -0
  71. {queryview-0.0.2 → queryview-0.0.3}/docs/explorer.md +0 -0
  72. {queryview-0.0.2 → queryview-0.0.3}/docs/export-import.md +0 -0
  73. {queryview-0.0.2 → queryview-0.0.3}/docs/future.md +0 -0
  74. {queryview-0.0.2 → queryview-0.0.3}/docs/gitsync.md +0 -0
  75. {queryview-0.0.2 → queryview-0.0.3}/docs/query.md +0 -0
  76. {queryview-0.0.2 → queryview-0.0.3}/docs/queryview.md +0 -0
  77. {queryview-0.0.2 → queryview-0.0.3}/docs/workspace.md +0 -0
  78. {queryview-0.0.2 → queryview-0.0.3}/e2e/conftest.py +0 -0
  79. {queryview-0.0.2 → queryview-0.0.3}/e2e/test_app.py +0 -0
  80. {queryview-0.0.2 → queryview-0.0.3}/e2e/test_dashboard.py +0 -0
  81. {queryview-0.0.2 → queryview-0.0.3}/e2e/test_drivers.py +0 -0
  82. {queryview-0.0.2 → queryview-0.0.3}/e2e/test_explorer.py +0 -0
  83. {queryview-0.0.2 → queryview-0.0.3}/e2e/test_export_import.py +0 -0
  84. {queryview-0.0.2 → queryview-0.0.3}/e2e/test_gitsync.py +0 -0
  85. {queryview-0.0.2 → queryview-0.0.3}/e2e/test_query.py +0 -0
  86. {queryview-0.0.2 → queryview-0.0.3}/e2e/test_remote.py +0 -0
  87. {queryview-0.0.2 → queryview-0.0.3}/e2e/test_workspaces.py +0 -0
  88. {queryview-0.0.2 → queryview-0.0.3}/frontend/.gitignore +0 -0
  89. {queryview-0.0.2 → queryview-0.0.3}/frontend/README.md +0 -0
  90. {queryview-0.0.2 → queryview-0.0.3}/frontend/eslint.config.js +0 -0
  91. {queryview-0.0.2 → queryview-0.0.3}/frontend/index.html +0 -0
  92. {queryview-0.0.2 → queryview-0.0.3}/frontend/package.json +0 -0
  93. {queryview-0.0.2 → queryview-0.0.3}/frontend/public/favicon.svg +0 -0
  94. {queryview-0.0.2 → queryview-0.0.3}/frontend/src/App.tsx +0 -0
  95. {queryview-0.0.2 → queryview-0.0.3}/frontend/src/CellViewModal.tsx +0 -0
  96. {queryview-0.0.2 → queryview-0.0.3}/frontend/src/ComplexCell.tsx +0 -0
  97. {queryview-0.0.2 → queryview-0.0.3}/frontend/src/DashboardView.tsx +0 -0
  98. {queryview-0.0.2 → queryview-0.0.3}/frontend/src/ExplorerView.tsx +0 -0
  99. {queryview-0.0.2 → queryview-0.0.3}/frontend/src/ExportImportControls.tsx +0 -0
  100. {queryview-0.0.2 → queryview-0.0.3}/frontend/src/FieldPickers.tsx +0 -0
  101. {queryview-0.0.2 → queryview-0.0.3}/frontend/src/GitSyncControls.tsx +0 -0
  102. {queryview-0.0.2 → queryview-0.0.3}/frontend/src/QueryView.tsx +0 -0
  103. {queryview-0.0.2 → queryview-0.0.3}/frontend/src/ResultsTable.tsx +0 -0
  104. {queryview-0.0.2 → queryview-0.0.3}/frontend/src/Toast.tsx +0 -0
  105. {queryview-0.0.2 → queryview-0.0.3}/frontend/src/WorkspaceSwitcher.tsx +0 -0
  106. {queryview-0.0.2 → queryview-0.0.3}/frontend/src/cellView.test.ts +0 -0
  107. {queryview-0.0.2 → queryview-0.0.3}/frontend/src/cellView.ts +0 -0
  108. {queryview-0.0.2 → queryview-0.0.3}/frontend/src/compactNumber.test.ts +0 -0
  109. {queryview-0.0.2 → queryview-0.0.3}/frontend/src/compactNumber.ts +0 -0
  110. {queryview-0.0.2 → queryview-0.0.3}/frontend/src/complexCellParsing.test.ts +0 -0
  111. {queryview-0.0.2 → queryview-0.0.3}/frontend/src/complexCellParsing.ts +0 -0
  112. {queryview-0.0.2 → queryview-0.0.3}/frontend/src/drivers.ts +0 -0
  113. {queryview-0.0.2 → queryview-0.0.3}/frontend/src/gitsync.test.ts +0 -0
  114. {queryview-0.0.2 → queryview-0.0.3}/frontend/src/gitsync.ts +0 -0
  115. {queryview-0.0.2 → queryview-0.0.3}/frontend/src/index.css +0 -0
  116. {queryview-0.0.2 → queryview-0.0.3}/frontend/src/main.tsx +0 -0
  117. {queryview-0.0.2 → queryview-0.0.3}/frontend/src/presentation.test.ts +0 -0
  118. {queryview-0.0.2 → queryview-0.0.3}/frontend/src/presentation.ts +0 -0
  119. {queryview-0.0.2 → queryview-0.0.3}/frontend/src/promptSuggestions.test.ts +0 -0
  120. {queryview-0.0.2 → queryview-0.0.3}/frontend/src/promptSuggestions.ts +0 -0
  121. {queryview-0.0.2 → queryview-0.0.3}/frontend/src/queryParams.test.ts +0 -0
  122. {queryview-0.0.2 → queryview-0.0.3}/frontend/src/queryParams.ts +0 -0
  123. {queryview-0.0.2 → queryview-0.0.3}/frontend/src/sessionLock.test.ts +0 -0
  124. {queryview-0.0.2 → queryview-0.0.3}/frontend/src/sessionLock.ts +0 -0
  125. {queryview-0.0.2 → queryview-0.0.3}/frontend/src/tsv.ts +0 -0
  126. {queryview-0.0.2 → queryview-0.0.3}/frontend/src/workspace.test.ts +0 -0
  127. {queryview-0.0.2 → queryview-0.0.3}/frontend/src/workspace.ts +0 -0
  128. {queryview-0.0.2 → queryview-0.0.3}/frontend/src/yamlio.test.ts +0 -0
  129. {queryview-0.0.2 → queryview-0.0.3}/frontend/src/yamlio.ts +0 -0
  130. {queryview-0.0.2 → queryview-0.0.3}/frontend/tsconfig.app.json +0 -0
  131. {queryview-0.0.2 → queryview-0.0.3}/frontend/tsconfig.json +0 -0
  132. {queryview-0.0.2 → queryview-0.0.3}/frontend/tsconfig.node.json +0 -0
  133. {queryview-0.0.2 → queryview-0.0.3}/frontend/vite.config.ts +0 -0
  134. {queryview-0.0.2 → queryview-0.0.3}/package-lock.json +0 -0
  135. {queryview-0.0.2 → queryview-0.0.3}/package.json +0 -0
  136. {queryview-0.0.2 → queryview-0.0.3}/pyrightconfig.json +0 -0
  137. {queryview-0.0.2 → queryview-0.0.3}/scripts/dev.sh +0 -0
  138. {queryview-0.0.2 → queryview-0.0.3}/scripts/setup.sh +0 -0
  139. {queryview-0.0.2 → queryview-0.0.3}/scripts/setup_browser.sh +0 -0
  140. {queryview-0.0.2 → queryview-0.0.3}/scripts/setup_clickhouse.sh +0 -0
  141. {queryview-0.0.2 → queryview-0.0.3}/scripts/setup_postgres.sh +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: queryview
3
- Version: 0.0.2
3
+ Version: 0.0.3
4
4
  Summary: QueryView: FastAPI + SQLModel backend and React SPA over the ClickHouse HTTP interface
5
5
  Project-URL: Homepage, https://github.com/kolodkin/queryview
6
6
  Project-URL: Repository, https://github.com/kolodkin/queryview
@@ -16,6 +16,7 @@ Requires-Dist: duckdb>=1.0
16
16
  Requires-Dist: fastapi>=0.110
17
17
  Requires-Dist: httpx>=0.27
18
18
  Requires-Dist: mcp<2,>=1.9
19
+ Requires-Dist: platformdirs>=4
19
20
  Requires-Dist: pyyaml>=6
20
21
  Requires-Dist: sqlalchemy[asyncio]>=2.0
21
22
  Requires-Dist: sqlmodel>=0.0.16
@@ -46,7 +47,9 @@ docker run -p 8000:8000 ghcr.io/kolodkin/queryview:latest
46
47
  ```
47
48
 
48
49
  To serve on a different host port, remap it (the container keeps listening on
49
- 8000, which its healthcheck probes): `docker run -p 9000:8000 ...`.
50
+ 8000, which its healthcheck probes): `docker run -p 9000:8000 ...`. QueryView
51
+ expects to be reached from localhost only, so prefer binding the published port
52
+ to loopback: `docker run -p 127.0.0.1:8000:8000 ...`.
50
53
 
51
54
  State (the SQLite DB and its encryption key) lives in `/home/queryview`; mount a
52
55
  volume there to persist it across containers:
@@ -154,14 +157,34 @@ An installed wheel serves the bundled UI by default — see
154
157
  ## MCP server
155
158
 
156
159
  The backend mounts a FastMCP server (Streamable HTTP) at
157
- `http://localhost:8000/mcp`. There is nothing extra to start — it runs inside
158
- the server process (`uvx queryview`, `npm run dev`, ...). Hook up an MCP client,
159
- e.g.:
160
+ `http://localhost:8000/mcp/`. There is nothing extra to start — it runs inside
161
+ the server process (`uvx queryview`, `npm run dev`, ...). Registering the client
162
+ is a separate, one-time step on the machine running the agent: an HTTP MCP
163
+ server can't install itself into someone else's client.
160
164
 
161
165
  ```bash
162
- claude mcp add --transport http queryview http://localhost:8000/mcp
166
+ claude mcp add --transport http queryview http://localhost:8000/mcp/
163
167
  ```
164
168
 
169
+ Three things to get right:
170
+
171
+ - **Prefer the trailing slash.** The mount serves `/mcp/`. The slashless
172
+ `/mcp` also works — it 307-redirects — but registering the canonical path
173
+ skips a round trip on every call.
174
+ - **Match the port.** The URL must point at the port QueryView actually
175
+ listens on — `--port 9000` means `http://localhost:9000/mcp/`, and
176
+ `docker run -p 9000:8000` means the *host* port, `9000`, not the container's
177
+ `8000`.
178
+ - **Start QueryView first.** The client dials this URL when it starts; if
179
+ nothing is listening it reports a connection error and stays failed until you
180
+ reconnect it.
181
+
182
+ QueryView is a **local, single-user tool**: it assumes it is reachable only from
183
+ localhost. `/mcp/` is unauthenticated, and its tools can query every configured
184
+ connection and rewrite workspace git state, so don't publish the port. Bind the
185
+ container to loopback — `docker run -p 127.0.0.1:8000:8000 ...` — since a plain
186
+ `-p 8000:8000` listens on all interfaces.
187
+
165
188
  Tools: `run_query` (read-only SQL, rows returned to the agent), `push_query`
166
189
  and `push_dashboard` (fill a live browser session), `list_queries` /
167
190
  `list_dashboards`, and `git_store` / `git_history` / `git_restore` (workspace
@@ -178,6 +201,10 @@ The single-page prompt UI is described in [docs/queryview.md](docs/queryview.md)
178
201
  connecting (`new <type>` / `connect <name>`), SQLite persistence, and session
179
202
  auto-connect are specified in [docs/connect.md](docs/connect.md).
180
203
 
181
- Connections are stored in SQLite (`backend/queryview.db`, override with
182
- `DB_PATH`); the backend writes that file and a local password-encryption key
183
- (`backend/queryview.db.key`, override with `DB_KEY_PATH`).
204
+ Connections are stored in SQLite. The default location is the platform's
205
+ user-data directory — `$XDG_DATA_HOME/queryview/queryview.db` (i.e.
206
+ `~/.local/share/queryview/`) on Linux, `~/Library/Application Support/queryview/`
207
+ on macOS, `%LOCALAPPDATA%\queryview\` on Windows — overridable with `DB_PATH`.
208
+ Alongside it the backend writes a local password-encryption key
209
+ (`<db>.key`, override with `DB_KEY_PATH`) and the workspace git-sync clones
210
+ (`<db>.gitsync/`, override with `GIT_SYNC_DIR`).
@@ -22,7 +22,9 @@ docker run -p 8000:8000 ghcr.io/kolodkin/queryview:latest
22
22
  ```
23
23
 
24
24
  To serve on a different host port, remap it (the container keeps listening on
25
- 8000, which its healthcheck probes): `docker run -p 9000:8000 ...`.
25
+ 8000, which its healthcheck probes): `docker run -p 9000:8000 ...`. QueryView
26
+ expects to be reached from localhost only, so prefer binding the published port
27
+ to loopback: `docker run -p 127.0.0.1:8000:8000 ...`.
26
28
 
27
29
  State (the SQLite DB and its encryption key) lives in `/home/queryview`; mount a
28
30
  volume there to persist it across containers:
@@ -130,14 +132,34 @@ An installed wheel serves the bundled UI by default — see
130
132
  ## MCP server
131
133
 
132
134
  The backend mounts a FastMCP server (Streamable HTTP) at
133
- `http://localhost:8000/mcp`. There is nothing extra to start — it runs inside
134
- the server process (`uvx queryview`, `npm run dev`, ...). Hook up an MCP client,
135
- e.g.:
135
+ `http://localhost:8000/mcp/`. There is nothing extra to start — it runs inside
136
+ the server process (`uvx queryview`, `npm run dev`, ...). Registering the client
137
+ is a separate, one-time step on the machine running the agent: an HTTP MCP
138
+ server can't install itself into someone else's client.
136
139
 
137
140
  ```bash
138
- claude mcp add --transport http queryview http://localhost:8000/mcp
141
+ claude mcp add --transport http queryview http://localhost:8000/mcp/
139
142
  ```
140
143
 
144
+ Three things to get right:
145
+
146
+ - **Prefer the trailing slash.** The mount serves `/mcp/`. The slashless
147
+ `/mcp` also works — it 307-redirects — but registering the canonical path
148
+ skips a round trip on every call.
149
+ - **Match the port.** The URL must point at the port QueryView actually
150
+ listens on — `--port 9000` means `http://localhost:9000/mcp/`, and
151
+ `docker run -p 9000:8000` means the *host* port, `9000`, not the container's
152
+ `8000`.
153
+ - **Start QueryView first.** The client dials this URL when it starts; if
154
+ nothing is listening it reports a connection error and stays failed until you
155
+ reconnect it.
156
+
157
+ QueryView is a **local, single-user tool**: it assumes it is reachable only from
158
+ localhost. `/mcp/` is unauthenticated, and its tools can query every configured
159
+ connection and rewrite workspace git state, so don't publish the port. Bind the
160
+ container to loopback — `docker run -p 127.0.0.1:8000:8000 ...` — since a plain
161
+ `-p 8000:8000` listens on all interfaces.
162
+
141
163
  Tools: `run_query` (read-only SQL, rows returned to the agent), `push_query`
142
164
  and `push_dashboard` (fill a live browser session), `list_queries` /
143
165
  `list_dashboards`, and `git_store` / `git_history` / `git_restore` (workspace
@@ -154,6 +176,10 @@ The single-page prompt UI is described in [docs/queryview.md](docs/queryview.md)
154
176
  connecting (`new <type>` / `connect <name>`), SQLite persistence, and session
155
177
  auto-connect are specified in [docs/connect.md](docs/connect.md).
156
178
 
157
- Connections are stored in SQLite (`backend/queryview.db`, override with
158
- `DB_PATH`); the backend writes that file and a local password-encryption key
159
- (`backend/queryview.db.key`, override with `DB_KEY_PATH`).
179
+ Connections are stored in SQLite. The default location is the platform's
180
+ user-data directory — `$XDG_DATA_HOME/queryview/queryview.db` (i.e.
181
+ `~/.local/share/queryview/`) on Linux, `~/Library/Application Support/queryview/`
182
+ on macOS, `%LOCALAPPDATA%\queryview\` on Windows — overridable with `DB_PATH`.
183
+ Alongside it the backend writes a local password-encryption key
184
+ (`<db>.key`, override with `DB_KEY_PATH`) and the workspace git-sync clones
185
+ (`<db>.gitsync/`, override with `GIT_SYNC_DIR`).
@@ -14,6 +14,7 @@ from dataclasses import dataclass
14
14
  from pathlib import Path
15
15
  from typing import TYPE_CHECKING, Any, ClassVar
16
16
 
17
+ import platformdirs
17
18
  from cryptography.hazmat.primitives.ciphers.aead import AESGCM
18
19
  from sqlalchemy.ext.asyncio import create_async_engine
19
20
  from sqlmodel import Field, SQLModel, col, select
@@ -40,10 +41,14 @@ class Connection(SQLModel, table=True):
40
41
 
41
42
 
42
43
  def _db_path() -> Path:
44
+ """The SQLite store's path: DB_PATH, else the platform's user-data dir
45
+ (`$XDG_DATA_HOME/queryview` on Linux, `Application Support` on macOS,
46
+ `%LOCALAPPDATA%` on Windows). Deliberately not package-relative — that put
47
+ the DB in site-packages, which is uv's disposable cache under `uvx`."""
43
48
  env = os.environ.get("DB_PATH")
44
49
  if env:
45
50
  return Path(env)
46
- return Path(__file__).resolve().parent.parent / "queryview.db"
51
+ return Path(platformdirs.user_data_dir("queryview")) / "queryview.db"
47
52
 
48
53
 
49
54
  _engine = None
@@ -80,6 +85,9 @@ async def _ensure_schema() -> None:
80
85
  return
81
86
  from alembic import command
82
87
 
88
+ # SQLite won't create a missing parent, and the user-data dir doesn't exist
89
+ # on a fresh install; the key file and git-sync clones land here too.
90
+ _db_path().parent.mkdir(parents=True, exist_ok=True)
83
91
  command.upgrade(_alembic_config(), "head")
84
92
  _schema_ready = True
85
93
 
@@ -11,7 +11,13 @@ from pathlib import Path
11
11
  from typing import Any
12
12
 
13
13
  from fastapi import FastAPI, Request
14
- from fastapi.responses import FileResponse, JSONResponse, PlainTextResponse, StreamingResponse
14
+ from fastapi.responses import (
15
+ FileResponse,
16
+ JSONResponse,
17
+ PlainTextResponse,
18
+ RedirectResponse,
19
+ StreamingResponse,
20
+ )
15
21
 
16
22
  from . import gitsync, remote, workspaces, yamlio
17
23
  from .connect import (
@@ -100,6 +106,14 @@ app = FastAPI(title="queryview-backend", lifespan=lifespan)
100
106
  app.mount("/mcp", mcp.streamable_http_app())
101
107
 
102
108
 
109
+ # The GET-only catch-all below suppresses Starlette's own /mcp -> /mcp/ redirect
110
+ # (see test_mcp_mount.py), so issue it explicitly. 307 preserves method and
111
+ # body; Streamable HTTP uses POST, GET and DELETE.
112
+ @app.api_route("/mcp", methods=["GET", "POST", "DELETE"], include_in_schema=False)
113
+ async def mcp_slash_redirect() -> RedirectResponse:
114
+ return RedirectResponse("/mcp/", status_code=307)
115
+
116
+
103
117
  @app.middleware("http")
104
118
  async def session_cookie(request: Request, call_next):
105
119
  sid = request.cookies.get("qv_session")
@@ -0,0 +1,58 @@
1
+ """Where the SQLite store lives.
2
+
3
+ The default used to be package-relative (`Path(__file__).parent.parent`), which
4
+ meant `backend/queryview.db` in a checkout but `<site-packages>/queryview.db`
5
+ once the wheel shipped — under `uvx`, that is inside uv's *cache*, where a
6
+ `uv cache clean` or a version bump silently takes the DB with it. The default
7
+ is now the platform's user-data directory.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from pathlib import Path
13
+
14
+ import platformdirs
15
+
16
+ import queryview
17
+ from queryview.connect import _db_path, _key_path
18
+
19
+
20
+ def test_db_path_defaults_to_user_data_dir(monkeypatch):
21
+ monkeypatch.delenv("DB_PATH", raising=False)
22
+ expected = Path(platformdirs.user_data_dir("queryview")) / "queryview.db"
23
+ assert _db_path() == expected
24
+
25
+
26
+ def test_db_path_is_not_inside_the_installed_package(monkeypatch):
27
+ """The regression: a package-relative default puts user data in
28
+ site-packages (and under uvx, in a disposable cache directory)."""
29
+ monkeypatch.delenv("DB_PATH", raising=False)
30
+ package_root = Path(queryview.__file__).resolve().parent.parent
31
+ assert package_root not in _db_path().resolve().parents
32
+
33
+
34
+ def test_db_path_env_var_still_wins(monkeypatch, tmp_path):
35
+ monkeypatch.setenv("DB_PATH", str(tmp_path / "custom.db"))
36
+ assert _db_path() == tmp_path / "custom.db"
37
+ # The key file follows the DB unless DB_KEY_PATH says otherwise.
38
+ monkeypatch.delenv("DB_KEY_PATH", raising=False)
39
+ assert _key_path() == tmp_path / "custom.db.key"
40
+
41
+
42
+ def test_ensure_schema_creates_a_missing_data_dir(monkeypatch, tmp_path):
43
+ """The user-data dir does not exist on a fresh install, so startup has to
44
+ create it — SQLite will not create a missing parent directory."""
45
+ import asyncio
46
+
47
+ import queryview.connect as c
48
+
49
+ target = tmp_path / "fresh" / "nested" / "queryview.db"
50
+ monkeypatch.setenv("DB_PATH", str(target))
51
+ monkeypatch.setenv("DB_KEY_PATH", str(target.with_suffix(".db.key")))
52
+ monkeypatch.setattr(c, "_engine", None)
53
+ monkeypatch.setattr(c, "_schema_ready", False)
54
+ assert not target.parent.exists()
55
+
56
+ asyncio.run(c._ensure_schema())
57
+
58
+ assert target.parent.is_dir()
@@ -0,0 +1,38 @@
1
+ """The MCP mount's slashless path.
2
+
3
+ The FastMCP app is mounted at `/mcp` with its own path set to `/`, so the
4
+ canonical endpoint is `/mcp/`. Starlette would normally redirect `/mcp` there,
5
+ but the SPA catch-all (`@app.get("/{full_path:path}")`) matches the slashless
6
+ path on GET only, which Starlette scores as a partial match — that suppresses
7
+ the redirect and answers `405 allow: GET` instead, pointing a client at a path
8
+ the MCP server never serves. These tests pin the redirect so the catch-all
9
+ can't swallow it again.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import pytest
15
+ from fastapi.testclient import TestClient
16
+
17
+ from queryview.main import app
18
+
19
+ # Streamable HTTP uses POST (messages), GET (the SSE stream) and DELETE
20
+ # (session termination) — all three must reach the mount.
21
+ MCP_METHODS = ["POST", "GET", "DELETE"]
22
+
23
+
24
+ @pytest.mark.parametrize("method", MCP_METHODS)
25
+ def test_slashless_mcp_redirects_to_canonical_path(method):
26
+ c = TestClient(app, follow_redirects=False)
27
+ r = c.request(method, "/mcp")
28
+ assert r.status_code == 307, f"{method} /mcp returned {r.status_code}, not a redirect"
29
+ assert r.headers["location"].endswith("/mcp/")
30
+
31
+
32
+ @pytest.mark.parametrize("method", MCP_METHODS)
33
+ def test_canonical_mcp_path_is_not_redirected(method):
34
+ """The redirect must not shadow the mount itself."""
35
+ c = TestClient(app, follow_redirects=False, raise_server_exceptions=False)
36
+ r = c.request(method, "/mcp/")
37
+ assert r.status_code != 307
38
+ assert "location" not in r.headers
@@ -18,9 +18,10 @@ RUN useradd --create-home --uid 1000 queryview
18
18
  WORKDIR /home/queryview
19
19
  USER queryview
20
20
 
21
- # The default DB path sits inside site-packages, which the non-root user can't
22
- # write; keep the SQLite file (and its sibling .key file) in the home dir so a
23
- # volume mounted at /home/queryview persists all state.
21
+ # The default is the user-data dir (/home/queryview/.local/share/queryview);
22
+ # pin the SQLite file (and its sibling .key file and git-sync clones) to the top
23
+ # of the home dir instead, so a volume mounted at /home/queryview persists all
24
+ # state at a flat, documented path.
24
25
  ENV DB_PATH=/home/queryview/queryview.db
25
26
 
26
27
  EXPOSE 8000
@@ -30,9 +30,10 @@ independently. Saved connections are shared (SQLite).
30
30
  | GET | `/api/dashboards` | `?workspace=` | List a workspace's dashboards (no payload): `{dashboards:[{name, connection, updated_at}]}`, ordered by name. |
31
31
  | GET | `/api/dashboards/{name}` | `?workspace=` | A saved dashboard `{name, connection, html, queries}` (`queries` parsed to a dict), or `404 {error:"not found"}`. |
32
32
 
33
- **MCP:** a FastMCP server is mounted at `/mcp` (Streamable HTTP) exposing
34
- `push_query` (push SQL to a session's query panel) and `upsert_dashboard`
35
- (persist a dashboard and push it to a session). Both delegate to the in-process
33
+ **MCP:** a FastMCP server is mounted at `/mcp/` (Streamable HTTP) exposing
34
+ `push_query` (push SQL to a session's query panel), `push_dashboard` (push a
35
+ dashboard draft to a session), `run_query`, `list_queries` / `list_dashboards`
36
+ and `git_store` / `git_history` / `git_restore`. They delegate to the in-process
36
37
  hubs the matching REST endpoints call. See [remote.md](./remote.md) and
37
38
  [dashboard.md](./dashboard.md).
38
39
 
@@ -16,9 +16,17 @@ session's **id** and a copyable command, e.g.:
16
16
  Turning the toggle off (or closing the tab) disarms the session immediately —
17
17
  pushes to its id are then reported as not delivered.
18
18
 
19
+ ## Connecting a client
20
+
21
+ The server runs inside the backend process — nothing extra to start — but the
22
+ client registration is a separate one-time step: point it at
23
+ `http://localhost:8000/mcp/` (the slashless `/mcp` redirects there; match the
24
+ port QueryView listens on, and start it first). See
25
+ [the README](../README.md#mcp-server).
26
+
19
27
  ## MCP tools
20
28
 
21
- The backend mounts a FastMCP server (Streamable HTTP) at `/mcp` exposing four
29
+ The backend mounts a FastMCP server (Streamable HTTP) at `/mcp/` exposing four
22
30
  tools:
23
31
 
24
32
  - `push_query(session_id, query, limit?=100, offset?=0, order_by?, fields?, cell_view?, name?)` —
@@ -18,6 +18,7 @@ dependencies = [
18
18
  "asyncpg>=0.29",
19
19
  "duckdb>=1.0",
20
20
  "pyyaml>=6",
21
+ "platformdirs>=4",
21
22
  ]
22
23
 
23
24
  [project.urls]
@@ -1185,6 +1185,7 @@ dependencies = [
1185
1185
  { name = "fastapi" },
1186
1186
  { name = "httpx" },
1187
1187
  { name = "mcp" },
1188
+ { name = "platformdirs" },
1188
1189
  { name = "pyyaml" },
1189
1190
  { name = "sqlalchemy", extra = ["asyncio"] },
1190
1191
  { name = "sqlmodel" },
@@ -1214,6 +1215,7 @@ requires-dist = [
1214
1215
  { name = "fastapi", specifier = ">=0.110" },
1215
1216
  { name = "httpx", specifier = ">=0.27" },
1216
1217
  { name = "mcp", specifier = ">=1.9,<2" },
1218
+ { name = "platformdirs", specifier = ">=4" },
1217
1219
  { name = "pyyaml", specifier = ">=6" },
1218
1220
  { name = "sqlalchemy", extras = ["asyncio"], specifier = ">=2.0" },
1219
1221
  { name = "sqlmodel", specifier = ">=0.0.16" },
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