adapt-server 0.2.3__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.3 → adapt_server-0.2.4}/PKG-INFO +76 -2
- {adapt_server-0.2.3 → adapt_server-0.2.4}/README.md +73 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/__init__.py +1 -1
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/admin/ui.py +1 -1
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/app.py +119 -32
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/cli.py +8 -1
- {adapt_server-0.2.3 → 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.3 → adapt_server-0.2.4}/adapt/config.py +13 -2
- adapt_server-0.2.4/adapt/mcp.py +162 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/permissions.py +26 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/plugins/base.py +24 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/plugins/dataset_plugin.py +39 -1
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/plugins/html_plugin.py +59 -1
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/plugins/markdown_plugin.py +50 -3
- {adapt_server-0.2.3 → 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.3 → adapt_server-0.2.4}/adapt/templates/admin_base.html +7 -0
- {adapt_server-0.2.3 → 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.3 → adapt_server-0.2.4}/adapt/utils/query.py +1 -1
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt_server.egg-info/PKG-INFO +76 -2
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt_server.egg-info/SOURCES.txt +8 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt_server.egg-info/requires.txt +2 -1
- {adapt_server-0.2.3 → adapt_server-0.2.4}/pyproject.toml +3 -2
- {adapt_server-0.2.3 → adapt_server-0.2.4}/tests/test_admin.py +18 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/tests/test_config.py +2 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/tests/test_integration.py +24 -0
- {adapt_server-0.2.3 → 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.3 → 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.3/adapt/routes.py +0 -69
- {adapt_server-0.2.3 → adapt_server-0.2.4}/LICENSE +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/__main__.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/admin/__init__.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/admin/api_keys.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/admin/audit_logs.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/admin/cache.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/admin/groups.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/admin/locks.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/admin/models.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/admin/permissions.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/admin/resources.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/admin/users.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/api_keys.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/audit.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/auth/__init__.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/auth/dependencies.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/auth/password.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/auth/routes.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/auth/session.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/cache.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/commands/__init__.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/commands/addsuperuser.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/commands/admin/__init__.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/commands/admin/add_to_group.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/commands/admin/create_group.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/commands/admin/create_permissions.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/commands/admin/create_user.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/commands/admin/delete_group.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/commands/admin/delete_user.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/commands/admin/list_groups.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/commands/admin/list_resources.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/commands/admin/list_users.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/commands/admin/remove_from_group.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/commands/list_endpoints.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/commands/serve.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/discovery.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/locks.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/models.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/plugins/__init__.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/plugins/csv_plugin.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/plugins/excel_plugin.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/plugins/parquet_plugin.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/plugins/python_plugin.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/security.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/security_urls.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/static/admin/app.js +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/static/admin/style.css +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/storage.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/templates/admin/index.html +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/templates/datatable.html +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/templates/landing.html +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/templates/login.html +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/templates/media_gallery.html +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/templates/media_player.html +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/templates/profile.html +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt/utils/__init__.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt_server.egg-info/dependency_links.txt +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt_server.egg-info/entry_points.txt +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/adapt_server.egg-info/top_level.txt +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/setup.cfg +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/tests/test_auth.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/tests/test_cache.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/tests/test_cli_integration.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/tests/test_constraints.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/tests/test_csv_plugin.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/tests/test_dataset_plugin.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/tests/test_dependencies.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/tests/test_discovery.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/tests/test_html_plugin.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/tests/test_locks.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/tests/test_on_delete.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/tests/test_parquet_plugin.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/tests/test_phase3.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/tests/test_plugin_interface.py +0 -0
- {adapt_server-0.2.3 → adapt_server-0.2.4}/tests/test_python_plugin.py +0 -0
- {adapt_server-0.2.3 → 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
|
|
@@ -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)
|
|
@@ -12,7 +12,6 @@ from fastapi.openapi.docs import get_swagger_ui_html, get_swagger_ui_oauth2_redi
|
|
|
12
12
|
from fastapi.openapi.utils import get_openapi
|
|
13
13
|
from fastapi.responses import JSONResponse, RedirectResponse
|
|
14
14
|
from fastapi.middleware.trustedhost import TrustedHostMiddleware
|
|
15
|
-
from fastapi.routing import APIRoute
|
|
16
15
|
from fastapi.staticfiles import StaticFiles
|
|
17
16
|
from fastapi.templating import Jinja2Templates
|
|
18
17
|
from sqlmodel import Session, select, delete
|
|
@@ -25,11 +24,13 @@ from .admin import router as admin_router
|
|
|
25
24
|
from .config import AdaptConfig
|
|
26
25
|
from .discovery import discover_resources
|
|
27
26
|
from .permissions import PermissionChecker
|
|
28
|
-
from .routes import generate_routes
|
|
27
|
+
from .routes import build_resource_registry, generate_routes, resource_namespaces as _routes_resource_namespaces
|
|
28
|
+
from .routes_search import router as search_router, safe_snippet
|
|
29
29
|
from .storage import User, DBSession, init_database
|
|
30
30
|
from .locks import LockManager
|
|
31
|
+
from .mcp import build_mcp_server
|
|
31
32
|
from .utils import build_accessible_ui_links
|
|
32
|
-
from . import cache
|
|
33
|
+
from . import cache, search
|
|
33
34
|
from .security import (
|
|
34
35
|
apply_security_headers,
|
|
35
36
|
build_allowed_hosts,
|
|
@@ -45,7 +46,7 @@ logger = logging.getLogger(__name__)
|
|
|
45
46
|
|
|
46
47
|
_DOCS_INTERNAL_PATHS = frozenset({"/docs", "/docs/", "/docs/oauth2-redirect", "/openapi.json"})
|
|
47
48
|
_PUBLIC_OPENAPI_PATHS = frozenset({"/", "/auth/login", "/health"})
|
|
48
|
-
_AUTHENTICATED_OPENAPI_PATHS = frozenset({"/auth/logout", "/auth/me", "/profile", "/api/apikeys"})
|
|
49
|
+
_AUTHENTICATED_OPENAPI_PATHS = frozenset({"/auth/logout", "/auth/me", "/profile", "/api/apikeys", "/search"})
|
|
49
50
|
|
|
50
51
|
|
|
51
52
|
async def cleanup_expired_sessions(engine, interval_hours=24):
|
|
@@ -72,13 +73,7 @@ def _normalize_path(path: str) -> str:
|
|
|
72
73
|
|
|
73
74
|
def _resource_namespaces(resource) -> set[str]:
|
|
74
75
|
"""Return the supported permission namespaces for a discovered resource."""
|
|
75
|
-
|
|
76
|
-
namespace_with_ext = resource.relative_path.as_posix()
|
|
77
|
-
if "sub_namespace" in resource.metadata:
|
|
78
|
-
suffix = f"/{resource.metadata['sub_namespace']}"
|
|
79
|
-
namespace_no_ext += suffix
|
|
80
|
-
namespace_with_ext += suffix
|
|
81
|
-
return {namespace_no_ext, namespace_with_ext}
|
|
76
|
+
return set(_routes_resource_namespaces(resource))
|
|
82
77
|
|
|
83
78
|
|
|
84
79
|
def _all_resource_namespaces(resources) -> set[str]:
|
|
@@ -125,14 +120,14 @@ def _extract_resource_namespace(path: str, all_namespaces: set[str]) -> str | No
|
|
|
125
120
|
return None
|
|
126
121
|
|
|
127
122
|
|
|
128
|
-
def
|
|
129
|
-
"""Decide whether
|
|
130
|
-
path = _normalize_path(
|
|
123
|
+
def _operation_is_visible(path: str, operation: dict, request: Request, user: User | None, all_namespaces: set[str], visible_namespaces: set[str]) -> bool:
|
|
124
|
+
"""Decide whether an OpenAPI operation should appear in the current request's schema."""
|
|
125
|
+
path = _normalize_path(path)
|
|
131
126
|
|
|
132
127
|
if path in _DOCS_INTERNAL_PATHS:
|
|
133
128
|
return False
|
|
134
129
|
|
|
135
|
-
if path.startswith("/admin") or "admin" in (
|
|
130
|
+
if path.startswith("/admin") or "admin" in (operation.get("tags") or []):
|
|
136
131
|
return bool(user and getattr(user, "is_superuser", False))
|
|
137
132
|
|
|
138
133
|
if path in _PUBLIC_OPENAPI_PATHS:
|
|
@@ -160,24 +155,73 @@ def _route_is_visible(route: APIRoute, request: Request, user: User | None, all_
|
|
|
160
155
|
return False
|
|
161
156
|
|
|
162
157
|
|
|
158
|
+
def _iter_schema_refs(node):
|
|
159
|
+
"""Yield component schema names referenced anywhere within a JSON-like node."""
|
|
160
|
+
if isinstance(node, dict):
|
|
161
|
+
ref = node.get("$ref")
|
|
162
|
+
if isinstance(ref, str) and ref.startswith("#/components/schemas/"):
|
|
163
|
+
yield ref.removeprefix("#/components/schemas/")
|
|
164
|
+
for value in node.values():
|
|
165
|
+
yield from _iter_schema_refs(value)
|
|
166
|
+
elif isinstance(node, list):
|
|
167
|
+
for item in node:
|
|
168
|
+
yield from _iter_schema_refs(item)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _prune_unreferenced_components(schema: dict) -> None:
|
|
172
|
+
"""Drop component schemas no longer reachable from any kept path.
|
|
173
|
+
|
|
174
|
+
Filtering removes paths but leaves their request/response models behind in
|
|
175
|
+
components.schemas — a non-superuser would otherwise receive the
|
|
176
|
+
field-level shape of admin-only models like UserPublic or Permission.
|
|
177
|
+
"""
|
|
178
|
+
components = schema.get("components") or {}
|
|
179
|
+
schemas = components.get("schemas")
|
|
180
|
+
if not schemas:
|
|
181
|
+
return
|
|
182
|
+
|
|
183
|
+
reachable: set[str] = set()
|
|
184
|
+
pending = set(_iter_schema_refs(schema.get("paths", {})))
|
|
185
|
+
while pending:
|
|
186
|
+
name = pending.pop()
|
|
187
|
+
if name in reachable:
|
|
188
|
+
continue
|
|
189
|
+
reachable.add(name)
|
|
190
|
+
pending.update(_iter_schema_refs(schemas.get(name, {})))
|
|
191
|
+
|
|
192
|
+
pruned = {name: definition for name, definition in schemas.items() if name in reachable}
|
|
193
|
+
if pruned:
|
|
194
|
+
components["schemas"] = pruned
|
|
195
|
+
else:
|
|
196
|
+
components.pop("schemas", None)
|
|
197
|
+
if not components:
|
|
198
|
+
schema.pop("components", None)
|
|
199
|
+
|
|
200
|
+
|
|
163
201
|
def _build_openapi_schema(app: FastAPI, request: Request, user: User | None) -> dict:
|
|
164
202
|
"""Build a filtered OpenAPI schema for the current request context."""
|
|
165
|
-
|
|
166
|
-
visible_namespaces = _visible_resource_namespaces(request, user)
|
|
167
|
-
visible_routes = [
|
|
168
|
-
route
|
|
169
|
-
for route in app.routes
|
|
170
|
-
if isinstance(route, APIRoute)
|
|
171
|
-
and route.include_in_schema
|
|
172
|
-
and _route_is_visible(route, request, user, all_namespaces, visible_namespaces)
|
|
173
|
-
]
|
|
174
|
-
|
|
175
|
-
return get_openapi(
|
|
203
|
+
schema = get_openapi(
|
|
176
204
|
title=app.title,
|
|
177
205
|
version=app.version,
|
|
178
206
|
description=app.description,
|
|
179
|
-
routes=
|
|
207
|
+
routes=app.routes,
|
|
180
208
|
)
|
|
209
|
+
all_namespaces = _all_resource_namespaces(request.app.state.resources)
|
|
210
|
+
visible_namespaces = _visible_resource_namespaces(request, user)
|
|
211
|
+
|
|
212
|
+
visible_paths: dict[str, dict] = {}
|
|
213
|
+
for path, operations in schema.get("paths", {}).items():
|
|
214
|
+
kept = {
|
|
215
|
+
method: operation
|
|
216
|
+
for method, operation in operations.items()
|
|
217
|
+
if _operation_is_visible(path, operation, request, user, all_namespaces, visible_namespaces)
|
|
218
|
+
}
|
|
219
|
+
if kept:
|
|
220
|
+
visible_paths[path] = kept
|
|
221
|
+
|
|
222
|
+
schema["paths"] = visible_paths
|
|
223
|
+
_prune_unreferenced_components(schema)
|
|
224
|
+
return schema
|
|
181
225
|
|
|
182
226
|
|
|
183
227
|
def _visible_resource_paths(request: Request, user: User | None) -> list[str]:
|
|
@@ -192,14 +236,25 @@ def _visible_resource_paths(request: Request, user: User | None) -> list[str]:
|
|
|
192
236
|
|
|
193
237
|
@asynccontextmanager
|
|
194
238
|
async def lifespan(app: FastAPI):
|
|
195
|
-
"""Handle application startup and shutdown events.
|
|
239
|
+
"""Handle application startup and shutdown events.
|
|
240
|
+
|
|
241
|
+
A mounted MCP sub-app's own `lifespan` is never invoked by the ASGI
|
|
242
|
+
server: Starlette's `Mount` only forwards `http`/`websocket` scope types,
|
|
243
|
+
never `lifespan`. So the MCP session manager's `run()` context must be
|
|
244
|
+
entered here, alongside the rest of this app's own startup/shutdown.
|
|
245
|
+
"""
|
|
196
246
|
# Startup: Start background cleanup task
|
|
197
247
|
engine = app.state.db_engine
|
|
198
248
|
cleanup_task = asyncio.create_task(cleanup_expired_sessions(engine))
|
|
199
249
|
logger.debug("Application startup: background cleanup task started")
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
250
|
+
|
|
251
|
+
mcp_server = getattr(app.state, "mcp_server", None)
|
|
252
|
+
if mcp_server is not None:
|
|
253
|
+
async with mcp_server.session_manager.run():
|
|
254
|
+
yield
|
|
255
|
+
else:
|
|
256
|
+
yield
|
|
257
|
+
|
|
203
258
|
# Shutdown: Could add cleanup logic here if needed
|
|
204
259
|
cleanup_task.cancel()
|
|
205
260
|
try:
|
|
@@ -219,12 +274,16 @@ def _init_infrastructure(config: AdaptConfig):
|
|
|
219
274
|
"""
|
|
220
275
|
engine = init_database(config.db_path)
|
|
221
276
|
cache.configure(str(config.db_path))
|
|
277
|
+
search.configure(str(config.db_path))
|
|
222
278
|
lock_manager = LockManager(engine)
|
|
223
279
|
cleaned = lock_manager.release_stale_locks(max_age_seconds=300)
|
|
224
280
|
if cleaned > 0:
|
|
225
281
|
logging.warning("Cleaned %d stale locks on startup", cleaned)
|
|
226
282
|
resources = discover_resources(config.root, config)
|
|
227
283
|
logger.debug("Discovered %d resources", len(resources))
|
|
284
|
+
if config.search_on_startup:
|
|
285
|
+
# Incremental: unchanged files are skipped, so restarts stay cheap.
|
|
286
|
+
search.reindex_all(resources, config)
|
|
228
287
|
return engine, lock_manager, resources
|
|
229
288
|
|
|
230
289
|
|
|
@@ -246,6 +305,7 @@ def create_app(config: AdaptConfig) -> FastAPI:
|
|
|
246
305
|
app.state.use_tls = bool(config.tls_cert and config.tls_key)
|
|
247
306
|
app.state.lock_manager = lock_manager
|
|
248
307
|
app.state.resources = resources
|
|
308
|
+
app.state.resource_registry = build_resource_registry(resources, config)
|
|
249
309
|
|
|
250
310
|
allowed_hosts = build_allowed_hosts(config.host)
|
|
251
311
|
if allowed_hosts != ["*"]:
|
|
@@ -254,6 +314,9 @@ def create_app(config: AdaptConfig) -> FastAPI:
|
|
|
254
314
|
# Set up Jinja2 templates
|
|
255
315
|
templates_dir = Path(__file__).parent / "templates"
|
|
256
316
|
templates = Jinja2Templates(directory=str(templates_dir))
|
|
317
|
+
# Search snippets hold raw docroot text; this escapes them and restores only
|
|
318
|
+
# the <mark> highlights. See routes_search.safe_snippet.
|
|
319
|
+
templates.env.filters["safe_snippet"] = safe_snippet
|
|
257
320
|
app.state.templates = templates
|
|
258
321
|
|
|
259
322
|
# Mount static files
|
|
@@ -310,8 +373,32 @@ def create_app(config: AdaptConfig) -> FastAPI:
|
|
|
310
373
|
# Mount admin routes
|
|
311
374
|
app.include_router(admin_router)
|
|
312
375
|
|
|
376
|
+
# Mount search routes
|
|
377
|
+
app.include_router(search_router)
|
|
378
|
+
|
|
313
379
|
# Generate and mount routes
|
|
314
|
-
generate_routes(app,
|
|
380
|
+
generate_routes(app, app.state.resource_registry)
|
|
381
|
+
|
|
382
|
+
# Mount the MCP server, exposing resources as agent-facing tools
|
|
383
|
+
if config.mcp_enabled:
|
|
384
|
+
mcp_server = build_mcp_server(config)
|
|
385
|
+
mcp_app = mcp_server.streamable_http_app()
|
|
386
|
+
# A mounted sub-app's `request.app` is itself, not the main app (see
|
|
387
|
+
# lifespan() docstring for the related lifespan gotcha) — mirror the
|
|
388
|
+
# slice of state every tool/helper needs onto it.
|
|
389
|
+
mcp_app.state.db_engine = engine
|
|
390
|
+
mcp_app.state.resources = resources
|
|
391
|
+
mcp_app.state.config = config
|
|
392
|
+
mcp_app.state.lock_manager = lock_manager
|
|
393
|
+
mcp_app.state.resource_registry = app.state.resource_registry
|
|
394
|
+
# The outer app's own middleware still runs for /mcp requests (it wraps
|
|
395
|
+
# routing, including the Mount), and by the time it inspects
|
|
396
|
+
# `request.app.state` post-routing, `scope["app"]` has already been
|
|
397
|
+
# overwritten to `mcp_app` — so anything that middleware reads off
|
|
398
|
+
# app.state must be mirrored here too, not just what the tools need.
|
|
399
|
+
mcp_app.state.use_tls = app.state.use_tls
|
|
400
|
+
app.state.mcp_server = mcp_server
|
|
401
|
+
app.mount("/mcp", mcp_app)
|
|
315
402
|
|
|
316
403
|
@app.get("/openapi.json", include_in_schema=False)
|
|
317
404
|
def openapi_schema(request: Request):
|
|
@@ -7,7 +7,7 @@ import logging.config
|
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
|
|
9
9
|
from .config import AdaptConfig
|
|
10
|
-
from .commands import check, addsuperuser, list_endpoints, serve
|
|
10
|
+
from .commands import check, addsuperuser, list_endpoints, reindex, serve
|
|
11
11
|
from .commands.admin import run_admin
|
|
12
12
|
|
|
13
13
|
logger = logging.getLogger(__name__)
|
|
@@ -40,6 +40,10 @@ def main() -> None:
|
|
|
40
40
|
list_parser = subparsers.add_parser("list-endpoints", help="List the auto-generated REST/UI endpoints")
|
|
41
41
|
list_parser.add_argument("root", nargs="?", default=".", help="Document root to inspect")
|
|
42
42
|
|
|
43
|
+
reindex_parser = subparsers.add_parser("reindex", help="Rebuild the full-text search index")
|
|
44
|
+
reindex_parser.add_argument("root", nargs="?", default=".", help="Document root to index")
|
|
45
|
+
reindex_parser.add_argument("--force", action="store_true", help="Reindex even unchanged files")
|
|
46
|
+
|
|
43
47
|
admin_parser = subparsers.add_parser("admin", help="Admin tasks")
|
|
44
48
|
admin_subparsers = admin_parser.add_subparsers(dest="admin_command", required=True)
|
|
45
49
|
admin_list_resources_parser = admin_subparsers.add_parser("list-resources", help="List discovered resources")
|
|
@@ -116,6 +120,9 @@ def main() -> None:
|
|
|
116
120
|
elif args.command == "list-endpoints":
|
|
117
121
|
logger.info("Running list-endpoints command with root=%s", args.root)
|
|
118
122
|
list_endpoints.run_list_endpoints(Path(args.root).resolve())
|
|
123
|
+
elif args.command == "reindex":
|
|
124
|
+
logger.info("Running reindex command with root=%s force=%s", args.root, args.force)
|
|
125
|
+
reindex.run_reindex(Path(args.root).resolve(), force=args.force)
|
|
119
126
|
elif args.command == "admin":
|
|
120
127
|
logger.info("Running admin command: %s", args.admin_command)
|
|
121
128
|
run_admin(args)
|
|
@@ -8,6 +8,12 @@ from .. import cache
|
|
|
8
8
|
|
|
9
9
|
logger = logging.getLogger(__name__)
|
|
10
10
|
|
|
11
|
+
# Namespaces claimed by built-in routes. A markdown or HTML file with one of
|
|
12
|
+
# these names mounts at the same path and shadows the built-in.
|
|
13
|
+
RESERVED_NAMESPACES = frozenset({
|
|
14
|
+
"search", "health", "profile", "admin", "docs", "static", "openapi.json",
|
|
15
|
+
})
|
|
16
|
+
|
|
11
17
|
|
|
12
18
|
def run_check(root: Path) -> None:
|
|
13
19
|
"""Check the configuration and discover resources.
|
|
@@ -31,6 +37,16 @@ def run_check(root: Path) -> None:
|
|
|
31
37
|
print(f"Document root: {config.root}")
|
|
32
38
|
print(f"SQLite store: {config.db_path} (engine {engine})")
|
|
33
39
|
print(f"Discovered {count} dataset(s)")
|
|
40
|
+
|
|
41
|
+
# Markdown and HTML resources mount at /<namespace>, so they can collide
|
|
42
|
+
# with built-in routes; datasets live under /api and /ui and cannot.
|
|
43
|
+
for resource in resources:
|
|
44
|
+
if resource.resource_type not in ("markdown", "html"):
|
|
45
|
+
continue
|
|
46
|
+
namespace = resource.relative_path.with_suffix("").as_posix()
|
|
47
|
+
if namespace in RESERVED_NAMESPACES:
|
|
48
|
+
logger.warning("Resource %s shadows the built-in /%s route", resource.path, namespace)
|
|
49
|
+
print(f"WARNING: {resource.relative_path} shadows the built-in /{namespace} route")
|
|
34
50
|
# Validate TLS if configured
|
|
35
51
|
if config.tls_cert or config.tls_key:
|
|
36
52
|
if config.tls_cert and not config.tls_cert.exists():
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
from ..config import AdaptConfig
|
|
5
|
+
from ..discovery import discover_resources
|
|
6
|
+
from ..storage import init_database
|
|
7
|
+
from .. import cache, search
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def run_reindex(root: Path, force: bool = False) -> None:
|
|
13
|
+
"""Rebuild the full-text search index for a document root.
|
|
14
|
+
|
|
15
|
+
The index normally refreshes on server startup. Run this to rebuild it
|
|
16
|
+
without starting the server, or with `force` after changing how a plugin
|
|
17
|
+
produces documents, since incremental refresh keys only on file mtime and
|
|
18
|
+
size and would otherwise skip unchanged files.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
root: The document root to index.
|
|
22
|
+
force: Reindex every resource, even ones that appear unchanged.
|
|
23
|
+
"""
|
|
24
|
+
config = AdaptConfig(root=root)
|
|
25
|
+
config.load_from_file()
|
|
26
|
+
init_database(config.db_path)
|
|
27
|
+
cache.configure(str(config.db_path))
|
|
28
|
+
search.configure(str(config.db_path))
|
|
29
|
+
|
|
30
|
+
if not search.is_available():
|
|
31
|
+
logger.error("Search index unavailable: this SQLite build lacks FTS5")
|
|
32
|
+
print("Search index unavailable: this SQLite build lacks FTS5.")
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
resources = discover_resources(config.root, config)
|
|
36
|
+
stats = search.reindex_all(resources, config, force=force)
|
|
37
|
+
|
|
38
|
+
print(f"Document root: {config.root}")
|
|
39
|
+
print(f"Search index: {config.db_path}")
|
|
40
|
+
print(
|
|
41
|
+
f"Indexed {stats['indexed']} resource(s) "
|
|
42
|
+
f"({stats['documents']} document(s)), "
|
|
43
|
+
f"skipped {stats['skipped']}, pruned {stats['pruned']}"
|
|
44
|
+
)
|
|
@@ -36,6 +36,8 @@ class AdaptConfig:
|
|
|
36
36
|
tls_cert: Path | None = None
|
|
37
37
|
tls_key: Path | None = None
|
|
38
38
|
secure_cookies: bool = False # Whether to set secure flag on cookies
|
|
39
|
+
search_on_startup: bool = True # Whether to refresh the search index on startup
|
|
40
|
+
mcp_enabled: bool = True # Whether to mount the MCP server at /mcp
|
|
39
41
|
plugin_registry: dict[str, str] = field(default_factory=lambda: {
|
|
40
42
|
".csv": "adapt.plugins.csv_plugin.CsvPlugin",
|
|
41
43
|
".xlsx": "adapt.plugins.excel_plugin.ExcelPlugin",
|
|
@@ -139,8 +141,10 @@ class AdaptConfig:
|
|
|
139
141
|
"tls_cert": str(self.tls_cert) if self.tls_cert else None,
|
|
140
142
|
"tls_key": str(self.tls_key) if self.tls_key else None,
|
|
141
143
|
"secure_cookies": self.secure_cookies,
|
|
144
|
+
"search_on_startup": self.search_on_startup,
|
|
142
145
|
"readonly": self.readonly,
|
|
143
146
|
"debug": self.debug,
|
|
147
|
+
"mcp_enabled": self.mcp_enabled,
|
|
144
148
|
"logging": self.logging.copy(),
|
|
145
149
|
}
|
|
146
150
|
with conf_path.open("w") as f:
|
|
@@ -159,7 +163,8 @@ class AdaptConfig:
|
|
|
159
163
|
"""Validate all keys and types in the loaded config dict, exiting on error."""
|
|
160
164
|
allowed_keys = {
|
|
161
165
|
"plugin_registry", "host", "port", "tls_cert", "tls_key",
|
|
162
|
-
"secure_cookies", "readonly", "debug", "logging",
|
|
166
|
+
"secure_cookies", "search_on_startup", "readonly", "debug", "logging",
|
|
167
|
+
"mcp_enabled",
|
|
163
168
|
}
|
|
164
169
|
for key in data:
|
|
165
170
|
if key not in allowed_keys:
|
|
@@ -192,7 +197,7 @@ class AdaptConfig:
|
|
|
192
197
|
if not isinstance(data["tls_key"], str):
|
|
193
198
|
logger.error("tls_key must be str or null")
|
|
194
199
|
sys.exit(1)
|
|
195
|
-
for bool_key in ("secure_cookies", "readonly", "debug"):
|
|
200
|
+
for bool_key in ("secure_cookies", "search_on_startup", "readonly", "debug", "mcp_enabled"):
|
|
196
201
|
if bool_key in data and not isinstance(data[bool_key], bool):
|
|
197
202
|
logger.error("%s must be bool", bool_key)
|
|
198
203
|
sys.exit(1)
|
|
@@ -214,10 +219,14 @@ class AdaptConfig:
|
|
|
214
219
|
self.tls_key = Path(data["tls_key"])
|
|
215
220
|
if "secure_cookies" in data:
|
|
216
221
|
self.secure_cookies = data["secure_cookies"]
|
|
222
|
+
if "search_on_startup" in data:
|
|
223
|
+
self.search_on_startup = data["search_on_startup"]
|
|
217
224
|
if "readonly" in data:
|
|
218
225
|
self.readonly = data["readonly"]
|
|
219
226
|
if "debug" in data:
|
|
220
227
|
self.debug = data["debug"]
|
|
228
|
+
if "mcp_enabled" in data:
|
|
229
|
+
self.mcp_enabled = data["mcp_enabled"]
|
|
221
230
|
if "logging" in data:
|
|
222
231
|
self.logging.update(data["logging"])
|
|
223
232
|
|
|
@@ -239,3 +248,5 @@ class AdaptConfig:
|
|
|
239
248
|
self.readonly = self._parse_env_bool(os.environ["ADAPT_READONLY"], "ADAPT_READONLY")
|
|
240
249
|
if "ADAPT_DEBUG" in os.environ:
|
|
241
250
|
self.debug = self._parse_env_bool(os.environ["ADAPT_DEBUG"], "ADAPT_DEBUG")
|
|
251
|
+
if "ADAPT_MCP_ENABLED" in os.environ:
|
|
252
|
+
self.mcp_enabled = self._parse_env_bool(os.environ["ADAPT_MCP_ENABLED"], "ADAPT_MCP_ENABLED")
|