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.
- {gumloop-0.5.4 → gumloop-0.5.6}/PKG-INFO +1 -1
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/_gumcp_transport.py +13 -1
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/_http.py +25 -2
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/_version.py +1 -1
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/cli/commands/agents.py +35 -11
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/cli/commands/mcp.py +22 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/cli/commands/sessions.py +11 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/cli/plugin_assets/gumloop/skills/gumloop-cli/SKILL.md +5 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/cli/plugin_assets/gumloop/skills/gumloop-cli/references/commands.md +9 -4
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/resources/agents.py +13 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/resources/mcp.py +64 -19
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/resources/sessions.py +14 -4
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/types.py +36 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/test_agents.py +58 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/test_mcp.py +28 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/test_sessions.py +18 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/sdk/test_agents.py +13 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/sdk/test_gumcp_transport.py +37 -0
- gumloop-0.5.6/tests/sdk/test_mcp_accounts.py +180 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/sdk/test_sessions.py +20 -2
- {gumloop-0.5.4 → gumloop-0.5.6}/.env.example +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/.github/workflows/ci.yml +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/.github/workflows/release.yml +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/.gitignore +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/.pre-commit-config.yaml +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/.python-version +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/LICENSE +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/README.md +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/pyproject.toml +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/scripts/check +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/scripts/fmt +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/scripts/import-logins.sh +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/scripts/install.sh +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/scripts/sdk_examples.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/__init__.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/_auth.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/_client.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/brain_sync.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/browser_logins/__init__.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/browser_logins/chromium_cookies.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/browser_logins/discovery.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/browser_logins/extract.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/browser_logins/filter.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/browser_logins/firefox_cookies.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/cli/__init__.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/cli/commands/__init__.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/cli/commands/_args_input.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/cli/commands/_downloads.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/cli/commands/_sync_output.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/cli/commands/artifacts.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/cli/commands/auth.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/cli/commands/brain.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/cli/commands/browser.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/cli/commands/chat.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/cli/commands/evaluations.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/cli/commands/models.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/cli/commands/plugin.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/cli/commands/skills.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/cli/commands/sync.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/cli/commands/update.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/cli/console.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/cli/context.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/cli/credentials.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/cli/errors.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/cli/main.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/cli/oauth.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/cli/plugin_assets/gumloop/plugin.json +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/errors.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/oauth.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/py.typed +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/resources/__init__.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/resources/artifacts.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/resources/brain.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/resources/browser_profiles.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/resources/chat.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/resources/decisions.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/resources/evaluations.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/resources/organizations.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/resources/skills.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/resources/sync.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/resources/teams.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/spec/__init__.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/spec/_compat.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/spec/_decisions.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/spec/_extensions.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/sync/__init__.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/sync/archive.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/sync/bundle.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/sync/bundle_validation.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/sync/errors.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/sync/hashing.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/sync/installed.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/sync/local_state.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/sync/lock.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/sync/markers.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/sync/reconcile.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/sync/reporting.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/sync/results.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/sync/run.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/sync/scheduler.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/sync/target_files.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/sync/targets.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/sync/wire.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/__init__.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/__init__.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/conftest.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/sync_scenario_helpers.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/sync_test_fakes.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/test_args_input.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/test_artifacts.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/test_brain.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/test_brain_sync.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/test_browser_logins.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/test_chat.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/test_context.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/test_credentials.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/test_evaluations.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/test_home_isolation.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/test_login.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/test_main.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/test_models.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/test_plugin.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/test_skills.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/test_sync_bundle.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/test_sync_command.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/test_sync_command_safety.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/test_sync_contract_fixtures.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/test_sync_enrollment.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/test_sync_fixture_corpus.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/test_sync_hashing.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/test_sync_installed.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/test_sync_reconciliation_safety.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/test_sync_scheduler_macos.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/cli/test_sync_targets.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/conftest.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/README.md +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/archives/generated-bundle-transfer-limit.json +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/archives/invalid-absolute-path.zip +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/archives/invalid-bytes-per-file-limit.zip +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/archives/invalid-device.zip +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/archives/invalid-duplicate-entry.zip +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/archives/invalid-fifo.zip +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/archives/invalid-files-per-skill-limit.zip +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/archives/invalid-nul-name.zip +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/archives/invalid-parent-traversal.zip +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/archives/invalid-reserved-marker.zip +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/archives/invalid-socket.zip +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/archives/invalid-symlink.zip +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/archives/invalid-total-uncompressed-limit.zip +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/archives/invalid-windows-drive-prefix.zip +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/archives/valid-single-skill-stored.zip +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/archives/valid-single-skill.zip +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/bundles/newer-than-plan.zip +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/bundles/normal.zip +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/catalog.json +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/corpus.sha256 +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/hashes/content-v1.json +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/hashes/manifest-v1.json +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/manifests/invalid-duplicate-identity.json +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/manifests/invalid-duplicate-install-name.json +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/manifests/invalid-manifest-hash.json +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/manifests/newer-than-plan.json +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/manifests/normal-bundle.json +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/responses/below-pro.json +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/responses/cli-upgrade-required.json +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/responses/lost-membership.json +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/responses/normal-empty-plan.json +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/fixtures/skill_sync/v1/responses/normal-plan.json +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/integration/conftest.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/integration/test_live.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/sdk/__init__.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/sdk/conftest.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/sdk/helpers.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/sdk/test_artifacts.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/sdk/test_brain.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/sdk/test_chat.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/sdk/test_client.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/sdk/test_decisions.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/sdk/test_errors.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/sdk/test_evaluations.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/sdk/test_mcp.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/sdk/test_models.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/sdk/test_oauth.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/sdk/test_organizations.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/sdk/test_skills.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/sdk/test_sync.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/sdk/test_teams.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/skill_sync_fixtures.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/tests/test_client.py +0 -0
- {gumloop-0.5.4 → gumloop-0.5.6}/uv.lock +0 -0
|
@@ -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(
|
|
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(
|
|
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:
|
|
@@ -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
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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:]:
|
{gumloop-0.5.4 → gumloop-0.5.6}/src/gumloop/cli/plugin_assets/gumloop/skills/gumloop-cli/SKILL.md
RENAMED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|