bounded log search, and one module per concern
search_modal_logs
A log search could only be given a start time, so "what happened in
those three seconds" was not expressible: since alone puts the CLI
in range mode and fetches every entry from then until now. Measured on
a busy app, --since 6h is 664KB against a 40k budget and a 30s fetch
deadline, so the search came back truncated and capped; the same query
bounded with --until is kilobytes.
untilbounds the window at both ends.prefilter=Truepushes the pattern down to Modal as a server-side
substring filter (modal app logs --search, 1.5+), so non-matching
lines are never fetched — the lever for logs too large to drain.
Requires regex=False, and context lines then show only other
matches, so the response flagsprefilteredand says so.- Truncation and zero-match messages now say to narrow the window
rather than to raise timeout_seconds, which only fetches more. returnedcounts matched lines actually shown, with the block count
moved toreturned_blocks. It counted blocks before, so 158 merged
matches reported as "showing 1 of 158" and invited a wider re-query.
A window the CLI rejects (reversed range, over 35 days, tail over
20000) now returns Modal's own message instead of "exit 2", and the
bounds it can check locally are checked before spawning a process.
get_modal_logs gets the same treatment: the range-mode caveat is
documented, and follow combined with since/until/tail is refused up
front instead of failing as a CLI usage error.
debug_modal_app teaches the bounded-window workflow (know the time ->
since+until; do not know it -> prefilter to locate, then re-query for
context), and adds a step to check whether sibling apps were hit in the
same window before reporting an incident as isolated to one app.
Module layout
server.py was 2092 lines. It is now the assembly point that imports
the pieces and re-exports the public names, with app.py (the FastMCP
instance and annotation policy), command.py (argv helpers and the two
runners), output.py (caps and the response envelope), logsearch.py and
billing.py (the pure local logic), prompts.py, and one module per tool
group under tools/. The dependency graph is one-directional and
acyclic. from mcp_modal.server import ... still resolves everything.
Every function is byte-identical to 0.4.0 except the three named
above; verified by diffing each tool, prompt and helper against the
previous release, and by comparing the full tool and prompt inventory.
Release plumbing
publish.yml now cuts the GitHub Release for a version tag, attaching
the same artifacts the PyPI job uploaded rather than a rebuild, and is
idempotent so a re-pushed tag refreshes an existing release. Every
earlier version has been given a tag and a release with its published
wheel and sdist, including 0.2.3, which was on PyPI with no tag at all.
Docs
README drops the 0.2.x upgrade table and the stale annotation counts
(four read-only tools and eight mutating, two of them non-destructive)
and documents the new log-search arguments. CLAUDE.md describes the
module layout and the log volume behavior. GUIDELINES.md covers the
release step.
Install
uvx mcp-modal@0.5.0 # run this exact version
claude mcp add mcp-modal -- uvx mcp-modal@0.5.0Also on PyPI: mcp-modal 0.5.0.
The .whl and .tar.gz below are the artifacts published to PyPI for this version.