All work

Mar 2025Open-source project

Published 4 min read

IMDb MCP Server

A Model Context Protocol server that gives AI agents structured movie and TV data through 23 tools, with an LRU cache that cuts upstream API calls by 50–75%.

  • Python
  • MCP
  • FastMCP
  • Caching
  • Docker
  • uv
tools exposed to agents
23
query types across search and filters
100+
fewer upstream API calls
50–75%
saved on each cached response
~70 ms

The Model Context Protocol (MCP) is a standard way to give AI agents tools. Instead of every app writing its own glue code, a server describes its tools once, and any MCP client can call them: Claude Desktop, Claude Code, Cursor and others.

I built this server so an agent can answer questions like “What are five action movies coming out soon?” with real data instead of its training cut-off. It wraps the IMDb API on RapidAPI and exposes it as 23 typed tools for search, title details, cast and crew, charts, box office and upcoming releases.

  1. MCP clientClaude Desktop, Claude Code, Cursor…
  2. IMDb MCP ServerTyped tools, pagination, error mapping
  3. Response cacheLRU, 10 min TTL, 100 keys
  4. IMDb APIvia RapidAPI
The agent picks a tool; the server checks its cache before calling IMDb.

Tools designed for agents, not humans

An API built for a web page and a tool built for an LLM have different needs. A few choices made the difference:

  • Docstrings are the interface. The model reads each tool’s description and argument docs to decide what to call, so every tool documents its parameters and points to helper tools, such as “get all possible genres with get_genres()”.
  • Small pages. Lists like the Top 250 return 5 items at a time with hasMore and nextStart. Dumping 250 movies into the context window wastes tokens and makes answers worse.
  • Errors the agent can act on. HTTP 401, 403, 404 and 429 responses become clear ToolError messages, so the agent can tell the user to fix their API key instead of retrying blindly.

The search tool shows the first point well. It is one function that covers dozens of filter combinations, each documented for the model:

@mcp.tool()
async def search_imdb(
    original_title: Optional[str] = None,
    primary_title: Optional[str] = None,
    genre: Optional[str] = None,
    average_rating_from: Optional[float] = None,
    start_year_from: Optional[int] = None,
    countries_of_origin: Optional[List[str]] = None,
    sort_field: Optional[Literal["id", "averageRating", "numVotes", "startYear"]] = None,
    # ... 11 more filters
) -> str:
    """Search for movies on IMDb. First 5 results are returned.
    Args:
        genre: The genre of the movie to search for. Get all possible genres with get_genres().
        countries_of_origin: ISO 3166-1 alpha-2 codes. Get all possible countries with get_countries().
        ...
    """

And every list tool shares one pagination helper:

def paginated_response(items, start, total_count=None):
    """Format a paginated response with a fixed page size of 5."""
    if total_count is None:
        total_count = len(items)

    start = max(0, min(total_count - 1 if total_count > 0 else 0, start))
    page_size = 5
    end = min(start + page_size, total_count)

    return {
        "items": items[start:end],
        "start": start,
        "count": end - start,
        "totalCount": total_count,
        "hasMore": end < total_count,
        "nextStart": end if end < total_count else None,
    }

Caching: the biggest win

Agents are repetitive. Paging through the Top 250 means calling the same endpoint again and again with a different start, and follow-up questions often hit the same title. The server keeps an in-memory cache keyed on URL plus query parameters. It uses LRU eviction (an OrderedDict), a 10-minute expiry, and a background thread that sweeps expired entries.

class ResponseCache:
    """In-memory cache with LRU eviction and automatic expiration."""

    def __init__(self, max_size=100, expiry_seconds=600):
        self.cache = OrderedDict()
        self.max_size = max_size
        self.expiry_seconds = expiry_seconds

    def get(self, key):
        if key in self.cache:
            _, data = self.cache[key]
            self.cache.move_to_end(key)  # most recently used
            return data
        return None

    def set(self, key, data):
        if len(self.cache) >= self.max_size:
            self.cache.popitem(last=False)  # evict least recently used
        self.cache[key] = (datetime.now(), data)
        self.cache.move_to_end(key)

Paging through a chart becomes one upstream request plus several cache hits. In my testing this cut upstream API calls by 50–75% in typical agent sessions and removed around 70 ms of latency from each cached response. That also keeps users inside RapidAPI’s free tier.

Two transports, one codebase

The server started on FastMCP and now runs on the MCP Python SDK 2.x. It speaks both standard transports, chosen by an environment variable:

if os.getenv("TRANSPORT", "stdio") == "http":
    host = os.getenv("HOST", "0.0.0.0")
    port = int(os.getenv("PORT", "8081"))
    server.run(transport="streamable-http", host=host, port=port)
else:
    server.run()  # stdio for local MCP clients
  • stdio for local clients. One line in claude_desktop_config.json runs it straight from GitHub with uvx, with no clone needed.
  • Streamable HTTP for self-hosting. A Docker image serves /mcp on port 8081 behind any TLS-terminating proxy.

Diagnostics go to stderr so they never corrupt the stdio JSON-RPC stream, a small detail that is easy to get wrong.

Example

Asked “What are 5 action movies that will be released soon?”, Claude calls get_upcoming_releases, filters the results by genre and answers with titles, release dates, genres and one-line synopses, all from live data.

{
  "mcpServers": {
    "imdb_server": {
      "command": "uvx",
      "args": ["--from", "git+https://github.com/uzaysozen/imdb-mcp-server", "imdb-server"],
      "env": { "RAPID_API_KEY_IMDB": "your_api_key_here" }
    }
  }
}

What I took away

Designing for an LLM caller is its own discipline. The best tool is not the most powerful one. It is the one whose description is clear enough that the model picks it correctly, and whose output is small enough to reason about. The project is listed and security-assessed on MseeP and MCP Review, and it is MIT licensed.