# Tool Designer

> >

- **Type:** Skill
- **Install:** `agentstack add skill-ravi2799-ai-agent-skills-tool-designer`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [ravi2799](https://agentstack.voostack.com/s/ravi2799)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [ravi2799](https://github.com/ravi2799)
- **Source:** https://github.com/ravi2799/ai-agent-skills/tree/main/skills/tool-designer
- **Website:** https://skills.sh

## Install

```sh
agentstack add skill-ravi2799-ai-agent-skills-tool-designer
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# Tool Designer Skill

A skill that governs how to **design**, **create**, **review**, or **convert** tool/function definitions for AI agents.

Think of this as: **an LLM comes to you and says "I want to enable this capability" — your job is to design and build the tool that grants it.**

Identify which operation applies, then follow the corresponding section.

---

## Operation: CREATE — Designing a New Tool

### Thinking Framework: Capability-First Design

When someone says "I want my LLM to be able to do X", follow this process:

1. **Define the capability** — What exactly should the LLM be able to do? (e.g., "search a database", "send an email", "check weather")
2. **Identify the boundary** — What does the LLM decide (parameters) vs. what the tool handles (execution logic)?
3. **Design the interface** — What inputs does the LLM need to provide? What output helps the LLM reason about the result?
4. **Choose the format** — JSON Schema definition, LangChain `@tool`, or `StructuredTool` depending on the stack

### Pre-Creation Checklist

Before writing a tool definition, verify:

- [ ] I understand the **capability** this tool grants to the LLM
- [ ] I know the **required vs optional** inputs
- [ ] I know the **expected output** format — what the LLM sees back
- [ ] I have considered **error cases** and what the tool returns on failure
- [ ] I have checked for **existing tools** that already cover this functionality
- [ ] I have considered **safety** — does this tool need confirmation for destructive actions?

---

### Format A: JSON Schema Tool Definition

Best for: provider-agnostic definitions, MCP tools, OpenAI function calling

```json
{
  "name": "search_issues",
  "description": "Search the issue tracker for bugs and feature requests matching the given criteria. Use this when the user asks about existing bugs, wants to find related issues, or needs to check for duplicates before filing a new issue.",
  "input_schema": {
    "type": "object",
    "properties": {
      "query": {
        "type": "string",
        "description": "Free-text search query matching issue titles and descriptions"
      },
      "status": {
        "type": "string",
        "enum": ["open", "closed", "all"],
        "description": "Filter by issue status. Defaults to 'open' if not specified"
      },
      "limit": {
        "type": "integer",
        "description": "Maximum number of results to return (1-100). Defaults to 10"
      }
    },
    "required": ["query"]
  }
}
```

---

### Format B: LangChain Python Tools

Best for: Python agents using LangChain/LangGraph

#### Method 1: `@tool` Decorator (Simplest)

The function docstring becomes the tool description the LLM reads. Type hints define the input schema automatically.

```python
from langchain.tools import tool

@tool
def search_issues(query: str, status: str = "open", limit: int = 10) -> str:
    """Search the issue tracker for bugs and feature requests matching the given criteria.
    Use this when the user asks about existing bugs, wants to find related issues,
    or needs to check for duplicates before filing a new issue.

    Args:
        query: Free-text search query matching issue titles and descriptions
        status: Filter by issue status — 'open', 'closed', or 'all'. Defaults to 'open'
        limit: Maximum number of results to return (1-100). Defaults to 10
    """
    # Tool implementation here
    results = db.search(query, status=status, limit=limit)
    return format_results(results)
```

**Key rules for `@tool`:**
- Type hints are **mandatory** — they define the input schema
- The docstring is **critical** — it is what the LLM reads to decide when to call this tool
- Use `Args:` section in docstring to describe each parameter
- Return a string for simple responses the LLM can read directly

#### Method 2: `@tool` with Custom Name and Pydantic Schema (Complex Inputs)

Use Pydantic models when you need validation, complex types, or detailed field descriptions:

```python
from langchain.tools import tool
from pydantic import BaseModel, Field
from typing import Literal

class WeatherInput(BaseModel):
    """Input for weather queries."""
    location: str = Field(description="City name or coordinates (e.g., 'San Francisco' or '37.7749,-122.4194')")
    units: Literal["celsius", "fahrenheit"] = Field(
        default="celsius",
        description="Temperature unit preference"
    )
    include_forecast: bool = Field(
        default=False,
        description="Whether to include a 5-day forecast in the response"
    )

@tool("get_weather", args_schema=WeatherInput)
def get_weather(location: str, units: str = "celsius", include_forecast: bool = False) -> str:
    """Get current weather conditions and optional forecast for a location.
    Use this when the user asks about weather, temperature, or outdoor conditions.

    Args:
        location: City name or coordinates
        units: Temperature unit — 'celsius' or 'fahrenheit'. Defaults to 'celsius'
        include_forecast: Include 5-day forecast. Defaults to False
    """
    data = weather_api.get(location, units=units)
    result = f"Current weather in {location}: {data['temp']}°{units[0].upper()}"
    if include_forecast:
        result += f"\nForecast: {data['forecast']}"
    return result
```

#### Method 3: StructuredTool (Programmatic Creation)

Use when building tools dynamically or from configuration:

```python
from langchain.tools import StructuredTool

def execute_sql(query: str, database: str = "main") -> str:
    """Run a read-only SQL query against the specified database."""
    # Implementation
    return str(db.execute(query, database))

sql_tool = StructuredTool.from_function(
    func=execute_sql,
    name="execute_sql_query",
    description="Run a read-only SQL query. Use this when the user asks data questions that require querying the database. Only SELECT statements are allowed.",
)
```

#### LangChain Tool Return Patterns

| Return Type | When to Use | Example |
|---|---|---|
| **`str`** | Simple human-readable output the LLM processes | `return "Found 5 matching records"` |
| **`dict`/object** | Structured data the LLM reasons about | `return {"count": 5, "results": [...]}` |
| **`Command`** | Tool needs to update agent state | `return Command(update={"user_name": name})` |

#### Reserved Parameter Names (LangChain)

These names **cannot** be used as tool arguments — they are reserved by the framework:
- **`config`** — Reserved for `RunnableConfig`
- **`runtime`** — Reserved for `ToolRuntime` (use this to access conversation state, store, and stream writer)

#### Accessing Runtime Context

Use `ToolRuntime` when the tool needs conversation state, user context, or persistent storage:

```python
from langchain.tools import tool, ToolRuntime

@tool
def get_user_preferences(runtime: ToolRuntime) -> str:
    """Retrieve the current user's saved preferences.
    Use this when the user asks to see their settings or when you need
    to personalize a response based on their preferences."""
    store = runtime.store
    prefs = store.get(("preferences",), runtime.context.user_id)
    return str(prefs.value) if prefs else "No preferences saved"
```

#### Streaming Progress Updates

For long-running tools, emit progress updates so the LLM can inform the user:

```python
@tool
def analyze_repository(repo_url: str, runtime: ToolRuntime) -> str:
    """Analyze a code repository for quality issues and generate a report.
    Use this when the user asks for a code review or quality analysis."""
    writer = runtime.stream_writer
    writer(f"Cloning repository: {repo_url}")
    repo = clone(repo_url)
    writer(f"Analyzing {len(repo.files)} files...")
    results = run_analysis(repo)
    writer("Generating report...")
    return format_report(results)
```

#### Registering Tools with a LangGraph Agent

```python
from langgraph.prebuilt import ToolNode, create_react_agent

# Collect all tools
tools = [search_issues, get_weather, execute_sql_query]

# Option 1: Create a ReAct agent directly
agent = create_react_agent(model, tools)

# Option 2: Use ToolNode for custom graph workflows
tool_node = ToolNode(tools, handle_tool_errors=True)
```

---

### Core Design Rules (All Formats)

**0. Consolidate — don't wrap APIs 1:1**

Do NOT create a separate tool for every API endpoint. Consolidate multi-step workflows into single, task-oriented tools.

| Bad (wrapping APIs) | Good (consolidating) |
|---|---|
| `list_users` + `list_events` + `create_event` | `schedule_event` (finds availability + schedules) |
| `read_logs` (returns everything) | `search_logs` (returns only relevant lines with context) |
| `get_customer_by_id` + `list_transactions` + `list_notes` | `get_customer_context` (compiles recent relevant info) |

**Why:** Each tool call consumes context. A `list_contacts` returning thousands of entries wastes the agent's limited attention budget. Design tools that filter and return only what the agent needs.

**1. Name — snake_case, verb_noun format, specific**
| Weak | Strong |
|---|---|
| `do_thing` | `search_issues` |
| `helper` | `validate_schema` |
| `process` | `parse_csv_to_json` |
| `run` | `execute_sql_query` |

**Namespacing** — when the agent has many tools from multiple services, use prefixes to prevent confusion:
- By service: `asana_search_tasks`, `jira_search_issues`
- By resource: `asana_projects_list`, `asana_users_search`

**2. Description — what it does AND when to use it**

Tool descriptions are **loaded into the agent's system prompt** — they are the highest-leverage text you can write. Even small refinements yield dramatic improvements.

Write descriptions as if explaining to a new hire — make implicit context explicit, define niche terminology, describe resource relationships.

The LLM reads the description to decide whether to call this tool. Include:
- What the tool does (first sentence)
- When to use it (second sentence)
- What it does NOT do, if there's a similar tool that could cause confusion

**Before:**
```
"description": "Gets data"
```
**After:**
```
"description": "Retrieve customer records from the database by customer ID, email, or name. Use this when the user asks about a specific customer's account, order history, or profile. For aggregate customer analytics, use customer_stats instead."
```

**3. Parameters — every parameter gets a description**

The LLM uses parameter descriptions to decide what values to pass. Include:
- What the parameter represents
- Valid values or ranges
- Default behavior when omitted
- An example for non-obvious formats

**Before:**
```json
{ "date": { "type": "string" } }
```
**After:**
```json
{ "date": { "type": "string", "description": "Date in ISO 8601 format (YYYY-MM-DD). Example '2025-03-15'" } }
```

In LangChain, this means either detailed `Args:` docstrings or Pydantic `Field(description=...)`.

**4. Required — only truly required fields**

Mark a parameter as required only when the tool cannot function without it. Optional parameters with sensible defaults reduce friction.

**5. Enum — constrain when values are known**

If a parameter accepts a fixed set of values, use `enum` (JSON Schema) or `Literal` (Python type hint). This prevents the LLM from guessing invalid values.

**6. Single responsibility — one tool, one action**

If a tool does multiple unrelated things based on a "mode" parameter, split it into separate tools.

**7. Use semantic identifiers — not UUIDs**

Agents hallucinate arbitrary alphanumeric IDs. Use human-readable identifiers whenever possible.

| Bad (agent hallucinates) | Good (agent reasons correctly) |
|---|---|
| `user: "a1b2c3d4-e5f6-..."` | `user_id: "jane.doe@company.com"` |
| `file_id: "0x7f3a..."` | `file_path: "src/main.py"` |
| Return raw UUIDs | Return names with IDs: `"Jane Doe (id: 123)"` |

**8. Response format control — let the agent choose verbosity**

Add a `response_format` parameter so the agent can request detailed or concise responses based on the task:

```python
@tool
def search_customers(query: str, response_format: Literal["detailed", "concise"] = "concise") -> str:
    """Search for customers by name or email.

    Args:
        query: Search term
        response_format: 'detailed' for full profiles (~200 tokens each),
                        'concise' for name + ID only (~20 tokens each)
    """
```

**9. Actionable error responses — not stack traces**

When a tool fails, return a message the agent can act on — not a raw error.

| Bad | Good |
|---|---|
| `Error 400: Invalid parameter` | `Parameter 'date_range' must use format YYYY-MM-DD. Example: date_range='2025-01-01'` |
| `NoneType has no attribute 'get'` | `Customer not found for ID 'xyz'. Try searching by email instead using search_customers.` |
| `TimeoutError` | `Database query timed out after 30s. Try narrowing the date range or adding filters.` |

**10. Context-aware result sizing — don't dump AND don't lose signal**

Not all tools should handle large results the same way. The right strategy depends on whether the tool is for **discovery** (finding something) or **analysis** (reasoning over everything).

| Tool Type | Strategy | Example |
|---|---|---|
| **Discovery** (searching, listing) | Truncate + guide to refine | `search_customers` → show top 25, suggest filters |
| **Analysis** (examining, correlating) | Pre-filter, never truncate | `get_error_logs` → filter to relevant severity/time, return ALL matching |

**For discovery tools** — truncate and tell the agent how to narrow down:

```python
def format_search_results(results, limit=25):
    if len(results) > limit:
        return (
            format_entries(results[:limit])
            + f"\n\n[Showing {limit} of {len(results)} results. "
            + "Use filters or increase 'limit' for more targeted results.]"
        )
    return format_entries(results)
```

**For analysis tools** — pre-filter by relevance, never truncate what remains:

```python
def get_error_logs(time_range: str, severity: str = "ERROR") -> str:
    """Return ALL log entries matching the criteria.
    Pre-filters by severity and time range to keep results
    focused, but never truncates matching entries — every
    line could contain the root cause."""
    lines = load_logs(time_range)
    filtered = [l for l in lines if severity in l]  # filter, not truncate
    return format_entries(filtered)  # return ALL matches
```

**The key principle:** Filtering removes noise (safe). Truncating removes signal (dangerous). Use `context-engineer` to pre-filter data before it enters context, and design analysis tools that return everything relevant within their filtered scope.

### Tool Design Patterns

| Pattern | When to Use | Example |
|---|---|---|
| **CRUD** | Managing resources | `create_user`, `get_user`, `update_user`, `delete_user` |
| **Query** | Flexible search | `search_logs` with filter params (date, severity, source) |
| **Batch** | Processing multiple items | `send_emails` with an array of recipients |
| **Confirmation** | Dangerous operations | `preview_delete` (dry run) + `confirm_delete` (execute) |
| **Paginated** | Large result sets | `list_records` with `cursor` and `limit` params |
| **Stateful** | Needs conversation/user context | Use `ToolRuntime` to access state and store |
| **Streaming** | Long-running operations | Use `runtime.stream_writer` for progress updates |

---

## Operation: REVIEW — Evaluating Tool Definitions

### Evaluation Dimensions

| Dimension | Score 1 | Score 5 |
|---|---|---|
| **Name clarity** | Vague (`helper`, `run`) | Specific snake_case verb_noun (`search_issues`) |
| **Description** | Missing or one word | Explains what, when, and disambiguation |
| **Schema completeness** | Missing types or properties | Full schema with types, constraints, and Literal/enum |
| **Parameter descriptions** | None or "the value" | Clear purpose, format, defaults, examples |
| **Error handling** | No mention of failures | Documents error responses

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [ravi2799](https://github.com/ravi2799)
- **Source:** [ravi2799/ai-agent-skills](https://github.com/ravi2799/ai-agent-skills)
- **License:** MIT
- **Homepage:** https://skills.sh

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-ravi2799-ai-agent-skills-tool-designer
- Seller: https://agentstack.voostack.com/s/ravi2799
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
