md4a-fastapi 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- md4a_fastapi-0.1.0/.gitignore +7 -0
- md4a_fastapi-0.1.0/CHANGELOG.md +58 -0
- md4a_fastapi-0.1.0/LICENSE +21 -0
- md4a_fastapi-0.1.0/PKG-INFO +351 -0
- md4a_fastapi-0.1.0/README.md +306 -0
- md4a_fastapi-0.1.0/md4a/__init__.py +57 -0
- md4a_fastapi-0.1.0/md4a/_extras.py +16 -0
- md4a_fastapi-0.1.0/md4a/_negotiation.py +169 -0
- md4a_fastapi-0.1.0/md4a/app.py +72 -0
- md4a_fastapi-0.1.0/md4a/cli.py +90 -0
- md4a_fastapi-0.1.0/md4a/client.py +324 -0
- md4a_fastapi-0.1.0/md4a/middleware.py +245 -0
- md4a_fastapi-0.1.0/md4a/py.typed +1 -0
- md4a_fastapi-0.1.0/md4a/store.py +122 -0
- md4a_fastapi-0.1.0/md4a/wsgi.py +292 -0
- md4a_fastapi-0.1.0/pyproject.toml +83 -0
- md4a_fastapi-0.1.0/tests/conftest.py +36 -0
- md4a_fastapi-0.1.0/tests/test_add_md4a.py +102 -0
- md4a_fastapi-0.1.0/tests/test_cli.py +121 -0
- md4a_fastapi-0.1.0/tests/test_client_addresses.py +513 -0
- md4a_fastapi-0.1.0/tests/test_content_selection.py +84 -0
- md4a_fastapi-0.1.0/tests/test_escaping.py +204 -0
- md4a_fastapi-0.1.0/tests/test_extras.py +62 -0
- md4a_fastapi-0.1.0/tests/test_md4a.py +75 -0
- md4a_fastapi-0.1.0/tests/test_middleware_buffering.py +137 -0
- md4a_fastapi-0.1.0/tests/test_middleware_charset.py +85 -0
- md4a_fastapi-0.1.0/tests/test_middleware_headers.py +271 -0
- md4a_fastapi-0.1.0/tests/test_store.py +174 -0
- md4a_fastapi-0.1.0/tests/test_wsgi.py +420 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `md4a-fastapi` are recorded here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project uses
|
|
5
|
+
[Semantic Versioning](https://semver.org/). Until 1.0, minor versions may change
|
|
6
|
+
the API.
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2026-10-17
|
|
11
|
+
|
|
12
|
+
First release.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- `MarkdownForAgentsMiddleware` and `add_md4a` for FastAPI and other ASGI apps:
|
|
17
|
+
a `GET` request that accepts `text/markdown` gets a successful HTML response
|
|
18
|
+
converted to Markdown, cached by path and query string; everything else passes
|
|
19
|
+
through unchanged.
|
|
20
|
+
- `MarkdownForAgentsWSGIMiddleware` for Flask and other WSGI apps, with the same
|
|
21
|
+
options and rules. Importing it does not import FastAPI, Starlette, or httpx.
|
|
22
|
+
- A synchronous `provider` hook for hand-written Markdown, served ahead of conversion.
|
|
23
|
+
- `MemoryStore` and `FileStore`, with `ttl`, `delete`, and `clear`, and
|
|
24
|
+
`max_entries` for `MemoryStore`. `cache_responses=False` stops writing
|
|
25
|
+
responses to the store.
|
|
26
|
+
- `content_selector` and `strip_selectors` to convert only the page content.
|
|
27
|
+
- `escape_misc` and `literal_selectors` (both off by default) to keep untrusted
|
|
28
|
+
text, such as user comments, from becoming links or images in the Markdown.
|
|
29
|
+
- `MarkdownForAgentsClient` for remote discovery: sends `Accept: text/markdown`,
|
|
30
|
+
accepts only `text/markdown` responses, and checks schemes, redirects, host
|
|
31
|
+
allowlists, timeouts, response size, and non-public destination addresses.
|
|
32
|
+
- A standalone FastAPI service (`/health`, `/fetch`) and the `md4a fetch` and
|
|
33
|
+
`md4a serve` commands.
|
|
34
|
+
|
|
35
|
+
### Behavior
|
|
36
|
+
|
|
37
|
+
- Converted responses keep application headers, including repeated `Set-Cookie`;
|
|
38
|
+
md4a recalculates the content type and length, merges `Accept` into `Vary`, and
|
|
39
|
+
drops validators, digests, and range metadata that described the HTML bytes.
|
|
40
|
+
- Only successful HTML and Markdown bodies are buffered, up to `max_html_bytes`
|
|
41
|
+
(5 MB by default). Other media types and compressed, partial, oversized, and
|
|
42
|
+
(ASGI) trailer-bearing responses pass through unconverted and uncached. A
|
|
43
|
+
streamed HTML or Markdown body is held until it ends or reaches the limit, so
|
|
44
|
+
conversion does not preserve chunk timing.
|
|
45
|
+
- Bodies are decoded with the charset declared in `Content-Type`; a response that
|
|
46
|
+
cannot be decoded is returned unchanged and is not cached.
|
|
47
|
+
|
|
48
|
+
### Packaging
|
|
49
|
+
|
|
50
|
+
- The base install (`beautifulsoup4`, `markdownify`, `fastapi`) covers both
|
|
51
|
+
middlewares and the stores. The `server` extra adds `httpx`, `anyio`, and
|
|
52
|
+
`uvicorn[standard]` for the client, the service, and the `md4a` command, which
|
|
53
|
+
say how to install it when it is missing. `flask` and `wsgi` are names for the
|
|
54
|
+
base install.
|
|
55
|
+
- Python 3.11 to 3.14. Typed (`py.typed`).
|
|
56
|
+
|
|
57
|
+
[Unreleased]: https://github.com/adamtasteslikegood/md4a-fastapi/compare/v0.1.0...HEAD
|
|
58
|
+
[0.1.0]: https://github.com/adamtasteslikegood/md4a-fastapi/releases/tag/v0.1.0
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 adam schoen - (https://github.com/adamtasteslikegood)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: md4a-fastapi
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Markdown for Agents content negotiation for FastAPI, Flask, and other WSGI apps
|
|
5
|
+
Project-URL: Changelog, https://github.com/adamtasteslikegood/md4a-fastapi/blob/main/CHANGELOG.md
|
|
6
|
+
Project-URL: Documentation, https://github.com/adamtasteslikegood/md4a-fastapi#readme
|
|
7
|
+
Project-URL: Issues, https://github.com/adamtasteslikegood/md4a-fastapi/issues
|
|
8
|
+
Project-URL: Repository, https://github.com/adamtasteslikegood/md4a-fastapi
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: agents,ai,asgi,fastapi,flask,markdown,wsgi
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Environment :: Web Environment
|
|
14
|
+
Classifier: Framework :: FastAPI
|
|
15
|
+
Classifier: Framework :: Flask
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware
|
|
24
|
+
Classifier: Topic :: Text Processing :: Markup :: Markdown
|
|
25
|
+
Classifier: Typing :: Typed
|
|
26
|
+
Requires-Python: >=3.11
|
|
27
|
+
Requires-Dist: beautifulsoup4<5,>=4.12
|
|
28
|
+
Requires-Dist: fastapi<1,>=0.115
|
|
29
|
+
Requires-Dist: markdownify<2,>=0.14
|
|
30
|
+
Provides-Extra: flask
|
|
31
|
+
Provides-Extra: server
|
|
32
|
+
Requires-Dist: anyio<5,>=3.6.2; extra == 'server'
|
|
33
|
+
Requires-Dist: httpx<1,>=0.27; extra == 'server'
|
|
34
|
+
Requires-Dist: uvicorn[standard]<1,>=0.30; extra == 'server'
|
|
35
|
+
Provides-Extra: test
|
|
36
|
+
Requires-Dist: anyio<5,>=3.6.2; extra == 'test'
|
|
37
|
+
Requires-Dist: httpx<1,>=0.27; extra == 'test'
|
|
38
|
+
Requires-Dist: markdown-it-py<5,>=3; extra == 'test'
|
|
39
|
+
Requires-Dist: pytest<10,>=8; extra == 'test'
|
|
40
|
+
Requires-Dist: ruff<1,>=0.12; extra == 'test'
|
|
41
|
+
Requires-Dist: trio<1,>=0.25; extra == 'test'
|
|
42
|
+
Requires-Dist: uvicorn[standard]<1,>=0.30; extra == 'test'
|
|
43
|
+
Provides-Extra: wsgi
|
|
44
|
+
Description-Content-Type: text/markdown
|
|
45
|
+
|
|
46
|
+
# md4a-fastapi
|
|
47
|
+
|
|
48
|
+
[](https://github.com/adamtasteslikegood/md4a-fastapi/actions/workflows/ci.yml)
|
|
49
|
+
[](https://github.com/adamtasteslikegood/md4a-fastapi/actions/workflows/codeql.yml)
|
|
50
|
+
[](https://www.python.org/downloads/)
|
|
51
|
+
|
|
52
|
+
`md4a-fastapi` adds Markdown for Agents content negotiation to FastAPI applications.
|
|
53
|
+
When an agent sends `Accept: text/markdown`, an existing HTML route can return a
|
|
54
|
+
compact Markdown representation while normal browser requests remain unchanged.
|
|
55
|
+
|
|
56
|
+
The package also includes a client and standalone service for discovering whether
|
|
57
|
+
remote pages natively serve Markdown, then buffering or storing verified Markdown
|
|
58
|
+
responses.
|
|
59
|
+
|
|
60
|
+
> Status: early alpha. APIs may change before the first stable release.
|
|
61
|
+
|
|
62
|
+
## Features
|
|
63
|
+
|
|
64
|
+
- ASGI middleware designed for FastAPI
|
|
65
|
+
- WSGI middleware for Flask and other WSGI applications
|
|
66
|
+
- `Accept: text/markdown` negotiation for GET requests
|
|
67
|
+
- HTML-to-Markdown conversion with transparent pass-through for other media types
|
|
68
|
+
- Synchronous provider hook for hand-authored Markdown
|
|
69
|
+
- In-memory and filesystem-backed content stores
|
|
70
|
+
- Remote Markdown discovery with redirect, host allowlist, timeout, and size controls
|
|
71
|
+
- Standalone FastAPI service and `md4a` command-line interface
|
|
72
|
+
|
|
73
|
+
## Installation
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
python -m pip install md4a-fastapi # FastAPI and Flask middleware
|
|
77
|
+
python -m pip install 'md4a-fastapi[server]' # adds the md4a command, client, and service
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
With uv, use `uv add md4a-fastapi` or `uv pip install md4a-fastapi` the same way.
|
|
81
|
+
|
|
82
|
+
| Install | What you get | Extra dependencies |
|
|
83
|
+
| --- | --- | --- |
|
|
84
|
+
| `md4a-fastapi` | ASGI and WSGI middleware, content stores | `beautifulsoup4`, `markdownify`, `fastapi` |
|
|
85
|
+
| `md4a-fastapi[server]` | also the remote client, standalone service, `md4a` command | `httpx`, `anyio`, `uvicorn[standard]` |
|
|
86
|
+
| `md4a-fastapi[flask]` | the same as the base install | none |
|
|
87
|
+
|
|
88
|
+
The WSGI middleware needs nothing beyond the base install and never imports
|
|
89
|
+
FastAPI, so a Flask application can use the base install as it is. `[flask]` and
|
|
90
|
+
`[wsgi]` are accepted as names for it. Without the server extra, importing
|
|
91
|
+
`md4a.client` or running `md4a` says which package to install.
|
|
92
|
+
|
|
93
|
+
### Development setup
|
|
94
|
+
|
|
95
|
+
Using [uv](https://docs.astral.sh/uv/) (recommended):
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
git clone https://github.com/adamtasteslikegood/md4a-fastapi.git
|
|
99
|
+
cd md4a-fastapi
|
|
100
|
+
uv sync --extra test
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
This creates or updates the project virtual environment in `.venv`, installs
|
|
104
|
+
the package in editable mode, and installs the test tools. No activation is
|
|
105
|
+
needed when using the `uv run` commands below. If you prefer to activate the
|
|
106
|
+
environment, use `source .venv/bin/activate` on macOS/Linux or
|
|
107
|
+
`.venv\Scripts\Activate.ps1` in Windows PowerShell, then run the commands
|
|
108
|
+
without the `uv run` prefix.
|
|
109
|
+
|
|
110
|
+
Using Python's built-in venv and pip:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
git clone https://github.com/adamtasteslikegood/md4a-fastapi.git
|
|
114
|
+
cd md4a-fastapi
|
|
115
|
+
python -m venv .venv
|
|
116
|
+
source .venv/bin/activate
|
|
117
|
+
python -m pip install --upgrade pip
|
|
118
|
+
python -m pip install -e '.[test]'
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## FastAPI middleware
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
from fastapi import FastAPI
|
|
125
|
+
from fastapi.responses import HTMLResponse
|
|
126
|
+
|
|
127
|
+
from md4a import FileStore, add_md4a
|
|
128
|
+
|
|
129
|
+
app = FastAPI()
|
|
130
|
+
add_md4a(app, store=FileStore(".md4a-cache"))
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@app.get("/", response_class=HTMLResponse)
|
|
134
|
+
def home() -> str:
|
|
135
|
+
return "<h1>Hello agents</h1><p>This remains HTML for browsers.</p>"
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Request either representation from the same URL:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
curl http://localhost:8000/
|
|
142
|
+
curl -H 'Accept: text/markdown' http://localhost:8000/
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
For hand-authored Markdown, provide a callable that returns Markdown or `None`:
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
pages = {"/": "# Hello agents\n"}
|
|
149
|
+
add_md4a(app, provider=lambda key: pages.get(key), convert_html=False)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Provider and cached content take precedence. Otherwise, when `convert_html=True`,
|
|
153
|
+
the middleware executes the normal route, converts a successful HTML response,
|
|
154
|
+
caches it by path and query string, and responds as `text/markdown`. JSON and other
|
|
155
|
+
response types pass through unchanged.
|
|
156
|
+
|
|
157
|
+
Converted responses retain application headers, including security headers, CORS,
|
|
158
|
+
and repeated `Set-Cookie` fields. md4a recalculates the content type and length,
|
|
159
|
+
merges `Accept` into `Vary`, and removes HTML-specific validators, digests, and
|
|
160
|
+
range metadata. Compressed, partial, and trailer-bearing responses pass through
|
|
161
|
+
unchanged.
|
|
162
|
+
|
|
163
|
+
Only successful HTML and Markdown responses are buffered, up to `max_html_bytes`
|
|
164
|
+
(5 MB by default; pass `max_html_bytes=` to `add_md4a`). Everything else, including
|
|
165
|
+
streaming responses and bodies over the limit, is forwarded as it arrives and is
|
|
166
|
+
neither converted nor cached. Bodies are decoded with the charset declared in
|
|
167
|
+
`Content-Type` (UTF-8 when absent); a response that cannot be decoded is returned
|
|
168
|
+
unchanged and is not cached.
|
|
169
|
+
|
|
170
|
+
For headers that must be present on **every** response, register your security/CORS
|
|
171
|
+
middleware **after** `add_md4a(app)` so it wraps md4a. Cache hits and provider
|
|
172
|
+
responses skip the wrapped application; stores hold Markdown text only, not
|
|
173
|
+
response headers. Use caching for public, non-personalized content, and keep
|
|
174
|
+
authentication outside md4a: route-level authentication does not run on these
|
|
175
|
+
shortcuts. See [middleware ordering](QUICKSTART.md#middleware-ordering-and-response-headers).
|
|
176
|
+
|
|
177
|
+
### Converting only the content
|
|
178
|
+
|
|
179
|
+
By default the whole HTML document is converted, navigation and footer included.
|
|
180
|
+
`content_selector` is a CSS selector; only its first match is converted.
|
|
181
|
+
`strip_selectors` are CSS selectors whose elements are removed first:
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
add_md4a(app, content_selector="main", strip_selectors=[".related", "form"])
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
A page the content selector does not match is converted whole. Provider, cached,
|
|
188
|
+
and application-authored Markdown are not affected.
|
|
189
|
+
|
|
190
|
+
### Escaping untrusted text
|
|
191
|
+
|
|
192
|
+
Text on a page can contain Markdown syntax that an agent would read as a link or
|
|
193
|
+
image, for example a user comment such as `[Free coupons](https://evil.example)`.
|
|
194
|
+
Two options keep such text inert. Both are off by default.
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
add_md4a(app, escape_misc=True, literal_selectors=['[data-md="plain"]'])
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
- `escape_misc=True` escapes Markdown punctuation (`[`, `]`, `#`, `>`, and the
|
|
201
|
+
like) in all page text, so it cannot form links, images, or headings. Some
|
|
202
|
+
text gains backslashes, which Markdown readers drop.
|
|
203
|
+
- `literal_selectors` are CSS selectors for elements whose text is emitted as an
|
|
204
|
+
inline code span. Escaping does not stop bare URLs and email addresses from
|
|
205
|
+
being linked, but nothing inside a code span is, so use this for user-written
|
|
206
|
+
text. Whitespace is collapsed to single spaces and nested markup is kept as text.
|
|
207
|
+
A matched link, image, or code element is replaced by the span, so its own
|
|
208
|
+
syntax cannot wrap the text; in a table cell, pipes are escaped.
|
|
209
|
+
|
|
210
|
+
Use the two together: without `escape_misc`, a stray backtick in the page's own
|
|
211
|
+
text can open a code span that runs into the literal one. Both options work the
|
|
212
|
+
same in the WSGI middleware.
|
|
213
|
+
|
|
214
|
+
## Flask and other WSGI applications
|
|
215
|
+
|
|
216
|
+
`MarkdownForAgentsWSGIMiddleware` applies the same negotiation to a WSGI
|
|
217
|
+
application. It takes the same options as `add_md4a`:
|
|
218
|
+
|
|
219
|
+
```python
|
|
220
|
+
from flask import Flask
|
|
221
|
+
|
|
222
|
+
from md4a import FileStore, MarkdownForAgentsWSGIMiddleware
|
|
223
|
+
|
|
224
|
+
app = Flask(__name__)
|
|
225
|
+
app.wsgi_app = MarkdownForAgentsWSGIMiddleware(app.wsgi_app, store=FileStore(".md4a-cache"))
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
@app.get("/")
|
|
229
|
+
def home() -> str:
|
|
230
|
+
return "<h1>Hello agents</h1><p>This remains HTML for browsers.</p>"
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
The rules above apply unchanged: provider and cached content take precedence,
|
|
234
|
+
only successful HTML and Markdown `GET` responses up to `max_html_bytes` are
|
|
235
|
+
buffered, application headers are kept, and compressed, partial, and oversized
|
|
236
|
+
responses pass through. The cache key is the percent-encoded `SCRIPT_NAME` plus
|
|
237
|
+
`PATH_INFO`, followed by `?` and the query string when there is one.
|
|
238
|
+
|
|
239
|
+
A response the application sends through the legacy `write()` callable passes
|
|
240
|
+
through unconverted. Importing `md4a.wsgi`, or `MarkdownForAgentsWSGIMiddleware`
|
|
241
|
+
from `md4a`, does not import FastAPI or Starlette, although the base install
|
|
242
|
+
still includes them.
|
|
243
|
+
|
|
244
|
+
Use WSGI conversion for ordinary buffered page responses, not streaming HTML
|
|
245
|
+
iterables. The middleware must see the complete eligible body before it can choose
|
|
246
|
+
converted headers or an unchanged size/decoding fallback, so that conversion path
|
|
247
|
+
cannot preserve PEP 3333 asynchronous block-boundary scheduling. Responses whose
|
|
248
|
+
initial status and headers establish pass-through return the application's original
|
|
249
|
+
iterable, including `wsgi.file_wrapper` objects used for optimized file transfer.
|
|
250
|
+
|
|
251
|
+
As with ASGI, cache and provider hits return before the wrapped application runs,
|
|
252
|
+
so wrap only public, non-personalized pages and keep authentication outside md4a.
|
|
253
|
+
|
|
254
|
+
## Cache lifetime
|
|
255
|
+
|
|
256
|
+
Cached Markdown is served until the store drops it, and by default a store never
|
|
257
|
+
does: a converted page stays as it was first rendered. Give the store a lifetime,
|
|
258
|
+
bound it, or stop caching responses:
|
|
259
|
+
|
|
260
|
+
```python
|
|
261
|
+
from md4a import FileStore, MemoryStore, add_md4a
|
|
262
|
+
|
|
263
|
+
add_md4a(app, store=MemoryStore(ttl=300, max_entries=1000)) # 5 minutes, 1000 pages
|
|
264
|
+
add_md4a(app, store=FileStore(".md4a-cache", ttl=3600)) # 1 hour, on disk
|
|
265
|
+
add_md4a(app, cache_responses=False) # convert every request
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
- `ttl` is in seconds. An entry that old is treated as missing, so the route runs
|
|
269
|
+
again and the fresh result is stored.
|
|
270
|
+
- `max_entries` (`MemoryStore` only) drops expired entries, then the oldest stored,
|
|
271
|
+
when the limit is reached. The cache key includes the query string, so set it
|
|
272
|
+
when callers control the query.
|
|
273
|
+
- `store.delete(key)` and `store.clear()` remove entries by hand, for example after
|
|
274
|
+
publishing a change.
|
|
275
|
+
- `cache_responses=False` stops md4a writing responses to the store. Content already
|
|
276
|
+
in the store, and provider content, is still served.
|
|
277
|
+
|
|
278
|
+
## Remote discovery and standalone service
|
|
279
|
+
|
|
280
|
+
These need the server extra: `python -m pip install 'md4a-fastapi[server]'`.
|
|
281
|
+
|
|
282
|
+
With the uv-managed project environment, run the CLI with `uv run`:
|
|
283
|
+
|
|
284
|
+
```bash
|
|
285
|
+
# Show all commands
|
|
286
|
+
uv run md4a -h
|
|
287
|
+
|
|
288
|
+
# Verify that the origin returns text/markdown, then save it
|
|
289
|
+
uv run md4a fetch https://blog.cloudflare.com/the-agentic-internet/ -o article.md
|
|
290
|
+
|
|
291
|
+
# Start the standalone API
|
|
292
|
+
uv run md4a serve --host 127.0.0.1 --port 8000
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
After installing with pip, use the same commands without the `uv run` prefix:
|
|
296
|
+
|
|
297
|
+
```bash
|
|
298
|
+
# Show all commands
|
|
299
|
+
md4a -h
|
|
300
|
+
|
|
301
|
+
# Verify that the origin returns text/markdown, then save it
|
|
302
|
+
md4a fetch https://blog.cloudflare.com/the-agentic-internet/ -o article.md
|
|
303
|
+
|
|
304
|
+
# Start the standalone API
|
|
305
|
+
md4a serve --host 127.0.0.1 --port 8000
|
|
306
|
+
curl -H 'Accept: text/markdown' \
|
|
307
|
+
'http://127.0.0.1:8000/fetch?url=https%3A%2F%2Fblog.cloudflare.com%2Fthe-agentic-internet%2F'
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
The uv and pip commands above start the same standalone FastAPI service. Keep
|
|
311
|
+
the server running in one terminal and use the `curl` request from another.
|
|
312
|
+
|
|
313
|
+
The remote client sends `Accept: text/markdown` and only considers the page
|
|
314
|
+
available when the origin confirms `Content-Type: text/markdown`. Redirects are
|
|
315
|
+
validated before following them, and responses are capped at 5 MB by default.
|
|
316
|
+
|
|
317
|
+
Standalone configuration:
|
|
318
|
+
|
|
319
|
+
| Variable | Default | Purpose |
|
|
320
|
+
| --- | --- | --- |
|
|
321
|
+
| `MD4A_CACHE_DIR` | `.md4a-cache` | Directory for fetched Markdown and its index |
|
|
322
|
+
| `MD4A_ALLOWED_HOSTS` | unrestricted | Comma-separated hostname allowlist |
|
|
323
|
+
| `MD4A_ALLOW_PRIVATE_ADDRESSES` | off | Set to `1`, `true`, or `yes` to permit non-public destinations |
|
|
324
|
+
|
|
325
|
+
The client refuses destinations that are, or resolve to, loopback, private,
|
|
326
|
+
link-local, or other non-public addresses, on the first request and on every
|
|
327
|
+
redirect, and connects to the address it checked. This applies to allowlisted hosts
|
|
328
|
+
too. Behind an environment proxy the proxy resolves names, so only literal addresses
|
|
329
|
+
and hosts that bypass the proxy are checked; see [SECURITY.md](SECURITY.md). For local development, pass `allow_private_addresses=True` to
|
|
330
|
+
`MarkdownForAgentsClient`, `--allow-private-addresses` to `md4a fetch`, or set
|
|
331
|
+
`MD4A_ALLOW_PRIVATE_ADDRESSES` for the service.
|
|
332
|
+
|
|
333
|
+
Always set `MD4A_ALLOWED_HOSTS` before exposing the standalone fetch service to
|
|
334
|
+
untrusted users. An unrestricted URL-fetching endpoint can create an SSRF risk.
|
|
335
|
+
|
|
336
|
+
## Documentation
|
|
337
|
+
|
|
338
|
+
- [Quick start](QUICKSTART.md)
|
|
339
|
+
- [Contributing](CONTRIBUTING.md)
|
|
340
|
+
- [Security policy](SECURITY.md)
|
|
341
|
+
|
|
342
|
+
## Project scope
|
|
343
|
+
|
|
344
|
+
`md4a-fastapi` focuses on FastAPI/ASGI integration, a WSGI middleware with the same
|
|
345
|
+
behavior, and verified Markdown retrieval.
|
|
346
|
+
It does not attempt to reproduce a browser, execute JavaScript, or claim that
|
|
347
|
+
converted Markdown is identical to a publisher-authored agent representation.
|
|
348
|
+
|
|
349
|
+
## License
|
|
350
|
+
|
|
351
|
+
Licensed under the [MIT License](LICENSE).
|