Skip to main content
Glama

AutoTool

A tool layer that grows, for any AI agent. Connect AutoTool to Claude Code, Codex, Cursor, Claude Desktop, OpenClaw or an agent you are building. When your agent needs something it has no tool for, such as a web API, live data or your company's internal service, it writes the tool. AutoTool then:

  • verifies it: lint, launch in a subprocess, smoke test;

  • guards it: the tool runs in its own process with only the credentials it declares;

  • mounts it, live, with no restart;

  • keeps it for every agent on your machine.

The model is told your keys' names (TAVILY_API_KEY), never their values, and values that a tool echoes back are redacted before the model sees them.

your agent (Claude Code, Codex, Cursor, your own) ──MCP──▶ autotool serve
                                                               ├─ create_tool   the agent writes a tool
                                                               ├─ run_tool      call any created tool
                                                               ├─ weather_tool__current  ─▶ own process, gets WEATHER_API_KEY only
                                                               └─ tavily_tool__search    ─▶ own process, gets TAVILY_API_KEY only

Quick start

Python 3.11+. Pick the way you work:

Build your own agent (Python)

pip install "autotool-mcp[models]"
export OPENAI_API_KEY=sk-...        # your agent's model (or ANTHROPIC_API_KEY)
import asyncio, autotool
from autotool.core.llm import default_provider

answer = asyncio.run(
    autotool.run_agent(
        "What are the top 3 stories on Hacker News right now?",
        default_provider(),
        on_consent=lambda question: input(question + " [y/N] ") == "y",  # asked before a tool gets a key
    )
)
print(answer)

The agent has no Hacker News tool. It writes one, AutoTool verifies and mounts it, and the agent uses it. Run it again and the tool is already there.

Local or other models. Any OpenAI-compatible endpoint works: Ollama, LM Studio, vLLM, OpenRouter, and others. Set a base URL and a model name; local servers need no key.

export OPENAI_BASE_URL=http://localhost:11434/v1   # Ollama; LM Studio: http://localhost:1234/v1
export OPENAI_LLM=qwen2.5:14b                      # the model name on that server

default_provider() then talks to that endpoint through the Chat Completions API. Pick a model with tool calling. Writing a working tool is real coding, so small models fail more often than frontier ones. AUTOTOOL_PROVIDER=openai keeps OpenAI's Responses API even with a custom base URL.

Already have an agent loop? Connect it to AutoTool and pass tool calls through:

async with autotool.connect() as client:  # starts AutoTool as an MCP server
    listed = await client.list_tools()  # create_tool, run_tool, every tool created so far
    tools = autotool.openai_tools(listed)  # or autotool.anthropic_tools(listed)
    result = await client.call_tool(name, arguments)  # whatever your model asked for

Frameworks with stdio MCP support (OpenAI Agents SDK MCPServerStdio, LangGraph via langchain-mcp-adapters) can also launch autotool serve directly.

Add it to Claude Code, Codex, Cursor, Claude Desktop or OpenClaw

pip install autotool-mcp            # or: pipx install autotool-mcp / uv tool install autotool-mcp
autotool setup                      # registers AutoTool with every agent app it finds
autotool keys add TAVILY_API_KEY    # stored in your OS keychain; add as many as you like

Restart your agent app and ask for something it can't do yet: "Search the web for this week's James Webb news". It writes a Tavily tool. AutoTool asks whether that tool may use TAVILY_API_KEY, verifies it, hands it that key and nothing else, and the answer comes back. Next time, the tool is already there.

autotool setup --dry-run shows what it would change first. JSON configs (Claude Desktop, Cursor) are backed up to .bak before editing. Existing entries keep their env block, and an existing Claude Code or Codex entry is left untouched.

Related MCP server: Enterprise MCP Gateway and Tool Registry

Adding credentials

You add environment variables. AutoTool offers their names to your agent, and a created tool gets a value only if it declares that name in its code (REQUIRED_ENV = ["TAVILY_API_KEY"]). Pick any of these places (the first match wins):

Where

How

Your host's MCP config

"env": {"AUTOTOOL_KEY_TAVILY_API_KEY": "tvly-..."}. The prefix makes sharing opt-in.

OS keychain

autotool keys add TAVILY_API_KEY (hidden prompt; --stdin for scripts)

~/.autotool/.env

TAVILY_API_KEY=tvly-..., one per line

Any file

autotool serve --env-file path/to/.env

A new key can be used by the next tool your agent creates, with no restart. Your agent's tool list shows the new name after its next refresh, and a cached tool that was skipped for a missing key loads on the next restart. autotool keys list shows names. autotool tools list shows which tools hold which credentials. If a tool needs a key you don't have, your agent is told exactly what to run (autotool keys add STRIPE_API_KEY).

Let AutoTool write the tools

If your agent is small or not good at writing code, give AutoTool a model of its own in ~/.autotool/.env: OPENAI_API_KEY (default model gpt-6-sol), ANTHROPIC_API_KEY (default claude-opus-5), or an OpenAI-compatible endpoint (OPENAI_BASE_URL + OPENAI_LLM, e.g. a local Ollama model). Only a key in that file counts: a key your host happens to have in its environment is never used, so nothing gets billed by surprise. A synthesize_tool tool then appears: your agent describes the capability, and AutoTool writes, verifies, repairs and mounts it.

What a created tool looks like

import os

import httpx2
from mcp.server.mcpserver import MCPServer
from mcp.server.mcpserver.exceptions import ToolError

REQUIRED_ENV = ["TAVILY_API_KEY"]  # the only credential this process will receive
mcp = MCPServer("tavily_tool")
API_BASE = os.environ.get("TAVILY_API_BASE", "https://api.tavily.com")


@mcp.tool()
def search(query: str, max_results: int = 5) -> str:
    """Search the web with Tavily."""
    with httpx2.Client(timeout=10) as client:
        r = client.post(
            f"{API_BASE}/search",
            json={"query": query, "max_results": max_results},
            headers={"Authorization": f"Bearer {os.environ['TAVILY_API_KEY']}"},
        )
    if r.status_code != 200:
        raise ToolError(f"Tavily returned HTTP {r.status_code}: {r.text[:200]}")
    return r.text


if __name__ == "__main__":
    mcp.run()

A tool is a plain MCP server built on the official Python SDK (v2). ToolError messages reach the agent; an unexpected exception's traceback goes to the verifier, which feeds it back so the tool can be fixed.

Security

Consent. The first time a tool asks for credentials, AutoTool asks you, through your agent app's approval prompt: "Allow tavily_tool to use TAVILY_API_KEY? It can only send them to api.tavily.com." Nothing runs with the key until you say yes. You are asked again only when the risk changes:

  • Key with a host allowlist (and the guard on): rewrites of the tool don't ask again. A rewrite can't send the key anywhere new. It asks again only if the tool wants more keys or you widen the allowlist.

  • Key without an allowlist: every new version of the tool asks, because a rewrite could send the key anywhere. Setting allowlists means fewer prompts.

Removing a tool revokes its approval. If your app can't show prompts, the agent is told to ask you to run the approval yourself:

autotool tools approve tavily_tool TAVILY_API_KEY
autotool tools revoke tavily_tool
autotool serve --dangerously-allow-all-tools   # never ask (or AUTOTOOL_CONSENT=dangerously-allow-all)

--dangerously-allow-all-tools gives every tool the credentials it declares without asking. Use it for CI, or if you'd rather not approve tools. The guard and host allowlists still apply, and the server logs a warning at startup.

Per-key host allowlists. Say where a key may be sent. A tool holding that key can then only reach those hosts (plus localhost, which never leaves your machine):

autotool keys allow TAVILY_API_KEY api.tavily.com
autotool keys allow GITHUB_TOKEN api.github.com "*.githubusercontent.com"
autotool keys allow TAVILY_API_KEY          # no hosts: remove the limit

The guard (on by default, optional). Every tool runs under a Python audit hook that the tool cannot remove. It enforces the allowlists and blocks:

  • subprocesses and shell commands;

  • loading native code;

  • the OS keychain and credential stores;

  • reading AutoTool's credential files and your agent apps' MCP configs;

  • creating, changing or deleting anything outside the temp directory.

This applies to every tool, including ones that declare no keys.

It is a guard, not a jail: native code or interpreter bugs can get past it. If AutoTool already runs inside a sandbox (a container, a VM, a devcontainer), turn it off: autotool serve --sandbox off (or AUTOTOOL_SANDBOX=off in your host's MCP config). Only an explicit off, 0, false or no turns it off; anything else keeps it on. Host allowlists are then not enforced; consent still is.

Always on:

  • The model is only ever told credential names. Values a tool echoes back (raw, URL-encoded or JSON-escaped) are redacted. A tool written to disguise a key (e.g. base64) could still reveal it; that is what consent and host allowlists are for.

  • Each tool process gets only the variables in its REQUIRED_ENV, plus an allowlist of OS basics: PATH, temp dirs, locale, proxy and CA settings, and *_API_BASE overrides. Nothing else from your host's environment reaches it (no AWS_*, no DATABASE_URL).

  • Before anything a tool produces reaches the model (output, errors, tracebacks, schemas), secret values are replaced with [REDACTED:NAME]. This covers raw, URL-encoded and JSON-escaped forms.

For a hard boundary, run AutoTool in a container with the guard off.

The design and its evaluation are written up in the paper.

Hosts

Host

Connect

Notes

Claude Code

autotool setup (runs claude mcp add --scope user)

Headless claude -p: set MCP_CONNECTION_NONBLOCKING=0 so the first turn waits for AutoTool.

Codex

autotool setup (runs codex mcp add)

Headless codex exec auto-rejects MCP calls: add -c mcp_servers.autotool.default_tools_approval_mode="approve".

Claude Desktop, Cursor

autotool setup (edits their mcpServers config)

Restart the app.

OpenClaw

autotool setup (openclaw mcp set on releases that have it)

Older releases: setup prints the config snippet to paste.

Anything else

point it at autotool serve (stdio)

AutoTool speaks both MCP protocol eras: hosts on the 2026-07-28 revision get consent through an input-required round trip and hear about new tools on subscriptions/listen; older hosts get a form elicitation and notifications/tools/list_changed.

Development

python -m venv venv && venv/bin/pip install -e ".[dev]"   # Windows: venv\Scripts\pip
pytest                                                     # offline suite
ruff check . && ruff format --check .                      # lint
autotool run "What is the top story on Hacker News?"       # demo agent: needs [models] + a key in ./.env;
                                                           # no consent prompts or allowlists, uses ./tools
python scripts/auth_benchmark.py --trials 3                # credentials benchmark
python scripts/host_benchmark.py --trials 3                # Claude Code + custom-agent benchmark

Built on the MCP Python SDK 2.x, httpx2, the OpenAI SDK 3.x (Responses API) and the Anthropic SDK 1.x.

Available Tools

2 tools
create_toolA

Create a new tool when none of your current tools can do what the task needs (a web API, live data, a computation). Write a complete Python MCP tool server script (MCPServer); AutoTool verifies it in a guarded subprocess, injects only the credentials it declares, and mounts it: its tools appear in your tool list (or call them through run_tool). If verification fails you get the error; fix the code and call create_tool again with the same name.

The script MUST follow this contract:

  • It is one self-contained file that runs as python <file>.py and serves MCP over stdio.

  • It uses from mcp.server.mcpserver import MCPServer and creates mcp = MCPServer("<tool_name>").

  • Every tool is a function decorated with @mcp.tool() with full type annotations and a docstring that explains what it does and each parameter. Prefer simple parameter types (str, int, float, bool) with sensible defaults. Return a str (plain text or json.dumps(...) of the result).

  • The file ends with: if name == "main": mcp.run()

  • Only import the standard library, httpx2, pydantic and mcp.

  • Use httpx2 with an explicit timeout of at most 10 seconds for network calls. Use public APIs, or APIs whose credentials the prompt lists as available environment variables. Never hard-code credentials.

  • Credentials: read each one with os.environ["NAME"] and declare every name you read at module level, e.g. REQUIRED_ENV = ["NAME"]. Only declared names are injected into the process. Send keys in headers when the API allows it; never return, log or put key values in error messages.

  • Make every external API base URL overridable through an environment variable named <SERVICE>_API_BASE (e.g. HN_API_BASE), defaulting to the real public URL.

  • Report failures the caller should see (an HTTP error status, nothing found) by raising ToolError("...") from mcp.server.mcpserver.exceptions, with a clear message that names the service and status. Let unexpected errors raise: they fail the call, and the traceback reaches the verification harness. Never return an error as a normal string, and never swallow exceptions.

  • No subprocesses, no filesystem writes, no eval/exec.

  • Keep the tool fast: bound fan-out (e.g. concurrent requests for at most ~30 items).

No credentials are available: only use public, keyless APIs and do not declare REQUIRED_ENV.

Structural reference:

import os

import httpx2
from mcp.server.mcpserver import MCPServer
from mcp.server.mcpserver.exceptions import ToolError

mcp = MCPServer("example_tool")
API_BASE = os.environ.get("EXAMPLE_API_BASE", "https://api.example.com")


@mcp.tool()
def get_thing(thing_id: int, verbose: bool = False) -> str:
    """Fetch a thing by id.

    Args:
        thing_id: Numeric id of the thing.
        verbose: Include extra fields.
    """
    with httpx2.Client(timeout=10) as client:
        resp = client.get(f"{API_BASE}/things/{thing_id}")
    if resp.status_code != 200:
        raise ToolError(f"Example API returned HTTP {resp.status_code} for thing {thing_id}")
    return resp.text


if __name__ == "__main__":
    mcp.run()
ParametersJSON Schema
NameRequiredDescriptionDefault
codeYesComplete Python source of the MCPServer tool script.
nameYessnake_case module name ending in _tool, e.g. 'weather_tool'.
test_toolNoTool to smoke-test (default: the first one).
test_argumentsNoSafe, realistic arguments for the smoke test.

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden and does so thoroughly: guarded subprocess verification, credential injection limited to declared REQUIRED_ENV names, secret-handling rules, error-surfacing via ToolError vs. uncaught tracebacks, and sandbox prohibitions. This is exactly the behavioral context an agent needs before writing a script.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Long, but appropriately so for a code-generation contract: it front-loads the when-to-use sentence, then organizes requirements as a scannable bullet list plus a structural reference. The example script restates some contract points, a minor duplication.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

No output schema or annotations exist, yet the description covers the full lifecycle: script contract, allowed imports, credential declaration/injection, base-URL overridability, error reporting, sandbox limits, and performance bounds. Nothing needed to call it correctly is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so baseline is 3. The description goes beyond the schema by tying the name parameter to the retry workflow ("call create_tool again with the same name") and elaborating the code contract in detail.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource ("Create a new tool") and immediately defines the scope and trigger ("when none of your current tools can do what the task needs"). It clearly distinguishes itself from the sibling run_tool, which it names as the invocation path for the resulting tools.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly states when to use it (no existing tool can perform the task) and what the alternative is (invoke mounted tools directly or via run_tool). It also gives recovery guidance: on verification failure, fix the code and call again with the same name.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

run_toolA

Call a tool created with create_tool by its full name (e.g. 'weather_tool__current'). Use this if a newly created tool does not show up in your tool list.

ParametersJSON Schema
NameRequiredDescriptionDefault
nameYes
argumentsNo

TDQS

A3.7/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the full behavioral burden. It usefully discloses the discovery-fallback scenario and the canonical name shape, but says nothing about error behavior when the name is unknown, whether 'arguments' is validated against the target tool's schema, or what the call returns.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two short sentences, zero filler. The core action is stated first, followed by the fallback condition, with the naming example embedded where it is needed.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a two-parameter pass-through tool with no output schema and no annotations, the description covers purpose and the discovery workaround adequately, but leaves gaps around argument forwarding semantics and failure modes that the empty schema cannot fill.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the description must compensate. It does explain the 'name' parameter format via the 'weather_tool__current' example, which is genuinely useful. However, the 'arguments' parameter (a nested object passed to the target tool) is never mentioned, leaving half the interface undocumented.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource ('Call a tool created with create_tool by its full name') and immediately contrasts with the sibling create_tool by implying invocation rather than creation. The example name 'weather_tool__current' pins down the full-name format, though it never states what happens when the call succeeds or fails.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives an explicit triggering condition: 'Use this if a newly created tool does not show up in your tool list.' That is a clear when-to-use rule, but it offers no when-not guidance (e.g., prefer the normal tool listing when the tool is visible) and no prerequisite about create_tool having been run first.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 2 tool updatesv0.1.0
    • First observedcreate_tool
    • First observedrun_tool

TDQS

A4.2/5.0

Scored across 2 tools

Disambiguation5/5

create_tool and run_tool have clearly distinct purposes: one writes and mounts a new Python MCP server, the other invokes an already-created tool by name. There is no overlap in intent or expected arguments.

Naming Consistency5/5

Both names follow a clean snake_case verb_noun pattern (create_tool, run_tool), sharing the same noun and differing only in the action verb. Fully predictable.

Tool Count4/5

Two tools is minimal, but for a self-extending meta-server this is the core lifecycle: author a tool, then call it. The actual capability surface is unbounded via generated tools, so the small count is justified rather than thin.

Completeness4/5

The create/execute loop is fully covered and re-calling create_tool with the same name handles regeneration. Minor gaps remain: no explicit list of generated tools, no removal or inspection of mounted tool metadata, though the host tool list largely compensates.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    A
    maintenance
    Enables users to define and run MCP tools using declarative YAML configs with built-in trust enforcement, credential brokering, and tamper-evident audit logging.
    14
    MIT
  • F
    license
    Not graded
    quality
    B
    maintenance
    Enables AI agents to securely discover, execute, and observe tools with role-based access control and audit logging. Serves tools over MCP stdio and HTTP for integration with Claude Desktop, Cursor, and other clients.
    1
    -
  • A
    license
    A
    quality
    B
    maintenance
    Finds MCP servers in the MCP Rating index and connects to them on demand, proxying their tools under namespaced names and notifying the client when tools change. Each server gets a short environment allowlist plus only the variables you name for it, so other API keys in your environment aren't passed on. Files aren't isolated unless you opt into Docker containers.
    13
    58 npm
    1
    MIT