Session Namespace
- Category
- core
- Namespace
- session
Curate a lens: a named, long-lived canvas where a person reads, plans and arranges a set of documents, with a shareable URL, and where you read back where the person's attention actually went.
- Access
- write
- Method
- focus
- Side Effect
- Yes
focus covers five operations at once: create, read, update, list, and destroy, dispatched by which fields you pass rather than by an operation name. A second method, listLenses, lists your lenses across all organizations where you may read documents, newest interaction first; open or change one with focus and its organizationId. Every call requires organizationId and a caller-minted UUIDv7 operationId. The result is always one flat object, never a union, so a list call returns the single-lens fields as null with the directory under lenses.
A lens is attention, never an access boundary: putting a document in a lens grants no one any access they did not already have, and removing it revokes nothing. There is no sharing operation and no permission surface on a lens. Sketches, the freehand ink strokes a human draws on a canvas, are readable as bounding boxes with a point count and cannot be written, moved, or deleted through this namespace. A lens holds at most 100 documents and 200 connections, each connection label at most 200 characters, and a document may belong to only a platform-guard maximum number of active lenses before further additions are refused. Canvas coordinates are bounded to plus or minus 100000, card width to 240 through 16000 and height to 180 through 16000. A lens is meant to last, yet after 30 days without activity it is retired from the recent directory and its runtime state goes dormant; its URL still opens it. Failures conceal existence deliberately, because lens ids are globally unique: a lens in another organization, a destroyed lens, a nonexistent lens, and a lens whose id does not match the organizationId you passed all produce the same "not configured" message, so that error never confirms or denies that a lens exists. cursor is valid on a list call and on a read of one lens (operationId, organizationId, lensId and cursor, nothing else); create, update, and destroy calls refuse it. A destroy call accepts only operationId, organizationId, and lensId; any other field is rejected.
Every call takes a caller-owned UUIDv7 operationId, including reads and lists; generate it on your own host, and call ids.mintOperationIds in a run of its own only if your host cannot generate a UUIDv7. Reuse the exact literal only when retrying the same call after an ambiguous outcome. An operationId reused for a call whose shape differs is rejected as an idempotency conflict, and one whose earlier attempt already settled answers with a replay error rather than running again. Creation is idempotent under creationId, which is itself a caller-owned UUIDv7 and becomes the new lens id, so a create retried with the same creationId repairs a half-finished creation instead of producing a second lens. A destroy is capacity-exempt on purpose, because you can always remove your own data. List calls paginate: pass the previous response's nextCursor back as cursor and stop when hasMore is false. Writing documents replaces the entire ordered projection, and writing connections replaces the entire arrow set and additionally requires the connectionsBasis your most recent read returned; omit either field to leave it unchanged.
Reads and list calls need mcp:read plus the organization's document read permission. Creates, updates, and destroys need mcp:write plus the document manage permission, and the read check runs first so a missing write scope can never be distinguished from a missing lens. organizationId is required on every call and must be the lens's owning organization; a mismatch produces the same concealed failure as a nonexistent lens. Documents from other organizations may be projected into a lens when you can read them, either through membership or through the owning organization's public visibility, and titles come back only for the documents you can actually read. A reader without manage permission gets a storage-only snapshot rather than a freshly refreshed one, so a strictly read-only role may see slightly staler state. Every call authorizes the resolved organization live, and an organization whose corpus is fenced by its lifecycle rejects lens mutations.
The lens is where documents work becomes visible to a person. Read or search with the documents namespace, then project the resulting document ids into a lens and hand the returned url to the user so they can see what you are working on. The organizationId comes from organizations.listMine, and the operationId and creationId are UUIDv7 values you generate on your own host, or from ids.mintOperationIds in a run of its own when the host cannot. Reading humanFocusedDocumentId back closes the loop in the other direction: it tells you which document the person clicked, which is the human half of a shared session.
Session
What it does
The session namespace contains one method, focus, which manages lenses. A lens is a named, long-lived canvas that holds an ordered set of documents as cards and has a URL a person can open. You curate it: choose the documents, label them, place them on the canvas and move the focus. The user sees what you assembled, and their clicks and arrangement come back to you on the next read.
A lens is attention and nothing else. It is not an access boundary, it grants no permissions, and it does not change who can read anything. Two people looking at the same lens see only the documents each of them could already read.
A lens is meant to last: the person comes back to it to read, plan and arrange. For a one-off "show me this", make an HTML page document instead (Choose a document, an HTML page or a Lens). After 30 days without activity a lens leaves the recent directory and goes dormant; its URL still opens it and its documents stay. Durable knowledge belongs in documents, never only on the board.
One method, five operations
focus dispatches on the shape of your input rather than on an operation name, which is the single thing most worth internalizing about this namespace.
To list, pass only organizationId and operationId, optionally with cursor. Any of lensId, operation, name, layout, documents, connections, focusedDocumentId, or creationId takes you out of list mode. Use this to recover a lens id you lost.
To create, omit lensId and pass name and creationId. Both are required; a create without a name fails, and a create without a creationId fails with a pointer to the minting ritual.
To read, pass lensId and nothing that mutates: no operation, name, layout, focusedDocumentId, creationId, documents, or connections. Adding any of those turns the call into an update. To read the next page of a large lens, pass cursor with operationId, organizationId, and lensId and nothing else.
To update, pass lensId plus whichever fields you want to change.
To destroy, pass operation: "destroy" with lensId and organizationId. Destroy accepts nothing else; passing name or documents alongside it is an error rather than a combined operation.
The return shape
Every call returns the same flat object. This is a deliberate constraint, because MCP structured output must be a single object rather than a union, and it is the return-shape gotcha that catches agents.
On a single-lens call, lensId, url, name, and layout are populated, documents, connections, and sketches carry the lens contents, and lenses is absent; a large lens comes in pages, so follow nextCursor while hasMore is true and compare what you collected with counts.
On a list call it inverts: lensId, url, name, layout, focusedDocumentId, humanFocusedDocumentId, humanFocusedAt, and connectionsBasis are all null, documents, connections, sketches, and warnings are empty arrays, and the directory arrives under lenses, newest first, with real nextCursor and hasMore values. Each entry carries lensId, url, name, layout, lastKnownDocumentCount, and updatedAt. Note that lastKnownDocumentCount is the count at the last mutation, so a document deleted since then makes the live projection smaller than the number shown.
On a destroy, lensId echoes back and everything else is null or empty.
So do not branch on the presence of a top-level key you expect only in one mode; branch on whether lenses is present.
Creating a lens
// operationId and creationId are UUIDv7 values generated on your own host and kept as literals.
const operationId = "0192f3c1-1111-7aaa-8bbb-cccccccccccc"
const creationId = "0192f3c1-2222-7aaa-8bbb-cccccccccccc"
const lens = await session.focus({
operationId,
creationId,
organizationId,
name: "Launch prep",
documents: [
{ documentId: draftId, label: "Variant A: formal", placement: { x: 0, y: 0 } },
{ documentId: altId, label: "Variant B: direct", placement: { x: 384, y: 0 } },
],
focusedDocumentId: draftId,
})
return lens.urlThe creationId you mint becomes the lens id, which means creation is genuinely idempotent: retrying the same create with the same creationId after an ambiguous failure repairs the half-finished creation and returns the same lens rather than making a second one. A creationId that already belongs to a different organization, or to a lens that was destroyed, comes back as unavailable, and the correct response is to mint a fresh one for a fresh lens rather than to reuse anything.
Hand the returned url to the user. It is the whole point of building a lens.
The example shows the flow; live server validation inside run is authoritative for exact parameters.
Placement, and reading the human's arrangement
Every lens is a canvas of freely placed cards. A card you never placed sits in a grid in its order. To compare variants or translations, place them side by side. To give one document the main place, make its card larger and put its sources around it.
Every document in a read carries an effective placement: x, y, z, width, and height, exactly as rendered. Two flags on it matter. derived: true marks a card that has never been moved and is sitting in its deterministic default slot; write a placement to commit it. Otherwise the geometry was committed and placedBy tells you whether an agent or a human put it there. Treat human-arranged spatial grouping as intent worth preserving, and arrange against the real bounds the read gives you rather than against assumed defaults. Smaller x is farther left and smaller y is higher.
Writing a placement applies in the same call that changes membership. Omitting a placement keeps the card where it is; existing geometry is never wiped, and omitting width or height keeps the current size. Stacking order is assigned by the server. The warnings array on every read and write carries canvas lints for overlapping, fully hidden, or stray cards, and it is worth fixing what a lint names before adding more arrangement on top of it.
Connections need a basis
Canvas arrows between member documents live in connections, and writing them is a full-set replacement: whatever you pass becomes the complete arrow set, [] clears them, and both endpoints must be in the lens. An arrow whose endpoint leaves the lens is removed with it.
Because it is a replacement, writing connections requires connectionsBasis, the fingerprint your most recent read of this lens returned. A missing or stale basis rejects the update. This exists so that arrows a human drew between your read and your write are never silently overwritten. On rejection, read the lens again, merge the human's arrows into your intended set, and retry with the fresh basis. Merging is the expected response; overwriting is not.
Each returned connection carries createdBy, so you can tell your own arrows from the human's.
Reading the human back
Three fields carry the human half of the session. humanFocusedDocumentId is the document the person last clicked on their screen, with humanFocusedAt as its millisecond timestamp; both are null when they have not clicked yet. sketches summarizes freehand ink as bounding boxes with a point count, and a stroke's area is usually a direct signal of what the person cares about right now. placedBy on each placement, and createdBy on each connection, distinguish human arrangement from your own.
A read of one lens also returns viewers, the people looking at its canvas right now: one entry per connected screen, most recently moved first. Each entry carries rect, the part of the canvas that screen shows, in the same units as placements (x and y of its top left corner, width and height); deviceClass, which is phone, tablet, or desktop; and ageMs, how long ago that rectangle last changed, so a person reading without scrolling shows a growing age. Use it to put what matters where the person is looking, and to keep what a phone viewer must read inside their narrow rectangle. The list is empty when nobody has the canvas open; a hidden tab and a screen that has published no view are not listed. Writes and list calls carry no viewers.
Viewers are presence, the same kind of information members already see as each other's cursors on the canvas. They carry no pixel sizes and no screenshots, nothing about them is stored, and they are not metered. Only members of the lens's organization can read a lens, so only agents working for its members see who is viewing it.
None of these are writable. You read them to decide what to do next.
Failures that look like other failures
The concealment rule is worth stating plainly, because it will otherwise mislead you. Lens ids are globally unique, so a distinguishable authorization error would confirm that a foreign lens exists. Instead, a lens owned by another organization, a destroyed lens, a nonexistent lens, an expired one, and a lens whose id does not match the organizationId you passed all fail identically with a message saying the lens is not configured. Do not read that message as proof the lens is gone; read it as "this lens is not available to you as addressed". The usual real cause is a wrong organizationId or a stale lens id, and a list call in the right organization resolves both.
A few other failures are specific and actionable. Adding a document you cannot read, or one deleted before the mutation committed, names the offending document id. Adding the same document twice in one projection is rejected with the duplicate ids listed. Focusing a document that is not in the projection is rejected: add it in the same call, or focus something already there. And a document that already belongs to the platform-guard maximum number of active lenses is refused, which you resolve by destroying a lens you no longer need.
For discovery, ask guide a direct question such as How do I show documents to a user in Oddlynew?. For exact parameters, live server validation inside run is authoritative.