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%.
- 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.
- MCP clientClaude Desktop, Claude Code, Cursor…
- IMDb MCP ServerTyped tools, pagination, error mapping
- Response cacheLRU, 10 min TTL, 100 keys
- IMDb APIvia RapidAPI
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
hasMoreandnextStart. 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
ToolErrormessages, 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.jsonruns it straight from GitHub withuvx, with no clone needed. - Streamable HTTP for self-hosting. A Docker image serves
/mcpon 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.