Task
Handle returned immediately and View polls job status.
The examples/task app starts a long ingest job and returns a handle right away. Job records live behind a HandleStore.
tool.ts
import { memoryHandleStore, type HandleStore } from "bitmcp";
type Job = { status: string; progress: number; source: string };
export const jobs: HandleStore<Job> = memoryHandleStore();
export default defineTool({
description: "Start a long ingest job and return a handle immediately",
input: z.object({
source: z.string(),
jobId: z.string().optional(),
}),
async execute({ source, jobId }, ctx) {
const id = jobId ?? ctx.handle();
const existing = await jobs.get(id);
if (!existing) {
await jobs.set(id, { status: "running", progress: 0, source });
}
const job = (await jobs.get(id)) ?? {
status: "running",
progress: 0,
source,
};
return { jobId: id, ...job };
},
});Polling
The View calls a status backing tool with the returned jobId. Each poll is a new stateless tools/call. callTool updates useApp().result from structuredContent.
Production stores must implement the same HandleStore { get, set } shape against KV, Redis, or D1. The in-memory adapter is for a single process.
What this proves
- No open SSE session
- Handles survive restarts when backed by storage you provide
- Stateless replicas can serve status checks with the same
jobIdarg
Run it:
cd examples/task
pnpm dev
pnpm preview