There are two kinds of tool providers, both configured under tools: in config.yaml:
- Tool providers (
BaseToolProvider) — tools the LLM can call during a conversation (e.g. search, export a PDF, run a calculation) - File importer providers (
BaseFileImporterProvider) — handlers that process files attached to Slack messages before they reach the LLM (e.g. extract text from a PDF, parse an Excel spreadsheet)
Both use the same allowed_functions regex filtering and are loaded as Python modules with a Provider class.
Tool providers give the LLM callable tools. Extend BaseToolProvider:
# my_tools/calculator.py
from slack_agents.tools.base import (
ERROR_INPUT_ERROR,
RECOVERY_ABORT,
BaseToolProvider,
ToolResult,
make_tool_error,
)
class Provider(BaseToolProvider):
def __init__(self, allowed_functions: list[str]):
super().__init__(allowed_functions)
def _get_all_tools(self) -> list[dict]:
return [
{
"name": "add",
"description": "Add two numbers",
"input_schema": {
"type": "object",
"properties": {
"a": {"type": "number"},
"b": {"type": "number"},
},
"required": ["a", "b"],
},
}
]
async def call_tool(self, name, arguments, user_context, storage) -> ToolResult:
if name == "add":
result = arguments["a"] + arguments["b"]
return {"content": str(result), "is_error": False, "files": []}
return make_tool_error(
error=ERROR_INPUT_ERROR,
code="unknown_tool",
tool=name,
recovery=RECOVERY_ABORT,
message=f"Tool {name!r} is not provided by this calculator.",
)_get_all_tools()returns tool definitions in Anthropic API formatallowed_functionsfiltering is handled by the base classcall_tool(name, arguments, user_context, storage)returns aToolResult({"content": str, "is_error": bool, "files": list[OutputFile]})- Files in the response are uploaded to Slack automatically
initialize()andclose()are optional lifecycle hooks- For error returns, use
make_tool_error(...)so the LLM gets a structured payload it can interpret consistently. See Tool error schema below.
File importer providers process files that users attach to Slack messages. They are invisible to the LLM — the framework calls them automatically to convert files into content the LLM can understand.
Extend BaseFileImporterProvider:
# my_tools/csv_importer.py
from slack_agents import InputFile
from slack_agents.tools.base import BaseFileImporterProvider, ContentBlock, FileImportToolException
class Provider(BaseFileImporterProvider):
def _get_all_tools(self) -> list[dict]:
return [
{
"name": "import_csv",
"mimes": {"text/csv"},
"max_size": 5_000_000,
}
]
async def call_tool(self, name, arguments, user_context, storage) -> ContentBlock:
if name == "import_csv":
text = arguments["file_bytes"].decode("utf-8", errors="replace")
return {"type": "text", "text": f"[File: {arguments['filename']}]\n\n{text}"}
raise FileImportToolException(f"Unknown handler: {name}")| Field | Type | Description |
|---|---|---|
name |
str |
Handler name, matched against allowed_functions (e.g. import_csv) |
mimes |
set[str] |
MIME types this handler processes |
max_size |
int |
Maximum file size in bytes |
call_tool() receives an InputFile dict (with keys file_bytes, mimetype, filename) as the arguments parameter, plus user_context and storage. Return a ContentBlock dict that will be included in the user message sent to the LLM:
- Text:
{"type": "text", "text": "..."} - Image:
{"type": "image", "source": {"type": "base64", "media_type": "...", "data": "..."}} - Raise
FileImportToolExceptionif extraction fails (the framework catches this and logs the error)
The built-in provider (slack_agents.tools.file_importer) handles PDF, DOCX, XLSX, PPTX, plain text, and images.
slack_agents.tools.mcp_http connects to any MCP server over HTTP. Tools are auto-discovered at startup.
tools:
my-mcp-server:
type: slack_agents.tools.mcp_http
url: "https://my-server.example.com/mcp"
headers:
Authorization: "Bearer {MCP_API_TOKEN}"
allowed_functions:
- "search_.*"
- "get_document"
init_retries: [5, 10, 30]| Field | Type | Default | Description |
|---|---|---|---|
url |
str |
required | MCP server endpoint |
headers |
dict |
{} |
HTTP headers sent with every request |
allowed_functions |
list[str] |
required | Regex patterns to filter discovered tools |
init_retries |
list[int] |
[5, 10, 30] |
Seconds to wait between connection retries at startup. The server is tried once immediately, then once after each delay. Set to [] to disable retries. |
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.
slack_agents.tools.mcp_http_oauth is the OAuth-authenticated counterpart to
mcp_http — for MCP servers that require per-user OAuth 2.1 authentication
rather than a static API key. See docs/oauth.md for the full
guide. Minimal example:
tools:
my-mcp:
type: slack_agents.tools.mcp_http_oauth
url: "https://my-server.example.com/mcp"
allowed_functions: [".*"]This pulls in extra runtime requirements: PUBLIC_URL,
OAUTH_SECRET_KEY, and an in-process HTTP listener for OAuth callbacks. Read
the OAuth doc before configuring this in production.
When a tool returns is_error: True, every built-in tool emits a JSON-encoded
payload in content matching this shape, so the LLM consuming the result can
reason about the failure uniformly. Custom tools should produce the same shape
via the make_tool_error helper.
{
"error": "<type>", // required: e.g. "permission_denied"
"code": "<subtype>", // optional: stable per-error sub-classification
"tool": "<tool name>", // optional: which tool was being called
"server": "<server id>", // optional: provider/server context
"message": "<human>", // required: human-readable summary
"recovery": "<action>", // required: see below
"details": { ... } // optional: free-form per-error-type
}Top-level error types (error):
| Constant | When to use |
|---|---|
ERROR_PERMISSION_DENIED |
Auth/scope/role refusal — user-level |
ERROR_SYSTEM_ERROR |
Operational/library/transient failure |
ERROR_AUTH_SETUP_FAILED |
Auth flow itself broke (timeout, prompt failure) |
ERROR_INPUT_ERROR |
Bad call / unknown tool / bad arguments |
Recovery actions (recovery):
| Constant | Meaning |
|---|---|
RECOVERY_RETRY |
Transient or user-recoverable; just try again |
RECOVERY_CONTACT_ADMIN |
Requires realm/IdP/account admin |
RECOVERY_CONTACT_SUPPORT |
Framework operator/dev needs to investigate logs |
RECOVERY_ABORT |
Nothing to do for this call (LLM may try a different tool) |
Helper signature:
from slack_agents.tools.base import make_tool_error # plus ERROR_*, RECOVERY_*
return make_tool_error(
error=ERROR_SYSTEM_ERROR, # required
message="Server returned 502.", # required
recovery=RECOVERY_RETRY, # required
code="upstream_502", # optional
tool="search_docs", # optional
server="my-mcp", # optional
details={"status": 502}, # optional, schema-less
)details is intentionally schema-less — each error type can carry whatever
structured fields the LLM benefits from seeing (missing scopes, exception
types, timestamps for support correlation, etc.).
Both types are configured the same way in config.yaml:
tools:
calculator:
type: my_tools.calculator
allowed_functions: [".*"]
import-documents:
type: slack_agents.tools.file_importer
allowed_functions: [".*"]The module must be importable from your Python path.