Skip to content

feat: add airport search by city name, airport name, or IATA code - #115

Merged
punitarani merged 2 commits into
punitarani:mainfrom
tmchow:feat/airport-search
May 12, 2026
Merged

feat: add airport search by city name, airport name, or IATA code#115
punitarani merged 2 commits into
punitarani:mainfrom
tmchow:feat/airport-search

Conversation

@tmchow

@tmchow tmchow commented Apr 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Add search_airports() utility that looks up IATA codes from city names, airport names, or partial codes. Exposed as CLI command (fli airports), MCP tool (find_airports), and Python API (from fli.core import search_airports).

Why this matters

Users must know IATA codes to use any fli command. When using fli through MCP with an AI assistant, the assistant needs to map "San Francisco" to "SFO" but fli provides no way to do this lookup. The competitor library fast-flights had an abandoned PR #66 for the same gap ("Enhance airport search to support multiple search criteria").

For MCP-powered travel agents, this is a prerequisite. You can't search flights if you don't know the airport code.

Demo

Airport Search Demo

Changes

  • fli/core/airports.py (NEW, 152 lines) - Search function with 5-priority matching: exact IATA code, city name lookup (50+ major cities), partial city match, airport name substring, IATA prefix. City mapping covers cases where the city name isn't in the airport name (e.g., "new york" returns JFK, LGA, EWR since "John F Kennedy International Airport" doesn't contain "New York").

  • fli/cli/commands/airports.py (NEW, 51 lines) - CLI command with rich table output and --json flag.

  • fli/mcp/server.py (+36 lines) - find_airports MCP tool so AI assistants can resolve city names to codes before calling search_flights.

  • tests/core/test_airports.py (NEW, 99 lines) - 15 tests covering exact codes, city names, partial matches, edge cases.

Testing

uv run pytest -vv tests/core/test_airports.py  # 15 passed
uv run ruff check .                             # all checks passed
uv run fli airports "new york"                  # JFK, LGA, EWR
uv run fli airports heathrow                    # LHR
uv run fli airports sf                          # SFO, OAK, SJC

This contribution was developed with AI assistance (Codex + Claude Code).

Greptile Summary

This PR adds search_airports() as a core utility, CLI command (fli airports), and MCP tool (find_airports) to let users (and AI agents) resolve city names or partial strings to IATA codes before calling flight search. The 5-priority matching algorithm (exact code → city map → partial city → airport name substring → IATA prefix) is well-structured and the test coverage is solid.

Confidence Score: 5/5

Safe to merge; all findings are P2 style suggestions with no correctness or runtime impact.

No P0 or P1 issues found. The logic is correct, tests cover all priority levels and edge cases, and the CLI/MCP wiring is done properly. Remaining comments are minor: a redundant condition placement, missing lower-bound validation on a parameter, an inconsistent return type on one MCP tool, and an import style mismatch.

fli/core/airports.py (minor loop guard) and fli/mcp/server.py (return type inconsistency) are the only files worth a second look, but neither blocks merge.

Important Files Changed

Filename Overview
fli/core/airports.py New 5-priority search function; well-structured but Priority 3 guard is checked inside the loop and limit parameter accepts negative values silently
fli/cli/commands/airports.py Clean CLI command with rich table output and --json flag; no issues
fli/mcp/server.py find_airports MCP tool added correctly but returns plain str while other tools return dict; search_airports imported from submodule instead of fli.core
fli/cli/main.py airports command registered and added to CLI dispatch guard correctly
fli/core/init.py search_airports correctly re-exported in all
tests/core/test_airports.py 15 tests covering all priority levels, edge cases, and abbreviations; good coverage

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[search_airports query] --> B{query empty?}
    B -- yes --> Z[return empty list]
    B -- no --> C[Priority 1: Exact IATA code match]
    C --> D{Airport enum match?}
    D -- yes --> E[Add with score 100]
    D -- no --> F[Priority 2: Exact city name lookup in CITY_AIRPORTS]
    E --> F
    F --> G{Exact city match?}
    G -- yes --> H[Add mapped airports with score 90]
    G -- no --> I[Priority 3: Partial city name match]
    H --> I
    I --> J{Any city starts with query AND query not in map?}
    J -- yes --> K[Add airports with score 80]
    J -- no --> L[Priority 4: Airport name substring scan]
    K --> L
    L --> M{query substring of any Airport.value?}
    M -- yes --> N[Add with score 70 minus position offset]
    M -- no --> O[Priority 5: IATA prefix scan]
    N --> O
    O --> P{len query <= 3?}
    P -- yes --> Q{Any IATA code starts with query?}
    Q -- yes --> R[Add with score 60]
    P -- no --> S[Sort by score desc then code asc]
    R --> S
    S --> T[Return results up to limit]
Loading
Prompt To Fix All With AI
This is a comment left during a code review.
Path: fli/core/airports.py
Line: 118-127

Comment:
**Priority 3 condition is re-evaluated on every loop iteration**

The guard `query_lower not in CITY_AIRPORTS` is constant across all iterations of the `CITY_AIRPORTS` loop. When the query has an exact city match, the loop still executes but immediately short-circuits on every entry — O(N) wasted evaluations. Hoist the check outside the loop for clarity and efficiency.

```suggestion
    # Priority 3: Partial city name match (handles "new yo" matching "new york")
    if query_lower not in CITY_AIRPORTS:
        for city, codes in CITY_AIRPORTS.items():
            if city.startswith(query_lower):
                for code in codes:
                    if code not in seen_codes:
                        try:
                            airport = Airport[code]
                            results.append(AirportMatch(code, airport.value, "city", 80.0))
                            seen_codes.add(code)
                        except KeyError:
                            pass
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: fli/core/airports.py
Line: 79

Comment:
**`limit` not validated against non-positive values**

`results[:limit]` with `limit=0` silently returns an empty list; with `limit=-1` it drops the last result. The MCP tool enforces `ge=1`, but the public `search_airports` function has no guard, so callers using the Python API directly can trigger this silently incorrect behaviour.

```suggestion
def search_airports(query: str, limit: int = 10) -> list[AirportMatch]:
    if limit < 1:
        raise ValueError(f"limit must be >= 1, got {limit}")
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: fli/mcp/server.py
Line: 692-717

Comment:
**`find_airports` returns a plain string; other tools return structured dicts**

`search_flights` and `search_dates` both return `dict[str, Any]`, which AI agents can consume directly. `find_airports` returns a pre-formatted string, forcing any downstream caller (human or agent) to parse text to extract codes. Returning a structured response like `{"airports": [{"code": ..., "name": ...}]}` would be consistent with the rest of the API and easier to integrate.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: fli/mcp/server.py
Line: 41

Comment:
**`search_airports` imported directly from submodule, not from `fli.core`**

All other utilities (`build_flight_segments`, `parse_airlines`, etc.) are imported from the `fli.core` package, which already re-exports `search_airports` in its `__all__`. Importing directly from `fli.core.airports` here is inconsistent with the established pattern.

```suggestion
from fli.core import (
    build_date_search_segments,
    build_flight_segments,
    build_time_restrictions,
    parse_airlines,
    parse_cabin_class,
    parse_emissions,
    parse_max_stops,
    parse_sort_by,
    resolve_airport,
    search_airports,
)
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "feat: add airport search by city name, a..." | Re-trigger Greptile

Greptile also left 4 inline comments on this PR.

Comment thread fli/core/airports.py Outdated
Comment thread fli/core/airports.py
Comment thread fli/mcp/server.py Outdated
Comment thread fli/mcp/server.py Outdated
@punitarani

Copy link
Copy Markdown
Owner

this is awesome :) will take a look!

@tmchow

tmchow commented Apr 7, 2026

Copy link
Copy Markdown
Contributor Author

this is awesome :) will take a look!

Thanks!

tmchow added 2 commits April 7, 2026 14:04
Add search_airports() utility that fuzzy-matches queries against the
existing Airport enum. Supports exact IATA codes, city name lookups
(with a mapping for ~50 major cities), airport name substrings, and
partial code prefixes.

Exposed as:
- CLI: fli airports "new york"
- MCP: find_airports tool for AI assistant integration
- Python: from fli.core import search_airports
- Hoist constant `query_lower not in CITY_AIRPORTS` guard outside the
  Priority 3 loop to avoid O(N) wasted evaluations
- Guard against limit < 1 in search_airports() for direct Python callers
- Return structured dict from MCP find_airports (consistent with
  search_flights/search_dates)
- Import search_airports from fli.core instead of fli.core.airports
@tmchow
tmchow force-pushed the feat/airport-search branch from eb9bb0e to 489d534 Compare April 7, 2026 21:04
@punitarani
punitarani merged commit beab57a into punitarani:main May 12, 2026
punitarani added a commit that referenced this pull request May 12, 2026
PR #115 left an extra blank line after the find_airports MCP tool,
causing `ruff format --check` to fail on main and blocking the test
job (which depends on lint). Removing the line fixes both jobs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
punitarani added a commit that referenced this pull request May 12, 2026
PR #115 added the `find_airports` tool on main. After merging main into
this branch, the strict-equality assertions in `test_mcp_http.py` started
failing in CI because the merged tree exposes three tools, not two.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
punitarani added a commit that referenced this pull request May 12, 2026
* Setup Railway HTTP MCP deployment and add boot-up integration tests

- Simplify railway.toml (remove unnecessary nixpacksPlan repo reference)
- Change run_http() default host to 0.0.0.0 for containerized deployments
- Add test_mcp_http.py with in-process and HTTP transport tests verifying
  the server boots and exposes search_flights and search_dates tools
- Add mcp-http Makefile target

https://claude.ai/code/session_01MkjS2kP1d43DGvqr818MkJ

* Address review feedback on MCP HTTP tests

- Replace `_free_port()` with an `http_mcp_url` pytest fixture that uses
  uvicorn `port=0` and reads back the bound port via `server.servers[0]`,
  eliminating the TOCTOU race between probe-close and uvicorn-bind.
- Collapse duplicated server lifecycle (start / wait-for-ready / shutdown)
  from both HTTP tests into the shared fixture; each test shrinks to ~5 lines.
- Document the `0.0.0.0` default in `run_http()` so local users know to set
  `HOST=127.0.0.1` for loopback-only behavior.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Add `find_airports` to expected MCP tools in HTTP boot tests

PR #115 added the `find_airports` tool on main. After merging main into
this branch, the strict-equality assertions in `test_mcp_http.py` started
failing in CI because the merged tree exposes three tools, not two.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants