Skip to content

feat(tanstack-ai): sandbox snapshot and MCP stores for TanStack AI 1.x [stack 1/4] - #102

Open
CahidArda wants to merge 8 commits into
chore/bump-upstreams-2026-11-07from
feat/tanstack-sandbox-mcp-stores
Open

CahidArda wants to merge 8 commits into
chore/bump-upstreams-2026-11-07from
feat/tanstack-sandbox-mcp-stores

Conversation

@CahidArda

@CahidArda CahidArda commented Nov 11, 2026 •

Copy link
Copy Markdown
Collaborator

Stack: #100 → #102 → #103 → #104 → #106. Merge in order.

Adds Upstash Redis stores for TanStack AI's sandbox and MCP packages, under two new entry points.

Sandbox snapshots (@upstash/agentkit-tanstack-ai/sandbox)

Sandbox files survive after the provider sandbox is gone, any server can resume a thread's sandbox, and you can fork a conversation (files included) from an earlier checkpoint.

import { createSandboxSnapshots, withSandbox } from "@tanstack/ai-sandbox";
import { upstashLocks } from "@upstash/agentkit-tanstack-ai";
import { upstashPersistence } from "@upstash/agentkit-tanstack-ai/persistence";
import {
  upstashSandboxCheckpointStore,
  upstashSandboxInstanceStore,
} from "@upstash/agentkit-tanstack-ai/sandbox";

const persistence = upstashPersistence({ bucket: Bucket.fromEnv() });
const instances = upstashSandboxInstanceStore();
const snapshots = createSandboxSnapshots({
  persistence,
  checkpoints: upstashSandboxCheckpointStore(), // same Redis + prefix as `persistence`
  instances,
  locks: upstashLocks(),
});

chat({
  adapter,
  messages,
  threadId,
  middleware: [withPersistence(snapshots.persistence), withSandbox(sandbox, { instances, snapshots })],
});

await snapshots.fork({ threadId, checkpointId, destinationThreadId: "thread-2" });

MCP stores (@upstash/agentkit-tanstack-ai/mcp)

MCP tasks, event subscriptions and MCP Apps sessions are shared across instances and survive restarts.

import { upstashMcpEventStore, upstashMcpTaskStore } from "@upstash/agentkit-tanstack-ai/mcp";

const server = createMCPServer({ name: "tools", version: "1.0.0", tools, taskStore: upstashMcpTaskStore() });

const mcp = await createMCPClient({
  transport: { type: "http", url: "https://mcp.example.com/mcp" },
  experimental_events: { store: upstashMcpEventStore() },
});

upstashMcpSessionStore() stores MCP Apps server descriptors. Transport headers can be encrypted with encryptionKey, and code-only fields like needsApproval come back through a resolve callback. If one is missing, get returns null, so a widget never reaches a server without its approval gate.

Also

  • upstashPersistence({ bucket }) is now typed with a required blobs store, so it can be passed straight to createSandboxSnapshots().
  • New optional peers: @tanstack/ai-sandbox >=1.0.0, @tanstack/ai-mcp >=1.0.0. The root entry doesn't import them.

Tested against TanStack's own sandbox conformance suites on live Redis, plus MCP store tests. Changeset: minor for @upstash/agentkit-tanstack-ai.

…ent stores

Add two entry points that implement more TanStack AI 1.x backend contracts
on Upstash Redis:

- ./sandbox: upstashSandboxCheckpointStore (SandboxCheckpointStore with
  forkFromCheckpoint) and upstashSandboxInstanceStore. They pass
  runSandboxCheckpointStoreConformance, runSandboxCheckpointForkConformance
  and runSandboxInstanceStoreConformance. The run store also passes
  runDurableRunFieldsConformance.
- ./mcp: upstashMcpTaskStore (createMCPServer taskStore) and
  upstashMcpEventStore (MCP Events direct mode).

upstashPersistence({ bucket }) now returns a required blobs store in its
type, so it plugs straight into createSandboxSnapshots.

Optional peers: @tanstack/ai-sandbox and @tanstack/ai-mcp >=1.0.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CMK4etJMDDBFkjEyWDVCdy
McpServerDescriptor carries functions and objects (needsApproval,
toolFilter, toolName, a transport's fetch and authProvider) that cannot be
stored in Redis. So set() stores the JSON part and records which
code-only fields were dropped. get() takes them from a user-supplied
resolve() callback.

get() fails closed: if a recorded field is still missing, it returns
null, so createMcpAppCallHandler falls back to its registry or refuses
the call. A widget can never reach a server that lost its approval gate.

Transport headers and stdio env can be encrypted at rest with
AES-256-GCM, bound to the prefix, thread and server. The helpers move to
src/secret-box.ts, which the BYOK credential store now shares.

The TTL slides like the in-memory store's (1800 s by default). An
end-to-end test drives createMcpAppCallHandler against a real MCP server
over HTTP.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CMK4etJMDDBFkjEyWDVCdy
@linear-code

linear-code Bot commented Nov 11, 2026

Copy link
Copy Markdown

DX-3285

…fore the first poll

subscribe() took its baseline from its first poll, which runs after subscribe
returns. A write from another machine in between became the baseline and was
never reported. Start each watch from this store's own last write to the log
instead.
2.44.3 exits with code 1 and no output, even for --help, so the e2e failed
before it started.
latest points to a working release again.
… into fix/stack-102-ci

# Conflicts:
#	CLAUDE.md
- MCP sessions: with encryptionKey, seal each stored entry whole, so a Redis
  writer cannot change the server url or strip the fail-closed field list
- MCP events: revisions come from one counter, so a stale update cannot
  match a deleted and re-created subscription; drop the key-to-id hash
- fork: write the transcript first, so a failing JSON.SET leaves nothing
  half-written; reject a non-integer messagesTtlSeconds
- sandbox validation: keep only the checks this store relies on
- BYOK: use the shared secretBox (same stored format)
- regression test: another store's write before the first poll is reported
- drop references to the removed baseline read and header-only encryption
- move the MCP session store notes in CLAUDE.md under the stores entry
@CahidArda CahidArda changed the title feat(tanstack-ai): sandbox snapshot and MCP stores for TanStack AI 1.x [stack 1/3] feat(tanstack-ai): sandbox snapshot and MCP stores for TanStack AI 1.x [stack 1/4] Nov 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants