python-slack-agents 0.7.0__tar.gz → 0.8.1__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.
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/.gitignore +2 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/CHANGELOG.md +33 -1
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/PKG-INFO +16 -2
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/README.md +14 -1
- python_slack_agents-0.8.1/docs/oauth.md +292 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/docs/tools.md +91 -2
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/llms-full.txt +91 -2
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/pyproject.toml +2 -1
- python_slack_agents-0.8.1/src/slack_agents/__init__.py +78 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/src/slack_agents/agent_loop.py +17 -2
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/src/slack_agents/config.py +85 -2
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/src/slack_agents/main.py +5 -1
- python_slack_agents-0.8.1/src/slack_agents/oauth/__init__.py +6 -0
- python_slack_agents-0.8.1/src/slack_agents/oauth/crypto.py +47 -0
- python_slack_agents-0.8.1/src/slack_agents/oauth/prompts.py +67 -0
- python_slack_agents-0.8.1/src/slack_agents/oauth/server.py +124 -0
- python_slack_agents-0.8.1/src/slack_agents/oauth/state.py +106 -0
- python_slack_agents-0.8.1/src/slack_agents/oauth/storage.py +124 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/src/slack_agents/slack/agent.py +191 -25
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/src/slack_agents/storage/base.py +57 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/src/slack_agents/storage/postgres.py +113 -1
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/src/slack_agents/storage/postgres.sql +25 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/src/slack_agents/storage/sqlite.py +98 -1
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/src/slack_agents/storage/sqlite.sql +25 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/src/slack_agents/tools/base.py +69 -1
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/src/slack_agents/tools/canvas.py +40 -21
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/src/slack_agents/tools/file_exporter.py +23 -3
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/src/slack_agents/tools/mcp_http.py +25 -3
- python_slack_agents-0.8.1/src/slack_agents/tools/mcp_http_oauth.py +1098 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/src/slack_agents/tools/user_context.py +45 -18
- python_slack_agents-0.8.1/tests/test_llm_error_classification.py +72 -0
- python_slack_agents-0.8.1/tests/test_load_plugin.py +47 -0
- python_slack_agents-0.8.1/tests/test_mcp_http_oauth.py +626 -0
- python_slack_agents-0.8.1/tests/test_oauth_crypto.py +72 -0
- python_slack_agents-0.8.1/tests/test_oauth_integration.py +165 -0
- python_slack_agents-0.8.1/tests/test_oauth_prompts.py +68 -0
- python_slack_agents-0.8.1/tests/test_oauth_server.py +105 -0
- python_slack_agents-0.8.1/tests/test_oauth_state.py +78 -0
- python_slack_agents-0.8.1/tests/test_oauth_storage.py +189 -0
- python_slack_agents-0.8.1/tests/test_oauth_validation.py +77 -0
- python_slack_agents-0.8.1/tests/test_storage_oauth.py +132 -0
- python_slack_agents-0.8.1/tests/test_tool_errors.py +92 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/uv.lock +30 -46
- python_slack_agents-0.7.0/src/slack_agents/__init__.py +0 -26
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/.dockerignore +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/.env.example +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/.github/workflows/ci.yml +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/.github/workflows/publish.yml +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/.pre-commit-config.yaml +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/AGENTS.md +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/CODE_OF_CONDUCT.md +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/CONTRIBUTING.md +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/LICENSE +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/SECURITY.md +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/agents/README.md +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/agents/docs-assistant/config.yaml +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/agents/docs-assistant/system_prompt.txt +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/agents/hello-world/config.yaml +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/agents/hello-world/system_prompt.txt +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/agents/kitchen-sink/config.yaml +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/agents/kitchen-sink/system_prompt.txt +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/docs/access-control.md +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/docs/agents.md +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/docs/canvas.md +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/docs/cli.md +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/docs/deployment.md +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/docs/llm.md +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/docs/media/demo.gif +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/docs/observability.md +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/docs/private-repo.md +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/docs/setup.md +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/docs/slack-app-manifest.json +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/docs/storage.md +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/docs/user-context.md +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/llms.txt +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/src/slack_agents/Dockerfile +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/src/slack_agents/access/__init__.py +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/src/slack_agents/access/allow_all.py +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/src/slack_agents/access/allow_list.py +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/src/slack_agents/access/base.py +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/src/slack_agents/cli/__init__.py +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/src/slack_agents/cli/build_docker.py +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/src/slack_agents/cli/export_conversations.py +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/src/slack_agents/cli/export_conversations_html.py +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/src/slack_agents/cli/export_usage.py +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/src/slack_agents/cli/export_usage_csv.py +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/src/slack_agents/cli/healthcheck.py +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/src/slack_agents/cli/init.py +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/src/slack_agents/cli/run.py +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/src/slack_agents/conversations.py +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/src/slack_agents/files.py +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/src/slack_agents/llm/__init__.py +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/src/slack_agents/llm/anthropic.py +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/src/slack_agents/llm/base.py +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/src/slack_agents/llm/openai.py +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/src/slack_agents/observability.py +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/src/slack_agents/py.typed +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/src/slack_agents/scripts/__init__.py +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/src/slack_agents/scripts/download_fonts.py +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/src/slack_agents/scripts/generate_llms_full.py +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/src/slack_agents/slack/__init__.py +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/src/slack_agents/slack/actions.py +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/src/slack_agents/slack/canvas_auth.py +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/src/slack_agents/slack/canvases.py +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/src/slack_agents/slack/files.py +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/src/slack_agents/slack/format.py +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/src/slack_agents/slack/streaming.py +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/src/slack_agents/slack/streaming_formatter.py +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/src/slack_agents/slack/tool_blocks.py +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/src/slack_agents/storage/__init__.py +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/src/slack_agents/tools/__init__.py +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/src/slack_agents/tools/canvas_importer.py +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/src/slack_agents/tools/file_importer.py +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/tests/__init__.py +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/tests/test_access.py +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/tests/test_agent_loop.py +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/tests/test_canvas_auth.py +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/tests/test_canvas_importer.py +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/tests/test_cli.py +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/tests/test_config.py +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/tests/test_conversations.py +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/tests/test_cost.py +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/tests/test_export_documents.py +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/tests/test_export_usage.py +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/tests/test_file_extractors.py +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/tests/test_format.py +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/tests/test_init.py +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/tests/test_llm_factory.py +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/tests/test_mcp_client.py +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/tests/test_openai_convert.py +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/tests/test_overlay_integration.py +0 -0
- {python_slack_agents-0.7.0 → python_slack_agents-0.8.1}/tests/test_tool_blocks.py +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Changelog
|
|
1
|
+
we don# Changelog
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
@@ -6,6 +6,38 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.8.1] - 2026-05-07
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- `oauth_clients` cache is now keyed by `(server_id, redirect_uri)` instead of `server_id` alone. Two `mcp_http_oauth` providers pointing at the same MCP server but with different `OAUTH_PUBLIC_URL` values (e.g. agents sharing a database, or a single agent whose tunnel hostname rotates) used to collide on the cached client registration; the second one would reuse a row whose `redirect_uri` the IdP no longer accepted, producing `Invalid parameter: redirect_uri` from the IdP.
|
|
14
|
+
- `mcp_http_oauth.Provider.call_tool` now detects an IdP `Invalid parameter: redirect_uri` (or `redirect_uri_mismatch`) rejection, deletes the stale cached client registration and the user's cached tokens, and surfaces a structured `system_error` with `code="redirect_uri_mismatch"` so the LLM can explain the situation. The next call re-registers a fresh client and prompts for re-auth.
|
|
15
|
+
|
|
16
|
+
### Migration
|
|
17
|
+
|
|
18
|
+
- The `oauth_clients` table primary key changed from `(server_id)` to `(server_id, redirect_uri)`. There is no automatic migration: any rows from 0.8.0 will be re-created on demand the first time each `(server_id, redirect_uri)` pair is used, leaving harmless orphan rows behind. Operators who want a clean slate can `DELETE FROM oauth_clients;` before upgrading.
|
|
19
|
+
|
|
20
|
+
## [0.8.0] - 2026-05-06
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- `slack_agents.tools.mcp_http_oauth` — OAuth-authenticated MCP tool provider with per-Slack-user tokens. Each user authenticates separately to the upstream service; refresh tokens are AES-GCM-encrypted at rest. The provider runs an in-process aiohttp callback listener alongside Slack Bolt's WebSocket connection — no public ingress beyond a single `/oauth/callback` path. Includes Dynamic Client Registration with PRM-driven scope catalog, scope-merging on every authorize request (OIDC baseline + cached-token scopes + server-hinted scopes), and post-step-up permission-denied detection. See `docs/oauth.md`.
|
|
25
|
+
- `slack_agents.oauth/` package — signed-state codec, HKDF/AES-GCM crypto, callback listener, ephemeral auth-prompt builder, `DBTokenStorage` bridging the MCP SDK's `TokenStorage` Protocol to the agent's storage backend.
|
|
26
|
+
- `oauth_tokens` and `oauth_clients` tables on both SQLite and PostgreSQL backends (created idempotently at startup).
|
|
27
|
+
- `FrameworkContext` injection in `load_plugin` — providers that declare a `framework_ctx` parameter receive a shared object holding the bot token, Slack client, storage backend, and OAuth pending-flows registry. Existing providers are unaffected.
|
|
28
|
+
- `validate_oauth_env(tools_config)` consolidated startup check. Required env vars when at least one `mcp_http_oauth` provider is configured: `OAUTH_PUBLIC_URL`, `OAUTH_SECRET_KEY`. Optional: `OAUTH_BIND_HOST` (default `0.0.0.0`), `OAUTH_BIND_PORT` (default `8080`). Missing/malformed values produce a single, actionable error and refuse to start.
|
|
29
|
+
- Eager-auth pre-LLM hook in `SlackAgent` — each user's first message triggers OAuth setup before the LLM is invoked, so the LLM sees real tool lists rather than an empty/placeholder set.
|
|
30
|
+
- `make_tool_error(...)` helper plus `ERROR_*` and `RECOVERY_*` constants in `slack_agents.tools.base`. Uniform JSON schema for tool-error payloads in `ToolResult.content` so the LLM consuming a tool result can reason about errors (permission_denied / system_error / input_error / auth_setup_failed) uniformly. The `recovery` enum (retry / contact_admin / contact_support / abort) drives how the LLM should advise the user. See `docs/tools.md` "Tool error schema".
|
|
31
|
+
- LLM error classification in `slack/agent.py` — transient provider errors (`overloaded_error`, `rate_limit_error`, `api_error`, `timeout_error`) produce friendly user messages and log at WARNING (no traceback noise); configuration errors (`authentication_error`, `permission_error`, `not_found_error`, `invalid_request_error`) stay ERROR with full traceback.
|
|
32
|
+
- `cryptography` runtime dependency (HKDF + AES-GCM).
|
|
33
|
+
- `docs/oauth.md` — operator guide, scope-handling story, MCP SDK workarounds, troubleshooting (Trusted Hosts and Allowed Client Scopes policy gates, post-step-up permission denial).
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- Every built-in tool error site (`mcp_http`, `mcp_http_oauth`, `canvas`, `user_context`, `file_exporter`, plus `agent_loop`'s unknown-tool fallback) now emits the unified structured-error JSON schema in `ToolResult.content` when `is_error=True`. Custom tools should use `make_tool_error(...)`.
|
|
38
|
+
- The Slack-user-facing message on unrecognized exceptions ("Sorry, I encountered an error processing your request.") is now the fallback only — known LLM-provider errors get specific messages instead.
|
|
39
|
+
- `docs/tools.md` — example uses `make_tool_error` for the unknown-tool branch; new "Tool error schema" section.
|
|
40
|
+
|
|
9
41
|
## [0.7.0] - 2026-04-14
|
|
10
42
|
|
|
11
43
|
### Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-slack-agents
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.1
|
|
4
4
|
Summary: A Python framework for deploying AI agents as Slack bots
|
|
5
5
|
Project-URL: Homepage, https://github.com/CompareNetworks/python-slack-agents
|
|
6
6
|
Project-URL: Repository, https://github.com/CompareNetworks/python-slack-agents
|
|
@@ -21,6 +21,7 @@ Requires-Dist: aiohttp<4,>=3.9
|
|
|
21
21
|
Requires-Dist: aiosqlite<1,>=0.20
|
|
22
22
|
Requires-Dist: anthropic<1,>=0.40
|
|
23
23
|
Requires-Dist: asyncpg<1,>=0.30
|
|
24
|
+
Requires-Dist: cryptography<46,>=42
|
|
24
25
|
Requires-Dist: fpdf2<3,>=2.8
|
|
25
26
|
Requires-Dist: httpx<1,>=0.27
|
|
26
27
|
Requires-Dist: mcp<2,>=1.0
|
|
@@ -63,7 +64,7 @@ Each agent is a directory with two files: a `config.yaml` and a `system_prompt.t
|
|
|
63
64
|
|
|
64
65
|
**LLM providers** — Anthropic and OpenAI built in, plus any OpenAI-compatible API (Mistral, Groq, Together, Ollama, vLLM). Extend to any other provider by implementing a simple base class.
|
|
65
66
|
|
|
66
|
-
**Tool calling with MCP** — Connect any [MCP server](https://modelcontextprotocol.io/) over HTTP. Tools are discovered automatically, executed in parallel, and filtered with regex patterns. No tool registration boilerplate.
|
|
67
|
+
**Tool calling with MCP** — Connect any [MCP server](https://modelcontextprotocol.io/) over HTTP. Tools are discovered automatically, executed in parallel, and filtered with regex patterns. No tool registration boilerplate. **OAuth-protected MCP servers are supported with per-Slack-user tokens** — see [docs/oauth.md](docs/oauth.md).
|
|
67
68
|
|
|
68
69
|
**File handling** — Agents can read files your users upload (PDF, DOCX, XLSX, PPTX, CSV, images) and generate documents back (PDF, DOCX, XLSX, CSV, PPTX). All built in, no extra setup.
|
|
69
70
|
|
|
@@ -241,6 +242,18 @@ tools:
|
|
|
241
242
|
- "get_document" # exact match works too
|
|
242
243
|
```
|
|
243
244
|
|
|
245
|
+
For MCP servers that require OAuth 2.1 (per-user authentication), use `mcp_http_oauth` — same auto-discovery, with each Slack user authenticating separately to the upstream service:
|
|
246
|
+
|
|
247
|
+
```yaml
|
|
248
|
+
tools:
|
|
249
|
+
my-oauth-mcp:
|
|
250
|
+
type: slack_agents.tools.mcp_http_oauth
|
|
251
|
+
url: "https://my-server.example.com/mcp"
|
|
252
|
+
allowed_functions: [".*"]
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Tokens are persisted per Slack user, refresh tokens are AES-GCM-encrypted at rest, and an in-process callback listener handles the OAuth dance alongside the Slack Bolt connection. See [docs/oauth.md](https://github.com/CompareNetworks/python-slack-agents/blob/main/docs/oauth.md) for setup (env vars, tunnel for local dev, scope handling, troubleshooting).
|
|
256
|
+
|
|
244
257
|
## Project Structure
|
|
245
258
|
|
|
246
259
|
Your overlay is a plain git repo — not a Python package. You edit configs, commit, and run; there's no
|
|
@@ -326,6 +339,7 @@ To create a Slack app, use the manifest in [`docs/slack-app-manifest.json`](http
|
|
|
326
339
|
- [Setup](https://github.com/CompareNetworks/python-slack-agents/blob/main/docs/setup.md) — installation and Slack app creation
|
|
327
340
|
- [Agents](https://github.com/CompareNetworks/python-slack-agents/blob/main/docs/agents.md) — creating and configuring agents
|
|
328
341
|
- [Tools](https://github.com/CompareNetworks/python-slack-agents/blob/main/docs/tools.md) — MCP servers and custom tool providers
|
|
342
|
+
- [OAuth-protected MCP servers](https://github.com/CompareNetworks/python-slack-agents/blob/main/docs/oauth.md) — per-Slack-user OAuth for MCP servers requiring authentication
|
|
329
343
|
- [LLM](https://github.com/CompareNetworks/python-slack-agents/blob/main/docs/llm.md) — supported providers and adding your own
|
|
330
344
|
- [Storage](https://github.com/CompareNetworks/python-slack-agents/blob/main/docs/storage.md) — SQLite, PostgreSQL, and custom backends
|
|
331
345
|
- [Access control](https://github.com/CompareNetworks/python-slack-agents/blob/main/docs/access-control.md) — controlling who can use an agent
|
|
@@ -17,7 +17,7 @@ Each agent is a directory with two files: a `config.yaml` and a `system_prompt.t
|
|
|
17
17
|
|
|
18
18
|
**LLM providers** — Anthropic and OpenAI built in, plus any OpenAI-compatible API (Mistral, Groq, Together, Ollama, vLLM). Extend to any other provider by implementing a simple base class.
|
|
19
19
|
|
|
20
|
-
**Tool calling with MCP** — Connect any [MCP server](https://modelcontextprotocol.io/) over HTTP. Tools are discovered automatically, executed in parallel, and filtered with regex patterns. No tool registration boilerplate.
|
|
20
|
+
**Tool calling with MCP** — Connect any [MCP server](https://modelcontextprotocol.io/) over HTTP. Tools are discovered automatically, executed in parallel, and filtered with regex patterns. No tool registration boilerplate. **OAuth-protected MCP servers are supported with per-Slack-user tokens** — see [docs/oauth.md](docs/oauth.md).
|
|
21
21
|
|
|
22
22
|
**File handling** — Agents can read files your users upload (PDF, DOCX, XLSX, PPTX, CSV, images) and generate documents back (PDF, DOCX, XLSX, CSV, PPTX). All built in, no extra setup.
|
|
23
23
|
|
|
@@ -195,6 +195,18 @@ tools:
|
|
|
195
195
|
- "get_document" # exact match works too
|
|
196
196
|
```
|
|
197
197
|
|
|
198
|
+
For MCP servers that require OAuth 2.1 (per-user authentication), use `mcp_http_oauth` — same auto-discovery, with each Slack user authenticating separately to the upstream service:
|
|
199
|
+
|
|
200
|
+
```yaml
|
|
201
|
+
tools:
|
|
202
|
+
my-oauth-mcp:
|
|
203
|
+
type: slack_agents.tools.mcp_http_oauth
|
|
204
|
+
url: "https://my-server.example.com/mcp"
|
|
205
|
+
allowed_functions: [".*"]
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Tokens are persisted per Slack user, refresh tokens are AES-GCM-encrypted at rest, and an in-process callback listener handles the OAuth dance alongside the Slack Bolt connection. See [docs/oauth.md](https://github.com/CompareNetworks/python-slack-agents/blob/main/docs/oauth.md) for setup (env vars, tunnel for local dev, scope handling, troubleshooting).
|
|
209
|
+
|
|
198
210
|
## Project Structure
|
|
199
211
|
|
|
200
212
|
Your overlay is a plain git repo — not a Python package. You edit configs, commit, and run; there's no
|
|
@@ -280,6 +292,7 @@ To create a Slack app, use the manifest in [`docs/slack-app-manifest.json`](http
|
|
|
280
292
|
- [Setup](https://github.com/CompareNetworks/python-slack-agents/blob/main/docs/setup.md) — installation and Slack app creation
|
|
281
293
|
- [Agents](https://github.com/CompareNetworks/python-slack-agents/blob/main/docs/agents.md) — creating and configuring agents
|
|
282
294
|
- [Tools](https://github.com/CompareNetworks/python-slack-agents/blob/main/docs/tools.md) — MCP servers and custom tool providers
|
|
295
|
+
- [OAuth-protected MCP servers](https://github.com/CompareNetworks/python-slack-agents/blob/main/docs/oauth.md) — per-Slack-user OAuth for MCP servers requiring authentication
|
|
283
296
|
- [LLM](https://github.com/CompareNetworks/python-slack-agents/blob/main/docs/llm.md) — supported providers and adding your own
|
|
284
297
|
- [Storage](https://github.com/CompareNetworks/python-slack-agents/blob/main/docs/storage.md) — SQLite, PostgreSQL, and custom backends
|
|
285
298
|
- [Access control](https://github.com/CompareNetworks/python-slack-agents/blob/main/docs/access-control.md) — controlling who can use an agent
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
# OAuth-protected MCP servers
|
|
2
|
+
|
|
3
|
+
`slack_agents.tools.mcp_http_oauth` connects to MCP servers that require OAuth 2.1
|
|
4
|
+
authentication, with **per-Slack-user tokens**: each user authenticates separately,
|
|
5
|
+
and the agent uses that user's access token when calling tools on their behalf.
|
|
6
|
+
|
|
7
|
+
This complements `slack_agents.tools.mcp_http`, which is for servers that issue
|
|
8
|
+
long-lived API keys you put in YAML headers. If your MCP server speaks the MCP
|
|
9
|
+
authorization spec (Dynamic Client Registration + auth-code + PKCE), use
|
|
10
|
+
`mcp_http_oauth`.
|
|
11
|
+
|
|
12
|
+
## Configuration
|
|
13
|
+
|
|
14
|
+
```yaml
|
|
15
|
+
tools:
|
|
16
|
+
my-mcp:
|
|
17
|
+
type: slack_agents.tools.mcp_http_oauth
|
|
18
|
+
url: "https://my-server.example.com/mcp"
|
|
19
|
+
allowed_functions: [".*"]
|
|
20
|
+
init_retries: [5, 10, 30] # optional
|
|
21
|
+
auth_timeout: 300 # optional, seconds, default 300
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Only `url` and `allowed_functions` are required. There is intentionally no
|
|
25
|
+
`client_id` / `client_secret` / `scopes` field — the provider performs Dynamic
|
|
26
|
+
Client Registration against the MCP server's authorization server, registers
|
|
27
|
+
with whatever scopes the server's PRM document advertises, and discovers
|
|
28
|
+
runtime scopes through standard 401/403 step-up challenges.
|
|
29
|
+
|
|
30
|
+
## Required environment variables
|
|
31
|
+
|
|
32
|
+
These are validated at startup. If any `mcp_http_oauth` provider is configured
|
|
33
|
+
and any of these are missing or malformed, the agent refuses to start with a
|
|
34
|
+
single consolidated error message.
|
|
35
|
+
|
|
36
|
+
| Variable | Required | Default | Description |
|
|
37
|
+
|---|---|---|---|
|
|
38
|
+
| `OAUTH_PUBLIC_URL` | yes | — | Externally reachable base URL of this agent process. Must be `https://`, or `http://` with a loopback host (`localhost`, `127.0.0.1`, `[::1]`) for local dev. |
|
|
39
|
+
| `OAUTH_SECRET_KEY` | yes | — | Root key for HKDF; ≥32 bytes after base64 decode. Used to sign OAuth state tokens and encrypt refresh tokens at rest. |
|
|
40
|
+
| `OAUTH_BIND_HOST` | no | `0.0.0.0` | Interface the in-process callback listener binds to. |
|
|
41
|
+
| `OAUTH_BIND_PORT` | no | `8080` | TCP port for the callback listener. |
|
|
42
|
+
|
|
43
|
+
### Generating `OAUTH_SECRET_KEY`
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
openssl rand -base64 32
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
or
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
python3 -c "import secrets; print(secrets.token_urlsafe(32))"
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Treat this value like any other long-lived secret: keep it out of source
|
|
56
|
+
control, rotate it the same way you rotate database credentials. Rotating it
|
|
57
|
+
forces every user to re-authenticate but does not break the agent.
|
|
58
|
+
|
|
59
|
+
## Local development
|
|
60
|
+
|
|
61
|
+
OAuth callbacks need a URL the user's browser can reach. For local dev use a
|
|
62
|
+
tunnel (ngrok, cloudflared, tailscale funnel, etc.):
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# Terminal 1 — start the tunnel pointing at your bind port:
|
|
66
|
+
ngrok http 8080
|
|
67
|
+
# → forwards https://abcd-1234.ngrok-free.app to localhost:8080
|
|
68
|
+
|
|
69
|
+
# Terminal 2 — set env vars and run the agent:
|
|
70
|
+
export OAUTH_PUBLIC_URL=https://abcd-1234.ngrok-free.app
|
|
71
|
+
export OAUTH_SECRET_KEY=$(openssl rand -base64 32)
|
|
72
|
+
slack-agents run agents/my-agent
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
If you'd rather not use a tunnel, you can run with
|
|
76
|
+
`OAUTH_PUBLIC_URL=http://localhost:8080` — the validator allows loopback
|
|
77
|
+
addresses over plain HTTP per RFC 8252.
|
|
78
|
+
|
|
79
|
+
## What a Slack user sees
|
|
80
|
+
|
|
81
|
+
1. They ask the bot to do something that needs OAuth-protected tools.
|
|
82
|
+
2. The bot replies with an ephemeral message in the same thread (visible only to
|
|
83
|
+
them) containing an "Authenticate" button.
|
|
84
|
+
3. They click; the browser opens the upstream service's login page.
|
|
85
|
+
4. They log in and click Allow.
|
|
86
|
+
5. The browser shows "Authentication completed — you can close this tab and
|
|
87
|
+
return to Slack."
|
|
88
|
+
6. Slack shows a brief "✅ Authenticated to *server*" ephemeral, the agent
|
|
89
|
+
picks up the new token, runs tool discovery, and the conversation continues
|
|
90
|
+
normally.
|
|
91
|
+
|
|
92
|
+
If they don't click within the configured `auth_timeout` (default 5 minutes),
|
|
93
|
+
the agent surfaces a "timed out — please try again" error and the tool call
|
|
94
|
+
ends. They can re-ask whenever they're ready and a fresh prompt appears.
|
|
95
|
+
|
|
96
|
+
If the upstream tool later requires additional permissions (e.g. they had read
|
|
97
|
+
access but are now trying to write), the same flow re-runs requesting the
|
|
98
|
+
broader scope. Most identity providers auto-collapse the consent screen if the
|
|
99
|
+
broader scope is a superset of what they've already approved.
|
|
100
|
+
|
|
101
|
+
If the user's account doesn't actually have the role the upstream needs (so
|
|
102
|
+
the IdP silently issues a token without the requested scope), the agent
|
|
103
|
+
detects this on the next tool call and surfaces a clear permission-denied
|
|
104
|
+
message naming the specific missing scope — rather than a generic error.
|
|
105
|
+
|
|
106
|
+
## How scopes work
|
|
107
|
+
|
|
108
|
+
Three scope-related decisions happen at different times, with different
|
|
109
|
+
sources of truth. Knowing which is which makes troubleshooting much easier.
|
|
110
|
+
|
|
111
|
+
### 1. DCR registration scope (one-time, per server)
|
|
112
|
+
|
|
113
|
+
When the agent first encounters an OAuth-protected MCP server, it does
|
|
114
|
+
Dynamic Client Registration with the server's authorization server. The
|
|
115
|
+
client registration declares **all the scopes this client could ever
|
|
116
|
+
legitimately request** — this is the catalog, not the per-call request.
|
|
117
|
+
|
|
118
|
+
The agent uses **PRM `scopes_supported`** as that catalog: it pre-fetches
|
|
119
|
+
`/.well-known/oauth-protected-resource` from the resource server and
|
|
120
|
+
registers with exactly that list. No heuristics, no extrapolation.
|
|
121
|
+
|
|
122
|
+
This means the **resource server's PRM document must advertise every scope
|
|
123
|
+
that any of its tools might ever require**, not just the default tier. If
|
|
124
|
+
PRM only advertises `mcp:foo:read` but a tool returns a 403 demanding
|
|
125
|
+
`mcp:foo:write`, the registered client was never permitted to request
|
|
126
|
+
`mcp:foo:write` and the step-up will fail with `invalid_scope`.
|
|
127
|
+
|
|
128
|
+
### 2. Per-request authorize scope (every tool call)
|
|
129
|
+
|
|
130
|
+
For each authorize request to the IdP (initial auth and step-up alike), the
|
|
131
|
+
agent computes the union of three sources:
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
authorize_scope =
|
|
135
|
+
{openid, offline_access} # OIDC protocol baseline (always)
|
|
136
|
+
∪ scopes from the user's currently-cached token
|
|
137
|
+
∪ scopes the server hinted in `WWW-Authenticate scope=`
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
The OIDC baseline is added by the agent unconditionally — without `openid`
|
|
141
|
+
the IdP issues a non-OIDC token (no identity claims) and without
|
|
142
|
+
`offline_access` no refresh token is issued (forcing fresh auth on every
|
|
143
|
+
token expiry). The cached scopes preserve what's already been granted, so
|
|
144
|
+
step-up never accidentally narrows what the user has. The hint from
|
|
145
|
+
`WWW-Authenticate` is what the resource server *just* asked for, this call.
|
|
146
|
+
|
|
147
|
+
The resource server can be stateless: it can return either the cumulative
|
|
148
|
+
set the user now needs (`scope="mcp:foo:read mcp:foo:write"`) or just the
|
|
149
|
+
delta scope for this call (`scope="mcp:foo:write"`). The client merges with
|
|
150
|
+
its own state either way.
|
|
151
|
+
|
|
152
|
+
### 3. What the token actually grants (decided by the IdP)
|
|
153
|
+
|
|
154
|
+
After the user consents, the IdP issues a token with whatever scopes their
|
|
155
|
+
roles actually permit — it may silently drop scopes the user can't have.
|
|
156
|
+
The agent compares the post-step-up token's scope against the server's
|
|
157
|
+
demand. If a required scope wasn't granted, the agent surfaces a clean
|
|
158
|
+
permission-denied error naming the specific missing scope — instead of
|
|
159
|
+
retrying forever or surfacing the upstream 403 verbatim.
|
|
160
|
+
|
|
161
|
+
### Resource server expectations
|
|
162
|
+
|
|
163
|
+
For the agent to behave correctly out of the box, your MCP server should:
|
|
164
|
+
|
|
165
|
+
- **PRM `/.well-known/oauth-protected-resource`** should advertise every
|
|
166
|
+
scope its tools might require, including step-up scopes (e.g. read AND
|
|
167
|
+
write AND admin), not just the default tier.
|
|
168
|
+
- **401 responses** (no token at all) should include `scope=` with the
|
|
169
|
+
minimum needed to use the resource (typically the read-equivalent).
|
|
170
|
+
- **403 responses** (token with insufficient scope) should include
|
|
171
|
+
`WWW-Authenticate: Bearer error="insufficient_scope" scope="…"` per RFC
|
|
172
|
+
9470, naming the scope(s) needed for this specific call. The server can
|
|
173
|
+
return either the cumulative set or the delta — the client tolerates both.
|
|
174
|
+
- **OIDC scopes** (`openid`, `offline_access`) don't need to appear in
|
|
175
|
+
either header — the client always adds them on its own.
|
|
176
|
+
|
|
177
|
+
## Token storage
|
|
178
|
+
|
|
179
|
+
Tokens are persisted via the agent's normal storage backend (SQLite or
|
|
180
|
+
Postgres) in two new tables:
|
|
181
|
+
|
|
182
|
+
- `oauth_tokens` — per (user_id, server_id) access token + encrypted refresh
|
|
183
|
+
token, scopes, expiry.
|
|
184
|
+
- `oauth_clients` — per server_id Dynamic Client Registration result, shared
|
|
185
|
+
across all users connecting to that server through this agent.
|
|
186
|
+
|
|
187
|
+
Refresh tokens are AES-GCM-encrypted at rest using a subkey derived from
|
|
188
|
+
`OAUTH_SECRET_KEY` via HKDF. Access tokens are short-lived and stored
|
|
189
|
+
plaintext (still in the private DB).
|
|
190
|
+
|
|
191
|
+
## Troubleshooting
|
|
192
|
+
|
|
193
|
+
**"Configuration error: ... OAUTH_PUBLIC_URL is not set"** — set the env vars
|
|
194
|
+
listed in the message and restart.
|
|
195
|
+
|
|
196
|
+
**"Authentication timed out"** — the user didn't click the link within
|
|
197
|
+
`auth_timeout`. They can re-ask the bot whenever they're ready.
|
|
198
|
+
|
|
199
|
+
**"<server> does not support dynamic client registration"** — the upstream
|
|
200
|
+
authorization server doesn't speak RFC 7591, or it has a Client Registration
|
|
201
|
+
Policy that rejects requests from your agent's host. Common Keycloak gates:
|
|
202
|
+
|
|
203
|
+
- *Trusted Hosts* policy — the realm admin must add your agent's
|
|
204
|
+
externally-reachable host to the trusted-hosts list.
|
|
205
|
+
- A CDN/WAF in front of the IdP — some Cloudflare bot-management rules block
|
|
206
|
+
anonymous DCR requests; the realm admin needs an exception for the
|
|
207
|
+
`/clients-registrations/openid-connect` endpoint.
|
|
208
|
+
|
|
209
|
+
This provider is DCR-only by design. Static pre-registered client credentials
|
|
210
|
+
are not currently supported.
|
|
211
|
+
|
|
212
|
+
**`invalid_scope` on step-up after a successful first auth** — the
|
|
213
|
+
DCR-registered client doesn't have the requested scope in its allowed-request
|
|
214
|
+
list, even though it would have been included in the registration request.
|
|
215
|
+
This is Keycloak's *Allowed Client Scopes* policy under
|
|
216
|
+
`Realm Settings → Client Registration → Anonymous Access Policies`: the
|
|
217
|
+
realm silently filters DCR registration scope to a permitted subset. The
|
|
218
|
+
realm admin must add the missing scope (e.g. `mcp:foo:write`) to that
|
|
219
|
+
policy. Verify what the realm actually registered for your client by querying
|
|
220
|
+
the local DB:
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
sqlite3 /tmp/<agent>.slack-agents.db \
|
|
224
|
+
"SELECT json_extract(metadata_json, '\$.scope') FROM oauth_clients WHERE server_id='<server>';"
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
If a scope is missing here, the policy filtered it out at DCR time.
|
|
228
|
+
|
|
229
|
+
**"This action cannot be run on `<server>`: your account does not have a role
|
|
230
|
+
granting the required scope"** — the IdP issued a token but silently dropped
|
|
231
|
+
the requested scope because the user's role doesn't include it. The user (or
|
|
232
|
+
their admin) needs to grant the missing scope at the role level. The agent
|
|
233
|
+
names the specific missing scope in the message.
|
|
234
|
+
|
|
235
|
+
**"You declined access"** — the user clicked Deny on the consent screen, or
|
|
236
|
+
the IdP returned `error=access_denied`. They can re-ask to retry.
|
|
237
|
+
|
|
238
|
+
**Tokens disappear after a key rotation** — expected. Rotating
|
|
239
|
+
`OAUTH_SECRET_KEY` invalidates all stored refresh tokens (the agent detects
|
|
240
|
+
this on the first read and deletes the row, then prompts for fresh auth on the
|
|
241
|
+
next call).
|
|
242
|
+
|
|
243
|
+
## Implementation notes
|
|
244
|
+
|
|
245
|
+
- The in-process callback listener runs alongside the Slack Bolt connection
|
|
246
|
+
(same asyncio loop, same process). It only listens when at least one
|
|
247
|
+
`mcp_http_oauth` provider is configured.
|
|
248
|
+
- The listener exposes exactly two routes: `/oauth/start/{signed_state}` and
|
|
249
|
+
`/oauth/callback`. Anything else returns 404.
|
|
250
|
+
- OAuth state is signed (HMAC-SHA256) and includes a single-use nonce; replays
|
|
251
|
+
are rejected.
|
|
252
|
+
- Restarting the agent during a pending auth flow drops that flow — the user
|
|
253
|
+
re-asks and gets a fresh prompt. Persistent mid-flow recovery is intentionally
|
|
254
|
+
not implemented.
|
|
255
|
+
|
|
256
|
+
### MCP SDK workarounds
|
|
257
|
+
|
|
258
|
+
The provider patches around four behaviors of the `mcp` Python SDK
|
|
259
|
+
(`mcp.client.auth`) at the time of writing. When the SDK addresses any of
|
|
260
|
+
these upstream, the corresponding shim can be removed:
|
|
261
|
+
|
|
262
|
+
1. **Pre-DCR with full PRM scope set.** The SDK's `async_auth_flow`
|
|
263
|
+
overwrites `client_metadata.scope` with the runtime authorize scope
|
|
264
|
+
(`get_client_metadata_scopes`) *before* running DCR. If we let the SDK do
|
|
265
|
+
DCR, the registered client gets only "what's needed for the current
|
|
266
|
+
operation," not the full catalog, and step-up later fails with
|
|
267
|
+
`invalid_scope`. We do DCR ourselves with PRM's `scopes_supported`,
|
|
268
|
+
persist the result, and the SDK's `if not self.context.client_info: …
|
|
269
|
+
register …` branch is skipped.
|
|
270
|
+
2. **Discovery on every fresh `OAuthClientProvider`.** The SDK's 403 step-up
|
|
271
|
+
path calls `_perform_authorization()` without first running protected-
|
|
272
|
+
resource discovery, so a freshly constructed provider falls back to
|
|
273
|
+
`urljoin(server_url, "/authorize")` (wrong when the AS is on a different
|
|
274
|
+
host). We pre-populate `oauth.context.protected_resource_metadata` and
|
|
275
|
+
`oauth_metadata` from a per-Provider cache.
|
|
276
|
+
3. **`token_expiry_time` not propagated from storage.** The SDK's
|
|
277
|
+
`_initialize` loads `current_tokens` from storage but doesn't set
|
|
278
|
+
`token_expiry_time`, so `is_token_valid()` returns True for any cached
|
|
279
|
+
token regardless of actual expiry — meaning a stale token is sent at
|
|
280
|
+
restart, the server returns 401, the SDK skips the refresh-token branch
|
|
281
|
+
entirely, and the user is re-prompted. We force `_initialize` plus
|
|
282
|
+
`update_token_expiry(tokens)` after construction so refresh works
|
|
283
|
+
silently across restarts.
|
|
284
|
+
4. **Scope merge on `WWW-Authenticate`.** The SDK uses the server's
|
|
285
|
+
`WWW-Authenticate scope=` value verbatim, dropping anything the cached
|
|
286
|
+
token already had and the OIDC baseline. We attach an httpx response
|
|
287
|
+
hook to every MCP request that augments the header in place to be the
|
|
288
|
+
union of `{openid, offline_access}` ∪ cached-token scopes ∪ server-hinted
|
|
289
|
+
scopes, so the SDK's verbatim use produces the correct cumulative set.
|
|
290
|
+
|
|
291
|
+
Each shim is annotated in the source with a comment explaining the SDK
|
|
292
|
+
behavior it works around.
|
|
@@ -13,7 +13,13 @@ Tool providers give the LLM callable tools. Extend `BaseToolProvider`:
|
|
|
13
13
|
|
|
14
14
|
```python
|
|
15
15
|
# my_tools/calculator.py
|
|
16
|
-
from slack_agents.tools.base import
|
|
16
|
+
from slack_agents.tools.base import (
|
|
17
|
+
ERROR_INPUT_ERROR,
|
|
18
|
+
RECOVERY_ABORT,
|
|
19
|
+
BaseToolProvider,
|
|
20
|
+
ToolResult,
|
|
21
|
+
make_tool_error,
|
|
22
|
+
)
|
|
17
23
|
|
|
18
24
|
class Provider(BaseToolProvider):
|
|
19
25
|
def __init__(self, allowed_functions: list[str]):
|
|
@@ -39,7 +45,13 @@ class Provider(BaseToolProvider):
|
|
|
39
45
|
if name == "add":
|
|
40
46
|
result = arguments["a"] + arguments["b"]
|
|
41
47
|
return {"content": str(result), "is_error": False, "files": []}
|
|
42
|
-
return
|
|
48
|
+
return make_tool_error(
|
|
49
|
+
error=ERROR_INPUT_ERROR,
|
|
50
|
+
code="unknown_tool",
|
|
51
|
+
tool=name,
|
|
52
|
+
recovery=RECOVERY_ABORT,
|
|
53
|
+
message=f"Tool {name!r} is not provided by this calculator.",
|
|
54
|
+
)
|
|
43
55
|
```
|
|
44
56
|
|
|
45
57
|
### Key points
|
|
@@ -49,6 +61,7 @@ class Provider(BaseToolProvider):
|
|
|
49
61
|
- `call_tool(name, arguments, user_context, storage)` returns a `ToolResult` (`{"content": str, "is_error": bool, "files": list[OutputFile]}`)
|
|
50
62
|
- Files in the response are uploaded to Slack automatically
|
|
51
63
|
- `initialize()` and `close()` are optional lifecycle hooks
|
|
64
|
+
- For error returns, use `make_tool_error(...)` so the LLM gets a structured payload it can interpret consistently. See [Tool error schema](#tool-error-schema) below.
|
|
52
65
|
|
|
53
66
|
## File Importer Providers
|
|
54
67
|
|
|
@@ -124,6 +137,82 @@ tools:
|
|
|
124
137
|
|
|
125
138
|
All MCP tool providers are initialized in parallel at startup. If any provider fails to connect after exhausting its retries, the agent exits with an error.
|
|
126
139
|
|
|
140
|
+
## MCP over HTTP with OAuth
|
|
141
|
+
|
|
142
|
+
`slack_agents.tools.mcp_http_oauth` is the OAuth-authenticated counterpart to
|
|
143
|
+
`mcp_http` — for MCP servers that require per-user OAuth 2.1 authentication
|
|
144
|
+
rather than a static API key. See [`docs/oauth.md`](oauth.md) for the full
|
|
145
|
+
guide. Minimal example:
|
|
146
|
+
|
|
147
|
+
```yaml
|
|
148
|
+
tools:
|
|
149
|
+
my-mcp:
|
|
150
|
+
type: slack_agents.tools.mcp_http_oauth
|
|
151
|
+
url: "https://my-server.example.com/mcp"
|
|
152
|
+
allowed_functions: [".*"]
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
This pulls in extra runtime requirements: `OAUTH_PUBLIC_URL`,
|
|
156
|
+
`OAUTH_SECRET_KEY`, and an in-process HTTP listener for OAuth callbacks. Read
|
|
157
|
+
the OAuth doc before configuring this in production.
|
|
158
|
+
|
|
159
|
+
## Tool error schema
|
|
160
|
+
|
|
161
|
+
When a tool returns `is_error: True`, every built-in tool emits a JSON-encoded
|
|
162
|
+
payload in `content` matching this shape, so the LLM consuming the result can
|
|
163
|
+
reason about the failure uniformly. Custom tools should produce the same shape
|
|
164
|
+
via the `make_tool_error` helper.
|
|
165
|
+
|
|
166
|
+
```json
|
|
167
|
+
{
|
|
168
|
+
"error": "<type>", // required: e.g. "permission_denied"
|
|
169
|
+
"code": "<subtype>", // optional: stable per-error sub-classification
|
|
170
|
+
"tool": "<tool name>", // optional: which tool was being called
|
|
171
|
+
"server": "<server id>", // optional: provider/server context
|
|
172
|
+
"message": "<human>", // required: human-readable summary
|
|
173
|
+
"recovery": "<action>", // required: see below
|
|
174
|
+
"details": { ... } // optional: free-form per-error-type
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**Top-level error types** (`error`):
|
|
179
|
+
|
|
180
|
+
| Constant | When to use |
|
|
181
|
+
|---|---|
|
|
182
|
+
| `ERROR_PERMISSION_DENIED` | Auth/scope/role refusal — user-level |
|
|
183
|
+
| `ERROR_SYSTEM_ERROR` | Operational/library/transient failure |
|
|
184
|
+
| `ERROR_AUTH_SETUP_FAILED` | Auth flow itself broke (timeout, prompt failure) |
|
|
185
|
+
| `ERROR_INPUT_ERROR` | Bad call / unknown tool / bad arguments |
|
|
186
|
+
|
|
187
|
+
**Recovery actions** (`recovery`):
|
|
188
|
+
|
|
189
|
+
| Constant | Meaning |
|
|
190
|
+
|---|---|
|
|
191
|
+
| `RECOVERY_RETRY` | Transient or user-recoverable; just try again |
|
|
192
|
+
| `RECOVERY_CONTACT_ADMIN` | Requires realm/IdP/account admin |
|
|
193
|
+
| `RECOVERY_CONTACT_SUPPORT` | Framework operator/dev needs to investigate logs |
|
|
194
|
+
| `RECOVERY_ABORT` | Nothing to do for this call (LLM may try a different tool) |
|
|
195
|
+
|
|
196
|
+
**Helper signature:**
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
from slack_agents.tools.base import make_tool_error # plus ERROR_*, RECOVERY_*
|
|
200
|
+
|
|
201
|
+
return make_tool_error(
|
|
202
|
+
error=ERROR_SYSTEM_ERROR, # required
|
|
203
|
+
message="Server returned 502.", # required
|
|
204
|
+
recovery=RECOVERY_RETRY, # required
|
|
205
|
+
code="upstream_502", # optional
|
|
206
|
+
tool="search_docs", # optional
|
|
207
|
+
server="my-mcp", # optional
|
|
208
|
+
details={"status": 502}, # optional, schema-less
|
|
209
|
+
)
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
`details` is intentionally schema-less — each error type can carry whatever
|
|
213
|
+
structured fields the LLM benefits from seeing (missing scopes, exception
|
|
214
|
+
types, timestamps for support correlation, etc.).
|
|
215
|
+
|
|
127
216
|
## Configuration
|
|
128
217
|
|
|
129
218
|
Both types are configured the same way in `config.yaml`:
|