adapt-server 0.2.2__tar.gz → 0.2.4__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.
- {adapt_server-0.2.2 → adapt_server-0.2.4}/PKG-INFO +76 -2
- {adapt_server-0.2.2 → adapt_server-0.2.4}/README.md +73 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/__init__.py +1 -1
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/admin/api_keys.py +15 -33
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/admin/locks.py +5 -5
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/admin/ui.py +1 -1
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/admin/users.py +5 -7
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/api_keys.py +51 -1
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/app.py +151 -65
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/audit.py +1 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/auth/dependencies.py +3 -2
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/auth/routes.py +17 -47
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/cli.py +8 -1
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/commands/check.py +16 -0
- adapt_server-0.2.4/adapt/commands/reindex.py +44 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/config.py +67 -46
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/discovery.py +1 -15
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/locks.py +1 -0
- adapt_server-0.2.4/adapt/mcp.py +162 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/permissions.py +26 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/plugins/base.py +25 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/plugins/dataset_plugin.py +46 -18
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/plugins/html_plugin.py +59 -1
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/plugins/markdown_plugin.py +50 -3
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/plugins/media_plugin.py +20 -1
- adapt_server-0.2.4/adapt/routes.py +127 -0
- adapt_server-0.2.4/adapt/routes_search.py +173 -0
- adapt_server-0.2.4/adapt/search.py +399 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/security.py +1 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/storage.py +1 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/templates/admin_base.html +7 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/templates/base.html +10 -0
- adapt_server-0.2.4/adapt/templates/search_results.html +57 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/utils/query.py +1 -1
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt_server.egg-info/PKG-INFO +76 -2
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt_server.egg-info/SOURCES.txt +10 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt_server.egg-info/requires.txt +2 -1
- {adapt_server-0.2.2 → adapt_server-0.2.4}/pyproject.toml +3 -2
- {adapt_server-0.2.2 → adapt_server-0.2.4}/tests/test_admin.py +18 -0
- adapt_server-0.2.4/tests/test_cache.py +113 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/tests/test_config.py +2 -0
- adapt_server-0.2.4/tests/test_dependencies.py +214 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/tests/test_integration.py +24 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/tests/test_markdown_plugin.py +3 -1
- adapt_server-0.2.4/tests/test_mcp.py +343 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/tests/test_readonly.py +6 -8
- adapt_server-0.2.4/tests/test_routes.py +28 -0
- adapt_server-0.2.4/tests/test_search.py +539 -0
- adapt_server-0.2.2/adapt/routes.py +0 -68
- {adapt_server-0.2.2 → adapt_server-0.2.4}/LICENSE +0 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/__main__.py +0 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/admin/__init__.py +0 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/admin/audit_logs.py +0 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/admin/cache.py +0 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/admin/groups.py +0 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/admin/models.py +0 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/admin/permissions.py +0 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/admin/resources.py +0 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/auth/__init__.py +0 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/auth/password.py +0 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/auth/session.py +0 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/cache.py +0 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/commands/__init__.py +0 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/commands/addsuperuser.py +0 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/commands/admin/__init__.py +0 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/commands/admin/add_to_group.py +0 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/commands/admin/create_group.py +0 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/commands/admin/create_permissions.py +0 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/commands/admin/create_user.py +0 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/commands/admin/delete_group.py +0 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/commands/admin/delete_user.py +0 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/commands/admin/list_groups.py +0 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/commands/admin/list_resources.py +0 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/commands/admin/list_users.py +0 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/commands/admin/remove_from_group.py +0 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/commands/list_endpoints.py +0 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/commands/serve.py +0 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/models.py +0 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/plugins/__init__.py +0 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/plugins/csv_plugin.py +0 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/plugins/excel_plugin.py +0 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/plugins/parquet_plugin.py +0 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/plugins/python_plugin.py +0 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/security_urls.py +0 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/static/admin/app.js +0 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/static/admin/style.css +0 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/templates/admin/index.html +0 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/templates/datatable.html +0 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/templates/landing.html +0 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/templates/login.html +0 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/templates/media_gallery.html +0 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/templates/media_player.html +0 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/templates/profile.html +0 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/utils/__init__.py +0 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt_server.egg-info/dependency_links.txt +0 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt_server.egg-info/entry_points.txt +0 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt_server.egg-info/top_level.txt +0 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/setup.cfg +0 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/tests/test_auth.py +0 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/tests/test_cli_integration.py +0 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/tests/test_constraints.py +0 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/tests/test_csv_plugin.py +0 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/tests/test_dataset_plugin.py +0 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/tests/test_discovery.py +0 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/tests/test_html_plugin.py +0 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/tests/test_locks.py +0 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/tests/test_on_delete.py +0 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/tests/test_parquet_plugin.py +0 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/tests/test_phase3.py +0 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/tests/test_plugin_interface.py +0 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/tests/test_python_plugin.py +0 -0
- {adapt_server-0.2.2 → adapt_server-0.2.4}/tests/test_security_urls.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: adapt-server
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: Adaptive file-backed FastAPI server that turns datasets into CRUD APIs and UIs.
|
|
5
5
|
Author-email: notesofcliff <notesofcliff@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -21,7 +21,7 @@ Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
|
|
|
21
21
|
Requires-Python: >=3.11
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
23
|
License-File: LICENSE
|
|
24
|
-
Requires-Dist: fastapi
|
|
24
|
+
Requires-Dist: fastapi>=0.115
|
|
25
25
|
Requires-Dist: uvicorn
|
|
26
26
|
Requires-Dist: sqlmodel
|
|
27
27
|
Requires-Dist: pendulum
|
|
@@ -36,6 +36,7 @@ Requires-Dist: pillow
|
|
|
36
36
|
Requires-Dist: python-json-logger
|
|
37
37
|
Requires-Dist: pandas
|
|
38
38
|
Requires-Dist: fastparquet
|
|
39
|
+
Requires-Dist: mcp<2,>=1.28
|
|
39
40
|
Provides-Extra: dev
|
|
40
41
|
Requires-Dist: pytest; extra == "dev"
|
|
41
42
|
Requires-Dist: httpx; extra == "dev"
|
|
@@ -51,6 +52,8 @@ Adapt is a FastAPI server that turns files in a directory into APIs and UIs.
|
|
|
51
52
|
- Markdown/HTML become browsable pages
|
|
52
53
|
- Media files become streaming endpoints and player/gallery UIs
|
|
53
54
|
- Python files can register custom routers
|
|
55
|
+
- Everything is searchable in one place via full-text `/search`
|
|
56
|
+
- Everything is reachable by agentic tools via an MCP server at `/mcp`
|
|
54
57
|
|
|
55
58
|
## Quick Start
|
|
56
59
|
|
|
@@ -81,6 +84,8 @@ Useful URLs:
|
|
|
81
84
|
- `/api/<resource>` resource API
|
|
82
85
|
- `/ui/<resource>` resource UI
|
|
83
86
|
- `/schema/<resource>` resource schema
|
|
87
|
+
- `/search` full-text search across every resource you can read
|
|
88
|
+
- `/mcp` MCP server for agentic tools (see [MCP Interface](#mcp-interface) below)
|
|
84
89
|
|
|
85
90
|
## What Adapt Generates
|
|
86
91
|
|
|
@@ -135,6 +140,73 @@ This reflects the current implementation in the codebase.
|
|
|
135
140
|
- Caching with invalidation on mutations
|
|
136
141
|
- Built-in admin UI for users/groups/permissions/locks/cache/api keys/audit logs
|
|
137
142
|
- Plugin architecture with companion overrides in `.adapt/`
|
|
143
|
+
- Permission-filtered full-text search across every resource type
|
|
144
|
+
- MCP server for agentic tool access, mounted alongside the REST API
|
|
145
|
+
|
|
146
|
+
## Full-Text Search
|
|
147
|
+
|
|
148
|
+
`GET /search?q=<query>` searches datasets, Markdown, HTML, and media metadata
|
|
149
|
+
in one ranked list, filtered to what the caller may read — a query term that
|
|
150
|
+
matches a resource you can't see never shows up, and never leaks via the
|
|
151
|
+
result count either.
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
curl -H "X-API-Key: <key>" "http://localhost:8000/search?q=parental+leave"
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
The index refreshes incrementally on startup (`search_on_startup`, default
|
|
158
|
+
`true`) and can be rebuilt on demand with `adapt reindex <root>`. See the
|
|
159
|
+
[API Reference](docs/manual/api_reference.md#search-endpoint) for query
|
|
160
|
+
parameters and result shape.
|
|
161
|
+
|
|
162
|
+
## MCP Interface
|
|
163
|
+
|
|
164
|
+
Adapt mounts a [Model Context Protocol](https://modelcontextprotocol.io)
|
|
165
|
+
server at `/mcp`, on the same host/port as everything else, exposing five
|
|
166
|
+
tools that wrap the same permission checks and plugin methods as the REST
|
|
167
|
+
API — `list_resources`, `get_schema`, `read_resource`, `write_resource`, and
|
|
168
|
+
`search`. There's no separate process, no separate API surface, and no
|
|
169
|
+
extra permission model to maintain.
|
|
170
|
+
|
|
171
|
+
Minimal walkthrough — create an account for the agent, grant it read access,
|
|
172
|
+
mint an API key, and connect a client:
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
adapt addsuperuser /path/to/docroot --username admin
|
|
176
|
+
adapt serve /path/to/docroot &
|
|
177
|
+
|
|
178
|
+
adapt admin create-permissions /path/to/docroot __all__
|
|
179
|
+
adapt admin create-user /path/to/docroot --username agent --password <strong-password>
|
|
180
|
+
adapt admin add-to-group /path/to/docroot --username agent --group <resource>_readonly
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Log in as `agent` and self-issue an API key from `/profile` (any
|
|
184
|
+
authenticated user can create their own key — no superuser needed), then
|
|
185
|
+
point a client at `/mcp` with that key:
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
# Claude Code CLI
|
|
189
|
+
claude mcp add --transport http adapt http://localhost:8000/mcp \
|
|
190
|
+
--header "X-API-Key: <key>"
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
```json
|
|
194
|
+
// Generic MCP client config (Claude Desktop and similar)
|
|
195
|
+
{
|
|
196
|
+
"mcpServers": {
|
|
197
|
+
"adapt": {
|
|
198
|
+
"url": "http://localhost:8000/mcp",
|
|
199
|
+
"headers": { "X-API-Key": "<key>" }
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
MCP requires an API key on every call — there's no session-cookie or
|
|
206
|
+
anonymous path, since MCP has no concept of a browser session. Set
|
|
207
|
+
`mcp_enabled: false` in `.adapt/conf.json` (or `ADAPT_MCP_ENABLED=false`) to
|
|
208
|
+
remove `/mcp` entirely. Full walkthrough, troubleshooting, and the tool
|
|
209
|
+
reference table: [docs/manual/mcp_guide.md](docs/manual/mcp_guide.md).
|
|
138
210
|
|
|
139
211
|
## Dataset Mutation Envelope
|
|
140
212
|
|
|
@@ -156,6 +228,7 @@ adapt serve <root> [--host ... --port ... --tls-cert ... --tls-key ... --reload
|
|
|
156
228
|
adapt check <root>
|
|
157
229
|
adapt addsuperuser <root> --username <name>
|
|
158
230
|
adapt list-endpoints <root>
|
|
231
|
+
adapt reindex <root> [--force]
|
|
159
232
|
adapt admin list-resources <root>
|
|
160
233
|
adapt admin create-permissions <root> __all__
|
|
161
234
|
```
|
|
@@ -169,6 +242,7 @@ Detailed docs live under `docs/manual/`.
|
|
|
169
242
|
- Quick start: [docs/manual/quick_start.md](docs/manual/quick_start.md)
|
|
170
243
|
- Configuration: [docs/manual/configuration.md](docs/manual/configuration.md)
|
|
171
244
|
- API reference: [docs/manual/api_reference.md](docs/manual/api_reference.md)
|
|
245
|
+
- MCP guide: [docs/manual/mcp_guide.md](docs/manual/mcp_guide.md)
|
|
172
246
|
- Plugin development: [docs/manual/plugin_development.md](docs/manual/plugin_development.md)
|
|
173
247
|
|
|
174
248
|
## License
|
|
@@ -6,6 +6,8 @@ Adapt is a FastAPI server that turns files in a directory into APIs and UIs.
|
|
|
6
6
|
- Markdown/HTML become browsable pages
|
|
7
7
|
- Media files become streaming endpoints and player/gallery UIs
|
|
8
8
|
- Python files can register custom routers
|
|
9
|
+
- Everything is searchable in one place via full-text `/search`
|
|
10
|
+
- Everything is reachable by agentic tools via an MCP server at `/mcp`
|
|
9
11
|
|
|
10
12
|
## Quick Start
|
|
11
13
|
|
|
@@ -36,6 +38,8 @@ Useful URLs:
|
|
|
36
38
|
- `/api/<resource>` resource API
|
|
37
39
|
- `/ui/<resource>` resource UI
|
|
38
40
|
- `/schema/<resource>` resource schema
|
|
41
|
+
- `/search` full-text search across every resource you can read
|
|
42
|
+
- `/mcp` MCP server for agentic tools (see [MCP Interface](#mcp-interface) below)
|
|
39
43
|
|
|
40
44
|
## What Adapt Generates
|
|
41
45
|
|
|
@@ -90,6 +94,73 @@ This reflects the current implementation in the codebase.
|
|
|
90
94
|
- Caching with invalidation on mutations
|
|
91
95
|
- Built-in admin UI for users/groups/permissions/locks/cache/api keys/audit logs
|
|
92
96
|
- Plugin architecture with companion overrides in `.adapt/`
|
|
97
|
+
- Permission-filtered full-text search across every resource type
|
|
98
|
+
- MCP server for agentic tool access, mounted alongside the REST API
|
|
99
|
+
|
|
100
|
+
## Full-Text Search
|
|
101
|
+
|
|
102
|
+
`GET /search?q=<query>` searches datasets, Markdown, HTML, and media metadata
|
|
103
|
+
in one ranked list, filtered to what the caller may read — a query term that
|
|
104
|
+
matches a resource you can't see never shows up, and never leaks via the
|
|
105
|
+
result count either.
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
curl -H "X-API-Key: <key>" "http://localhost:8000/search?q=parental+leave"
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The index refreshes incrementally on startup (`search_on_startup`, default
|
|
112
|
+
`true`) and can be rebuilt on demand with `adapt reindex <root>`. See the
|
|
113
|
+
[API Reference](docs/manual/api_reference.md#search-endpoint) for query
|
|
114
|
+
parameters and result shape.
|
|
115
|
+
|
|
116
|
+
## MCP Interface
|
|
117
|
+
|
|
118
|
+
Adapt mounts a [Model Context Protocol](https://modelcontextprotocol.io)
|
|
119
|
+
server at `/mcp`, on the same host/port as everything else, exposing five
|
|
120
|
+
tools that wrap the same permission checks and plugin methods as the REST
|
|
121
|
+
API — `list_resources`, `get_schema`, `read_resource`, `write_resource`, and
|
|
122
|
+
`search`. There's no separate process, no separate API surface, and no
|
|
123
|
+
extra permission model to maintain.
|
|
124
|
+
|
|
125
|
+
Minimal walkthrough — create an account for the agent, grant it read access,
|
|
126
|
+
mint an API key, and connect a client:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
adapt addsuperuser /path/to/docroot --username admin
|
|
130
|
+
adapt serve /path/to/docroot &
|
|
131
|
+
|
|
132
|
+
adapt admin create-permissions /path/to/docroot __all__
|
|
133
|
+
adapt admin create-user /path/to/docroot --username agent --password <strong-password>
|
|
134
|
+
adapt admin add-to-group /path/to/docroot --username agent --group <resource>_readonly
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Log in as `agent` and self-issue an API key from `/profile` (any
|
|
138
|
+
authenticated user can create their own key — no superuser needed), then
|
|
139
|
+
point a client at `/mcp` with that key:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
# Claude Code CLI
|
|
143
|
+
claude mcp add --transport http adapt http://localhost:8000/mcp \
|
|
144
|
+
--header "X-API-Key: <key>"
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
```json
|
|
148
|
+
// Generic MCP client config (Claude Desktop and similar)
|
|
149
|
+
{
|
|
150
|
+
"mcpServers": {
|
|
151
|
+
"adapt": {
|
|
152
|
+
"url": "http://localhost:8000/mcp",
|
|
153
|
+
"headers": { "X-API-Key": "<key>" }
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
MCP requires an API key on every call — there's no session-cookie or
|
|
160
|
+
anonymous path, since MCP has no concept of a browser session. Set
|
|
161
|
+
`mcp_enabled: false` in `.adapt/conf.json` (or `ADAPT_MCP_ENABLED=false`) to
|
|
162
|
+
remove `/mcp` entirely. Full walkthrough, troubleshooting, and the tool
|
|
163
|
+
reference table: [docs/manual/mcp_guide.md](docs/manual/mcp_guide.md).
|
|
93
164
|
|
|
94
165
|
## Dataset Mutation Envelope
|
|
95
166
|
|
|
@@ -111,6 +182,7 @@ adapt serve <root> [--host ... --port ... --tls-cert ... --tls-key ... --reload
|
|
|
111
182
|
adapt check <root>
|
|
112
183
|
adapt addsuperuser <root> --username <name>
|
|
113
184
|
adapt list-endpoints <root>
|
|
185
|
+
adapt reindex <root> [--force]
|
|
114
186
|
adapt admin list-resources <root>
|
|
115
187
|
adapt admin create-permissions <root> __all__
|
|
116
188
|
```
|
|
@@ -124,6 +196,7 @@ Detailed docs live under `docs/manual/`.
|
|
|
124
196
|
- Quick start: [docs/manual/quick_start.md](docs/manual/quick_start.md)
|
|
125
197
|
- Configuration: [docs/manual/configuration.md](docs/manual/configuration.md)
|
|
126
198
|
- API reference: [docs/manual/api_reference.md](docs/manual/api_reference.md)
|
|
199
|
+
- MCP guide: [docs/manual/mcp_guide.md](docs/manual/mcp_guide.md)
|
|
127
200
|
- Plugin development: [docs/manual/plugin_development.md](docs/manual/plugin_development.md)
|
|
128
201
|
|
|
129
202
|
## License
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
from typing import List
|
|
4
|
+
|
|
1
5
|
from fastapi import Depends, HTTPException, Request, Query
|
|
2
6
|
from sqlalchemy import asc, desc
|
|
3
7
|
from sqlmodel import Session, select
|
|
4
|
-
from typing import List
|
|
5
|
-
from datetime import datetime, timedelta, timezone
|
|
6
|
-
import logging
|
|
7
8
|
|
|
8
9
|
from ..auth import require_superuser
|
|
9
10
|
from ..storage import User, APIKey, get_db_session
|
|
10
|
-
from ..api_keys import
|
|
11
|
+
from ..api_keys import create_api_key_record, revoke_api_key_record
|
|
11
12
|
from ..audit import log_action
|
|
12
13
|
from . import router
|
|
13
14
|
from .models import APIKeyCreate
|
|
@@ -27,8 +28,6 @@ def list_api_keys(
|
|
|
27
28
|
user = Depends(require_superuser)
|
|
28
29
|
):
|
|
29
30
|
"""List all API keys in the system with optional query parameters."""
|
|
30
|
-
import json
|
|
31
|
-
|
|
32
31
|
stmt = select(APIKey)
|
|
33
32
|
|
|
34
33
|
if user_id is not None:
|
|
@@ -73,28 +72,15 @@ def create_api_key(key_data: APIKeyCreate, request: Request, db: Session = Depen
|
|
|
73
72
|
if not target_user:
|
|
74
73
|
logger.warning("Attempted to create API key for non-existent user %d", key_data.user_id)
|
|
75
74
|
raise HTTPException(status_code=404, detail="User not found")
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
raise HTTPException(status_code=400, detail="Expiration cannot exceed 1 year (365 days)")
|
|
83
|
-
expires_at = datetime.now(tz=timezone.utc) + timedelta(days=key_data.expires_in_days)
|
|
84
|
-
|
|
85
|
-
api_key = APIKey(
|
|
86
|
-
user_id=key_data.user_id,
|
|
87
|
-
key_hash=key_hash,
|
|
88
|
-
description=key_data.description,
|
|
89
|
-
expires_at=expires_at
|
|
90
|
-
)
|
|
91
|
-
db.add(api_key)
|
|
92
|
-
db.commit()
|
|
93
|
-
db.refresh(api_key)
|
|
94
|
-
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
raw_key, api_key = create_api_key_record(db, key_data.user_id, key_data.description, key_data.expires_in_days)
|
|
78
|
+
except ValueError as e:
|
|
79
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
80
|
+
|
|
95
81
|
log_action(request, "create_api_key", "apikey", f"Created API key for user {target_user.username}", user.id)
|
|
96
82
|
logger.info("Created API key %d for user %s", api_key.id, target_user.username)
|
|
97
|
-
|
|
83
|
+
|
|
98
84
|
# Return the raw key only once!
|
|
99
85
|
return {
|
|
100
86
|
"id": api_key.id,
|
|
@@ -108,16 +94,12 @@ def create_api_key(key_data: APIKeyCreate, request: Request, db: Session = Depen
|
|
|
108
94
|
@router.delete("/api-keys/{key_id}")
|
|
109
95
|
def revoke_api_key(key_id: int, request: Request, db: Session = Depends(get_db_session), user = Depends(require_superuser)):
|
|
110
96
|
"""Revoke an API key by ID."""
|
|
111
|
-
api_key = db
|
|
97
|
+
api_key = revoke_api_key_record(db, key_id)
|
|
112
98
|
if not api_key:
|
|
113
99
|
logger.warning("Attempted to revoke non-existent API key %d", key_id)
|
|
114
100
|
raise HTTPException(status_code=404, detail="API Key not found")
|
|
115
|
-
|
|
116
|
-
api_key.is_active = False
|
|
117
|
-
db.add(api_key)
|
|
118
|
-
db.commit()
|
|
119
|
-
|
|
101
|
+
|
|
120
102
|
log_action(request, "revoke_api_key", "apikey", f"Revoked API key {key_id}", user.id)
|
|
121
103
|
logger.info("Revoked API key %d", key_id)
|
|
122
|
-
|
|
104
|
+
|
|
123
105
|
return {"success": True}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
from typing import List
|
|
4
|
+
|
|
1
5
|
from fastapi import Depends, HTTPException, Request, Query
|
|
2
6
|
from sqlmodel import Session, select
|
|
3
|
-
from typing import List
|
|
4
|
-
import logging
|
|
5
7
|
|
|
6
8
|
from ..auth import require_superuser
|
|
7
9
|
from ..storage import LockRecord, get_db_session
|
|
8
10
|
from ..locks import LockManager
|
|
11
|
+
from ..utils.query import apply_filter, apply_sort, apply_pagination
|
|
9
12
|
from ..audit import log_action
|
|
10
13
|
from . import router
|
|
11
14
|
|
|
@@ -22,9 +25,6 @@ def list_locks(
|
|
|
22
25
|
user = Depends(require_superuser)
|
|
23
26
|
):
|
|
24
27
|
"""List all active locks with optional query parameters."""
|
|
25
|
-
from ..utils.query import apply_filter, apply_sort, apply_pagination
|
|
26
|
-
import json
|
|
27
|
-
|
|
28
28
|
locks = db.exec(select(LockRecord)).all()
|
|
29
29
|
|
|
30
30
|
# Convert to dicts for filtering/sorting
|
|
@@ -30,4 +30,4 @@ def admin_ui(request: Request):
|
|
|
30
30
|
|
|
31
31
|
# Serve the admin template
|
|
32
32
|
logger.debug("Rendering admin UI for superuser %s", user.username)
|
|
33
|
-
return request.app.state.templates.TemplateResponse("admin/index.html", context)
|
|
33
|
+
return request.app.state.templates.TemplateResponse(request, "admin/index.html", context)
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
from typing import List
|
|
4
|
+
|
|
1
5
|
from fastapi import Depends, HTTPException, Request, Query
|
|
2
6
|
from sqlmodel import Session, select
|
|
3
|
-
from typing import List
|
|
4
|
-
import logging
|
|
5
7
|
|
|
6
8
|
from ..auth import require_superuser, hash_password
|
|
7
9
|
from ..storage import User, get_db_session
|
|
10
|
+
from ..utils.query import apply_filter, apply_sort, apply_pagination
|
|
8
11
|
from ..audit import log_action
|
|
9
12
|
from . import router
|
|
10
13
|
from .models import UserCreate, UserPublic
|
|
@@ -22,9 +25,6 @@ def list_users(
|
|
|
22
25
|
user: User = Depends(require_superuser)
|
|
23
26
|
):
|
|
24
27
|
"""List all users with optional query parameters."""
|
|
25
|
-
from ..utils.query import apply_filter, apply_sort, apply_pagination
|
|
26
|
-
import json
|
|
27
|
-
|
|
28
28
|
query = select(User)
|
|
29
29
|
users = db.exec(query).all()
|
|
30
30
|
|
|
@@ -75,7 +75,6 @@ def create_user(user_data: UserCreate, request: Request, db: Session = Depends(g
|
|
|
75
75
|
"""Create a new user."""
|
|
76
76
|
# Check if server is in read-only mode
|
|
77
77
|
if request.app.state.config.readonly:
|
|
78
|
-
from fastapi import HTTPException
|
|
79
78
|
raise HTTPException(status_code=405, detail="Server is in read-only mode")
|
|
80
79
|
|
|
81
80
|
existing = db.exec(select(User).where(User.username == user_data.username)).first()
|
|
@@ -109,7 +108,6 @@ def delete_user(user_id: int, request: Request, db: Session = Depends(get_db_ses
|
|
|
109
108
|
"""Delete a user by ID."""
|
|
110
109
|
# Check if server is in read-only mode
|
|
111
110
|
if request.app.state.config.readonly:
|
|
112
|
-
from fastapi import HTTPException
|
|
113
111
|
raise HTTPException(status_code=405, detail="Server is in read-only mode")
|
|
114
112
|
|
|
115
113
|
target = db.get(User, user_id)
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
"""adapt.api_keys — API key generation, verification, and lifecycle helpers."""
|
|
1
2
|
from __future__ import annotations
|
|
2
3
|
|
|
3
4
|
import logging
|
|
4
5
|
import secrets
|
|
5
6
|
import hashlib
|
|
6
|
-
from datetime import datetime, timezone
|
|
7
|
+
from datetime import datetime, timedelta, timezone
|
|
7
8
|
from sqlmodel import Session, select
|
|
8
9
|
from fastapi import Request, HTTPException, Security
|
|
9
10
|
from fastapi.security import APIKeyHeader
|
|
@@ -60,6 +61,55 @@ def verify_api_key(db: Session, raw_key: str) -> User | None:
|
|
|
60
61
|
logger.info(f"API key verified successfully for user {api_key.user_id}")
|
|
61
62
|
return db.get(User, api_key.user_id)
|
|
62
63
|
|
|
64
|
+
def create_api_key_record(
|
|
65
|
+
db: Session,
|
|
66
|
+
user_id: int,
|
|
67
|
+
description: str | None,
|
|
68
|
+
expires_in_days: int | None,
|
|
69
|
+
) -> tuple[str, APIKey]:
|
|
70
|
+
"""Create and persist an API key. Returns (raw_key, api_key).
|
|
71
|
+
|
|
72
|
+
Raises ValueError if expires_in_days exceeds 365.
|
|
73
|
+
"""
|
|
74
|
+
if expires_in_days is not None and expires_in_days > 365:
|
|
75
|
+
raise ValueError("Expiration cannot exceed 1 year (365 days)")
|
|
76
|
+
expires_at = None
|
|
77
|
+
if expires_in_days is not None:
|
|
78
|
+
expires_at = datetime.now(tz=timezone.utc) + timedelta(days=expires_in_days)
|
|
79
|
+
raw_key, key_hash = generate_api_key()
|
|
80
|
+
api_key = APIKey(
|
|
81
|
+
user_id=user_id,
|
|
82
|
+
key_hash=key_hash,
|
|
83
|
+
description=description,
|
|
84
|
+
expires_at=expires_at,
|
|
85
|
+
created_at=datetime.now(tz=timezone.utc),
|
|
86
|
+
is_active=True,
|
|
87
|
+
)
|
|
88
|
+
db.add(api_key)
|
|
89
|
+
db.commit()
|
|
90
|
+
db.refresh(api_key)
|
|
91
|
+
return raw_key, api_key
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def revoke_api_key_record(
|
|
95
|
+
db: Session,
|
|
96
|
+
key_id: int,
|
|
97
|
+
*,
|
|
98
|
+
owner_id: int | None = None,
|
|
99
|
+
) -> APIKey | None:
|
|
100
|
+
"""Set an API key inactive. If owner_id is given, enforces ownership.
|
|
101
|
+
|
|
102
|
+
Returns the key on success, None if not found or ownership mismatch.
|
|
103
|
+
"""
|
|
104
|
+
api_key = db.get(APIKey, key_id)
|
|
105
|
+
if not api_key or (owner_id is not None and api_key.user_id != owner_id):
|
|
106
|
+
return None
|
|
107
|
+
api_key.is_active = False
|
|
108
|
+
db.add(api_key)
|
|
109
|
+
db.commit()
|
|
110
|
+
return api_key
|
|
111
|
+
|
|
112
|
+
|
|
63
113
|
def get_user_from_api_key(
|
|
64
114
|
request: Request,
|
|
65
115
|
api_key: str = Security(API_KEY_HEADER)
|