Two cleanup-plan items, both Python-side.
Item 29 -- `mirage mcp`. TypeScript shipped a six-tool stdio MCP server;
Python had none, so a pip-install user could not point Cursor or Claude
Desktop at a workspace and `mirage --help` differed by distribution.
Adding the entry point alone would have duplicated the tools, because
this side kept them private inside the Claude Agent SDK integration, so
the shared layer comes first:
- agents/tool_descriptions.py -- the six strings, one copy.
- agents/tool_operations.py -- MirageToolOperations, lifted out of the
SDK server's private _MirageTools.
- agents/file_version.py -- stale-write protection, which this side
lacked entirely. TS stamps stored bytes; here the stamp covers the
rendered bytes, because this read tool has always rendered and an
edit must search what the agent was actually shown.
- agents/mcp/server.py + cli/mcp.py -- the server and `mirage mcp`.
- server/workspace_config.py -- config discovery (candidates, env
names, walk up from cwd), which Python had nowhere, so every entry
point had to be handed an explicit path.
The server is the low-level MCP Server rather than FastMCP: FastMCP does
not forward a version, and TS advertises one. Handlers are bound methods,
not decorated closures, so nothing nests.
Item 28 -- the mypy allowlist. 54 modules opted *in* to annotation
checking against 1826, so the default was unchecked and every new file
joined the unchecked side. The default is now strict, with a list of
what is not yet annotated that only shrinks. 166 annotations cleared
along the way; the remainder is named module by module.
Two real defects surfaced by the annotations, neither of them typing:
- Workspace._original_open / _original_os were invented by assignment
in lifecycle.patch_process, so unpatch without a patch raised
AttributeError. Declared, and the restore is guarded.
- sed_generic declared a non-optional writer while its own docstring
and its `write_bytes is None` branch said otherwise; the builder
passes None whenever the backend cannot write.
Tests keep the PathSpec rule instead of full strict: measured, full
strict on python/tests is 2374 errors, of which 634 are `str` where a
pydantic field declares SecretStr -- which pydantic coerces at runtime --
and most of the rest is the monkeypatched-fake pattern CLAUDE.md
sanctions. The rule that is violated for real is PathSpec, 19 times, and
scripts/check_test_pathspec.py now holds that line. One of the 19 was a
latent AttributeError: tests/e2e passes a str to s3 write_bytes, which
reads .mount_path, and the test skips without a live versioned bucket.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(sizes): render-at-readdir sizes for github_ci, trello and notion
Batch 2 of the fskit size push-down (plan: ~/Desktop/fskit.md). Each
listing already carries the payload read renders, so readdir seeds a
sized IndexEntry and stat serves it. Zero added API calls.
github_ci workflow/run/job JSON and trello workspace/board/list/card
JSON use the shared node-dir shape: the parent readdir set_dirs each
child with a sized node.json, and the child readdir consults list_dir
before bootstrapping the parent. Notion differs because a database dir
listing is dynamic, so database.json's size travels in
extra[database_json_size].
Artifacts stay class T: size_in_bytes is the zip's exact byte length,
confirmed against two live samples. Flags stay False for all three:
job logs, annotations, comments.jsonl and page.json are class N.
integ gains a github_ci target backed by a fixed Actions dataset in the
fake server, with 24 stat==read pins.
* feat(sizes): render-at-readdir sizes for jaeger, qdrant and lancedb
Batch 3 of the fskit size push-down. qdrant and lancedb flip
SIZES_ALWAYS_KNOWN; jaeger stays False because operations.json is class
C and traces are class P.
jaeger traces are genuinely P: verified live that the /api/traces search
document renders byte-identically to the by-id fetch for all four seeded
traces, including the one listed under two services. operations.json is
sized by one /api/operations call at the service dir readdir.
lancedb needed table_columns/tableColumns to widen the select beyond
ids; it excludes vector and blob columns in schema order so projected
rows render byte-identically. Blob entries stay unsized on purpose,
since sizing a blob at listing time would let one undecodable value fail
the whole directory listing rather than just that file's read.
A previously pinned integ expectation was wrong: jg_read_28 asserted du
-sh printed 0B, which was an artifact of unknown sizes, and now prints
the real total.
* feat(sizes): sizes for discord, email, langfuse and chroma
Batch 4, the last of the fskit size push-down. discord, email and chroma
flip SIZES_ALWAYS_KNOWN; langfuse stays False because traces and prompts
are class N.
chroma's producer-supplied size was measurably wrong for every file in
our own fixture: 180 declared against 166 rendered, 90 against 52, and
150 against 159, which truncated reads. It describes the source document
rather than the chunk join mirage serves, so it can never be the byte
length. It moved to extra.source_size and the real size is measured by
one batched chunk scan per directory, run lazily from stat and cached.
This also corrected provenance byte accounting and du totals.
langfuse items.jsonl follows the class C shape: the dataset dir readdir
makes the one call and seeds a sized entry.
Carries the discord fake-API arc. The new integ/server/discord_server.py
exposed four real backend bugs: message pagination used the oldest id as
the after cursor when discord answers newest-first, attachments were
classified as directories, unknown paths returned an empty listing
instead of ENOENT/ENOTDIR, and a cold ls of a guild dir ENOENTed in
python only.
* fix(sizes): propagate backend failures from the stat parent-listing fallback
Review follow-up on the stat fallback these three backends use to
populate a cold index: the TypeScript side caught every error from the
parent readdir and fell through to ENOENT, so an auth failure, a rate
limit or a transport error read back as "file not found". Python already
caught only FileNotFoundError. Now only isMissingPath errors fall
through, which is the rule that helper's own contract states.
Reachable through the new size lookups, since workspace.json and the
other sized files resolve through this path on a cold index.
Also pins that discord's base_url config key reaches DiscordResource as
baseUrl through the registry: normalizeFields snake-to-camels by default
so no explicit rename is needed, and a silent miss there would send
configured mounts to the public API.
* refactor(types): stdin annotations use the ByteSource alias
* refactor(generics): injected ops are path-only, accessor+index bind at the wrapper
Generic commands no longer take accessor (or a dead index) just to
thread them back into injected callables. Builders and bespoke wrappers
bind both via bound_op (None-passthrough) or partial for write-side
ops, mirroring the TS builders' closures. call_*/resolve_pattern/relay
helpers and the cache read-through wrappers gain path-first forms;
CommandIO-level ops keep the raw (accessor, path, index) shape.
* refactor(index): NULL_INDEX everywhere, no index-is-None branches
index is a required IndexCacheStore on every op and wrapper; callers
with no real index pass NULL_INDEX (the null object built for exactly
this). Dead is-None guards drop out of trello/linear/github_ci/google
readdir+stat, CacheManager, and the github wrappers.
* style(types): parameterize every bare generic, gate with disallow_any_generics
dict -> dict[str, Any] (payloads) / dict[str, object] (flag bags),
Callable -> Callable[..., Any], plus list/tuple/Awaitable/Pattern/
Task/Future/Token/AsyncMongoClient. The mypy gate now carries
disallow_any_generics so bare generics cannot come back.
* style: pre-commit formatting + parameterize main's new bare dicts
* fix: post-merge gate fixes + make security barriers analyzer-recognizable
- resolve_within_root: startswith(root + sep) guard instead of
commonpath (same semantics; the shape CodeQL models as a
path-injection barrier)
- cli table output: trimEnd() instead of the polynomial /\s+$/ trim
- example proxies: re-encode validated endpoint segments before URL
construction (recognized SSRF barrier; no-op for the allowed charset)
- parameterize bare generics arriving from #540 (the new mypy gate
caught them)
* fix(server): plain startswith barrier in resolve_within_root
* Fix latent type and lifecycle bugs
* fix: close ownership for shared resources, gate find -empty, gdrive populate parity, drop mongo DriverInfo
* style: yapf/isort formatting on merged test files
* fix: fan-out grep keeps GNU any-match exit 0, mock github_ci stat populate readdir, current mock s3 mtime
* fix(ts): mirror close ownership, shared stores and copy-shared resources stay open
* refactor: shared op-fn aliases and copy/move strategies in mirage.types, tar package with types and constants
* refactor: single mtime-window primitive, drop msgraph.time re-export, github declares supportsSnapshot
* fix: find -mtime keeps unknown-mtime dirs (s3 integ), mktemp -p PATH-kind spec parity
* fix(find): remote -mtime reverts to TS parity, onedrive root stat carries mtime
- generic_bind find builder: stat filter only for local backends (matches
main + TS; remote backend find ops ignore mtime like the TS s3 core)
- s3/onedrive core find: drop in-core mtime filtering, document the
deliberate no-op in the s3 docstring
- onedrive stat: mount root fetches the real Graph root item so
size/modified are populated (du/find no longer see modified=None)
- integ: fake Graph stamps items at run time like moto; bespoke
onedrive suite freezes the clock for deterministic ls -l; stat_dir
case prints stable fields only
* test: drop remote find -mtime tests reverted to TS-parity semantics
* chore(py): shrink mypy baseline 117->84 (resource accessor typing)
Narrow each resource's accessor attribute to its concrete type so the
backend resolve_glob/stat calls type-check; wrap fingerprint stat() paths
with PathSpec.from_str_path (fixes a latent str-to-stat bug); retype the
shared hf_buckets core to _HfAccessor (common base of the hf siblings);
make DevStore a RAMStore subclass (_DevFiles a dict subclass); narrow the
sync redis get_state() reads (redis-py ResponseT is an async/sync union).
Full Python test suite green (9139 passed; fuse excluded, macOS limit).
* chore(py): shrink mypy baseline 84->74 (workspace/executor/shell/resource typing)
* chore(py): clear non-generic mypy errors, shrink baseline 74->56
* chore(py): clear all generic command-layer mypy errors (127->0)
* chore(py): enable mypy as a hard gate (pre-commit + CI), drop ratchet baseline
* fix(py): sed -i guards write availability; dedupe mypy pre-commit hook post-merge
* fix(py): pyproject trailing newline; run mypy on hosted pre-commit too
Resolve cache-consistency conflict: keep the single CacheManager
invalidation path and drop invalidate_index_dirs.
Remove is_remote and cacheable in favor of a single caches_reads flag
declared per resource. Live-insert databases (postgres, mongodb,
chroma) set caches_reads=False so reads always hit the backend;
lancedb caches only for remote-scheme tables. Fix the postgres and
chroma cold grep -e multi-pattern path (route multi-pattern to the
generic grep instead of the single-pattern pushdown).
A unified virtual filesystem for AI agents. Mount S3, Google Drive,
Slack, Gmail, GitHub, Linear, Notion, Postgres, MongoDB, SSH, and
more behind one filesystem so agents read, write, and pipe across
services with familiar shell commands.
Ships Python (mirage-ai) and TypeScript (@struktoai/mirage-*) SDKs,
a CLI, FUSE mounts, and adapters for OpenAI Agents SDK, Vercel AI
SDK, LangChain deepagents, Pydantic AI, CAMEL, OpenHands, Mastra,
Pi Coding Agent, plus FUSE-based integration with Claude Code and
Codex.
Apache 2.0 licensed.