{"record":{"id":"acdeaf5f6fe49446","repo":"can1357/oh-my-pi","slug":"tool-write-not-available-this-session-cannot-do","errorCode":null,"errorMessage":"Tool \"write\" not available: this session cannot download resources to disk.","messagePattern":"Tool \"write\" not available: this session cannot download resources to disk\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/cursor.ts","lineNumber":793,"sourceCode":"\t * workspace-relative path and answer with the path alone, so a large binary\n\t * lands on disk instead of in the model's context. That makes it a workspace\n\t * mutation reached without a registry tool, so it is gated exactly like the\n\t * native `delete` frame — on the session actually granting a file-writing\n\t * tool, and on the user's `write`-tier policy. The gate runs before the read\n\t * so a refused download never fetches the resource either.\n\t */\n\tasync readMcpResource({\n\t\tserver,\n\t\turi,\n\t\tdownloadPath,\n\t}: {\n\t\tserver: string;\n\t\turi: string;\n\t\tdownloadPath?: string;\n\t}): Promise<CursorMcpResourceContent | null> {\n\t\tif (downloadPath) {\n\t\t\tif (!allowsDirectFileMutation(this.options)) {\n\t\t\t\tthrow new Error('Tool \"write\" not available: this session cannot download resources to disk.');\n\t\t\t}\n\t\t\tconst refusal = refuseByWritePolicy(this.options, \"write\", downloadPath);\n\t\t\tif (refusal) throw new Error(refusal);\n\t\t}\n\t\tconst mcp = this.options.mcpResources;\n\t\tif (!mcp) return null;\n\t\tconst read = await mcp.readServerResource(server, uri);\n\t\tif (!read) return null;\n\t\t// The mime type must describe the bytes actually sent, not whatever item\n\t\t// happened to be first: an image blob followed by a text note would\n\t\t// otherwise label the text `image/png` and mislead the model about what\n\t\t// it is holding. Each branch below takes the type from its own producer.\n\t\tconst textItems = read.contents.filter(item => item.text !== undefined);\n\t\tconst texts = textItems.map(item => item.text as string);\n\t\tconst blobItem = read.contents.find(item => item.blob !== undefined);\n\t\tconst blob = blobItem?.blob;\n\t\tconst textMimeType = textItems[0]?.mimeType;\n\t\tconst blobMimeType = blobItem?.mimeType;","sourceCodeStart":775,"sourceCodeEnd":811,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/cursor.ts#L775-L811","documentation":"Reading an MCP resource with a downloadPath requires the session to allow direct file mutation (equivalent to the 'write' tool being enabled). If the session was started without file-write permissions, the download is rejected up front rather than failing later at the write step. This is a capability gate, not a bug.","triggerScenarios":"Calling readMcpResource (Cursor exec bridge) with downloadPath set in a session whose options disable direct file mutation — e.g. write tool disabled, sandboxed/read-only session mode, or an SDK embedding that did not grant file tools.","commonSituations":"Running the agent in a restricted mode (plan/read-only) and asking it to save a fetched MCP resource to disk; embedding the library with a limited tool allowlist; CI sessions without write access.","solutions":["Enable the write tool / direct file mutation in the session options, or restart the session in a mode that grants it.","Omit downloadPath and consume the resource bytes in-memory instead of writing to disk.","If running via the CLI, remove the permission restriction or approve write access for the session/workspace."],"exampleFix":"// before: read-only session options\nconst handlers = new CursorExecHandlers({ ...options, toolPermissions: { write: false } });\n// after: allow writes (or don't pass downloadPath)\nconst handlers = new CursorExecHandlers({ ...options, toolPermissions: { write: true } });","handlingStrategy":"try-catch","validationCode":"if (downloadPath && !sessionAllowsDirectFileMutation) {\n  // don't request a download at all; read in-memory instead\n  delete request.downloadPath;\n}","typeGuard":null,"tryCatchPattern":"try {\n  return await readMcpResource({ server, uri, downloadPath });\n} catch (e) {\n  if (String(e.message).includes('Tool \"write\" not available')) {\n    return readMcpResource({ server, uri }); // fall back to in-memory content\n  } else throw e;\n}","preventionTips":["Only pass downloadPath when the session's write tool is enabled.","Check session tool permissions before offering download affordances.","In SDK/embedding contexts, grant write access or omit downloadPath."],"tags":["permissions","mcp","security"],"backgroundTag":"tool-not-available","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}