@MrNiceTry The lists are fully open source already at appwrite/appwrite including the scrapers we use to build them.
1 like · 10 views
A cloud backend for AI agents
Appwrite bundles Auth, databases, storage, functions, messaging and realtime into one self-hostable platform.
🍒 Built by the Appwrite team
appwrite/appwrite
#opensource #typescript
2 likes · 2 reposts
Keeping track of your agent's work
Agents are long-running LLMs, and long-running LLMs lose context. Not sometimes. Eventually, almost always.
The agent doesn't tell you when it happens. It compacts, summarises, or starts a new session, and whatever didn't make the cut is gone. The worse part is that you lose context too, and you usually don't notice either.
How the context goes missing
Three things keep happening to me.
The agent finds something important and mentions it once. Halfway through a task it notices a second bug, a migration that will break existing data, or a flag that does something nobody expected. It writes one line about it, carries on with what you asked for, and never brings it up again. After the next compaction it doesn't know either.
You can't see what it forgot. When I lose track of my own work, I at least know I've lost it. With an agent the context is invisible. I don't know what's in its window, what got summarised away, or which decision from two hours ago is still being honoured.
It writes more than you can read. An agent can produce pages of output, plans, and reasoning in the time it takes me to read one paragraph. Most of it is fine. Some of it matters. Finding the part that matters takes longer than the agent took to write all of it, so I skim, and skimming is how the one important line gets missed.
Put those together and the important stuff ends up nowhere: not in the agent's context, not in your head, and buried somewhere in a transcript nobody will read again.
Give every piece of work a ticket
The fix I've settled on is boring: an issue tracker. One place for each feature or fix I'm working on, where the state of that work lives outside both the agent and me.
A few rules make it work.
Scope it however you like. A ticket can be as broad as "ship passkeys" or as narrow as "fix the logo on the consent screen". When something gets big, split it into sub-issues instead of growing one giant description. The parent says what we're doing and why. The children say what's left.
It has to make sense to you. This is the whole point. If the ticket reads like the agent's scratchpad, you've just moved the gibberish somewhere else. Decisions, open questions, things that were found along the way, what's done, what isn't. Short enough that I can read it in a minute and know where things stand.
The agent keeps it up to date, always. Not at the end. Every time it finds something, decides something, or finishes a step, it goes in the ticket. That one-line mention of a second bug becomes a sub-issue instead of a sentence that disappears after the next compaction.
The side benefit: anyone can pick it back up
Once the state lives in the ticket, the session stops mattering.
If I hit a usage limit mid-task, or a session dies, or I just close the laptop and come back a day later, a fresh agent can read the ticket and carry on. It doesn't need the old transcript or me re-explaining everything. The same goes for a different agent, or a teammate, or me in a week when I've forgotten all of it.
Examples
All of these are from the last month of my own sessions.
Mentioned once, then gone. While fixing text responses in the SDK generator (#1944), the agent noticed Dart models type nullable fields as non-null and offered to leave it out of the PR. I never replied. It never came up again, and the template still only checks whether a field is required.
!Line 12 of the Dart model template on main, adding a question mark only when a property is not required
_Still on main a month later (model.dart.twig)_
A ticket that reads like a status page. #13828 tracked moving our libraries into the monorepo in waves. The agent rewrote the top of it after each wave, so "where we are" was always one glance away.
!GitHub issue 13828 with a Where we are section: waves 1 to 5 done, 46 packages live
_#13828, kept current by the agent_
A finding becomes its own issue. While researching something else, an agent found endpoints returning a 500 for values too long for their column. It filed #14293 instead of mentioning it in passing, so I could hand it to the database team. It's closed now.
!GitHub issue 14293 summary: several endpoints return 500 general_unknown instead of 400
_#14293, filed by the agent_
Parked work anyone can resume. When we deferred the last wave, it got its own issue, #14255, with the branch, the commit and exactly what to do next. Any session, or person, can start from here.
!GitHub issue 14255: the work is done on branch chore/absorb-database at head 9f26684; reopen the PR, merge main and re-run CI
_#14255, the handoff for wave 6_
What I'd tell someone starting
- Assume your agent will lose context. Plan for it instead of hoping it won't.
- Give every feature or fix a ticket. Split big ones into sub-issues.
- Write tickets for a human. If you can't read it in a minute, it's not doing its job.
- Make the agent update the ticket as it goes, not at the end.
- Treat the ticket as the handoff. Any session should be able to start from it.
The agent will forget. The ticket won't.
_Originally published on chiragaggarwal.tech._
1 reaction
My agent has two Playwrights
My agent made 1,929 browser calls through Playwright MCP last month, and almost none of them were tests.
That surprised me, because I do have tests. The Appwrite console has a playwright.config.ts, CI runs four Console E2E jobs on every pull request, and I spent part of the month fixing flakes in them. None of the 1,929 calls, spread over 62 sessions in my Claude Code transcripts between 17 September and 17 October, ran that suite. They used the browser for something else.
So my agent has two Playwrights, and I've stopped treating them as one tool.
Two jobs
The test suite answers one question: did something that used to work stop working? It runs headless in CI, it's mostly deterministic, and it doesn't care what anything looks like.
Playwright MCP answers a different one: does the thing I just built look right? Nobody has written an assertion for that, because an hour ago the thing didn't exist. The agent opens a real browser, goes to the page, clicks through it, reads it back and takes a picture.
!1,929 Playwright MCP calls by tool: navigate 372, take_screenshot 332, evaluate 304, click 247, run_code_unsafe 188, wait_for 160, snapshot 120, resize 45, other 10 tools 161
_Interactive version on the original post._
Screenshots are the second most common call overall. In the sessions where the agent was checking its own work, they're the most common: 281 screenshots against 226 navigations, and 209 of those navigations went to localhost or a local file. It isn't browsing the web. It's looking at what it just made.
browser_evaluate is mostly reading, not poking. Of its 304 calls plus 188 run_code_unsafe calls, 175 use querySelectorAll and 151 read innerText. A handful flip the dark class on <html> so the next screenshot shows the other theme.
This is what one of those pictures looks like. When Appwrite added function intervals as an alternative to cron, the agent built the console picker, then opened it in the browser and screenshotted every state before the PR went up.
!Appwrite console function settings with the Interval tab selected and a dropdown of presets from every 5 minutes to daily
_The interval picker, checked by the agent in a local console before review (#14319)_
Where the calls went
| What the session was for | Sessions | Calls |
| ------------------------------------- | -------- | ----- |
| Checking a UI, docs or release change | 34 | 1,209 |
| Chores in logged-in web apps | 23 | 553 |
| Research | 5 | 167 |
Nearly two thirds is verification. The rest is a different story, which I'll come back to.
Three sessions
"The logo looks off"
I pasted a screenshot of the Appwrite CLI's device sign-in screen and typed four words: "the logo looks off, debug why". The logo was a black mark sitting on a near-black card.
The asset was fine. Every official app logo in that storage bucket is black on transparent, on purpose. The marketplace already knew this: its logo component puts the image on a muted tile and inverts it in dark mode. The OAuth consent screens just didn't use that component.
The fix was to move it into a shared AppLogo and use it in both places. The part I care about came after. I asked the agent to check the other OAuth clients too, and it used the console's debug preview route to render the consent and result screens for Claude Code, Cursor, Codex, Claude Desktop and Gemini CLI in dark mode, then did one pass in light mode. That took 19 Playwright calls, with no errors, and produced eight screenshots.
No test in the suite would have caught this, because no test checks that a logo is visible against its background. If I wrote one now, it would only protect against the last bug.
The OG image X wouldn't show
The passkeys announcement showed a grey placeholder when shared on X. The agent found the cause quickly: the cover image was AVIF, and X only renders JPG, PNG, WEBP and GIF for cards. Every blog and changelog cover in the repo is AVIF, all 538 of them.
The fix was a route that converts the cover to JPEG for og:image and twitter:image only. Then I asked for "pure reproduction and fix verification screenshots in pr desc". The agent built two small pages showing the share card before and after, resized the browser to the card's size and took a screenshot of each. Its first attempt failed because Playwright MCP blocks file:// URLs, so it served the pages over local HTTP instead.
!Two panels. Before: production serves an AVIF twitter:image and X shows a grey placeholder. After: the branch serves a JPEG and the passkeys cover renders.
_The two screenshots that went into the PR description_
Then the suite took over. The review bot flagged two problems, a type-baseline check broke because of the new route, and the four Console E2E jobs ran. Once all 20 checks were green, #14622 was ready. Both Playwrights were in that PR: one showed the reviewer what changed, the other showed that nothing else did.
Release QA for 2.3.0
The biggest session of the month used 183 Playwright calls. I'd asked the agent to "do end to end testing of release notes, especially frontend side" before shipping Appwrite 2.3.0.
It built the 2.3.0 image locally, installed 2.2.0 in a separate Docker Compose project so my dev data stayed safe, seeded it, and upgraded. migrate failed: projects that already existed never got the new pushLedger collection. After fixing that it found a second gap: existing topics were missing the new qos, expiry and sequence fields. Both fixes shipped with regression tests and went into the release branch before anyone upgraded.
To be fair, the browser didn't find the migration bug. migrate exiting with code 1 did. What Playwright did was walk the console against the upgraded install and check each claim in the release notes, one screen at a time. Still, nothing in the test suite runs an upgrade. Release QA used to be manual QA. Now it's manual QA I hand to an agent.
Where the browser does earn its keep is reproducing what users report. After 2.4.0, someone in the community said the upgrade wizard ignored their custom ports. I pasted the feedback and asked the agent to confirm each issue before fixing anything. It installed a stack on ports 8080 and 8443 with HTTPS on, opened the upgrade wizard, and screenshotted it offering 80, 443 and HTTPS off. Then it fixed the bug and took the same screenshot again. Nobody writes a test for this kind of bug until it bites someone.
!Two upgrade wizards side by side. 2.4.0 shows HTTP port 80, HTTPS port 443 and HTTPS off. The fixed version keeps 8080, 8443 and HTTPS on.
_Same install, two upgrade wizards (#14345)_
Every PR gets a picture
This is the habit I care most about. A reviewer can't run your branch, but they can look at it.
The loop has four steps:
1. The agent checks the change with Playwright MCP.
2. It saves each screenshot under a name that says what it shows: passkeys-off.png, passkeys-on.png, cli-device-light.png. 317 of the month's 332 screenshots had a name like that.
3. It writes the PR description with relative image links like  and runs gh pr create --attach or gh pr edit --attach. Recent versions of gh upload each file to GitHub and rewrite the link to point at the upload.
4. It deletes .playwright-mcp/ and any loose PNGs so none of it gets committed.
A good pair needs no explanation. When self-hosted consoles stopped offering OAuth and passkey sign-in, the PR showed the page before and after, and the reviewer could see the change in a second:
!Appwrite console sign-in before and after. Before shows Google, GitHub, GitLab, Bitbucket and passkey buttons. After shows only email and password.
_Self-hosted console sign-in, before and after (#14434)_
Dark mode gets its own screenshot, because that's where things like the invisible logo hide:
!Appwrite console sign-in in dark mode with a Sign in with a passkey button and a Last used badge on the Login button
_Passkey sign-in in dark mode, with the last-used badge (#14400)_
I didn't start with --attach. Over the month, screenshots reached my PRs three ways: committed to an orphan branch on my fork and linked through raw.githubusercontent.com, committed to a repo as docs assets, and attached. A reviewer caught the problem with the first one: _"Screenshot evidence disappears after merge. Screenshots reference an assets branch that the instructions then delete."_ If the screenshots live on a branch you delete, the evidence goes with it. Attachments stay. 32 of the 53 PRs with screenshots now use them.
And 53 out of 904 is the honest number. The rate depends on the repo. In the two public Appwrite repos where most of my UI work lands:
| Repo | PRs | With screenshots |
| ------------------- | --- | ---------------- |
| appwrite/vibes | 39 | 11 (28%) |
| appwrite/appwrite | 224 | 25 (11%) |
Backend-heavy repos sit far lower, which is partly fair, since there's often nothing to look at. But there's a less flattering reason too: I asked for screenshots by hand about 30 times this month ("add screenshots in pr desc", "before and after screenshot in pr desc", "show screenshots of ur testing in pr desc"). The habit only happened when I remembered to type it.
So I made it a rule. This is now in my global CLAUDE.md:
> If a PR changes anything a reviewer can see (UI, rendered docs, emails, OG cards, dashboards), verify it with Playwright MCP and put screenshots in the PR description without being asked: before and after for fixes, light and dark for UI. Name each file for what it shows, reference it as  in the body, and upload with gh pr create --attach / gh pr edit --attach. Never host screenshots on a branch that gets deleted, and never commit .playwright-mcp/ or stray PNGs.
The other 553 calls
These had nothing to do with UI work. Help Scout, the university's Brightspace, the Google Cloud and AWS consoles, an SMS provider's dashboard. These are tools I use daily that either have no API or none I have access to.
The pattern is always the same: "use playwright to login to helpscout, i want you to check some tickets". I log in myself in the browser window, and the agent reads. The browser becomes an API for software that doesn't offer one. That's a different post, but it's why the server stays installed even in weeks when I'm not building UI.
Where it falls short
- About 10% of calls fail. 195 of 1,929 returned an error. The biggest causes were boring: the browser or tab had already closed (36), scripts trying to read files outside the sandbox (25), and timeouts waiting for a page or element (around 30). Four were "Browser is already in use", which usually means two parallel worktree sessions wanted the same browser profile.
- Phones barely exist. There were 45 resizes this month and exactly one was a phone viewport, 390 by 844. Everything else was desktop. That's a blind spot, not a policy.
- file:// is blocked. Any quick HTML mockup has to be served over HTTP first.
- Screenshots land in the repo. By default they go into .playwright-mcp/ at the repository root. Several sessions ended with the agent cleaning up after itself, and in at least one session the screenshots ended up in the main checkout instead of the worktree.
What I'd tell someone starting
- Keep the test suite for anything that must never break. A screenshot is evidence, not a regression test.
- Use the MCP browser for the question the suite can't ask: does this look right?
- Name every screenshot and check both themes.
- Put before and after in the PR as attachments, not links to a branch.
- Write the rule down, so it doesn't depend on you remembering to type it.
The suite catches what broke. The screenshots show the reviewer what's new.
_Originally published on chiragaggarwal.tech._
5 reactions · 1 comment
MCP 2.0: the release that deleted the handshake
MCP versions are dates. 2024-11-05, 2025-03-26, 2025-06-18, 2025-11-25, and now 2026-07-28. From the outside they all look like the same kind of event.
The last one isn't. The working group called the release candidate "the largest revision of the protocol since launch," which usually means a long changelog. Here it means the connection model is gone. No initialize. No session id. No held-open stream. If you'd asked me a year ago to describe MCP in one sentence, that sentence would be wrong now.
Nothing you have deployed today stops working, which is exactly why it's easy to skim past.
The handshake era
Every MCP revision up to and including 2025-11-25 opened the same way. The client sends initialize with its protocol version, its capabilities and its clientInfo. The server answers with the version it picked, its own capabilities and its serverInfo. The client sends notifications/initialized. Only then is the connection allowed to carry anything useful.
That exchange establishes a session, and everything after it is defined relative to that session. Over Streamable HTTP the server could mint an Mcp-Session-Id, which the client then echoed on every request; DELETE ended it, Last-Event-ID resumed a dropped SSE stream.
The design makes complete sense if you remember where MCP started. In November 2024 it was a thing you ran as a subprocess of your editor over stdio. One client, one server, one pipe, and the pipe's lifetime _is_ the session. Negotiating capabilities once at the start costs nothing when there is exactly one of everything.
Streamable HTTP arrived in 2025-03-26 and carried that model onto the network, where "the connection has a memory" stops being free.
!2025-11-25: the load balancer uses sticky routing, so every request goes to replica 1, which holds the session, and replicas 2 and 3 cannot answer
!2026-07-28: the client sends only a bearer token, the load balancer round-robins, and all three replicas can answer anything
_Interactive version on the original post._
So you pick one: sticky routing, or a shared session store. Same problem, different bill. Every deploy drops live sessions. A crashed replica takes conversations with it. And since the session id tells a gateway nothing about what the request wants, the gateway has to open the JSON body to find out whether it's metering a tools/list or a database drop.
!MCP revisions from 2024-11-05 (stdio, one subprocess) through 2025-03-26, 2025-06-18 and 2025-11-25 to 2026-07-28 (no session, no handshake)
_Four revisions of adding things to a connection, then one that removed the connection._
What 2026-07-28 actually does
The handshake is gone. No initialize, no initialized, no Mcp-Session-Id. Everything that used to be negotiated once now travels on every request, in _meta. The same tool call, before and after:
Before: 2 round trips
POST /mcp HTTP/1.1
Content-Type: application/json
{"jsonrpc":"2.0","id":1,"method":"initialize",
"params":{"protocolVersion":"2025-11-25","capabilities":{},
"clientInfo":{"name":"my-app","version":"1.0"}}}
POST /mcp HTTP/1.1
Mcp-Session-Id: 1868a90c-3a3f-4f5b
Content-Type: application/json
{"jsonrpc":"2.0","id":2,"method":"tools/call",
"params":{"name":"search","arguments":{"q":"otters"}}}
After: 1 request
POST /mcp HTTP/1.1
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: search
Content-Type: application/json
{"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"search","arguments":{"q":"otters"},
"_meta":{"io.modelcontextprotocol/clientInfo":{"name":"my-app","version":"1.0"}}}}
The second one is cold. That client has never spoken to this server, nothing was set up first, and nothing is kept afterward. The three new headers are there so a gateway can route and meter it without opening the body.
Version negotiation stopped being a phase and became a field. The server accepts or rejects each request on its own, and if it can't speak the version it answers UnsupportedProtocolVersionError listing the versions it can, so the client retries. There's a server/discover method that returns supported versions and capabilities in one call, which servers MUST implement and clients don't have to call. Discovery is a convenience now, not a precondition.
Server-to-client calls stopped needing an open pipe. That was the load-bearing dependency on sessions. A tool that needs to ask the user something used to do it over a stream held open for the length of the call, which pins the call to one process.
!A tool that has to stop and ask the user something. 2025-11-25 elicitation/create holds an SSE stream open on one pinned instance; 2026-07-28 multi round-trip returns InputRequiredResult and pins nothing
_Interactive version on the original post._
Under SEP-2322 the server returns an InputRequiredResult and hangs up. The client answers later with inputResponses and the requestState it was handed, and any instance can take it from there. SEP-2260 tightens things further: a server may only start a request while it's actively processing one from the client. Between them they replace elicitation/create and sampling/createMessage.
State moved into the open. The spec is blunt about where it went:
> Servers that need to carry state across calls can do what HTTP APIs have always done: mint an explicit handle (a basket_id, a browser_id) from a tool and have the model pass it back as an ordinary argument on later calls.
The spec's own phrasing is that this "makes the state visible to the model rather than hidden away." A session was state the model never saw and could not recover once it was gone.
Infrastructure can read the traffic without parsing it.
| SEP | What it adds | What it buys |
| ---- | --------------------------------------------------- | ------------------------------------------------------------------- |
| 2243 | Mcp-Method and Mcp-Name request headers | Routing, metering and WAF rules with no body inspection |
| 2549 | ttlMs and cacheScope on list and read | Clients know how long a tool list is good for, and who may share it |
| 414 | traceparent / tracestate / baggage in _meta | One trace across client, gateway and server, into any OTel backend |
A server rejects any request where the headers and the body disagree, which is what makes the headers safe to trust for anything that never looks inside.
Why this is 2.0 and not 1.4
The protocol changes are the part you notice. The governance changes are the part that decides whether any of it survives the next revision, and there are three.
Extensions became first class. They get reverse-DNS identifiers, negotiate through an extensions map, version independently of the spec, and live in their own ext-* repositories with their own maintainers. Tasks graduated out of the core and into io.modelcontextprotocol/tasks. MCP Apps, which is server-rendered HTML in a sandboxed iframe talking back over the same JSON-RPC, ships as one too. The core stays small and the interesting parts move at their own speed.
Features now have a lifecycle: Active, Deprecated, Removed, with a documented migration path and twelve months minimum before anything deprecated is eligible for removal. Three features entered it immediately.
| Deprecated | Use instead |
| ---------- | -------------------------------------------------------- |
| Roots | Tool parameters, resource URIs, or server configuration |
| Sampling | Your LLM provider's API, directly |
| Logging | stderr on stdio, OpenTelemetry for anything structured |
All three still work. That's the point of writing the policy down.
Conformance became a gate. A Standards Track SEP can't reach Final until matching scenarios land in the conformance suite, and official SDKs are scored against that suite under a tier system. All four Tier 1 SDKs (TypeScript, Python, Go, C#) shipped 2026-07-28 support the day the spec did, with Rust in beta.
The remaining breaking changes are small but worth knowing. Tool schemas moved to full JSON Schema 2020-12, so oneOf, $ref and conditionals are legal now, and the spec says out loud that you must not auto-dereference external $ref URIs. structuredContent takes any JSON value instead of only objects. The missing-resource error went from MCP's custom -32002 to the standard -32602, which breaks anyone matching on that literal. Every 2025-11-25 Tasks implementation needs migrating.
We had already bet on this
I spent sixteen months building Appwrite's MCP server, and most of that time went to authorization. When I got to the transport, the hosted server went out with one flag set:
StreamableHTTPSessionManager(app=server, json_response=False, stateless=True)
No sessions. Every request carries a bearer token; verify it, build a client from it, serve the call, forget everything. I'd love to claim foresight here. The real reason is duller: OAuth had already made session state pointless, because the token identifies the user on every request, and I didn't want to run a Redis to hold something I had no use for.
The spec shipped on July 28. We merged 2026-07-28 support on July 29, in PR #89, by moving from MCP Python SDK v1.28 to v2.0.0. The HTTP and OAuth surface didn't move at all. What made it a day rather than a quarter is that v2's session manager routes on the protocol version header, so one deployment serves both eras:
def _is_modern_request(scope: Scope) -> bool:
"""Match the SDK's era-routing predicate: a present ``MCP-Protocol-Version``
that is not a handshake-era revision goes to the modern path."""
version = _header(scope, b"mcp-protocol-version")
return version is not None and version not in HANDSHAKE_PROTOCOL_VERSIONS
Handshake versions, 2024-11-05 through 2025-11-25, take the legacy path. Anything else takes the per-request one. Nobody's client broke.
Three things did cost real work:
- Tool errors. v1 wrapped a raised exception into CallToolResult(isError=true). v2 turns an uncaught exception into a sanitized -32603, so the model never learns why the call failed. Our confirm_write refusals are only worth anything if the model can read them, so every handler now returns is_error=True explicitly.
- CORS. Mcp-Method and Mcp-Name ride on every modern request. If your allow-list doesn't name them, browser-based clients fail before they ever reach your auth code.
- Metrics. Modern clients never send initialize, so our handshake counter would have quietly flatlined exactly as clients migrated, which is the worst possible time to lose a graph. It's keyed off activity windows now, so the timeseries stays comparable through the transition.
None of that is protocol work. It's the ordinary cost of a dependency changing shape under you, and it only took a day because a decision made eighteen months earlier, for completely unrelated reasons, happened to line up.
The good kind of boring
A 2026-07-28 MCP server takes an HTTP request, reads some headers, and returns a response. That's the whole shape of it. You can put it behind a CDN, cache its list endpoints, and restart it in the middle of someone's conversation without telling anyone.
It's a less interesting protocol than the one that launched in 2024. I think that's the point. The bidirectional stateful version was more fun to read about and considerably worse to operate, and operating it is what most of us actually do.
Reference
| Term | What it means here |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| handshake era | Every MCP revision through 2025-11-25, where a connection began with initialize and carried a session from then on. |
| Mcp-Session-Id | The header that identified that session. Removed in 2026-07-28. |
| _meta | A metadata object on every request. Now carries protocol version, client info and capabilities, one copy per request. |
| server/discover | A single call returning supported versions, capabilities and identity. Mandatory for servers, optional for clients. |
| sticky routing | Making a load balancer send every request from one client to the same backend. The price of server-side session state. |
| requestState | The opaque blob a server hands back with an InputRequiredResult so the follow-up can be processed by any instance. |
| SEP | Specification Enhancement Proposal, MCP's change process. Standards Track SEPs now need conformance scenarios before reaching Final. |
| Streamable HTTP | The HTTP transport introduced in 2025-03-26, replacing the original HTTP+SSE one. |
| CIMD | Client ID Metadata Documents, the replacement for Dynamic Client Registration in OAuth flows. |
The spec
- The release candidate announcement and the release itself
- 2026-07-28, versioning and negotiation, and server/discover
- The feature lifecycle policy and the deprecated features registry
- The 2025-11-25 changelog, for what the last handshake-era revision was still adding
The code
- appwrite/mcp, and PR #89 — the dual-era migration
- The hosted server is at https://mcp.appwrite.io/, or claude mcp add --transport http appwrite https://mcp.appwrite.io/
_Originally published on chiragaggarwal.tech._
5 reactions
Backend plumbing is a product tax; platforms can reduce it.
appwrite (~58k★): Open-source backend services for web and mobile. appwrite/appwrite
Which backend service do you least want to maintain?
8 views
Appwrite: 5,102 titles, 4,094 fingerprints, and 531 hosts on the default web port
Appwrite: 5,102 titles, 4,094 fingerprints, and 531 hosts on the default web port
Appwrite is a self-hosted backend platform. It bundles authentication, a document database, file storage, and function execution behind an API and a console, and it is normally deployed as a set of containers behind a reverse proxy. The public footprint reflects that deployment model closely.
The three measurements
On 23 October 2026, ZoomEye returned 5,102 matches for title="Appwrite", 4,094 matches for app="Appwrite", and 531 matches for title="Appwrite" && port="80".
The title and fingerprint counts agree within twenty percent, which is the pattern of a product with a distinctive interface and a stable signature. The port-constrained count is a small fraction of both, and that is the informative part.
Appwrite's self-hosting documentation assumes a reverse proxy in front of the stack, with TLS terminating there and the console and API served on a hostname. A deployment that answers on plain port 80 with the product title is either a development instance or a deployment where TLS was never completed. Both are configurations worth noting, and neither is a vulnerability by itself.
What the console controls
The Appwrite console is where projects are created, API keys are issued, and function deployments are configured. An API key in Appwrite grants programmatic access to the project it belongs to, which typically includes reading and writing the document database and the file storage buckets.
Those are the two data stores most applications care about. A backend platform that holds them is a backend platform whose compromise is a data breach, and the console is the interface that mints the credentials.
The server-side rendering of functions adds a second dimension. Appwrite can execute code on behalf of the project, which means the platform's own runtime is a code execution environment by design. That is a feature, and it is also the reason the isolation boundary around a function matters more than the isolation boundary around a static site.
The meaning of 531
A title match on port 80 is not automatically a development instance. It can be a host behind a proxy that forwards port 80 internally while the external name serves TLS elsewhere. External measurement cannot distinguish the two, and claiming otherwise would be overreach.
What the number does support is a statement about the tail. Most Appwrite deployments are on a hostname with TLS and do not appear in a port-80 title query. Five hundred and thirty-one is the tail where the deployment either skipped TLS or sits behind a different topology, and a tail is exactly what an automated scan is looking for.
Direct actions
Confirm TLS termination and redirect behaviour on your own instance. A console that answers on plain HTTP is a credential exposure before it is anything else, because the login form submits a password over a channel anyone on the path can read.
Review API key scope. A key created for a script and never rotated is a persistent credential, and the console shows when each key was created. The review is a five-minute task with a clear output.
Restrict the console by source address. The API is meant to be reachable from applications; the console is meant to be reachable from administrators. Separating them is a proxy rule.
Check how functions are isolated. A function runtime that shares a network namespace with the database container is a design choice that turns any function-level issue into a platform-level issue.
The honest limitation
None of the three counts reports a release. Appwrite ships frequently, and its release notes are the authoritative record of what changed. Claims about the security of a specific version are not derivable from a scan, and readers should treat any article that implies otherwise with suspicion.
The defensible claim here is narrower and still useful: this is a platform deployed widely enough to matter, with a fingerprint that works, and a small visible tail of deployments that did not complete the standard TLS configuration.
References
- Appwrite documentation, self-hosting, appwrite.io
- Appwrite project repository, appwrite/appwrite
- ZoomEye AI query results collected 23 October 2026 for title="Appwrite", app="Appwrite", and title="Appwrite" && port="80", sub-type all
React Native SDK that talks to the cloud
A TypeScript SDK that lets your React Native app talk directly to Appwrite services.
🍒 Built by the Appwrite team
appwrite/sdk-for-react-native
#opensource #typescript
Quer construir um aplicativo sem desenvolver toda a infraestrutura do backend?
O Appwrite oferece autenticação, banco de dados, armazenamento, funções e recursos em tempo real.
Uma opção para desenvolver SaaS e aplicativos.
GitHub:
appwrite/appwrite
12 views
做小产品最烦搭后端:登录、存储、权限一件件来。
Appwrite 打包成一套能自己部署的后端:账号、文件、数据库、云函数一次到位。
appwrite/appwrite
1 like · 41 views
Mentions
@MrNiceTry The lists are fully open source already at appwrite/appwrite including the scrapers we use to build them.
A cloud backend for AI agents Appwrite bundles Auth, databases, storage, functions, messaging and realtime into one self-hostable platform. 🍒 Built by the Appwrite team appwrite/appwrite #opensource #typescript
Keeping track of your agent's work Agents are long-running LLMs, and long-running LLMs lose context. Not sometimes. Eventually, almost always. The agent doesn't tell you when it happens. It compacts, summarises, or starts a new session, and whatever didn't make the cut is gone. The worse part is that you lose context too, and you usually don't notice either. How the context goes missing Three things keep happening to me. The agent finds something important and mentions it once. Halfway through a task it notices a second bug, a migration that will break existing data, or a flag that does something nobody expected. It writes one line about it, carries on with what you asked for, and never brings it up again. After the next compaction it doesn't know either. You can't see what it forgot. When I lose track of my own work, I at least know I've lost it. With an agent the context is invisible. I don't know what's in its window, what got summarised away, or which decision from two hours ago is still being honoured. It writes more than you can read. An agent can produce pages of output, plans, and reasoning in the time it takes me to read one paragraph. Most of it is fine. Some of it matters. Finding the part that matters takes longer than the agent took to write all of it, so I skim, and skimming is how the one important line gets missed. Put those together and the important stuff ends up nowhere: not in the agent's context, not in your head, and buried somewhere in a transcript nobody will read again. Give every piece of work a ticket The fix I've settled on is boring: an issue tracker. One place for each feature or fix I'm working on, where the state of that work lives outside both the agent and me. A few rules make it work. Scope it however you like. A ticket can be as broad as "ship passkeys" or as narrow as "fix the logo on the consent screen". When something gets big, split it into sub-issues instead of growing one giant description. The parent says what we're doing and why. The children say what's left. It has to make sense to you. This is the whole point. If the ticket reads like the agent's scratchpad, you've just moved the gibberish somewhere else. Decisions, open questions, things that were found along the way, what's done, what isn't. Short enough that I can read it in a minute and know where things stand. The agent keeps it up to date, always. Not at the end. Every time it finds something, decides something, or finishes a step, it goes in the ticket. That one-line mention of a second bug becomes a sub-issue instead of a sentence that disappears after the next compaction. The side benefit: anyone can pick it back up Once the state lives in the ticket, the session stops mattering. If I hit a usage limit mid-task, or a session dies, or I just close the laptop and come back a day later, a fresh agent can read the ticket and carry on. It doesn't need the old transcript or me re-explaining everything. The same goes for a different agent, or a teammate, or me in a week when I've forgotten all of it. Examples All of these are from the last month of my own sessions. Mentioned once, then gone. While fixing text responses in the SDK generator (#1944), the agent noticed Dart models type nullable fields as non-null and offered to leave it out of the PR. I never replied. It never came up again, and the template still only checks whether a field is required. !Line 12 of the Dart model template on main, adding a question mark only when a property is not required _Still on main a month later (model.dart.twig)_ A ticket that reads like a status page. #13828 tracked moving our libraries into the monorepo in waves. The agent rewrote the top of it after each wave, so "where we are" was always one glance away. !GitHub issue 13828 with a Where we are section: waves 1 to 5 done, 46 packages live _#13828, kept current by the agent_ A finding becomes its own issue. While researching something else, an agent found endpoints returning a 500 for values too long for their column. It filed #14293 instead of mentioning it in passing, so I could hand it to the database team. It's closed now. !GitHub issue 14293 summary: several endpoints return 500 general_unknown instead of 400 _#14293, filed by the agent_ Parked work anyone can resume. When we deferred the last wave, it got its own issue, #14255, with the branch, the commit and exactly what to do next. Any session, or person, can start from here. !GitHub issue 14255: the work is done on branch chore/absorb-database at head 9f26684; reopen the PR, merge main and re-run CI _#14255, the handoff for wave 6_ What I'd tell someone starting - Assume your agent will lose context. Plan for it instead of hoping it won't. - Give every feature or fix a ticket. Split big ones into sub-issues. - Write tickets for a human. If you can't read it in a minute, it's not doing its job. - Make the agent update the ticket as it goes, not at the end. - Treat the ticket as the handoff. Any session should be able to start from it. The agent will forget. The ticket won't. _Originally published on chiragaggarwal.tech._
My agent has two Playwrights My agent made 1,929 browser calls through Playwright MCP last month, and almost none of them were tests. That surprised me, because I do have tests. The Appwrite console has a playwright.config.ts, CI runs four Console E2E jobs on every pull request, and I spent part of the month fixing flakes in them. None of the 1,929 calls, spread over 62 sessions in my Claude Code transcripts between 17 September and 17 October, ran that suite. They used the browser for something else. So my agent has two Playwrights, and I've stopped treating them as one tool. Two jobs The test suite answers one question: did something that used to work stop working? It runs headless in CI, it's mostly deterministic, and it doesn't care what anything looks like. Playwright MCP answers a different one: does the thing I just built look right? Nobody has written an assertion for that, because an hour ago the thing didn't exist. The agent opens a real browser, goes to the page, clicks through it, reads it back and takes a picture. !1,929 Playwright MCP calls by tool: navigate 372, take_screenshot 332, evaluate 304, click 247, run_code_unsafe 188, wait_for 160, snapshot 120, resize 45, other 10 tools 161 _Interactive version on the original post._ Screenshots are the second most common call overall. In the sessions where the agent was checking its own work, they're the most common: 281 screenshots against 226 navigations, and 209 of those navigations went to localhost or a local file. It isn't browsing the web. It's looking at what it just made. browser_evaluate is mostly reading, not poking. Of its 304 calls plus 188 run_code_unsafe calls, 175 use querySelectorAll and 151 read innerText. A handful flip the dark class on <html> so the next screenshot shows the other theme. This is what one of those pictures looks like. When Appwrite added function intervals as an alternative to cron, the agent built the console picker, then opened it in the browser and screenshotted every state before the PR went up. !Appwrite console function settings with the Interval tab selected and a dropdown of presets from every 5 minutes to daily _The interval picker, checked by the agent in a local console before review (#14319)_ Where the calls went | What the session was for | Sessions | Calls | | ------------------------------------- | -------- | ----- | | Checking a UI, docs or release change | 34 | 1,209 | | Chores in logged-in web apps | 23 | 553 | | Research | 5 | 167 | Nearly two thirds is verification. The rest is a different story, which I'll come back to. Three sessions "The logo looks off" I pasted a screenshot of the Appwrite CLI's device sign-in screen and typed four words: "the logo looks off, debug why". The logo was a black mark sitting on a near-black card. The asset was fine. Every official app logo in that storage bucket is black on transparent, on purpose. The marketplace already knew this: its logo component puts the image on a muted tile and inverts it in dark mode. The OAuth consent screens just didn't use that component. The fix was to move it into a shared AppLogo and use it in both places. The part I care about came after. I asked the agent to check the other OAuth clients too, and it used the console's debug preview route to render the consent and result screens for Claude Code, Cursor, Codex, Claude Desktop and Gemini CLI in dark mode, then did one pass in light mode. That took 19 Playwright calls, with no errors, and produced eight screenshots. No test in the suite would have caught this, because no test checks that a logo is visible against its background. If I wrote one now, it would only protect against the last bug. The OG image X wouldn't show The passkeys announcement showed a grey placeholder when shared on X. The agent found the cause quickly: the cover image was AVIF, and X only renders JPG, PNG, WEBP and GIF for cards. Every blog and changelog cover in the repo is AVIF, all 538 of them. The fix was a route that converts the cover to JPEG for og:image and twitter:image only. Then I asked for "pure reproduction and fix verification screenshots in pr desc". The agent built two small pages showing the share card before and after, resized the browser to the card's size and took a screenshot of each. Its first attempt failed because Playwright MCP blocks file:// URLs, so it served the pages over local HTTP instead. !Two panels. Before: production serves an AVIF twitter:image and X shows a grey placeholder. After: the branch serves a JPEG and the passkeys cover renders. _The two screenshots that went into the PR description_ Then the suite took over. The review bot flagged two problems, a type-baseline check broke because of the new route, and the four Console E2E jobs ran. Once all 20 checks were green, #14622 was ready. Both Playwrights were in that PR: one showed the reviewer what changed, the other showed that nothing else did. Release QA for 2.3.0 The biggest session of the month used 183 Playwright calls. I'd asked the agent to "do end to end testing of release notes, especially frontend side" before shipping Appwrite 2.3.0. It built the 2.3.0 image locally, installed 2.2.0 in a separate Docker Compose project so my dev data stayed safe, seeded it, and upgraded. migrate failed: projects that already existed never got the new pushLedger collection. After fixing that it found a second gap: existing topics were missing the new qos, expiry and sequence fields. Both fixes shipped with regression tests and went into the release branch before anyone upgraded. To be fair, the browser didn't find the migration bug. migrate exiting with code 1 did. What Playwright did was walk the console against the upgraded install and check each claim in the release notes, one screen at a time. Still, nothing in the test suite runs an upgrade. Release QA used to be manual QA. Now it's manual QA I hand to an agent. Where the browser does earn its keep is reproducing what users report. After 2.4.0, someone in the community said the upgrade wizard ignored their custom ports. I pasted the feedback and asked the agent to confirm each issue before fixing anything. It installed a stack on ports 8080 and 8443 with HTTPS on, opened the upgrade wizard, and screenshotted it offering 80, 443 and HTTPS off. Then it fixed the bug and took the same screenshot again. Nobody writes a test for this kind of bug until it bites someone. !Two upgrade wizards side by side. 2.4.0 shows HTTP port 80, HTTPS port 443 and HTTPS off. The fixed version keeps 8080, 8443 and HTTPS on. _Same install, two upgrade wizards (#14345)_ Every PR gets a picture This is the habit I care most about. A reviewer can't run your branch, but they can look at it. The loop has four steps: 1. The agent checks the change with Playwright MCP. 2. It saves each screenshot under a name that says what it shows: passkeys-off.png, passkeys-on.png, cli-device-light.png. 317 of the month's 332 screenshots had a name like that. 3. It writes the PR description with relative image links like  and runs gh pr create --attach or gh pr edit --attach. Recent versions of gh upload each file to GitHub and rewrite the link to point at the upload. 4. It deletes .playwright-mcp/ and any loose PNGs so none of it gets committed. A good pair needs no explanation. When self-hosted consoles stopped offering OAuth and passkey sign-in, the PR showed the page before and after, and the reviewer could see the change in a second: !Appwrite console sign-in before and after. Before shows Google, GitHub, GitLab, Bitbucket and passkey buttons. After shows only email and password. _Self-hosted console sign-in, before and after (#14434)_ Dark mode gets its own screenshot, because that's where things like the invisible logo hide: !Appwrite console sign-in in dark mode with a Sign in with a passkey button and a Last used badge on the Login button _Passkey sign-in in dark mode, with the last-used badge (#14400)_ I didn't start with --attach. Over the month, screenshots reached my PRs three ways: committed to an orphan branch on my fork and linked through raw.githubusercontent.com, committed to a repo as docs assets, and attached. A reviewer caught the problem with the first one: _"Screenshot evidence disappears after merge. Screenshots reference an assets branch that the instructions then delete."_ If the screenshots live on a branch you delete, the evidence goes with it. Attachments stay. 32 of the 53 PRs with screenshots now use them. And 53 out of 904 is the honest number. The rate depends on the repo. In the two public Appwrite repos where most of my UI work lands: | Repo | PRs | With screenshots | | ------------------- | --- | ---------------- | | appwrite/vibes | 39 | 11 (28%) | | appwrite/appwrite | 224 | 25 (11%) | Backend-heavy repos sit far lower, which is partly fair, since there's often nothing to look at. But there's a less flattering reason too: I asked for screenshots by hand about 30 times this month ("add screenshots in pr desc", "before and after screenshot in pr desc", "show screenshots of ur testing in pr desc"). The habit only happened when I remembered to type it. So I made it a rule. This is now in my global CLAUDE.md: > If a PR changes anything a reviewer can see (UI, rendered docs, emails, OG cards, dashboards), verify it with Playwright MCP and put screenshots in the PR description without being asked: before and after for fixes, light and dark for UI. Name each file for what it shows, reference it as  in the body, and upload with gh pr create --attach / gh pr edit --attach. Never host screenshots on a branch that gets deleted, and never commit .playwright-mcp/ or stray PNGs. The other 553 calls These had nothing to do with UI work. Help Scout, the university's Brightspace, the Google Cloud and AWS consoles, an SMS provider's dashboard. These are tools I use daily that either have no API or none I have access to. The pattern is always the same: "use playwright to login to helpscout, i want you to check some tickets". I log in myself in the browser window, and the agent reads. The browser becomes an API for software that doesn't offer one. That's a different post, but it's why the server stays installed even in weeks when I'm not building UI. Where it falls short - About 10% of calls fail. 195 of 1,929 returned an error. The biggest causes were boring: the browser or tab had already closed (36), scripts trying to read files outside the sandbox (25), and timeouts waiting for a page or element (around 30). Four were "Browser is already in use", which usually means two parallel worktree sessions wanted the same browser profile. - Phones barely exist. There were 45 resizes this month and exactly one was a phone viewport, 390 by 844. Everything else was desktop. That's a blind spot, not a policy. - file:// is blocked. Any quick HTML mockup has to be served over HTTP first. - Screenshots land in the repo. By default they go into .playwright-mcp/ at the repository root. Several sessions ended with the agent cleaning up after itself, and in at least one session the screenshots ended up in the main checkout instead of the worktree. What I'd tell someone starting - Keep the test suite for anything that must never break. A screenshot is evidence, not a regression test. - Use the MCP browser for the question the suite can't ask: does this look right? - Name every screenshot and check both themes. - Put before and after in the PR as attachments, not links to a branch. - Write the rule down, so it doesn't depend on you remembering to type it. The suite catches what broke. The screenshots show the reviewer what's new. _Originally published on chiragaggarwal.tech._
MCP 2.0: the release that deleted the handshake MCP versions are dates. 2024-11-05, 2025-03-26, 2025-06-18, 2025-11-25, and now 2026-07-28. From the outside they all look like the same kind of event. The last one isn't. The working group called the release candidate "the largest revision of the protocol since launch," which usually means a long changelog. Here it means the connection model is gone. No initialize. No session id. No held-open stream. If you'd asked me a year ago to describe MCP in one sentence, that sentence would be wrong now. Nothing you have deployed today stops working, which is exactly why it's easy to skim past. The handshake era Every MCP revision up to and including 2025-11-25 opened the same way. The client sends initialize with its protocol version, its capabilities and its clientInfo. The server answers with the version it picked, its own capabilities and its serverInfo. The client sends notifications/initialized. Only then is the connection allowed to carry anything useful. That exchange establishes a session, and everything after it is defined relative to that session. Over Streamable HTTP the server could mint an Mcp-Session-Id, which the client then echoed on every request; DELETE ended it, Last-Event-ID resumed a dropped SSE stream. The design makes complete sense if you remember where MCP started. In November 2024 it was a thing you ran as a subprocess of your editor over stdio. One client, one server, one pipe, and the pipe's lifetime _is_ the session. Negotiating capabilities once at the start costs nothing when there is exactly one of everything. Streamable HTTP arrived in 2025-03-26 and carried that model onto the network, where "the connection has a memory" stops being free. !2025-11-25: the load balancer uses sticky routing, so every request goes to replica 1, which holds the session, and replicas 2 and 3 cannot answer !2026-07-28: the client sends only a bearer token, the load balancer round-robins, and all three replicas can answer anything _Interactive version on the original post._ So you pick one: sticky routing, or a shared session store. Same problem, different bill. Every deploy drops live sessions. A crashed replica takes conversations with it. And since the session id tells a gateway nothing about what the request wants, the gateway has to open the JSON body to find out whether it's metering a tools/list or a database drop. !MCP revisions from 2024-11-05 (stdio, one subprocess) through 2025-03-26, 2025-06-18 and 2025-11-25 to 2026-07-28 (no session, no handshake) _Four revisions of adding things to a connection, then one that removed the connection._ What 2026-07-28 actually does The handshake is gone. No initialize, no initialized, no Mcp-Session-Id. Everything that used to be negotiated once now travels on every request, in _meta. The same tool call, before and after: Before: 2 round trips POST /mcp HTTP/1.1 Content-Type: application/json {"jsonrpc":"2.0","id":1,"method":"initialize", "params":{"protocolVersion":"2025-11-25","capabilities":{}, "clientInfo":{"name":"my-app","version":"1.0"}}} POST /mcp HTTP/1.1 Mcp-Session-Id: 1868a90c-3a3f-4f5b Content-Type: application/json {"jsonrpc":"2.0","id":2,"method":"tools/call", "params":{"name":"search","arguments":{"q":"otters"}}} After: 1 request POST /mcp HTTP/1.1 MCP-Protocol-Version: 2026-07-28 Mcp-Method: tools/call Mcp-Name: search Content-Type: application/json {"jsonrpc":"2.0","id":1,"method":"tools/call", "params":{"name":"search","arguments":{"q":"otters"}, "_meta":{"io.modelcontextprotocol/clientInfo":{"name":"my-app","version":"1.0"}}}} The second one is cold. That client has never spoken to this server, nothing was set up first, and nothing is kept afterward. The three new headers are there so a gateway can route and meter it without opening the body. Version negotiation stopped being a phase and became a field. The server accepts or rejects each request on its own, and if it can't speak the version it answers UnsupportedProtocolVersionError listing the versions it can, so the client retries. There's a server/discover method that returns supported versions and capabilities in one call, which servers MUST implement and clients don't have to call. Discovery is a convenience now, not a precondition. Server-to-client calls stopped needing an open pipe. That was the load-bearing dependency on sessions. A tool that needs to ask the user something used to do it over a stream held open for the length of the call, which pins the call to one process. !A tool that has to stop and ask the user something. 2025-11-25 elicitation/create holds an SSE stream open on one pinned instance; 2026-07-28 multi round-trip returns InputRequiredResult and pins nothing _Interactive version on the original post._ Under SEP-2322 the server returns an InputRequiredResult and hangs up. The client answers later with inputResponses and the requestState it was handed, and any instance can take it from there. SEP-2260 tightens things further: a server may only start a request while it's actively processing one from the client. Between them they replace elicitation/create and sampling/createMessage. State moved into the open. The spec is blunt about where it went: > Servers that need to carry state across calls can do what HTTP APIs have always done: mint an explicit handle (a basket_id, a browser_id) from a tool and have the model pass it back as an ordinary argument on later calls. The spec's own phrasing is that this "makes the state visible to the model rather than hidden away." A session was state the model never saw and could not recover once it was gone. Infrastructure can read the traffic without parsing it. | SEP | What it adds | What it buys | | ---- | --------------------------------------------------- | ------------------------------------------------------------------- | | 2243 | Mcp-Method and Mcp-Name request headers | Routing, metering and WAF rules with no body inspection | | 2549 | ttlMs and cacheScope on list and read | Clients know how long a tool list is good for, and who may share it | | 414 | traceparent / tracestate / baggage in _meta | One trace across client, gateway and server, into any OTel backend | A server rejects any request where the headers and the body disagree, which is what makes the headers safe to trust for anything that never looks inside. Why this is 2.0 and not 1.4 The protocol changes are the part you notice. The governance changes are the part that decides whether any of it survives the next revision, and there are three. Extensions became first class. They get reverse-DNS identifiers, negotiate through an extensions map, version independently of the spec, and live in their own ext-* repositories with their own maintainers. Tasks graduated out of the core and into io.modelcontextprotocol/tasks. MCP Apps, which is server-rendered HTML in a sandboxed iframe talking back over the same JSON-RPC, ships as one too. The core stays small and the interesting parts move at their own speed. Features now have a lifecycle: Active, Deprecated, Removed, with a documented migration path and twelve months minimum before anything deprecated is eligible for removal. Three features entered it immediately. | Deprecated | Use instead | | ---------- | -------------------------------------------------------- | | Roots | Tool parameters, resource URIs, or server configuration | | Sampling | Your LLM provider's API, directly | | Logging | stderr on stdio, OpenTelemetry for anything structured | All three still work. That's the point of writing the policy down. Conformance became a gate. A Standards Track SEP can't reach Final until matching scenarios land in the conformance suite, and official SDKs are scored against that suite under a tier system. All four Tier 1 SDKs (TypeScript, Python, Go, C#) shipped 2026-07-28 support the day the spec did, with Rust in beta. The remaining breaking changes are small but worth knowing. Tool schemas moved to full JSON Schema 2020-12, so oneOf, $ref and conditionals are legal now, and the spec says out loud that you must not auto-dereference external $ref URIs. structuredContent takes any JSON value instead of only objects. The missing-resource error went from MCP's custom -32002 to the standard -32602, which breaks anyone matching on that literal. Every 2025-11-25 Tasks implementation needs migrating. We had already bet on this I spent sixteen months building Appwrite's MCP server, and most of that time went to authorization. When I got to the transport, the hosted server went out with one flag set: StreamableHTTPSessionManager(app=server, json_response=False, stateless=True) No sessions. Every request carries a bearer token; verify it, build a client from it, serve the call, forget everything. I'd love to claim foresight here. The real reason is duller: OAuth had already made session state pointless, because the token identifies the user on every request, and I didn't want to run a Redis to hold something I had no use for. The spec shipped on July 28. We merged 2026-07-28 support on July 29, in PR #89, by moving from MCP Python SDK v1.28 to v2.0.0. The HTTP and OAuth surface didn't move at all. What made it a day rather than a quarter is that v2's session manager routes on the protocol version header, so one deployment serves both eras: def _is_modern_request(scope: Scope) -> bool: """Match the SDK's era-routing predicate: a present ``MCP-Protocol-Version`` that is not a handshake-era revision goes to the modern path.""" version = _header(scope, b"mcp-protocol-version") return version is not None and version not in HANDSHAKE_PROTOCOL_VERSIONS Handshake versions, 2024-11-05 through 2025-11-25, take the legacy path. Anything else takes the per-request one. Nobody's client broke. Three things did cost real work: - Tool errors. v1 wrapped a raised exception into CallToolResult(isError=true). v2 turns an uncaught exception into a sanitized -32603, so the model never learns why the call failed. Our confirm_write refusals are only worth anything if the model can read them, so every handler now returns is_error=True explicitly. - CORS. Mcp-Method and Mcp-Name ride on every modern request. If your allow-list doesn't name them, browser-based clients fail before they ever reach your auth code. - Metrics. Modern clients never send initialize, so our handshake counter would have quietly flatlined exactly as clients migrated, which is the worst possible time to lose a graph. It's keyed off activity windows now, so the timeseries stays comparable through the transition. None of that is protocol work. It's the ordinary cost of a dependency changing shape under you, and it only took a day because a decision made eighteen months earlier, for completely unrelated reasons, happened to line up. The good kind of boring A 2026-07-28 MCP server takes an HTTP request, reads some headers, and returns a response. That's the whole shape of it. You can put it behind a CDN, cache its list endpoints, and restart it in the middle of someone's conversation without telling anyone. It's a less interesting protocol than the one that launched in 2024. I think that's the point. The bidirectional stateful version was more fun to read about and considerably worse to operate, and operating it is what most of us actually do. Reference | Term | What it means here | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | handshake era | Every MCP revision through 2025-11-25, where a connection began with initialize and carried a session from then on. | | Mcp-Session-Id | The header that identified that session. Removed in 2026-07-28. | | _meta | A metadata object on every request. Now carries protocol version, client info and capabilities, one copy per request. | | server/discover | A single call returning supported versions, capabilities and identity. Mandatory for servers, optional for clients. | | sticky routing | Making a load balancer send every request from one client to the same backend. The price of server-side session state. | | requestState | The opaque blob a server hands back with an InputRequiredResult so the follow-up can be processed by any instance. | | SEP | Specification Enhancement Proposal, MCP's change process. Standards Track SEPs now need conformance scenarios before reaching Final. | | Streamable HTTP | The HTTP transport introduced in 2025-03-26, replacing the original HTTP+SSE one. | | CIMD | Client ID Metadata Documents, the replacement for Dynamic Client Registration in OAuth flows. | The spec - The release candidate announcement and the release itself - 2026-07-28, versioning and negotiation, and server/discover - The feature lifecycle policy and the deprecated features registry - The 2025-11-25 changelog, for what the last handshake-era revision was still adding The code - appwrite/mcp, and PR #89 — the dual-era migration - The hosted server is at https://mcp.appwrite.io/, or claude mcp add --transport http appwrite https://mcp.appwrite.io/ _Originally published on chiragaggarwal.tech._
Backend plumbing is a product tax; platforms can reduce it. appwrite (~58k★): Open-source backend services for web and mobile. appwrite/appwrite Which backend service do you least want to maintain?
Appwrite: 5,102 titles, 4,094 fingerprints, and 531 hosts on the default web port Appwrite: 5,102 titles, 4,094 fingerprints, and 531 hosts on the default web port Appwrite is a self-hosted backend platform. It bundles authentication, a document database, file storage, and function execution behind an API and a console, and it is normally deployed as a set of containers behind a reverse proxy. The public footprint reflects that deployment model closely. The three measurements On 23 October 2026, ZoomEye returned 5,102 matches for title="Appwrite", 4,094 matches for app="Appwrite", and 531 matches for title="Appwrite" && port="80". The title and fingerprint counts agree within twenty percent, which is the pattern of a product with a distinctive interface and a stable signature. The port-constrained count is a small fraction of both, and that is the informative part. Appwrite's self-hosting documentation assumes a reverse proxy in front of the stack, with TLS terminating there and the console and API served on a hostname. A deployment that answers on plain port 80 with the product title is either a development instance or a deployment where TLS was never completed. Both are configurations worth noting, and neither is a vulnerability by itself. What the console controls The Appwrite console is where projects are created, API keys are issued, and function deployments are configured. An API key in Appwrite grants programmatic access to the project it belongs to, which typically includes reading and writing the document database and the file storage buckets. Those are the two data stores most applications care about. A backend platform that holds them is a backend platform whose compromise is a data breach, and the console is the interface that mints the credentials. The server-side rendering of functions adds a second dimension. Appwrite can execute code on behalf of the project, which means the platform's own runtime is a code execution environment by design. That is a feature, and it is also the reason the isolation boundary around a function matters more than the isolation boundary around a static site. The meaning of 531 A title match on port 80 is not automatically a development instance. It can be a host behind a proxy that forwards port 80 internally while the external name serves TLS elsewhere. External measurement cannot distinguish the two, and claiming otherwise would be overreach. What the number does support is a statement about the tail. Most Appwrite deployments are on a hostname with TLS and do not appear in a port-80 title query. Five hundred and thirty-one is the tail where the deployment either skipped TLS or sits behind a different topology, and a tail is exactly what an automated scan is looking for. Direct actions Confirm TLS termination and redirect behaviour on your own instance. A console that answers on plain HTTP is a credential exposure before it is anything else, because the login form submits a password over a channel anyone on the path can read. Review API key scope. A key created for a script and never rotated is a persistent credential, and the console shows when each key was created. The review is a five-minute task with a clear output. Restrict the console by source address. The API is meant to be reachable from applications; the console is meant to be reachable from administrators. Separating them is a proxy rule. Check how functions are isolated. A function runtime that shares a network namespace with the database container is a design choice that turns any function-level issue into a platform-level issue. The honest limitation None of the three counts reports a release. Appwrite ships frequently, and its release notes are the authoritative record of what changed. Claims about the security of a specific version are not derivable from a scan, and readers should treat any article that implies otherwise with suspicion. The defensible claim here is narrower and still useful: this is a platform deployed widely enough to matter, with a fingerprint that works, and a small visible tail of deployments that did not complete the standard TLS configuration. References - Appwrite documentation, self-hosting, appwrite.io - Appwrite project repository, appwrite/appwrite - ZoomEye AI query results collected 23 October 2026 for title="Appwrite", app="Appwrite", and title="Appwrite" && port="80", sub-type all
React Native SDK that talks to the cloud A TypeScript SDK that lets your React Native app talk directly to Appwrite services. 🍒 Built by the Appwrite team appwrite/sdk-for-react-native #opensource #typescript
Quer construir um aplicativo sem desenvolver toda a infraestrutura do backend? O Appwrite oferece autenticação, banco de dados, armazenamento, funções e recursos em tempo real. Uma opção para desenvolver SaaS e aplicativos. GitHub: appwrite/appwrite
做小产品最烦搭后端:登录、存储、权限一件件来。 Appwrite 打包成一套能自己部署的后端:账号、文件、数据库、云函数一次到位。 appwrite/appwrite