Skip to content

0.2.17

Choose a tag to compare

@DEENUU1 DEENUU1 released this 25 Jul 14:16
· 7 commits to main since this release

[0.2.17] - 2026-07-25

Added

  • MCP client — new enable_mcp_client flag (off by default, PydanticAI + PostgreSQL only): end users connect external Model Context Protocol servers from Settings → Integrations and the agent picks up their tools. Ships a curated marketplace catalog (Notion, Linear, Jira/Confluence, Stripe, GitHub, Zapier, Exa, CoinGecko, …) with brand logos baked in as data URIs, per-user connections with Fernet-encrypted tokens, a connectivity test that lists the server's tools so the user can pick which ones the agent may call, and a per-chat "Plugins" tab to toggle them. Auth is either a static bearer token or one-click OAuth 2.1 (authorization-code + PKCE, RFC 9728/8414/7591 discovery and dynamic client registration), over both streamable-HTTP and SSE transports. Deployment-wide servers are pinned via MCP_SERVERS. Every URL the flow touches — including each redirect hop and every endpoint the remote server advertises — goes through the same SSRF policy as webhooks. Tools are prefixed per server, and an unreachable or unauthorized server is skipped for that turn instead of failing the chat. CLI: --mcp-client on create + wizard step (#124)
  • Version-upgrade toolingfastapi-fullstack upgrade pulls template improvements into an already-generated project without losing local edits. It renders the template at both the generated-from and target versions using the answers stored in .fastapi-fullstack.json, normalizes all three trees the way generation did (ruff check --fix + ruff format, Prettier on the frontend) so formatting can't read as an edit, and runs a real 3-way merge onto a template-upgrade/v<version> branch. Structural changes between releases (renames, removals, breaking notes, manual steps) are declared in UPGRADES.yaml, with a Rename guard CI job that fails any PR moving a template file without recording it. upgrade recover bootstraps a manifest for projects generated before this existed. Documented in docs/guides/version-upgrade.md (#114)

Fixed

  • MCP tools were missing on the channel pathagent_invocation.py is the second place the PydanticAI agent is built (Slack/Telegram), and it never received the user's MCP toolsets. A connection configured in Settings worked in web chat and silently did nothing in a channel, with no error on either side. build_toolsets_for_user now takes an optional user_id, so channel traffic with no mapped account still gets the deployment-managed MCP_SERVERS (#130)

  • Two chat turns could both spend the same OAuth refresh token — providers that rotate refresh tokens invalidate whichever copy is redeemed second, after which the connection stops working with nothing to point at. The refresh now happens under SELECT ... FOR UPDATE; the losing turn re-reads the row and uses the token the winner stored (#130)

  • An abandoned OAuth consent redirect stayed redeemable forever — the state token is the only thing authenticating the callback, and it travels through the provider and the browser's history. A pending flow now expires after FLOW_TTL_SECS, and a pending payload that can't be decrypted (rotated SECRET_KEY) asks the user to start again instead of raising out of the route as a 500 (#130)

  • A bad or duplicated MCP_SERVERS name silently dropped a server from every chat turn — the name is that server's tool prefix, so two servers sharing one prefix meant only the first was ever attached, visible nowhere but a log line. Names are now validated as slugs and checked for duplicates at startup, and the .env.example suggests github-internal, which can't collide with the marketplace's github (#130)

  • The MCP client had no CI job — the integration matrix ran only ruff + ty against an MCP render, so the generated project's MCP test suite and the frontend tsc/eslint never executed for that configuration. Adds Template - PostgreSQL + MCP Client, which renders with --slack (the only build containing agent_invocation.py) and runs both the backend and frontend suites (#130)

  • The plugin marketplace fetched 14 favicons from Google on every settings page view — the logos were already baked in as data URIs for the offline demo export; the Settings UI now uses those, so the app doesn't tell a third party which plugins its users are browsing. A catalog entry whose name a workspace server already claims now reads "Provided by your workspace" instead of offering a Connect button that would create a connection dropped on every turn, and editing a connection sends only the fields that changed, so renaming one no longer resets its last-checked status (#130)

  • A file replacing an untracked symlink landed on disk but never on the branch — the untracked-symlink exemption exists so a stray link where the template now ships a file doesn't abort the run, but it was subtracted from the staging set too. The link was replaced, the file was written, and git add skipped it: a silently partial upgrade, with the new file showing as untracked on a branch that claimed to be complete. materialize now keeps the two sets apart — tracked symlinks are the "never deleted, never restaged" guarantee, untracked ones only waive the collision guard

  • tar -x read $TAPE instead of the merged tree — the extraction ran with no -f, so tar falls back to the archive named by $TAPE (or a compiled-in default device). With that variable exported the whole extraction became a silent no-op — exit 0, nothing written — and the upgrade then staged the old content as if the merge had produced it. Now tar -x -f -

  • A broken symlink hid an untracked-file collision — the guard used exists(), which follows the link, so a dangling one read as absent and was overwritten without warning. .gitignore keeps such a link out of ls-files --others too, so nothing else caught it either. The guard now uses lexists

  • The rename guard still failed open on one path — a 404 was treated as "no published baseline yet" for either PyPI call, so a 404 fetching the wheel of a version PyPI says exists skipped the guard entirely and an unrecorded rename shipped on a green build. Only failing to resolve the latest version can mean "nothing published"; a failed template fetch is now exit 2

  • The rename guard printed a block its own stale-check would reject — the suggested - version: came from get_generator_version(), which mid-cycle is the baseline, and the half-open (from, to] range filters those straight back out. Pasting the suggestion turned an "uncovered" failure into a "stale" one. It now emits a <next-release> placeholder plus the reason whenever the working tree isn't already bumped

  • A transient PyPI blip silently downgraded the wheel download to unverified — the digest was fetched in a second metadata request, and any failure there returned None, which the caller reads as "no digest known". One request now carries both the URL and its sha256

  • Prettier's exit code is checked, like ruff's. Prettier formats what it can parse and exits non-zero on the rest, so one tree hitting an unparseable file came out formatted differently from the other two — and the "did it run" return value couldn't carry that, so the evenness warning stayed silent

  • A malformed UPGRADES.yaml block is rejected where the file is read, naming the file and the offending entry. Three consumers index r["from"] / r["to"] directly, so a hand-edit typo used to surface as a bare KeyError from whichever release script happened to run

  • read_manifest rejects a non-object context. It is the one key every consumer indexes into, so a hand-edit that turned it into a list got past the presence check and failed several modules downstream

  • Recovery lost the generation timestamp, so a legacy project's first upgrade conflicted on files nobody touchedgenerated_at is stamped into backend/pyproject.toml and into every alembic Create Date: header, and normalize blanks it in all three trees so it cancels out — but only when it knows the value. A recovered manifest never carried one, so strip_generated_at was a no-op, BASE and THEIRS rendered the stamp empty while the client's files carried the real one, and every stamped file read as an edit they never made. Measured on a minimal project: 3 of 208 files differed with zero client edits, one of them backend/pyproject.toml — which moves on nearly every release, so the guide's flagship "recover a legacy project" flow opened with a conflict. Alone among the value variables this one leaves a trace, so recovery now reads it back out of backend/pyproject.toml, and warns loudly when it can't

  • Prettier skipped the one tree that already had itformat_frontend bailed out whenever the tree already contained frontend/node_modules, which is exactly OURS when the client committed theirs and never the freshly rendered BASE/THEIRS. That is the uneven-formatting case the previous fix added a warning for; the install sitting in the tree is a perfectly good one, so it is now used in place (and left alone afterwards) instead of being a reason to skip

  • Directories left empty by a removal are pruned. git tracks files, not directories, so a release that dropped a whole subtree unlinked its files and left the folders behind for good — git checkout prunes them, hand-rolled deletion has to say so. A directory still holding an untracked or ignored file survives

  • _iter_text_files prunes skipped directories instead of filtering them one entry at a time. rglob cannot prune, and restore_generated_at runs over the client's whole repo, where node_modules alone is routinely six figures of entries

  • Upgrading a project that wasn't its own git repo deleted the project — nothing checked that the target directory was the repository root, and the two halves of the merge disagree about what a path means anywhere else: checkout-index builds OURS from index paths (repo-root-relative) while ls-tree run through git -C <subdir> emits cwd-relative ones. For a project at monorepo/myapp the rendered BASE/THEIRS therefore shared no path with OURS, every template file read as a client deletion, and materialize unlinked the client's real files and staged the deletions — with no error and a report claiming the template had removed them. upgrade now refuses up front (dry runs included, since the preview is wrong the same way)

  • The rename guard could fabricate a rename and then pass itself — CI honoured the removed: / waived: waivers recorded in UPGRADES.yaml, but scripts/record_renames.py only looked at its own --waive flag. A maintainer's intentional delete+add was therefore re-detected and written back as a rename, after which CI went green because the move was "covered" — and the client's next upgrade moved their copy of a deleted file onto an unrelated path. Both halves now read the same recorded_waivers()

  • Stale directory renames slipped past the release guard — the stale-version check only matched exact (from, to) entries, so a file covered by a directory rename a/ → b/ was neither uncovered nor stale. If that entry was recorded under a version <= the baseline, the half-open range dropped it and the whole subtree degraded to delete+add — the highest-stakes case, since one entry covers many files. The check now resolves the covering entry and reads the version from it

  • Uneven formatting across the three trees was undetectable — the merge is only sound if BASE, OURS and THEIRS are formatted identically, and both formatters already returned whether they had run, but normalize_tree discarded the answer. format_frontend bails out when a tree already contains frontend/node_modules — true for OURS whenever the client committed theirs, never for the freshly rendered BASE/THEIRS — so Prettier ran on two trees out of three and every .tsx file read as a client edit. normalize_tree now returns a FormattersRun and the upgrade warns when the three disagree

  • Two harmless, common outcomes were reported as "Other changes (review on the branch)" — a file the client deleted while the template still ships it, and a file both sides deleted. Neither needs any action, and the label read as "the upgrade lost my file". They now have their own report line ("You deleted these") and are folded into "Already converged" respectively

  • upgrade finalize raised a bare KeyError on a hand-edited .fastapi-fullstack.json.pending that was missing package_version; it now fails with a message that says what to delete. A corrupt (non-JSON) pending file gets the same treatment

  • The "invalid version" error from the template fetcher said target version even when it came from the manifest's package_version — which is "UNKNOWN" after a recovery that couldn't read the README footer, sending the user to look at a --to they never passed

  • make upgrade-finalize didn't pass $(ARGS) through, so --path was unreachable from make

  • .fastapi-fullstack.json.pending / .candidate are now gitignored in generated projects, so the "commit your resolved conflicts" step can't sweep the pending manifest into the commit

  • Upgrade read a third of the backend as edits you never made — the post-gen hook runs ruff check --fix and ruff format at generation time, but the upgrade's normalization ran only format. OURS therefore arrived with unused imports stripped and # ruff: noqa headers removed while the freshly rendered BASE/THEIRS still carried them, so 36 of 127 backend files in a stock PostgreSQL project differed with zero client edits in them. The report labelled them "Kept your changes (template unchanged)", and the next release touching any of their import blocks would have conflicted instead of auto-updating. normalize_tree now takes rendered=True for BASE/THEIRS and runs the autofix pass on them only — never on OURS, whose code must not be rewritten into the merged result. Measured on the same project: 36 spurious differences → 1 (the real edit), and the report goes from 30 false "Kept your changes" to 8 honest "Auto-updates"

  • Generation and upgrade crashed under a non-UTF-8 locale — the post-gen empty-file sweep, is_stub_file, and the __init__.py check all read rendered files with the platform default encoding. The template ships 209 .md/.mdx/.ts/.tsx and 163 .py files with non-ASCII content, so on Windows cp125x or a LC_ALL=C container generation died with UnicodeDecodeError — and so did rendering BASE/THEIRS during an upgrade. All three now read as UTF-8 through a shared read_text helper

  • upgrade --path X finalize told the user to do the thing that had just failed — the misplaced-flag guard advised "put it before the subcommand", which is exactly the form it rejects. finalize and recover carry their own --path, so the message now points at upgrade finalize --path ...; flags with no subcommand equivalent still say to drop it

  • RAG disabled left a broken frontend — generating a project with RAG off and Teams on kept the org "Integrations" screen (orgs/[id]/integrations) and its API proxy, while the post-gen hook removed every module they import (@/lib/rag-api, @/components/rag/*, use-org-integrations), so next build failed on four missing modules. The page and proxy are now removed with the rest of the RAG frontend, and the button linking to them is gated in orgs/page.tsx — the backend drops /api/v1/org/integrations in that configuration anyway (#128)

  • Code-execution tool broken against pydantic-monty 0.0.19 — the sandbox dependency was pinned >=0.0.18, and 0.0.19 replaced the one-shot Monty.acreate() / run_async() API with a worker pool (AsyncMontycheckout()session.feed_run()) and dropped the max_allocations resource limit. Generated projects with enable_code_execution therefore failed ty check and would have raised AttributeError on the first run_python call. code_execution.py now uses the session API, the pin is >=0.0.19, and CODE_EXECUTION_MAX_ALLOCATIONS is replaced by CODE_EXECUTION_MAX_MEMORY_MB (default 256, mapped to the max_memory limit in bytes)

  • Empty feature-gated files shipped in generated projects — the post-gen stub sweep only walked backend/app/**/*.py, so a doc or frontend module whose whole body sits behind a feature conditional was written out as an empty file instead of being removed: docs/howto/add-rag-source.md, add-sync-connector.md, configure-sync-sources.md with RAG off, plus four dead frontend modules. The sweep now covers .md, .mdx, .ts, and .tsx as well (#128)