This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Setup:
uv sync --group devRun server:
uv run api-agent # Local dev
# Or direct (no clone): uvx --from git+https://github.com/agoda-com/api-agent api-agent
# Server starts on http://localhost:3000/mcpTests:
uv run pytest tests/ -v # All tests
uv run pytest tests/test_foo.py -v # Single test file
uv run pytest tests/test_foo.py::test_bar -v # Single testLinting & Formatting:
uv run ruff check api_agent/
uv run ruff check --fix api_agent/ # Auto-fix
uv run ruff format api_agent/ # Format
uv run ty check # Type checkDocker:
docker build -t api-agent .
docker run -p 3000:3000 -e OPENAI_API_KEY="..." api-agentMCP Server (FastMCP) receives NL queries + headers → routes to Agents (OpenAI Agents SDK) → agents call target APIs + DuckDB for SQL processing.
- Client sends MCP request w/ headers (
X-Target-URL,X-API-Type,X-Target-Headers) - middleware.py:
DynamicToolNamingMiddlewaretransforms tool names per session (e.g.,_query→flights_api_querybased on URL) - context.py: Extracts
RequestContextfrom headers - tools/query.py: Routes to GraphQL or REST agent
- agent/graphql_agent.py or agent/rest_agent.py:
- Fetches schema (introspection or OpenAPI)
- Creates agent w/ dynamic tools (
graphql_query/rest_call,sql_query,search_schema) - Runs agent loop (max 30 turns)
- Returns results
- executor.py: DuckDB integration for SQL post-processing
-
api_agent/: Main package
- main.py: Entry point, creates FastMCP app w/ middleware
- config.py: Settings via
pydantic-settings(env vars w/API_AGENT_prefix) - context.py: Header parsing →
RequestContext, tool name generation - middleware.py: Dynamic tool naming per session
- tracing.py: OpenTelemetry tracing via OTLP (uses arize-otel for convenience, works with Arize Phoenix, Jaeger, Zipkin, Grafana Tempo, etc.)
-
api_agent/tools/: MCP tool implementations
- query.py:
_querytool (NL → agent) - execute.py:
_executetool (direct GraphQL/REST call)
- query.py:
-
api_agent/agent/: Agent logic (OpenAI Agents SDK)
- graphql_agent.py: GraphQL agent w/ introspection, query building, SQL
- rest_agent.py: REST agent w/ OpenAPI parsing, polling support
- prompts.py: Shared system prompt fragments
- model.py: LLM config (OpenAI-compatible)
- progress.py: Turn tracking
- schema_search.py: Grep-like schema search tool
- contextvar_utils.py: Safe ContextVar access helpers
-
api_agent/recipe/: Parameterized pipeline caching
- store.py:
RecipeStore(LRU in-memory cache, thread-safe) - extractor.py: Extract reusable recipes from agent runs
- runner.py: Execute recipes outside agent context (for MCP tools)
- common.py: Recipe validation, execution, parameter binding
- naming.py: Tool name sanitization
- store.py:
-
api_agent/utils/: Shared utilities
- csv.py: CSV conversion via DuckDB (for recipe
return_directlyoutput) - http_errors.py: HTTP error response extraction (used by both clients)
- csv.py: CSV conversion via DuckDB (for recipe
-
api_agent/graphql/: GraphQL client (httpx)
-
api_agent/rest/: REST client (httpx) + OpenAPI loader (supports OpenAPI 3.x and Swagger 2.0)
-
api_agent/executor.py: DuckDB SQL execution, table extraction, context truncation
All outputs capped at ~32k chars (MAX_TOOL_RESPONSE_CHARS) to prevent LLM overflow:
- Query results: Truncate by char count, show complete rows that fit
- Schema: Truncate large schemas, use
search_schema()for exploration - Single objects: Return DuckDB schema summary instead of full data
Agents use ContextVar for request isolation: _graphql_queries, _query_results, _last_result, _raw_schema. Use mutable containers (lists/dicts) since ContextVar.set() in child tasks doesn't propagate to parent.
Tools have internal names (_query, _execute) transformed by middleware per session:
- Format:
{prefix}_query,{prefix}_execute— prefix from hostname orX-API-Nameheader - Skips generic parts: TLDs,
api,qa,dev,internal - Recipe tools:
r_{slug}(not API-specific), max 60 chars;send_tool_list_changed()notifies clients
- GraphQL: Mutations blocked (queries only)
- REST: POST/PUT/DELETE/PATCH blocked by default, enable via
X-Allow-Unsafe-Pathsheader (glob patterns)
Set X-Poll-Paths header to enable poll_until_done tool:
- Auto-increments
polling.countin body between polls - Checks
done_field(dot-path like"status","trips.0.isCompleted") againstdone_value - Max 20 polls (configurable), default 3s delay
Caches parameterized API call + SQL pipelines from successful agent runs, exposed as MCP tools:
Query → Agent executes → Extractor LLM → Recipe stored → MCP tool `r_{name}` exposed
- Storage: LRU in-memory (default 64 entries), keyed by
(api_id, schema_hash)- auto-invalidates on schema change - Deduplication: Skips equivalent recipes, ensures unique tool names
- Templating: GraphQL
{{param}}, REST{"$param": "name"}, SQL{{param}} - Config:
ENABLE_RECIPES(default: True),RECIPE_CACHE_SIZE(default: 64)
Always run before marking task complete:
uv run ruff check --fix api_agent/ # Lint + auto-fix
uv run ruff format api_agent/ # Format
uv run ty check # Type check
uv run pytest tests/ -v # TestsTests use pytest-asyncio. Mock httpx for HTTP calls. See tests/test_*.py for patterns.
CI runs tests + linting on Python 3.11/3.12 (see .github/workflows/test.yml).