Initial commit: Plex MCP server with 6 tools and API passthrough
Some checks failed
Build and Push Docker Image / build (push) Failing after 0s

- get_libraries: List all library sections
- search_library: Search for media by title
- get_metadata: Get detailed item info by rating key
- get_recently_added: Get recently added content
- refresh_library: Trigger library scan
- plex_api_call: Raw API passthrough for any endpoint
- search_api_docs: Search OpenAPI spec for endpoint documentation

Includes Docker support and Gitea Actions workflow for container builds.
This commit is contained in:
Ben
2025-12-28 05:26:43 +00:00
commit 9931bdf2e4
11 changed files with 34120 additions and 0 deletions

14
.env.example Normal file
View File

@@ -0,0 +1,14 @@
# Plex MCP Server Configuration
# Plex Media Server URL (include port, typically 32400)
PLEX_URL=http://localhost:32400
# Plex authentication token
# Obtain from: https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/
PLEX_TOKEN=your-plex-token-here
# Unique client identifier for this MCP instance
PLEX_CLIENT_ID=plex-mcp-server
# Server port (optional, defaults to 8000)
PORT=8000

View File

@@ -0,0 +1,55 @@
name: Build and Push Docker Image
on:
push:
branches:
- main
tags:
- 'v*'
pull_request:
branches:
- main
env:
REGISTRY: gitea.ext.ben.io
IMAGE_NAME: b3nw/plex-mcp
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Gitea Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Extract metadata (tags, labels)
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha,prefix=
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

45
.gitignore vendored Normal file
View File

@@ -0,0 +1,45 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Virtual environments
.venv/
venv/
ENV/
# IDE
.idea/
.vscode/
*.swp
*.swo
# Environment
.env
.env.local
# Cache
.ruff_cache/
.mypy_cache/
.pytest_cache/
# OS
.DS_Store
Thumbs.db

24
Dockerfile Normal file
View File

@@ -0,0 +1,24 @@
FROM python:3.11-slim
WORKDIR /app
# Install dependencies
COPY pyproject.toml ./
RUN pip install --no-cache-dir .
# Copy application code
COPY server.py ./
COPY openapi.json ./
COPY docs/ ./docs/
# Create non-root user
RUN useradd --create-home --shell /bin/bash appuser && \
chown -R appuser:appuser /app
USER appuser
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD python -c "import httpx; httpx.get('http://localhost:8000/health').raise_for_status()"
CMD ["python", "server.py"]

151
README.md Normal file
View File

@@ -0,0 +1,151 @@
# Plex MCP Server
A lightweight MCP (Model Context Protocol) server for interacting with Plex Media Server.
## Features
- **5 Specific Tools** for common operations:
- `get_libraries` - List all library sections
- `search_library` - Search for media by title
- `get_metadata` - Get detailed item information
- `get_recently_added` - Get recently added content
- `refresh_library` - Trigger library scan for new content
- **API Pass-through** (`plex_api_call`) for accessing any of the 190+ Plex API endpoints
- **Documentation Resources**:
- `plex://api-reference` - Curated API quick reference
- `search_api_docs` - Search the full OpenAPI specification
## Quick Start
### Prerequisites
- Python 3.11+
- Plex Media Server with API access
- Plex authentication token ([how to find your token](https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/))
### Installation
```bash
# Clone the repository
cd plex-mcp-custom
# Install dependencies
pip install -e .
# Configure environment
cp .env.example .env
# Edit .env with your Plex URL and token
```
### Configuration
Edit `.env` with your Plex settings:
```env
PLEX_URL=http://your-plex-server:32400
PLEX_TOKEN=your-plex-token
PLEX_CLIENT_ID=plex-mcp-server
PORT=8000
```
### Running
```bash
# Direct
python server.py
# Or with uvicorn
uvicorn server:app --host 0.0.0.0 --port 8000
```
### Docker
```bash
# Build and run
docker-compose up -d
# Or build manually
docker build -t plex-mcp .
docker run -d --env-file .env -p 8000:8000 plex-mcp
```
## Usage
### MCP Client Configuration
Add to your MCP client configuration:
```json
{
"mcpServers": {
"plex": {
"url": "http://localhost:8000/mcp/v1"
}
}
}
```
### Tool Examples
**List libraries:**
```
get_libraries()
```
**Search for a movie:**
```
search_library(query="Inception", limit=5)
```
**Get item details:**
```
get_metadata(rating_key="12345", include_children=true)
```
**Trigger library scan:**
```
refresh_library(section_id=1)
```
**Raw API call (mark as watched):**
```
plex_api_call(
endpoint="/:scrobble",
params='{"key": "12345", "identifier": "com.plexapp.plugins.library"}'
)
```
### Finding Endpoints
Use `search_api_docs` to find endpoints for specific operations:
```
search_api_docs(query="playlist")
search_api_docs(query="transcode")
search_api_docs(query="rating")
```
Or access the curated reference via the `plex://api-reference` resource.
## API Reference
See [docs/api_reference.md](docs/api_reference.md) for a curated guide to common endpoints.
The full OpenAPI specification is available in `openapi.json` and can be searched via the `search_api_docs` tool.
## Health Check
```bash
curl http://localhost:8000/health
```
Returns:
```json
{"status": "ok", "plex_connected": true}
```
## License
MIT

2048
api_auth.txt Normal file

File diff suppressed because it is too large Load Diff

20
docker-compose.yml Normal file
View File

@@ -0,0 +1,20 @@
services:
plex-mcp:
build: .
container_name: plex-mcp
restart: unless-stopped
ports:
- "${PORT:-8000}:8000"
environment:
- PLEX_URL=${PLEX_URL}
- PLEX_TOKEN=${PLEX_TOKEN}
- PLEX_CLIENT_ID=${PLEX_CLIENT_ID:-plex-mcp-server}
- PORT=8000
env_file:
- .env
healthcheck:
test: ["CMD", "python", "-c", "import httpx; httpx.get('http://localhost:8000/health').raise_for_status()"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s

366
docs/api_reference.md Normal file
View File

@@ -0,0 +1,366 @@
# Plex API Quick Reference
This document provides a curated reference for the most commonly used Plex Media Server API endpoints. Use this with the `plex_api_call` tool for operations not covered by specific tools.
## Authentication
All requests require these headers (handled automatically by this MCP server):
- `X-Plex-Token`: Authentication token
- `X-Plex-Client-Identifier`: Unique client ID
- `Accept: application/json`: Request JSON responses
## Pagination
Many endpoints support pagination via headers:
- `X-Plex-Container-Start`: Starting offset (0-based)
- `X-Plex-Container-Size`: Number of items to return
---
## Libraries
### List All Libraries
```
GET /library/sections/all
```
Returns all library sections (Movies, TV Shows, Music, etc.)
### Get Library Details
```
GET /library/sections/{sectionId}
```
Get details about a specific library section.
### List Library Contents
```
GET /library/sections/{sectionId}/all
```
List all items in a library. Supports filtering and sorting.
Query params:
- `type`: Filter by type (1=movie, 2=show, 4=episode, 8=artist, 9=album, 10=track)
- `sort`: Sort field (e.g., `titleSort`, `addedAt:desc`, `year`)
- `X-Plex-Container-Start`: Pagination start
- `X-Plex-Container-Size`: Page size
### Refresh Library (Scan)
```
POST /library/sections/{sectionId}/refresh
```
Trigger a library scan for new content.
Query params:
- `force=1`: Force metadata refresh even if files unchanged
- `path`: Restrict scan to specific path
### Cancel Library Refresh
```
DELETE /library/sections/{sectionId}/refresh
```
---
## Metadata
### Get Item Metadata
```
GET /library/metadata/{ratingKey}
```
Get detailed metadata for a specific item.
### Get Children
```
GET /library/metadata/{ratingKey}/children
```
Get children of an item (e.g., seasons of a show, episodes of a season).
### Get All Leaves
```
GET /library/metadata/{ratingKey}/allLeaves
```
Get all leaf items (e.g., all episodes of a show).
### Refresh Item Metadata
```
POST /library/metadata/{ratingKey}/refresh
```
Refresh metadata for a specific item.
---
## Search
### Global Search
```
GET /hubs/search?query={searchTerm}
```
Search across all libraries.
Query params:
- `query`: Search term (required)
- `limit`: Max results per type
- `sectionId`: Restrict to specific library
### Voice Search
```
GET /hubs/search/voice?query={searchTerm}
```
---
## Hubs (Discovery)
### Get All Hubs
```
GET /hubs
```
Get discovery hubs (Continue Watching, Recently Added, etc.)
### Continue Watching
```
GET /hubs/continueWatching
```
### Library Hubs
```
GET /hubs/sections/{sectionId}
```
Get hubs for a specific library.
---
## Playback Tracking
### Mark as Played (Scrobble)
```
GET /:scrobble?key={ratingKey}&identifier=com.plexapp.plugins.library
```
### Mark as Unplayed
```
GET /:unscrobble?key={ratingKey}&identifier=com.plexapp.plugins.library
```
### Update Timeline
```
GET /:timeline
```
Report playback progress.
Query params:
- `ratingKey`: Item key
- `key`: Full key path
- `state`: playing, paused, stopped
- `time`: Current position in ms
- `duration`: Total duration in ms
---
## Ratings
### Rate an Item
```
PUT /:rate?key={ratingKey}&identifier=com.plexapp.plugins.library&rating={value}
```
Rating value: 0-10 (0 removes rating)
---
## Playlists
### List Playlists
```
GET /playlists
```
Query params:
- `playlistType`: audio, video, photo
- `smart`: 0 or 1
### Get Playlist
```
GET /playlists/{playlistId}
```
### Get Playlist Items
```
GET /playlists/{playlistId}/items
```
### Create Playlist
```
POST /playlists
```
Query params:
- `title`: Playlist name
- `type`: audio, video, photo
- `smart`: 0 or 1
- `uri`: Items to add (server://...)
### Add to Playlist
```
PUT /playlists/{playlistId}/items?uri={itemUri}
```
### Delete Playlist
```
DELETE /playlists/{playlistId}
```
---
## Collections
### List Collections
```
GET /library/sections/{sectionId}/collections
```
### Get Collection Items
```
GET /library/collections/{collectionId}/items
```
### Add to Collection
```
PUT /library/collections/{collectionId}/items?uri={itemUri}
```
### Remove from Collection
```
DELETE /library/collections/{collectionId}/items/{itemId}
```
---
## Play Queues
### Create Play Queue
```
POST /playQueues
```
Query params:
- `uri`: Source URI
- `type`: audio, video, photo
- `shuffle`: 0 or 1
- `repeat`: 0, 1, 2
- `continuous`: 0 or 1
### Get Play Queue
```
GET /playQueues/{playQueueId}
```
---
## Server Status
### Server Identity
```
GET /identity
```
Basic server info (machine identifier, version).
### Server Capabilities
```
GET /
```
Full server capabilities and features.
### Server Preferences
```
GET /:prefs
```
---
## Activities
### List Activities
```
GET /activities
```
Get running activities (transcoding, scanning, etc.)
### Cancel Activity
```
DELETE /activities/{activityId}
```
---
## Butler (Scheduled Tasks)
### Get Butler Status
```
GET /butler
```
List scheduled maintenance tasks.
### Run Butler Task
```
POST /butler/{taskName}
```
Task names: BackupDatabase, BuildGracenoteCollections, CheckForUpdates, CleanOldBundles, CleanOldCacheFiles, DeepMediaAnalysis, GenerateAutoTags, GenerateChapterThumbs, GenerateMediaIndexFiles, OptimizeDatabase, RefreshLibraries, RefreshLocalMedia, RefreshPeriodicMetadata, UpgradeMediaAnalysis
### Stop Butler Task
```
DELETE /butler/{taskName}
```
---
## Transcoding
### Get Transcode Sessions
```
GET /transcode/sessions
```
### Stop Transcode Session
```
DELETE /transcode/sessions/{sessionKey}
```
---
## Metadata Types Reference
| Type | Number | Description |
|------|--------|-------------|
| movie | 1 | Movies |
| show | 2 | TV Shows |
| season | 3 | Seasons |
| episode | 4 | Episodes |
| trailer | 5 | Trailers |
| artist | 8 | Music Artists |
| album | 9 | Music Albums |
| track | 10 | Music Tracks |
| photo | 13 | Photos |
| photoalbum | 14 | Photo Albums |
| playlist | 15 | Playlists |
| collection | 18 | Collections |
---
## Common Query Parameters
Many list endpoints accept these query parameters:
- `type`: Filter by metadata type number
- `sort`: Sort field with optional `:asc` or `:desc`
- `X-Plex-Container-Start`: Pagination offset
- `X-Plex-Container-Size`: Items per page
- `includeChildren`: Include child items (1/0)
- `includeElements`: Comma-separated list of elements to include
- `excludeElements`: Comma-separated list of elements to exclude
---
## URI Format
When referencing items for playlists/queues:
```
server://{machineIdentifier}/com.plexapp.plugins.library/library/metadata/{ratingKey}
```
Get machine identifier from `GET /identity`.

30969
openapi.json Normal file

File diff suppressed because one or more lines are too long

26
pyproject.toml Normal file
View File

@@ -0,0 +1,26 @@
[project]
name = "plex-mcp"
version = "0.1.0"
description = "MCP server for Plex Media Server API"
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"fastmcp>=2.0.0",
"httpx>=0.27.0",
"python-dotenv>=1.0.0",
"uvicorn>=0.30.0",
"starlette>=0.38.0",
]
[project.optional-dependencies]
dev = [
"pytest>=8.0.0",
"pytest-asyncio>=0.23.0",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["."]

402
server.py Normal file
View File

@@ -0,0 +1,402 @@
"""
Plex MCP Server - A lightweight MCP server for Plex Media Server API.
Follows the Hybrid MCP Light pattern:
- 5 specific tools for common operations
- 1 API pass-through for full API coverage
- Documentation resources for AI agent reference
"""
import json
import os
from pathlib import Path
from typing import Optional
import httpx
from dotenv import load_dotenv
from fastmcp import FastMCP
from starlette.applications import Starlette
from starlette.responses import JSONResponse
from starlette.routing import Mount, Route
load_dotenv()
# Configuration
PLEX_URL = os.getenv("PLEX_URL", "http://localhost:32400")
PLEX_TOKEN = os.getenv("PLEX_TOKEN", "")
PLEX_CLIENT_ID = os.getenv("PLEX_CLIENT_ID", "plex-mcp-server")
PORT = int(os.getenv("PORT", "8000"))
# Paths
SCRIPT_DIR = Path(__file__).parent
OPENAPI_PATH = SCRIPT_DIR / "openapi.json"
API_REFERENCE_PATH = SCRIPT_DIR / "docs" / "api_reference.md"
class PlexClient:
"""HTTP client for Plex Media Server API."""
def __init__(self, base_url: str, token: str, client_id: str):
self.base_url = base_url.rstrip("/")
self.token = token
self.client_id = client_id
self._client: Optional[httpx.AsyncClient] = None
@property
def headers(self) -> dict:
return {
"X-Plex-Token": self.token,
"X-Plex-Client-Identifier": self.client_id,
"Accept": "application/json",
}
async def get_client(self) -> httpx.AsyncClient:
if self._client is None or self._client.is_closed:
self._client = httpx.AsyncClient(
base_url=self.base_url,
headers=self.headers,
timeout=30.0,
)
return self._client
async def close(self):
if self._client and not self._client.is_closed:
await self._client.aclose()
async def request(
self,
method: str,
endpoint: str,
params: Optional[dict] = None,
body: Optional[dict] = None,
) -> dict:
"""Execute an API request to Plex."""
client = await self.get_client()
try:
response = await client.request(
method=method.upper(),
url=endpoint,
params=params,
json=body if body else None,
)
response.raise_for_status()
# Handle empty responses
if not response.content:
return {"status": "ok", "statusCode": response.status_code}
return response.json()
except httpx.HTTPStatusError as e:
return {
"error": True,
"statusCode": e.response.status_code,
"message": str(e),
}
except Exception as e:
return {"error": True, "message": str(e)}
async def health_check(self) -> bool:
"""Check if connection to Plex is working."""
try:
result = await self.request("GET", "/identity")
return "error" not in result
except Exception:
return False
# Initialize client and MCP server
plex_client = PlexClient(PLEX_URL, PLEX_TOKEN, PLEX_CLIENT_ID)
mcp = FastMCP(
"Plex MCP Server",
description="MCP server for interacting with Plex Media Server",
)
# =============================================================================
# Specific Tools (5 high-value operations)
# =============================================================================
@mcp.tool()
async def get_libraries() -> str:
"""List all Plex library sections (Movies, TV Shows, Music, etc.).
Returns a list of all libraries with their IDs, names, types, and item counts.
Use the section ID from this response with other tools like search_library or refresh_library.
"""
result = await plex_client.request("GET", "/library/sections/all")
return json.dumps(result, indent=2)
@mcp.tool()
async def search_library(
query: str, limit: int = 10, section_id: Optional[int] = None
) -> str:
"""Search for media across all libraries or within a specific library.
Args:
query: Search term (title, artist, etc.)
limit: Maximum results per media type (default: 10)
section_id: Optional library section ID to restrict search
Returns search results grouped by media type (movies, shows, episodes, etc.)
"""
params = {"query": query, "limit": limit}
if section_id is not None:
params["sectionId"] = section_id
result = await plex_client.request("GET", "/hubs/search", params=params)
return json.dumps(result, indent=2)
@mcp.tool()
async def get_metadata(rating_key: str, include_children: bool = False) -> str:
"""Get detailed metadata for a specific media item.
Args:
rating_key: The unique identifier (ratingKey) of the media item
include_children: If True, include child items (seasons for shows, episodes for seasons)
Returns detailed metadata including title, summary, ratings, cast, and more.
"""
endpoint = f"/library/metadata/{rating_key}"
params = {}
if include_children:
params["includeChildren"] = 1
result = await plex_client.request("GET", endpoint, params=params)
return json.dumps(result, indent=2)
@mcp.tool()
async def get_recently_added(section_id: Optional[int] = None, limit: int = 20) -> str:
"""Get recently added media items.
Args:
section_id: Optional library section ID to filter results
limit: Maximum number of items to return (default: 20)
Returns recently added items across all libraries or for a specific library.
"""
if section_id is not None:
# Get recently added for specific library via hubs
endpoint = f"/hubs/sections/{section_id}"
else:
# Get global recently added
endpoint = "/hubs"
params = {"X-Plex-Container-Size": limit}
result = await plex_client.request("GET", endpoint, params=params)
return json.dumps(result, indent=2)
@mcp.tool()
async def refresh_library(
section_id: int,
force: bool = False,
path: Optional[str] = None,
) -> str:
"""Trigger a library scan to detect new or changed media files.
Args:
section_id: The library section ID to refresh (get from get_libraries)
force: If True, force metadata refresh even if files appear unchanged
path: Optional path to restrict the scan to a specific directory
Returns confirmation of the refresh request.
"""
endpoint = f"/library/sections/{section_id}/refresh"
params = {}
if force:
params["force"] = 1
if path:
params["path"] = path
result = await plex_client.request("POST", endpoint, params=params)
return json.dumps(result, indent=2)
# =============================================================================
# API Pass-through Tool
# =============================================================================
@mcp.tool()
async def plex_api_call(
endpoint: str,
method: str = "GET",
params: str = "{}",
body: str = "{}",
) -> str:
"""Execute any Plex API call directly.
This is the escape hatch for accessing any Plex API endpoint not covered
by the specific tools. Refer to the 'plex://api-reference' resource or
use search_api_docs() to find available endpoints.
Args:
endpoint: API path (e.g., '/playlists', '/:scrobble', '/library/metadata/123')
method: HTTP method (GET, POST, PUT, DELETE)
params: JSON string of query parameters (e.g., '{"query": "test", "limit": 10}')
body: JSON string of request body for POST/PUT requests
Returns the API response as JSON.
Examples:
- Mark item watched: plex_api_call('/:scrobble', params='{"key": "12345", "identifier": "com.plexapp.plugins.library"}')
- Get playlists: plex_api_call('/playlists')
- Rate item: plex_api_call('/:rate', 'PUT', params='{"key": "12345", "identifier": "com.plexapp.plugins.library", "rating": 8}')
"""
try:
parsed_params = json.loads(params) if params and params != "{}" else None
parsed_body = json.loads(body) if body and body != "{}" else None
except json.JSONDecodeError as e:
return json.dumps({"error": True, "message": f"Invalid JSON: {e}"})
result = await plex_client.request(
method, endpoint, params=parsed_params, body=parsed_body
)
return json.dumps(result, indent=2)
# =============================================================================
# Documentation Tools & Resources
# =============================================================================
@mcp.tool()
async def search_api_docs(query: str, limit: int = 20) -> str:
"""Search the Plex OpenAPI specification for endpoints matching a query.
Args:
query: Search term to find in endpoint paths, summaries, or descriptions
limit: Maximum number of results to return (default: 20)
Returns matching endpoints with their methods, summaries, and parameters.
"""
if not OPENAPI_PATH.exists():
return json.dumps({"error": True, "message": "OpenAPI spec not found"})
try:
with open(OPENAPI_PATH) as f:
spec = json.load(f)
except Exception as e:
return json.dumps(
{"error": True, "message": f"Failed to load OpenAPI spec: {e}"}
)
query_lower = query.lower()
results = []
for path, methods in spec.get("paths", {}).items():
for method, details in methods.items():
if method.startswith("x-"): # Skip OpenAPI extensions
continue
# Search in path, summary, description, and tags
searchable = " ".join(
[
path,
details.get("summary", ""),
details.get("description", ""),
" ".join(details.get("tags", [])),
]
).lower()
if query_lower in searchable:
# Extract parameter info
params = []
for param in details.get("parameters", []):
params.append(
{
"name": param.get("name"),
"in": param.get("in"),
"required": param.get("required", False),
"description": param.get("description", ""),
}
)
results.append(
{
"path": path,
"method": method.upper(),
"summary": details.get("summary", ""),
"description": details.get("description", ""),
"tags": details.get("tags", []),
"parameters": params,
}
)
if len(results) >= limit:
break
if len(results) >= limit:
break
return json.dumps(
{
"query": query,
"count": len(results),
"results": results,
},
indent=2,
)
@mcp.resource("plex://api-reference")
async def get_api_reference() -> str:
"""Get the curated Plex API quick reference documentation.
This resource provides a human-readable guide to the most commonly used
Plex API endpoints, organized by category. Use this to understand how
to use the plex_api_call tool for operations not covered by specific tools.
"""
if not API_REFERENCE_PATH.exists():
return "API reference documentation not found."
return API_REFERENCE_PATH.read_text()
# =============================================================================
# Health Check & Application Setup
# =============================================================================
async def health_check(request):
"""Health check endpoint for Docker/orchestration."""
is_healthy = await plex_client.health_check()
status = "ok" if is_healthy else "degraded"
return JSONResponse(
{"status": status, "plex_connected": is_healthy},
status_code=200 if is_healthy else 503,
)
def create_app() -> Starlette:
"""Create the Starlette application wrapping the MCP server."""
mcp_app = mcp.http_app()
async def lifespan(app):
# Startup
yield
# Shutdown
await plex_client.close()
return Starlette(
routes=[
Route("/health", health_check),
Mount("/", app=mcp_app),
],
lifespan=lifespan,
)
app = create_app()
if __name__ == "__main__":
import uvicorn
print(f"Starting Plex MCP Server on port {PORT}")
print(f"Plex URL: {PLEX_URL}")
print(f"Health check: http://localhost:{PORT}/health")
uvicorn.run(app, host="0.0.0.0", port=PORT)