Most AI agent demos start with a comfortable assumption: we, the developers, already know which tools the agent will need. We write a get_customer tool, a create_ticket tool, maybe a query_database tool, and then the model decides when to use them.
That works, but real systems are not always that tidy. Internal APIs change. Teams expose new endpoints. A support agent may need to operate a service it has never seen before. So I wanted to test a slightly different idea:
What if the agent did not start with business tools at all?

That is my PoC. A small command-line application where a Strands agent starts only with meta-tools. It can inspect an OpenAPI contract, generate Python tools for the API operations, load those tools at runtime, and then use them to complete a task.
The example API is deliberately simple: a tiny commerce support API with delayed orders and discount vouchers. The interesting part is not the API. The interesting part is the sequence:
- Read the OpenAPI spec.
- Generate Strands-compatible Python tools.
- Load those tools dynamically.
- Use the newly created tools to solve the user request.
The agent begins without list_orders or create_discount_voucher. It builds them when it discovers that it needs them.
The idea
The local demo task is:
poetry run toolsmith agent \ "Find delayed orders in Spain and create a 15 percent discount voucher for each affected customer."
The agent receives the OpenAPI file and a tools directory. Its first tools are not business tools. They are tools for creating tools:
tooldef inspect_openapi(spec_path: str) -> dict: """Inspect an OpenAPI file and list the operations available for tool generation.""" return describe_openapi(spec_path)tooldef generate_openapi_tools(spec_path: str, output_dir: str) -> dict: """Generate Strands Python tools from an OpenAPI file into the selected directory.""" generated = generate_tools(spec_path, output_dir) return { "created": [ { "name": item.name, "path": str(item.path), "method": item.operation.method, "api_path": item.operation.path, } for item in generated ] }
The third tool is Strands’ load_tool. Once the Python files exist, the agent can load them into its own runtime and use them like any other tool.
This is the meta-tooling pattern: the agent does not only use capabilities; it can create new capabilities while it is running.
Project structure
The project is intentionally small:
src/
meta/
cli.py
settings.py
demo_api.py
agent/
factory.py
prompts.py
meta_tooling/
generator.py
models.py
openapi.py
runtime/
http_client.py
examples/
support/
openapi.yaml
tests/
The responsibilities are explicit:
meta_tooling/openapi.pyreads the OpenAPI contract and extracts operations.meta_tooling/generator.pywrites Python files that expose those operations as Strands tools.runtime/http_client.pycontains the deterministic HTTP boundary used by generated tools.agent/factory.pywires Strands Agents with Bedrock and the meta-tools.demo_api.pyruns a tiny local HTTP API for the example.cli.pyexposes the commands.
The OpenAPI contract
The demo contract describes three operations:
paths: /orders: get: operationId: list_orders summary: List customer orders parameters: - name: status in: query required: false schema: type: string - name: country in: query required: false schema: type: string /orders/{order_id}: get: operationId: get_order summary: Get one order /discount-vouchers: post: operationId: create_discount_voucher summary: Create a discount voucher for a customer order
The CLI can inspect it without calling an LLM:
poetry run toolsmith inspect
Output:
Support Commerce API (1.0)
Tool Method Path Summary
list_orders GET /orders List customer orders
get_order GET /orders/{order_id} Get one order
create_discount_voucher POST /discount-vouchers Create a discount voucher for a customer order
Generating tools
The generator turns each OpenAPI operation into a Python module. This can also be run directly:
poetry run toolsmith build
It creates files like:
generated_tools/
__init__.py
list_orders.py
get_order.py
create_discount_voucher.py
The generated list_orders tool looks like this:
from __future__ import annotationsfrom typing import Anyfrom strands import toolfrom meta.runtime.http_client import request_apitooldef list_orders(status: str | None = None, country: str | None = None) -> dict[str, Any]: """List customer orders Generated API tool for `GET /orders`. Returns customer orders filtered by status or country. """ return request_api( method="GET", path_template="/orders", path_params={}, query_params={"status": status, "country": country}, json_body=None, )
There is no magic in the HTTP call itself. It is just Python:
def request_api( *, method: str, path_template: str, path_params: dict[str, Any] | None = None, query_params: dict[str, Any] | None = None, json_body: dict[str, Any] | None = None, timeout: int = 15,) -> dict[str, Any]: base_url = os.getenv("TOOLSMITH_API_BASE_URL", "http://127.0.0.1:8010") path = _build_path(path_template, path_params or {}) query = {key: value for key, value in (query_params or {}).items() if value is not None} response = requests.request( method=method, url=f"{base_url.rstrip('/')}{path}", params=query, json=json_body, timeout=timeout, )
That boundary is important. The LLM can decide which operation it needs, but the generated tool uses deterministic Python for the actual API call.
The agent
The agent is created with Bedrock and Strands:
def create_agent(settings: Settings) -> Agent: session_kwargs = {"region_name": settings.aws_region} if settings.aws_profile: session_kwargs["profile_name"] = settings.aws_profile boto_session = boto3.Session(**session_kwargs) model = BedrockModel( model_id="us.anthropic.claude-sonnet-4-5-20250929-v1:0", boto_session=boto_session, temperature=0.2, ) return Agent( system_prompt=TOOLSMITH_SYSTEM_PROMPT, model=model, tools=[inspect_openapi, generate_openapi_tools, load_tool], )
The prompt is intentionally strict:
TOOLSMITH_SYSTEM_PROMPT = """You are an API toolsmith agent.You start with meta-tools, not with business tools. Your job is to inspect anOpenAPI contract, generate Python tools for the useful operations, load thosetools, and then use them to answer the user's question.Rules:- Inspect the OpenAPI file before generating tools.- Generate tools only inside the configured generated tools directory.- Load generated tools before trying to call them.- Prefer deterministic API calls through generated tools over guessing.- Explain briefly which tools you created and why they were useful."""
The model-driven part is deciding the workflow. The deterministic part is parsing OpenAPI, writing the Python modules, and making HTTP requests.
Running the demo
Install dependencies with Poetry:
poetry install
Run the local demo API in one terminal:
poetry run toolsmith demo-api
The API listens on:
http://127.0.0.1:8010
In another terminal, inspect the OpenAPI contract:
poetry run toolsmith inspect
Generate tools manually:
poetry run toolsmith build
Or let the agent do the generation and tool loading:
AWS_PROFILE=sandbox AWS_REGION=us-west-2 \poetry run toolsmith agent \ "Find delayed orders in Spain and create a 15 percent discount voucher for each affected customer."
The expected behavior is:
- The agent inspects
examples/support/openapi.yaml. - It generates
list_orders,get_order, andcreate_discount_voucher. - It loads the generated tools.
- It calls
list_orders(status="delayed", country="ES"). - It creates vouchers for the affected orders.
The generated tool can also be called directly from Python:
from pathlib import Pathfrom importlib.util import module_from_spec, spec_from_file_locationfrom meta.meta_tooling.generator import generate_tools_from_openapiout = Path("generated_tools")generate_tools_from_openapi("examples/support/openapi.yaml", out)spec = spec_from_file_location("list_orders", out / "list_orders.py")module = module_from_spec(spec)spec.loader.exec_module(module)print(module.list_orders(status="delayed", country="ES"))
Output:
{ "status_code": 200, "ok": True, "body": { "orders": [ {"id": "ORD-1001", "customer": "Ada Lovelace", "country": "ES", "status": "delayed", "total": 189.9, "days_late": 4}, {"id": "ORD-1003", "customer": "Barbara Liskov", "country": "ES", "status": "delayed", "total": 244.0, "days_late": 7}, ] },}
Tech stack
- Python 3.13 with Poetry.
- Strands Agents for agent orchestration and dynamic tool loading.
- AWS Bedrock for the LLM runtime.
- OpenAPI as the API capability source.
- Click for the CLI.
- Rich for terminal output.
- Requests for deterministic HTTP calls.
- pytest for tests.
A couple of notes
This is not an API gateway and it is not a replacement for careful integration design. It is a PoC for a specific agentic pattern: start with a narrow set of trusted meta-tools, then allow the agent to expand its own capabilities from a machine-readable contract.
The generated tools are intentionally boring. That is a feature. The agent can be creative in planning, but the API calls should be predictable, inspectable, and testable.
The next step would be to add stronger guardrails: endpoint allowlists, authentication strategies, versioned tools, and a review step before loading newly created code.
And that’s all. Full source code available on my GitHub.