Skip to content
Back to skills

1571 Building Tools 1169dabb

ASecurity

This guide explains how to create new tools for the Aden agent framework using FastMCP.

  • 9 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added October 11, 2026
testingpythongotestinggitapidocumentation

Works with

  • api
  • mcp

Security analysis

A100/100

Scanned October 11, 2026

npx -y skills add tools-only/X-Skills --skill 1571-building_tools_1169dabb --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of 1571 Building Tools 1169dabb?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for 1571 Building Tools 1169dabb
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/tools-only-1571-building-tools-1169dabb/badge)](https://www.skillsdirectory.com/skills/tools-only-1571-building-tools-1169dabb)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

SKILL.md
# Building Tools for Aden

This guide explains how to create new tools for the Aden agent framework using FastMCP.

## Quick Start Checklist

1. Create folder under `src/aden_tools/tools/<tool_name>/`
2. Implement a `register_tools(mcp: FastMCP)` function using the `@mcp.tool()` decorator
3. Add a `README.md` documenting your tool
4. Register in `src/aden_tools/tools/__init__.py`
5. Add tests in `tests/tools/`

## Tool Structure

Each tool lives in its own folder:

```
src/aden_tools/tools/my_tool/
├── __init__.py           # Export register_tools function
├── my_tool.py            # Tool implementation
└── README.md             # Documentation
```

## Implementation Pattern

Tools use FastMCP's native decorator pattern:

```python
from fastmcp import FastMCP


def register_tools(mcp: FastMCP) -> None:
    """Register my tools with the MCP server."""

    @mcp.tool()
    def my_tool(
        query: str,
        limit: int = 10,
    ) -> dict:
        """
        Search for items matching a query.

        Use this when you need to find specific information.

        Args:
            query: The search query (1-500 chars)
            limit: Maximum number of results (1-100)

        Returns:
            Dict with search results or error dict
        """
        # Validate inputs
        if not query or len(query) > 500:
            return {"error": "Query must be 1-500 characters"}
        if limit < 1 or limit > 100:
            limit = max(1, min(100, limit))

        try:
            # Your implementation here
            results = do_search(query, limit)
            return {
                "query": query,
                "results": results,
                "total": len(results),
            }
        except Exception as e:
            return {"error": f"Search failed: {str(e)}"}
```

## Exporting the Tool

In `src/aden_tools/tools/my_tool/__init__.py`:
```python
from .my_tool import register_tools

__all__ = ["register_tools"]
```

In `src/aden_tools/tools/__init__.py`, add to `_TOOL_MODULES`:
```python
_TOOL_MODULES = [
    # ... existing tools
    "my_tool",
]
```

## Credential Management

Tools fall into two categories based on whether they need external API credentials:

| Signature | Meaning | CI Enforcement |
|-----------|---------|----------------|
| `register_tools(mcp)` | No credentials needed | ✅ Just works |
| `register_tools(mcp, credentials=None)` | Requires credentials | ⚠️ Must have `CredentialSpec` |

**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.

### 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
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)}
```

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 typing import TYPE_CHECKING

if TYPE_CHECKING:
    from aden_tools.credentials import CredentialStoreAdapter


def register_tools(
    mcp: FastMCP,
    credentials: CredentialStoreAdapter | None = None,
) -> None:
    @mcp.tool()
    def my_api_tool(query: str) -> dict:
        """Tool that requires an API key."""
        # Use credentials adapter if provided, fallback to direct env access
        if credentials is not None:
            api_key = credentials.get("my_api")
        else:
            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-keys",
            }

        # Use the API key...
```

#### 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 CredentialStoreAdapter

def test_my_tool_with_valid_key(mcp):
    creds = CredentialStoreAdapter.for_testing({"my_api": "test-key"})
    register_tools(mcp, credentials=creds)
    tool_fn = mcp._tool_manager._tools["my_api_tool"].fn

    result = tool_fn(query="test")
    # Assertions...
```

### When Validation Happens

Credentials are validated when an agent is loaded (via `AgentRunner.validate()`), not at MCP server startup. This means:

1. The MCP server always starts (even if credentials are missing)
2. When you load an agent, validation checks which tools it needs
3. If credentials are missing, you get a clear error:

```
Cannot run agent: Missing credentials

The following tools require credentials that are not set:

  web_search requires BRAVE_SEARCH_API_KEY
    API key for Brave Search
    Get an API key at: https://brave.com/search/api/
    Set via: export BRAVE_SEARCH_API_KEY=your_key

Set these environment variables and re-run the agent.
```

## Best Practices

### Error Handling

Return error dicts instead of raising exceptions:

```python
@mcp.tool()
def my_tool(**kwargs) -> dict:
    try:
        result = do_work()
        return {"success": True, "data": result}
    except SpecificError as e:
        return {"error": f"Failed to process: {str(e)}"}
    except Exception as e:
        return {"error": f"Unexpected error: {str(e)}"}
```

### Return Values

- Return dicts for structured data
- Include relevant metadata (query, total count, etc.)
- Use `{"error": "message"}` for errors

### Documentation

The docstring becomes the tool description in MCP. Include:
- What the tool does
- When to use it
- Args with types and constraints
- What it returns

Every tool folder needs a `README.md` with:
- Description and use cases
- Usage examples
- Argument table
- Environment variables (if any)
- Error handling notes

## Testing

Place tests in `tests/tools/test_{{tool_name}}.py`:

```python
import pytest
from fastmcp import FastMCP

from aden_tools.tools.{{tool_name}} import register_tools


@pytest.fixture
def mcp():
    """Create a FastMCP instance with tools registered."""
    server = FastMCP("test")
    register_tools(server)
    return server


def test_my_tool_basic(mcp):
    """Test basic tool functionality."""
    tool_fn = mcp._tool_manager._tools["my_tool"].fn
    result = tool_fn(query="test")
    assert "results" in result


def test_my_tool_validation(mcp):
    """Test input validation."""
    tool_fn = mcp._tool_manager._tools["my_tool"].fn
    result = tool_fn(query="")
    assert "error" in result
```

Mock external APIs to keep tests fast and deterministic.

## Naming Conventions

- **Folder name**: `snake_case` with `_tool` suffix (e.g., `file_read_tool`)
- **Function name**: `snake_case` (e.g., `file_read`)
- **Tool description**: Clear, actionable docstring

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.