gumloop 0.5.4__tar.gz → 0.5.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. {gumloop-0.5.4 → gumloop-0.5.6}/PKG-INFO +1 -1
  2. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/_gumcp_transport.py +13 -1
  3. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/_http.py +25 -2
  4. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/_version.py +1 -1
  5. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/cli/commands/agents.py +35 -11
  6. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/cli/commands/mcp.py +22 -0
  7. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/cli/commands/sessions.py +11 -0
  8. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/cli/plugin_assets/gumloop/skills/gumloop-cli/SKILL.md +5 -0
  9. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/cli/plugin_assets/gumloop/skills/gumloop-cli/references/commands.md +9 -4
  10. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/resources/agents.py +13 -0
  11. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/resources/mcp.py +64 -19
  12. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/resources/sessions.py +14 -4
  13. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/types.py +36 -0
  14. {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/test_agents.py +58 -0
  15. {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/test_mcp.py +28 -0
  16. {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/test_sessions.py +18 -0
  17. {gumloop-0.5.4 → gumloop-0.5.6}/tests/sdk/test_agents.py +13 -0
  18. {gumloop-0.5.4 → gumloop-0.5.6}/tests/sdk/test_gumcp_transport.py +37 -0
  19. gumloop-0.5.6/tests/sdk/test_mcp_accounts.py +180 -0
  20. {gumloop-0.5.4 → gumloop-0.5.6}/tests/sdk/test_sessions.py +20 -2
  21. {gumloop-0.5.4 → gumloop-0.5.6}/.env.example +0 -0
  22. {gumloop-0.5.4 → gumloop-0.5.6}/.github/workflows/ci.yml +0 -0
  23. {gumloop-0.5.4 → gumloop-0.5.6}/.github/workflows/release.yml +0 -0
  24. {gumloop-0.5.4 → gumloop-0.5.6}/.gitignore +0 -0
  25. {gumloop-0.5.4 → gumloop-0.5.6}/.pre-commit-config.yaml +0 -0
  26. {gumloop-0.5.4 → gumloop-0.5.6}/.python-version +0 -0
  27. {gumloop-0.5.4 → gumloop-0.5.6}/LICENSE +0 -0
  28. {gumloop-0.5.4 → gumloop-0.5.6}/README.md +0 -0
  29. {gumloop-0.5.4 → gumloop-0.5.6}/pyproject.toml +0 -0
  30. {gumloop-0.5.4 → gumloop-0.5.6}/scripts/check +0 -0
  31. {gumloop-0.5.4 → gumloop-0.5.6}/scripts/fmt +0 -0
  32. {gumloop-0.5.4 → gumloop-0.5.6}/scripts/import-logins.sh +0 -0
  33. {gumloop-0.5.4 → gumloop-0.5.6}/scripts/install.sh +0 -0
  34. {gumloop-0.5.4 → gumloop-0.5.6}/scripts/sdk_examples.py +0 -0
  35. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/__init__.py +0 -0
  36. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/_auth.py +0 -0
  37. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/_client.py +0 -0
  38. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/brain_sync.py +0 -0
  39. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/browser_logins/__init__.py +0 -0
  40. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/browser_logins/chromium_cookies.py +0 -0
  41. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/browser_logins/discovery.py +0 -0
  42. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/browser_logins/extract.py +0 -0
  43. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/browser_logins/filter.py +0 -0
  44. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/browser_logins/firefox_cookies.py +0 -0
  45. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/cli/__init__.py +0 -0
  46. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/cli/commands/__init__.py +0 -0
  47. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/cli/commands/_args_input.py +0 -0
  48. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/cli/commands/_downloads.py +0 -0
  49. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/cli/commands/_sync_output.py +0 -0
  50. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/cli/commands/artifacts.py +0 -0
  51. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/cli/commands/auth.py +0 -0
  52. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/cli/commands/brain.py +0 -0
  53. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/cli/commands/browser.py +0 -0
  54. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/cli/commands/chat.py +0 -0
  55. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/cli/commands/evaluations.py +0 -0
  56. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/cli/commands/models.py +0 -0
  57. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/cli/commands/plugin.py +0 -0
  58. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/cli/commands/skills.py +0 -0
  59. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/cli/commands/sync.py +0 -0
  60. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/cli/commands/update.py +0 -0
  61. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/cli/console.py +0 -0
  62. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/cli/context.py +0 -0
  63. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/cli/credentials.py +0 -0
  64. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/cli/errors.py +0 -0
  65. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/cli/main.py +0 -0
  66. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/cli/oauth.py +0 -0
  67. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/cli/plugin_assets/gumloop/plugin.json +0 -0
  68. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/errors.py +0 -0
  69. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/oauth.py +0 -0
  70. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/py.typed +0 -0
  71. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/resources/__init__.py +0 -0
  72. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/resources/artifacts.py +0 -0
  73. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/resources/brain.py +0 -0
  74. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/resources/browser_profiles.py +0 -0
  75. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/resources/chat.py +0 -0
  76. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/resources/decisions.py +0 -0
  77. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/resources/evaluations.py +0 -0
  78. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/resources/organizations.py +0 -0
  79. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/resources/skills.py +0 -0
  80. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/resources/sync.py +0 -0
  81. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/resources/teams.py +0 -0
  82. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/spec/__init__.py +0 -0
  83. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/spec/_compat.py +0 -0
  84. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/spec/_decisions.py +0 -0
  85. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/spec/_extensions.py +0 -0
  86. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/sync/__init__.py +0 -0
  87. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/sync/archive.py +0 -0
  88. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/sync/bundle.py +0 -0
  89. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/sync/bundle_validation.py +0 -0
  90. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/sync/errors.py +0 -0
  91. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/sync/hashing.py +0 -0
  92. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/sync/installed.py +0 -0
  93. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/sync/local_state.py +0 -0
  94. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/sync/lock.py +0 -0
  95. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/sync/markers.py +0 -0
  96. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/sync/reconcile.py +0 -0
  97. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/sync/reporting.py +0 -0
  98. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/sync/results.py +0 -0
  99. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/sync/run.py +0 -0
  100. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/sync/scheduler.py +0 -0
  101. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/sync/target_files.py +0 -0
  102. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/sync/targets.py +0 -0
  103. {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/sync/wire.py +0 -0
  104. {gumloop-0.5.4 → gumloop-0.5.6}/tests/__init__.py +0 -0
  105. {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/__init__.py +0 -0
  106. {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/conftest.py +0 -0
  107. {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/sync_scenario_helpers.py +0 -0
  108. {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/sync_test_fakes.py +0 -0
  109. {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/test_args_input.py +0 -0
  110. {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/test_artifacts.py +0 -0
  111. {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/test_brain.py +0 -0
  112. {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/test_brain_sync.py +0 -0
  113. {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/test_browser_logins.py +0 -0
  114. {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/test_chat.py +0 -0
  115. {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/test_context.py +0 -0
  116. {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/test_credentials.py +0 -0
  117. {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/test_evaluations.py +0 -0
  118. {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/test_home_isolation.py +0 -0
  119. {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/test_login.py +0 -0
  120. {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/test_main.py +0 -0
  121. {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/test_models.py +0 -0
  122. {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/test_plugin.py +0 -0
  123. {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/test_skills.py +0 -0
  124. {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/test_sync_bundle.py +0 -0
  125. {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/test_sync_command.py +0 -0
  126. {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/test_sync_command_safety.py +0 -0
  127. {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/test_sync_contract_fixtures.py +0 -0
  128. {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/test_sync_enrollment.py +0 -0
  129. {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/test_sync_fixture_corpus.py +0 -0
  130. {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/test_sync_hashing.py +0 -0
  131. {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/test_sync_installed.py +0 -0
  132. {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/test_sync_reconciliation_safety.py +0 -0
  133. {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/test_sync_scheduler_macos.py +0 -0
  134. {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/test_sync_targets.py +0 -0
  135. {gumloop-0.5.4 → gumloop-0.5.6}/tests/conftest.py +0 -0
  136. {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/README.md +0 -0
  137. {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/archives/generated-bundle-transfer-limit.json +0 -0
  138. {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/archives/invalid-absolute-path.zip +0 -0
  139. {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/archives/invalid-bytes-per-file-limit.zip +0 -0
  140. {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/archives/invalid-device.zip +0 -0
  141. {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/archives/invalid-duplicate-entry.zip +0 -0
  142. {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/archives/invalid-fifo.zip +0 -0
  143. {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/archives/invalid-files-per-skill-limit.zip +0 -0
  144. {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/archives/invalid-nul-name.zip +0 -0
  145. {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/archives/invalid-parent-traversal.zip +0 -0
  146. {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/archives/invalid-reserved-marker.zip +0 -0
  147. {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/archives/invalid-socket.zip +0 -0
  148. {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/archives/invalid-symlink.zip +0 -0
  149. {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/archives/invalid-total-uncompressed-limit.zip +0 -0
  150. {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/archives/invalid-windows-drive-prefix.zip +0 -0
  151. {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/archives/valid-single-skill-stored.zip +0 -0
  152. {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/archives/valid-single-skill.zip +0 -0
  153. {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/bundles/newer-than-plan.zip +0 -0
  154. {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/bundles/normal.zip +0 -0
  155. {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/catalog.json +0 -0
  156. {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/corpus.sha256 +0 -0
  157. {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/hashes/content-v1.json +0 -0
  158. {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/hashes/manifest-v1.json +0 -0
  159. {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/manifests/invalid-duplicate-identity.json +0 -0
  160. {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/manifests/invalid-duplicate-install-name.json +0 -0
  161. {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/manifests/invalid-manifest-hash.json +0 -0
  162. {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/manifests/newer-than-plan.json +0 -0
  163. {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/manifests/normal-bundle.json +0 -0
  164. {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/responses/below-pro.json +0 -0
  165. {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/responses/cli-upgrade-required.json +0 -0
  166. {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/responses/lost-membership.json +0 -0
  167. {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/responses/normal-empty-plan.json +0 -0
  168. {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/responses/normal-plan.json +0 -0
  169. {gumloop-0.5.4 → gumloop-0.5.6}/tests/integration/conftest.py +0 -0
  170. {gumloop-0.5.4 → gumloop-0.5.6}/tests/integration/test_live.py +0 -0
  171. {gumloop-0.5.4 → gumloop-0.5.6}/tests/sdk/__init__.py +0 -0
  172. {gumloop-0.5.4 → gumloop-0.5.6}/tests/sdk/conftest.py +0 -0
  173. {gumloop-0.5.4 → gumloop-0.5.6}/tests/sdk/helpers.py +0 -0
  174. {gumloop-0.5.4 → gumloop-0.5.6}/tests/sdk/test_artifacts.py +0 -0
  175. {gumloop-0.5.4 → gumloop-0.5.6}/tests/sdk/test_brain.py +0 -0
  176. {gumloop-0.5.4 → gumloop-0.5.6}/tests/sdk/test_chat.py +0 -0
  177. {gumloop-0.5.4 → gumloop-0.5.6}/tests/sdk/test_client.py +0 -0
  178. {gumloop-0.5.4 → gumloop-0.5.6}/tests/sdk/test_decisions.py +0 -0
  179. {gumloop-0.5.4 → gumloop-0.5.6}/tests/sdk/test_errors.py +0 -0
  180. {gumloop-0.5.4 → gumloop-0.5.6}/tests/sdk/test_evaluations.py +0 -0
  181. {gumloop-0.5.4 → gumloop-0.5.6}/tests/sdk/test_mcp.py +0 -0
  182. {gumloop-0.5.4 → gumloop-0.5.6}/tests/sdk/test_models.py +0 -0
  183. {gumloop-0.5.4 → gumloop-0.5.6}/tests/sdk/test_oauth.py +0 -0
  184. {gumloop-0.5.4 → gumloop-0.5.6}/tests/sdk/test_organizations.py +0 -0
  185. {gumloop-0.5.4 → gumloop-0.5.6}/tests/sdk/test_skills.py +0 -0
  186. {gumloop-0.5.4 → gumloop-0.5.6}/tests/sdk/test_sync.py +0 -0
  187. {gumloop-0.5.4 → gumloop-0.5.6}/tests/sdk/test_teams.py +0 -0
  188. {gumloop-0.5.4 → gumloop-0.5.6}/tests/skill_sync_fixtures.py +0 -0
  189. {gumloop-0.5.4 → gumloop-0.5.6}/tests/test_client.py +0 -0
  190. {gumloop-0.5.4 → gumloop-0.5.6}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: gumloop
3
- Version: 0.5.4
3
+ Version: 0.5.6
4
4
  Summary: Python client for the Gumloop API
5
5
  Project-URL: Homepage, https://github.com/gumloop/gumloop-py
6
6
  Project-URL: Bug Tracker, https://github.com/gumloop/gumloop-py/issues
@@ -149,6 +149,7 @@ def _error_result(
149
149
  def _map_exception(exc: BaseException, *, ref: str, server_id: str, tool_name: str) -> McpToolCallResult:
150
150
  message = str(exc)
151
151
  lower = message.lower()
152
+ exc_name = type(exc).__name__
152
153
 
153
154
  if "credentials_not_found" in message or "authentication required" in lower:
154
155
  return _error_result(
@@ -163,6 +164,18 @@ def _map_exception(exc: BaseException, *, ref: str, server_id: str, tool_name: s
163
164
  details={"server_id": server_id, "tool_name": tool_name},
164
165
  )
165
166
 
167
+ if exc_name == "ToolResultError":
168
+ return _error_result(
169
+ ref=ref,
170
+ server_id=server_id,
171
+ tool_name=tool_name,
172
+ status="error",
173
+ code="mcp_tool_error",
174
+ message=message,
175
+ error_type="api_error",
176
+ details={"server_id": server_id, "tool_name": tool_name},
177
+ )
178
+
166
179
  if (
167
180
  "not permitted" in lower
168
181
  or "not allowed" in lower
@@ -215,7 +228,6 @@ def _map_exception(exc: BaseException, *, ref: str, server_id: str, tool_name: s
215
228
  )
216
229
 
217
230
  # Connection-class failures from gumcp_client.
218
- exc_name = type(exc).__name__
219
231
  if exc_name in {"ConnectionError", "SessionError"} or "failed to connect" in lower:
220
232
  return _error_result(
221
233
  ref=ref,
@@ -228,8 +228,11 @@ class HttpClient:
228
228
  *,
229
229
  json: Any = None,
230
230
  params: Mapping[str, Any] | None = None,
231
+ extra_headers: Mapping[str, str] | None = None,
231
232
  ) -> Iterator[StreamEvent]:
232
233
  headers = {**auth_headers(self.access_token, self.user_id), "Accept": "text/event-stream"}
234
+ if extra_headers:
235
+ headers.update(extra_headers)
233
236
  with self._client.stream(
234
237
  method,
235
238
  f"{self._stream_base_url}/{path.lstrip('/')}",
@@ -355,8 +358,16 @@ class AsyncHttpClient:
355
358
  json: Any = None,
356
359
  data: Mapping[str, Any] | None = None,
357
360
  files: list[tuple[str, Any]] | None = None,
361
+ extra_headers: Mapping[str, str] | None = None,
358
362
  ) -> Any:
359
- return await self._request("POST", path, json=json, data=_omit_none_params(data), files=files)
363
+ return await self._request(
364
+ "POST",
365
+ path,
366
+ json=json,
367
+ data=_omit_none_params(data),
368
+ files=files,
369
+ extra_headers=extra_headers,
370
+ )
360
371
 
361
372
  async def patch(
362
373
  self,
@@ -394,8 +405,11 @@ class AsyncHttpClient:
394
405
  *,
395
406
  json: Any = None,
396
407
  params: Mapping[str, Any] | None = None,
408
+ extra_headers: Mapping[str, str] | None = None,
397
409
  ) -> AsyncIterator[StreamEvent]:
398
410
  headers = {**auth_headers(self.access_token, self.user_id), "Accept": "text/event-stream"}
411
+ if extra_headers:
412
+ headers.update(extra_headers)
399
413
  async with self._client.stream(
400
414
  method,
401
415
  f"{self._stream_base_url}/{path.lstrip('/')}",
@@ -444,10 +458,19 @@ class AsyncHttpClient:
444
458
  logger.debug("dropped non-%s SSE: %s", response_model.__name__, event.data)
445
459
  continue
446
460
 
447
- async def _request(self, method: str, path: str, **kwargs: Any) -> Any:
461
+ async def _request(
462
+ self,
463
+ method: str,
464
+ path: str,
465
+ *,
466
+ extra_headers: Mapping[str, str] | None = None,
467
+ **kwargs: Any,
468
+ ) -> Any:
448
469
  headers = auth_headers(self.access_token, self.user_id)
449
470
  if not kwargs.get("files"):
450
471
  headers["Content-Type"] = "application/json"
472
+ if extra_headers:
473
+ headers.update(extra_headers)
451
474
  kwargs["params"] = self._scoped_params(kwargs.get("params"))
452
475
  response = await self._client.request(method, path, headers=headers, **kwargs)
453
476
  if response.status_code >= 400:
@@ -1,3 +1,3 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.5.4"
3
+ __version__ = "0.5.6"
@@ -106,6 +106,8 @@ def get_agent(
106
106
  value = getattr(agent, field, None)
107
107
  if value not in (None, ""):
108
108
  console.print(f" {field}: {value}", markup=False, highlight=False)
109
+ if agent.incognito.enforced:
110
+ console.print(" incognito: on (nothing is saved)", markup=False, highlight=False)
109
111
  if agent.system_prompt:
110
112
  console.print(" system_prompt:", markup=False, highlight=False)
111
113
  console.print(f" {agent.system_prompt}", markup=False, highlight=False)
@@ -253,6 +255,13 @@ def create_agent(
253
255
  str | None,
254
256
  typer.Option("--skill-ids", help="Comma-separated ids of existing skills to attach."),
255
257
  ] = None,
258
+ incognito: Annotated[
259
+ bool,
260
+ typer.Option(
261
+ "--incognito",
262
+ help="Every session on the agent runs incognito (nothing is saved). Needs a role that allows it.",
263
+ ),
264
+ ] = False,
256
265
  json_output: Annotated[
257
266
  bool,
258
267
  typer.Option("--json", help="Print the raw SDK response as JSON."),
@@ -310,6 +319,7 @@ def create_agent(
310
319
  tools=tools,
311
320
  skill_ids=parsed_skill_ids,
312
321
  team_id=cli.effective_team_id,
322
+ incognito=True if incognito else None,
313
323
  )
314
324
  )
315
325
  except GumloopError as error:
@@ -323,6 +333,8 @@ def create_agent(
323
333
  console.print(f"[green]Created agent[/green] {escape_markup(agent.id)}")
324
334
  if agent.name:
325
335
  console.print(f" Name: {agent.name}", markup=False, highlight=False)
336
+ if agent.incognito.enforced:
337
+ console.print(" Incognito: on (nothing is saved)", markup=False, highlight=False)
326
338
 
327
339
 
328
340
  @agents_app.command(
@@ -371,6 +383,13 @@ def update_agent(
371
383
  bool | None,
372
384
  typer.Option("--is-active/--inactive", help="Mark the agent as active or inactive."),
373
385
  ] = None,
386
+ incognito: Annotated[
387
+ bool | None,
388
+ typer.Option(
389
+ "--incognito/--no-incognito",
390
+ help="Run every session on the agent incognito (nothing is saved), or stop. Needs a role that allows it.",
391
+ ),
392
+ ] = None,
374
393
  json_output: Annotated[
375
394
  bool,
376
395
  typer.Option("--json", help="Print the raw SDK response as JSON."),
@@ -411,18 +430,21 @@ def update_agent(
411
430
  raise GumloopError("Tools JSON must be an array at the top level.")
412
431
  tools = parsed
413
432
 
414
- response = cli.call_with_refresh(
415
- lambda client: client.agents.update(
416
- agent_id,
417
- name=name,
418
- model_name=model,
419
- description=description,
420
- system_prompt=resolved_prompt,
421
- tools=tools,
422
- is_active=is_active,
423
- team_id=cli.effective_team_id,
433
+ fields = {
434
+ "name": name,
435
+ "model_name": model,
436
+ "description": description,
437
+ "system_prompt": resolved_prompt,
438
+ "tools": tools,
439
+ "is_active": is_active,
440
+ }
441
+ response = None
442
+ if incognito is not None:
443
+ response = cli.call_with_refresh(lambda client: client.agents.set_incognito(agent_id, incognito))
444
+ if incognito is None or any(value is not None for value in fields.values()):
445
+ response = cli.call_with_refresh(
446
+ lambda client: client.agents.update(agent_id, team_id=cli.effective_team_id, **fields)
424
447
  )
425
- )
426
448
  except GumloopError as error:
427
449
  exit_with_error(error, json_output=json_output)
428
450
 
@@ -431,6 +453,8 @@ def update_agent(
431
453
  return
432
454
 
433
455
  console.print(f"[green]Updated agent[/green] {agent_id}")
456
+ if incognito is not None:
457
+ console.print(f" Incognito: {'on (nothing is saved)' if incognito else 'off'}", markup=False, highlight=False)
434
458
 
435
459
 
436
460
  @agents_app.command(
@@ -76,6 +76,26 @@ def get_server(
76
76
  console.print(f" {field}: {value}", markup=False, highlight=False)
77
77
 
78
78
 
79
+ @mcp_app.command("accounts")
80
+ def list_accounts(
81
+ ctx: typer.Context,
82
+ server: Annotated[str, typer.Argument(help="MCP server id.")],
83
+ json_output: Annotated[bool, typer.Option("--json", help="Print the raw response as JSON.")] = False,
84
+ ) -> None:
85
+ """List connected accounts available for a tool call."""
86
+ cli: CliContext = ctx.obj
87
+ try:
88
+ response = cli.call_with_refresh(lambda client: client.mcp.list_accounts(server, team_id=cli.effective_team_id))
89
+ except GumloopError as error:
90
+ exit_with_error(error, json_output=json_output)
91
+ if json_output:
92
+ print_json(response)
93
+ return
94
+ console.print("ACCOUNT_ID", "LABEL", "DEFAULT", "IN_USE", sep="\t", soft_wrap=True)
95
+ for account in response.accounts:
96
+ console.print(account.id, account.label, str(account.is_default), str(account.in_use), sep="\t", markup=False)
97
+
98
+
79
99
  @mcp_app.command("tools", epilog="Example:\n gumloop mcp tools gmail")
80
100
  def list_tools(
81
101
  ctx: typer.Context,
@@ -134,6 +154,7 @@ def call_tool(
134
154
  str | None,
135
155
  typer.Option("--args", help="Use '-' to read JSON arguments from stdin.", metavar="-"),
136
156
  ] = None,
157
+ account: Annotated[str | None, typer.Option("--account", help="Connected account id to use.")] = None,
137
158
  ref: Annotated[
138
159
  str | None,
139
160
  typer.Option("--ref", help="Optional ref string echoed back in the response."),
@@ -153,6 +174,7 @@ def call_tool(
153
174
  tool,
154
175
  arguments,
155
176
  ref=ref,
177
+ account=account,
156
178
  team_id=cli.effective_team_id,
157
179
  )
158
180
  )
@@ -47,6 +47,10 @@ def create_session(
47
47
  str | None,
48
48
  typer.Option("--name", help="Session name shown in the sidebar (1-256 characters)."),
49
49
  ] = None,
50
+ incognito: Annotated[
51
+ bool,
52
+ typer.Option("--incognito", help="Run the session incognito: nothing is saved."),
53
+ ] = False,
50
54
  json_output: Annotated[
51
55
  bool,
52
56
  typer.Option("--json", help="Print the raw SDK response as JSON."),
@@ -75,6 +79,7 @@ def create_session(
75
79
  input=message,
76
80
  session_id=session_id,
77
81
  name=name,
82
+ incognito=True if incognito else None,
78
83
  )
79
84
  ),
80
85
  )
@@ -94,6 +99,8 @@ def create_session(
94
99
  value = getattr(session, field, None)
95
100
  if value not in (None, ""):
96
101
  console.print(f" {field}: {value}", markup=False, highlight=False)
102
+ if session.incognito:
103
+ console.print(" incognito: on (nothing is saved)", markup=False, highlight=False)
97
104
  if session.messages:
98
105
  console.print(f" messages: {len(session.messages)}")
99
106
  for m in session.messages[-5:]:
@@ -220,6 +227,8 @@ def get_session(
220
227
  value = getattr(session, field, None)
221
228
  if value not in (None, ""):
222
229
  console.print(f" {field}: {value}", markup=False, highlight=False)
230
+ if session.incognito:
231
+ console.print(" incognito: on (nothing is saved)", markup=False, highlight=False)
223
232
  if session.messages:
224
233
  console.print(f" messages: {len(session.messages)}")
225
234
  for m in session.messages[-5:]:
@@ -284,6 +293,8 @@ def send_session(
284
293
  value = getattr(session, field, None)
285
294
  if value not in (None, ""):
286
295
  console.print(f" {field}: {value}", markup=False, highlight=False)
296
+ if session.incognito:
297
+ console.print(" incognito: on (nothing is saved)", markup=False, highlight=False)
287
298
  if session.messages:
288
299
  console.print(f" messages: {len(session.messages)}")
289
300
  for m in session.messages[-5:]:
@@ -102,3 +102,8 @@ Structured output: `--schema-file schema.json`. Stdin input: `--message-stdin -`
102
102
  - `sessions create`/`send` return the final response only; there is no token streaming in the CLI (the Python SDK's `client.sessions.stream()` has it).
103
103
  - Company Brain requires the Pro plan and consumes credits per search.
104
104
  - On headless Linux without a keychain, `gumloop login` refuses to run; use the environment variables above.
105
+
106
+
107
+ ## Connected account selection
108
+
109
+ Use `gumloop mcp accounts <server> --json` to list available account IDs, then `gumloop mcp call <server> <tool> --account <id> --args-json '{...}'` to select one. IDs are stable credential IDs; labels are display-only. Account choice requires backend authorization and, for sandbox tokens, an account grant for that tool. Python supports `client.mcp.list_accounts(server_id)` and `client.mcp.execute(server_id, tool_name, arguments, account="id")`, including async clients and per-item `account` in `execute_many`. Account-selected calls use the backend HTTP API and require `GUMLOOP_ACCESS_TOKEN`; calls without an account retain direct guMCP transport when configured. Computer code must pass account IDs as literal strings so approval can bind them.
@@ -31,11 +31,11 @@ gumloop agents versions <agent_id> [--limit n] [--cursor c] [--json]
31
31
  gumloop agents export <agent_id> <version_id> [-o file.json]
32
32
  gumloop agents create --name NAME --model MODEL [--description d]
33
33
  [--system-prompt s | --system-prompt-file f]
34
- [--tools-json '[...]' | --tools-file f] [--json]
35
- gumloop agents update <agent_id> [same flags as create] [--is-active | --inactive]
34
+ [--tools-json '[...]' | --tools-file f] [--incognito] [--json]
35
+ gumloop agents update <agent_id> [same flags as create] [--is-active | --inactive] [--incognito | --no-incognito]
36
36
  ```
37
37
 
38
- `--model` accepts `auto` or a slug like `anthropic/claude-sonnet-4`. To learn the tool-config shape, run `gumloop agents get <id> --json` on an existing agent and copy its `tools` array. `--inactive` retires the agent irreversibly.
38
+ `--model` accepts `auto` or a slug like `anthropic/claude-sonnet-4`. To learn the tool-config shape, run `gumloop agents get <id> --json` on an existing agent and copy its `tools` array. `--inactive` retires the agent irreversibly. `--incognito` makes every session on the agent incognito (nothing is saved); it needs the Incognito agents role permission (separate from Incognito tasks), and on `create` a refusal leaves no agent behind.
39
39
 
40
40
  ## Models
41
41
 
@@ -51,7 +51,7 @@ cat task.txt | gumloop models route --input-stdin - --model gpt-5.6-luna --model
51
51
  ## Sessions
52
52
 
53
53
  ```bash
54
- gumloop sessions create <agent_id> [--input text | --input-stdin -] [--session-id id] [--name name] [--json]
54
+ gumloop sessions create <agent_id> [--input text | --input-stdin -] [--session-id id] [--name name] [--incognito] [--json]
55
55
  gumloop sessions list <agent_id> [--search text] [--state s] [--type t] [--sort order] [--limit n] [--json]
56
56
  gumloop sessions get <session_id> [--json] # --json returns the full transcript
57
57
  gumloop sessions send <session_id> [--input text | --input-stdin -] [--json]
@@ -127,3 +127,8 @@ gumloop sync --once --non-interactive # one-shot sync; requires GUMLOOP_API_KEY
127
127
  gumloop update # update the CLI in place
128
128
  gumloop plugin install gumloop # install this skill into detected agent skill directories
129
129
  ```
130
+
131
+
132
+ ## Connected account selection
133
+
134
+ Use `gumloop mcp accounts <server> --json` to list available account IDs, then `gumloop mcp call <server> <tool> --account <id> --args-json '{...}'` to select one. IDs are stable credential IDs; labels are display-only. Account choice requires backend authorization and, for sandbox tokens, an account grant for that tool. Python supports `client.mcp.list_accounts(server_id)` and `client.mcp.execute(server_id, tool_name, arguments, account="id")`, including async clients and per-item `account` in `execute_many`. Account-selected calls use the backend HTTP API and require `GUMLOOP_ACCESS_TOKEN`; calls without an account retain direct guMCP transport when configured. Computer code must pass account IDs as literal strings so approval can bind them.
@@ -184,6 +184,13 @@ class Agents:
184
184
  )
185
185
  )
186
186
 
187
+ def set_incognito(self, agent_id: str, enforced: bool) -> AgentResponse:
188
+ """Make every session on the agent incognito (nothing is saved), or stop doing so.
189
+ Needs the Incognito agents role permission (``organization_permission_required`` otherwise)."""
190
+ return AgentResponse.model_validate(
191
+ self._client.patch(f"agents/{agent_id}/incognito", json={"enforced": enforced})
192
+ )
193
+
187
194
  def list_knowledge_sources(self, agent_id: str) -> AgentKnowledgeSourcesResponse:
188
195
  return AgentKnowledgeSourcesResponse.model_validate(self._client.get(f"agents/{agent_id}/knowledge-sources"))
189
196
 
@@ -480,6 +487,12 @@ class AsyncAgents:
480
487
  )
481
488
  return AgentAbilitiesResponse.model_validate(data)
482
489
 
490
+ async def set_incognito(self, agent_id: str, enforced: bool) -> AgentResponse:
491
+ """Make every session on the agent incognito (nothing is saved), or stop doing so.
492
+ Needs the Incognito agents role permission (``organization_permission_required`` otherwise)."""
493
+ data = await self._client.patch(f"agents/{agent_id}/incognito", json={"enforced": enforced})
494
+ return AgentResponse.model_validate(data)
495
+
483
496
  async def list_knowledge_sources(self, agent_id: str) -> AgentKnowledgeSourcesResponse:
484
497
  data = await self._client.get(f"agents/{agent_id}/knowledge-sources")
485
498
  return AgentKnowledgeSourcesResponse.model_validate(data)
@@ -7,11 +7,13 @@ from gumloop._gumcp_transport import GumcpTransport
7
7
  from gumloop._gumcp_transport import gumcp_env_ready
8
8
  from gumloop._http import AsyncHttpClient
9
9
  from gumloop._http import HttpClient
10
+ from gumloop.errors import GumloopError
10
11
  from gumloop.types import McpExecuteResponse
11
12
  from gumloop.types import McpPromptResponse
12
13
  from gumloop.types import McpPromptsResponse
13
14
  from gumloop.types import McpResourceReadResponse
14
15
  from gumloop.types import McpResourcesResponse
16
+ from gumloop.types import McpServerAccountsResponse
15
17
  from gumloop.types import McpServerResponse
16
18
  from gumloop.types import McpServersResponse
17
19
  from gumloop.types import McpToolCallRequest
@@ -19,10 +21,10 @@ from gumloop.types import McpToolsResponse
19
21
 
20
22
 
21
23
  def _call_body(calls: Sequence[McpToolCallRequest | dict[str, Any]], team_id: str | None) -> dict[str, Any]:
22
- # Backend enforces 1–5 calls; the lower bound is checked client-side so
23
- # an empty list fails without a network round-trip.
24
24
  if not calls:
25
25
  raise ValueError("calls cannot be empty")
26
+ if len(calls) > 5:
27
+ raise ValueError("calls cannot exceed 5 items per request.")
26
28
  body: dict[str, Any] = {
27
29
  "calls": [c.model_dump(exclude_none=True) if isinstance(c, McpToolCallRequest) else dict(c) for c in calls],
28
30
  }
@@ -56,6 +58,10 @@ class MCP:
56
58
  self._client.get(f"mcp/servers/{server_id}", params={"team_id": team_id, **kwargs})
57
59
  )
58
60
 
61
+ def list_accounts(self, server_id: str, *, team_id: str | None = None) -> McpServerAccountsResponse:
62
+ data = self._client.get(f"mcp/servers/{server_id}/accounts", params={"team_id": team_id})
63
+ return McpServerAccountsResponse.model_validate(data)
64
+
59
65
  def list_tools(self, server_id: str, *, team_id: str | None = None, **kwargs: Any) -> McpToolsResponse:
60
66
  return McpToolsResponse.model_validate(
61
67
  self._client.get(f"mcp/servers/{server_id}/tools", params={"team_id": team_id, **kwargs})
@@ -108,20 +114,17 @@ class MCP:
108
114
  arguments: dict[str, Any] | None = None,
109
115
  *,
110
116
  ref: str | None = None,
117
+ account: str | None = None,
111
118
  team_id: str | None = None,
112
119
  ) -> McpExecuteResponse:
113
- # Direct transport: team_id comes from the token.
114
- transport = self._direct_transport()
115
- if transport is not None:
116
- return transport.execute(server_id, tool_name, arguments, ref=ref)
117
- team_id = team_id if team_id is not None else self._client.team_id
118
120
  call = McpToolCallRequest(
119
121
  server_id=server_id,
120
122
  tool_name=tool_name,
121
123
  arguments=arguments or {},
122
124
  ref=ref,
125
+ account=account,
123
126
  )
124
- return McpExecuteResponse.model_validate(self._client.post("mcp/tools/call", json=_call_body([call], team_id)))
127
+ return self.execute_many([call], team_id=team_id)
125
128
 
126
129
  def execute_many(
127
130
  self,
@@ -129,9 +132,30 @@ class MCP:
129
132
  *,
130
133
  team_id: str | None = None,
131
134
  ) -> McpExecuteResponse:
132
- transport = self._direct_transport()
135
+ normalized = _call_body(calls, team_id)["calls"]
136
+ has_account = any(call.get("account") is not None for call in normalized)
137
+ if has_account and not self._client.access_token:
138
+ raise GumloopError("Account selection requires GUMLOOP_ACCESS_TOKEN for the backend HTTP API.")
139
+ transport = self._direct_transport() if any(call.get("account") is None for call in normalized) else None
133
140
  if transport is not None:
134
- return transport.execute_many(calls)
141
+ for index, call in enumerate(normalized):
142
+ call["ref"] = call.get("ref") or str(index)
143
+ pinned = [(index, call) for index, call in enumerate(normalized) if call.get("account") is not None]
144
+ if not pinned:
145
+ return transport.execute_many(calls)
146
+ direct = [(index, call) for index, call in enumerate(normalized) if call.get("account") is None]
147
+ team_id = team_id if team_id is not None else self._client.team_id
148
+ data = self._client.post("mcp/tools/call", json=_call_body([call for _, call in pinned], team_id))
149
+ response = McpExecuteResponse.model_validate(data)
150
+ if len(response.results) != len(pinned):
151
+ raise GumloopError("Backend returned an unexpected number of account call results.")
152
+ indexed_results = list(zip((index for index, _ in pinned), response.results, strict=True))
153
+ if direct:
154
+ direct_response = transport.execute_many([call for _, call in direct])
155
+ if len(direct_response.results) != len(direct):
156
+ raise GumloopError("guMCP returned an unexpected number of tool call results.")
157
+ indexed_results.extend(zip((index for index, _ in direct), direct_response.results, strict=True))
158
+ return McpExecuteResponse(results=[result for _, result in sorted(indexed_results)])
135
159
  team_id = team_id if team_id is not None else self._client.team_id
136
160
  return McpExecuteResponse.model_validate(self._client.post("mcp/tools/call", json=_call_body(calls, team_id)))
137
161
 
@@ -161,6 +185,10 @@ class AsyncMCP:
161
185
  data = await self._client.get(f"mcp/servers/{server_id}", params={"team_id": team_id, **kwargs})
162
186
  return McpServerResponse.model_validate(data)
163
187
 
188
+ async def list_accounts(self, server_id: str, *, team_id: str | None = None) -> McpServerAccountsResponse:
189
+ data = await self._client.get(f"mcp/servers/{server_id}/accounts", params={"team_id": team_id})
190
+ return McpServerAccountsResponse.model_validate(data)
191
+
164
192
  async def list_tools(self, server_id: str, *, team_id: str | None = None, **kwargs: Any) -> McpToolsResponse:
165
193
  data = await self._client.get(f"mcp/servers/{server_id}/tools", params={"team_id": team_id, **kwargs})
166
194
  return McpToolsResponse.model_validate(data)
@@ -210,21 +238,17 @@ class AsyncMCP:
210
238
  arguments: dict[str, Any] | None = None,
211
239
  *,
212
240
  ref: str | None = None,
241
+ account: str | None = None,
213
242
  team_id: str | None = None,
214
243
  ) -> McpExecuteResponse:
215
- # Direct transport: team_id comes from the token.
216
- transport = self._direct_transport()
217
- if transport is not None:
218
- return await transport.execute_async(server_id, tool_name, arguments, ref=ref)
219
- team_id = team_id if team_id is not None else self._client.team_id
220
244
  call = McpToolCallRequest(
221
245
  server_id=server_id,
222
246
  tool_name=tool_name,
223
247
  arguments=arguments or {},
224
248
  ref=ref,
249
+ account=account,
225
250
  )
226
- data = await self._client.post("mcp/tools/call", json=_call_body([call], team_id))
227
- return McpExecuteResponse.model_validate(data)
251
+ return await self.execute_many([call], team_id=team_id)
228
252
 
229
253
  async def execute_many(
230
254
  self,
@@ -232,9 +256,30 @@ class AsyncMCP:
232
256
  *,
233
257
  team_id: str | None = None,
234
258
  ) -> McpExecuteResponse:
235
- transport = self._direct_transport()
259
+ normalized = _call_body(calls, team_id)["calls"]
260
+ has_account = any(call.get("account") is not None for call in normalized)
261
+ if has_account and not self._client.access_token:
262
+ raise GumloopError("Account selection requires GUMLOOP_ACCESS_TOKEN for the backend HTTP API.")
263
+ transport = self._direct_transport() if any(call.get("account") is None for call in normalized) else None
236
264
  if transport is not None:
237
- return await transport.execute_many_async(calls)
265
+ for index, call in enumerate(normalized):
266
+ call["ref"] = call.get("ref") or str(index)
267
+ pinned = [(index, call) for index, call in enumerate(normalized) if call.get("account") is not None]
268
+ if not pinned:
269
+ return await transport.execute_many_async(calls)
270
+ direct = [(index, call) for index, call in enumerate(normalized) if call.get("account") is None]
271
+ team_id = team_id if team_id is not None else self._client.team_id
272
+ data = await self._client.post("mcp/tools/call", json=_call_body([call for _, call in pinned], team_id))
273
+ response = McpExecuteResponse.model_validate(data)
274
+ if len(response.results) != len(pinned):
275
+ raise GumloopError("Backend returned an unexpected number of account call results.")
276
+ indexed_results = list(zip((index for index, _ in pinned), response.results, strict=True))
277
+ if direct:
278
+ direct_response = await transport.execute_many_async([call for _, call in direct])
279
+ if len(direct_response.results) != len(direct):
280
+ raise GumloopError("guMCP returned an unexpected number of tool call results.")
281
+ indexed_results.extend(zip((index for index, _ in direct), direct_response.results, strict=True))
282
+ return McpExecuteResponse(results=[result for _, result in sorted(indexed_results)])
238
283
  team_id = team_id if team_id is not None else self._client.team_id
239
284
  data = await self._client.post("mcp/tools/call", json=_call_body(calls, team_id))
240
285
  return McpExecuteResponse.model_validate(data)
@@ -59,6 +59,7 @@ class Sessions:
59
59
  request: SessionCreateRequest | Mapping[str, Any] | None = None,
60
60
  *,
61
61
  stream: Literal[True],
62
+ extra_headers: Mapping[str, str] | None = None,
62
63
  **kwargs: Any,
63
64
  ) -> Iterator[StreamEvent]: ...
64
65
  @overload
@@ -68,18 +69,23 @@ class Sessions:
68
69
  request: SessionCreateRequest | Mapping[str, Any] | None = None,
69
70
  *,
70
71
  stream: Literal[False] | None = None,
72
+ extra_headers: Mapping[str, str] | None = None,
71
73
  **kwargs: Any,
72
74
  ) -> SessionResponse: ...
73
75
  def create(
74
76
  self,
75
77
  agent_id: str,
76
78
  request: SessionCreateRequest | Mapping[str, Any] | None = None,
79
+ *,
80
+ extra_headers: Mapping[str, str] | None = None,
77
81
  **kwargs: Any,
78
82
  ) -> SessionResponse | Iterator[StreamEvent]:
79
83
  body = SessionCreateRequest.build(request, **kwargs)
80
84
  if body.get("stream") is True:
81
- return self._client.stream("POST", f"agents/{agent_id}/sessions", json=body)
82
- return SessionResponse.model_validate(self._client.post(f"agents/{agent_id}/sessions", json=body))
85
+ return self._client.stream("POST", f"agents/{agent_id}/sessions", json=body, extra_headers=extra_headers)
86
+ return SessionResponse.model_validate(
87
+ self._client.post(f"agents/{agent_id}/sessions", json=body, extra_headers=extra_headers)
88
+ )
83
89
 
84
90
  def stream(
85
91
  self,
@@ -197,6 +203,7 @@ class AsyncSessions:
197
203
  request: SessionCreateRequest | Mapping[str, Any] | None = None,
198
204
  *,
199
205
  stream: Literal[True],
206
+ extra_headers: Mapping[str, str] | None = None,
200
207
  **kwargs: Any,
201
208
  ) -> AsyncIterator[StreamEvent]: ...
202
209
  @overload
@@ -206,18 +213,21 @@ class AsyncSessions:
206
213
  request: SessionCreateRequest | Mapping[str, Any] | None = None,
207
214
  *,
208
215
  stream: Literal[False] | None = None,
216
+ extra_headers: Mapping[str, str] | None = None,
209
217
  **kwargs: Any,
210
218
  ) -> SessionResponse: ...
211
219
  async def create(
212
220
  self,
213
221
  agent_id: str,
214
222
  request: SessionCreateRequest | Mapping[str, Any] | None = None,
223
+ *,
224
+ extra_headers: Mapping[str, str] | None = None,
215
225
  **kwargs: Any,
216
226
  ) -> SessionResponse | AsyncIterator[StreamEvent]:
217
227
  body = SessionCreateRequest.build(request, **kwargs)
218
228
  if body.get("stream") is True:
219
- return self._client.stream("POST", f"agents/{agent_id}/sessions", json=body)
220
- data = await self._client.post(f"agents/{agent_id}/sessions", json=body)
229
+ return self._client.stream("POST", f"agents/{agent_id}/sessions", json=body, extra_headers=extra_headers)
230
+ data = await self._client.post(f"agents/{agent_id}/sessions", json=body, extra_headers=extra_headers)
221
231
  return SessionResponse.model_validate(data)
222
232
 
223
233
  def stream(