SuperAPI AI Directory

langchain-tool-builder

Coding & development

View repository

Updated 10-16 08:11

About this skill

LangChain Tool Builder

Helps define LangChain (Python) tools using Claude Code's buildTool() pattern: a unified class that co-locates identity, schema, security properties, and execution logic, with fail-closed defaults so new tools are safe by default.

Why this pattern matters

Claude Code enforces three things that vanilla LangChain tools lack:

  1. Fail-closed defaults — is_read_only, is_destructive, is_concurrency_safe all default to False. A tool that forgets to declare its properties is conservatively treated as write-capable.
  2. Layered execution — validate_semantics → check_permissions → _call are separate methods, so validation logic doesn't bleed into permission logic or business logic.
  3. Self-contained definition — schema, description, security metadata, and execution all live in one class. No separate permission middleware to wire up.

Workflow

Step 1 — Install the base class

Check if claude_style_tool.py exists in the project's utils directory. The expected location for the ai-base project is: /Users/jigege/ai-base/backend/base/utils/claude_style_tool.py

If it doesn't exist, copy it from references/claude_style_tool.py in this skill directory. Tell the user where it was placed and what it provides.

If working in a different project, ask the user where their utils/tools directory is.

Step 2 — Interview the user

Collect answers to these questions. Defaults are shown — skip questions where the default is clearly fine.

Naming convention: use {service}_{action}_{resource} format with a service prefix so the tool stays unambiguous when multiple tool sets are loaded simultaneously (e.g. stock_get_price, stock_list_symbols, github_create_issue). Start with a verb: get, list, search, create, delete.

Field Question Default
name Tool name (format:{service}_{action}_{resource}, e.g. stock_get_price) — required
description One-sentence description for the LLM:Exact matchActual functionality; do not vaguely broaden it, otherwise the agent will mistakenly call it in scenarios where it should not be used — required
Schema fields What parameters does the tool accept? (field names, types, descriptions; add an example in the Field description, e.g. e.g. '2024-01-01') — required
is_read_only Does this tool only read data, without writing or causing side effects? False
is_destructive Does this tool perform irreversible operations (deletion, overwriting)? False
is_concurrency_safe Can this tool run simultaneously with other tools? False
response_format Is the returned data for programmatic processing by the agent (JSON) or for display to the user (Markdown)? Depends on the scenario; default Markdown
Whether it is a list tool If multiple records are returned, should pagination be supported? Recommended if more than 50
_validate_input_semantics Are there any semantic issues that need to be intercepted before execution? (e.g.: parameter too short, incorrect path format) No
_check_permissions Are there any permissions that need to be checked? (e.g.: only allow reading specific paths, require a certain env var) No
_call What is the core execution logic of the tool? — required

You don't have to ask all questions upfront — you can infer reasonable answers from context. For example, a "search" tool is almost certainly is_read_only=True, is_concurrency_safe=True.

Step 3 — Generate the tool file

Create a .py file for the tool. Follow this field order (matches Claude Code's BashTool):

1. imports
2. Input schema (Pydantic BaseModel)
3. Tool class:
   a. name, description, args_schema      — identity
   b. is_read_only, is_destructive, is_concurrency_safe, max_result_chars  — security metadata
   c. _validate_input_semantics()         — semantic validation (omit if unneeded)
   d. _check_permissions()               — permission check (omit if unneeded)
   e. _call()                            — actual logic

Suggest a file path consistent with the project's tool/agent directory structure. For ai-base, suggest: /Users/jigege/ai-base/backend/base/tools/<tool_name>.py

Step 4 — Show security property summary

After generating, print a one-line summary of the tool's security posture:

SearchDocsTool: read_only=True  destructive=False  concurrency_safe=True  max_result=10K

This helps the user quickly verify the fail-closed properties are set correctly.


Output template

Use this structure when generating the tool file. Adjust based on what the user actually needs.

"""<tool_name>.py — <one-line description>"""

from typing import Optional
from pydantic import BaseModel, Field
from base.utils.claude_style_tool import ClaudeStyleTool


# ---------------------------------------------------------------------------
# Input schema
# ---------------------------------------------------------------------------

class <ToolName>Input(BaseModel):
    <field_name>: <type> = Field(description="<description>")
    # ... more fields


# ---------------------------------------------------------------------------
# Tool class
# ---------------------------------------------------------------------------

class <ToolName>Tool(ClaudeStyleTool):
    # — identity —
    name: str = "<tool_name>"
    description: str = "<one-sentence description for the LLM>"
    args_schema = <ToolName>Input

    # — security metadata (fail-closed: only set True when verified) —
    is_read_only: bool = <True/False>
    is_destructive: bool = <True/False>
    is_concurrency_safe: bool = <True/False>
    max_result_chars: int = 10_000

    # — semantic validation (omit if no input constraints needed) —
    def _validate_input_semantics(self, <params>) -> tuple[bool, Optional[str]]:
        if not <condition>:
            # Error messages must be actionable: tell the agent WHAT to do next
            return False, "<why invalid>. Try <concrete fix, e.g. 'use filter=active_only'>"
        return True, None

    # — permission check (omit if no access control needed) —
    def _check_permissions(self, <params>) -> tuple[bool, Optional[str]]:
        if not <allowed>:
            return False, "<why denied>. <suggested next step>"
        return True, None

    # — core logic —
    def _call(self, <params>, **kwargs) -> str:
        # ... implement tool logic here
        return result

Using build_tool() for simple tools

When the tool has no custom validation or permission logic, build_tool() is cleaner:

from base.utils.claude_style_tool import build_tool
from pydantic import BaseModel, Field

class SearchInput(BaseModel):
    query: str = Field(description="Search query string")

search_tool = build_tool(
    name="search_docs",
    description="Search the documentation index for relevant content.",
    args_schema=SearchInput,
    call_fn=lambda query, **_: search_index(query),
    is_read_only=True,
    is_concurrency_safe=True,
)

Common security property patterns

Tool type is_read_only is_destructive is_concurrency_safe
Search / Query True False True
File read True False True
File write / modify False False False
Delete operation False True False
API call (GET) True False True
API call (POST/DELETE) False Depends False
Database query True False True
Database write False False False

Output design principles

Atomic tools — one tool, one responsibility

Keep each tool focused on a single operation. Let the agent compose multiple tools to complete complex tasks. A tool that does too much is harder for the agent to reuse and reason about.

Response format — JSON vs Markdown

Format When to use
JSON Agent needs to parse/filter the result programmatically
Markdown Result will be shown directly to a user

Support both when uncertain — accept an optional response_format: str = "markdown" parameter and branch in _call. For JSON output use json.dumps(data, ensure_ascii=False, indent=2).

Pagination for list tools

Any tool that can return more than ~50 records should support pagination. Return a dict with these fields so the agent knows when to continue fetching:

return json.dumps({
    "items": [...],
    "total": 150,
    "count": 20,
    "offset": 0,
    "has_more": True,
    "next_offset": 20,
}, ensure_ascii=False, indent=2)

Add offset: int = Field(default=0, description="Pagination offset") and limit: int = Field(default=20, description="Max items to return, default 20") to the input schema.

Actionable error messages

Error strings returned from _validate_input_semantics and _check_permissions must guide the agent toward a fix — not just describe the failure:

# Bad: agent is stuck
return False, "Query too short."

# Good: agent knows exactly what to try next
return False, "Query too short (got 2 chars, need ≥ 3). Provide a more specific search term."

Reference files

  • references/claude_style_tool.py — Complete ClaudeStyleTool base class and build_tool() factory function Installation path:/Users/jigege/ai-base/backend/base/utils/claude_style_tool.py