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 jobId arg

Run it:

cd examples/task
pnpm dev
pnpm preview