feat: add slack credential spec and contribution doc

This commit is contained in:
Richard Tang
2026-02-05 17:39:44 -08:00
parent 50c0a5da9e
commit c68dd48f6d
8 changed files with 401 additions and 164 deletions
+124 -64
View File
@@ -86,63 +86,53 @@ _TOOL_MODULES = [
## Credential Management
For tools requiring API keys, use the centralized `CredentialManager`. This enables:
- **Agent-aware validation**: Credentials are checked when an agent loads, not at server startup
- **Better error messages**: Users see exactly which credentials are missing and how to get them
- **Easy testing**: Use `CredentialManager.for_testing()` to mock credentials
Tools fall into two categories based on whether they need external API credentials:
### Adding a New Credential
| Signature | Meaning | CI Enforcement |
|-----------|---------|----------------|
| `register_tools(mcp)` | No credentials needed | ✅ Just works |
| `register_tools(mcp, credentials=None)` | Requires credentials | ⚠️ Must have `CredentialSpec` |
1. Find the appropriate category file in `src/aden_tools/credentials/`:
- `llm.py` - LLM providers (anthropic, openai, etc.)
- `search.py` - Search tools (brave_search, google_search, etc.)
- Or create a new category file for integrations
**This is enforced by CI** — if your `register_tools` accepts a `credentials` parameter, every tool it registers must appear in a `CredentialSpec.tools` list. Otherwise, CI will fail with a clear error message.
2. Add the credential spec to the category's dict:
### Tools WITHOUT Credentials (Simple Case)
If your tool doesn't need external API keys (file operations, local processing, etc.), just use the simple signature:
```python
# In credentials/search.py
SEARCH_CREDENTIALS = {
# ... existing credentials
"my_api": CredentialSpec(
env_var="MY_API_KEY",
tools=["my_api_tool"], # Which tools need this credential
required=True, # or False for optional
help_url="https://example.com/api-keys",
description="API key for My Service",
),
}
def register_tools(mcp: FastMCP) -> None:
"""Register tools that don't need credentials."""
@mcp.tool()
def my_local_tool(path: str) -> dict:
"""Process a local file."""
# No credentials needed - just do the work
return {"result": process_file(path)}
```
3. If you created a new category file, import and merge it in `credentials/__init__.py`:
That's it! No additional configuration needed.
### Tools WITH Credentials (Integration Case)
For tools requiring API keys, follow these steps:
#### Step 1: Add the `credentials` parameter
```python
from .my_category import MY_CATEGORY_CREDENTIALS
CREDENTIAL_SPECS = {
**LLM_CREDENTIALS,
**SEARCH_CREDENTIALS,
**MY_CATEGORY_CREDENTIALS, # Add new category
}
```
4. Update your tool to accept the optional `credentials` parameter:
```python
from typing import Optional, TYPE_CHECKING
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from aden_tools.credentials import CredentialManager
from aden_tools.credentials import CredentialStoreAdapter
def register_tools(
mcp: FastMCP,
credentials: Optional["CredentialManager"] = None,
credentials: CredentialStoreAdapter | None = None,
) -> None:
@mcp.tool()
def my_api_tool(query: str) -> dict:
"""Tool that requires an API key."""
# Use CredentialManager if provided, fallback to direct env access
# Use credentials adapter if provided, fallback to direct env access
if credentials is not None:
api_key = credentials.get("my_api")
else:
@@ -157,15 +147,108 @@ def register_tools(
# Use the API key...
```
5. Update `register_all_tools()` in `tools/__init__.py` to pass credentials to your tool.
#### Step 2: Create a CredentialSpec
Find the appropriate category file in `src/aden_tools/credentials/` or create a new one:
| Category | File | Examples |
|----------|------|----------|
| LLM providers | `llm.py` | anthropic, openai |
| Search tools | `search.py` | brave_search, google_search |
| Email providers | `email.py` | resend, google/gmail |
| GitHub | `github.py` | github |
| CRM | `hubspot.py` | hubspot |
| Messaging | `slack.py` | slack |
Add your credential spec:
```python
# In credentials/<category>.py
from .base import CredentialSpec
MY_CREDENTIALS = {
"my_api": CredentialSpec(
env_var="MY_API_KEY",
tools=["my_api_tool"], # IMPORTANT: List ALL tool names this credential covers
required=True,
help_url="https://example.com/api-keys",
description="API key for My Service",
# Credential store mapping
credential_id="my_api",
credential_key="api_key",
),
}
```
**Important:** The `tools` list must include every tool name that your `register_tools` function creates. CI will fail if any tool is missing.
#### Step 3: Merge into CREDENTIAL_SPECS
If you created a new category file, import and merge it in `credentials/__init__.py`:
```python
from .my_category import MY_CREDENTIALS
CREDENTIAL_SPECS = {
**LLM_CREDENTIALS,
**SEARCH_CREDENTIALS,
**MY_CREDENTIALS, # Add new category
}
__all__ = [
# ... existing exports
"MY_CREDENTIALS",
]
```
#### Step 4: Update register_all_tools
In `tools/__init__.py`, add your tool registration with credentials:
```python
from .my_tool import register_tools as register_my_tool
def register_all_tools(mcp: FastMCP, credentials=None) -> list[str]:
# ... existing registrations
# Tools that need credentials
register_my_tool(mcp, credentials=credentials)
return [
# ... existing tool names
"my_api_tool",
]
```
### CI Enforcement Rules
The following conformance tests run in CI (`tests/integrations/test_spec_conformance.py`):
| Test | What It Checks |
|------|----------------|
| `TestModuleStructure` | Every tool module exports `register_tools` |
| `TestRegisterToolsSignature` | Correct function signature (`mcp` param, optional `credentials`) |
| `TestCredentialSpecFields` | All CredentialSpec fields are complete (`env_var`, `help_url`, `description`, `credential_id`, `credential_key`) |
| `TestSpecToolsMatchRegistered` | Tool names in `spec.tools` actually exist |
| `TestCredentialCoverage` | **Every tool from a module with `credentials` param has a spec** |
If `TestCredentialCoverage` fails, you'll see:
```
Tool 'my_new_tool' from module 'my_tool' accepts credentials but has no CredentialSpec.
Fix by either:
1. Adding a CredentialSpec in credentials/<category>.py with tools=['my_new_tool'], or
2. Removing 'credentials' param from register_tools() if this tool doesn't need credentials
```
### Testing with Mock Credentials
```python
from aden_tools.credentials import CredentialManager
from aden_tools.credentials import CredentialStoreAdapter
def test_my_tool_with_valid_key(mcp):
creds = CredentialManager.for_testing({"my_api": "test-key"})
creds = CredentialStoreAdapter.for_testing({"my_api": "test-key"})
register_tools(mcp, credentials=creds)
tool_fn = mcp._tool_manager._tools["my_api_tool"].fn
@@ -194,29 +277,6 @@ The following tools require credentials that are not set:
Set these environment variables and re-run the agent.
```
## Environment Variables (Legacy)
For simple cases or backward compatibility, you can still check environment variables directly:
```python
import os
def register_tools(mcp: FastMCP) -> None:
@mcp.tool()
def my_api_tool(query: str) -> dict:
"""Tool that requires an API key."""
api_key = os.getenv("MY_API_KEY")
if not api_key:
return {
"error": "MY_API_KEY environment variable not set",
"help": "Get an API key at https://example.com/api",
}
# Use the API key...
```
However, using `CredentialManager` is recommended for new tools as it provides better validation and testing support.
## Best Practices
### Error Handling