frigate-auth-mcp 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- frigate_auth_mcp-0.1.0/.github/workflows/publish.yml +22 -0
- frigate_auth_mcp-0.1.0/.github/workflows/tests.yml +23 -0
- frigate_auth_mcp-0.1.0/.gitignore +8 -0
- frigate_auth_mcp-0.1.0/LICENSE +21 -0
- frigate_auth_mcp-0.1.0/PKG-INFO +125 -0
- frigate_auth_mcp-0.1.0/README.md +112 -0
- frigate_auth_mcp-0.1.0/pyproject.toml +23 -0
- frigate_auth_mcp-0.1.0/src/frigate_auth_mcp/__init__.py +0 -0
- frigate_auth_mcp-0.1.0/src/frigate_auth_mcp/server.py +180 -0
- frigate_auth_mcp-0.1.0/tests/test_frigate_auth_mcp.py +188 -0
- frigate_auth_mcp-0.1.0/uv.lock +786 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
name: publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
pypi:
|
|
10
|
+
name: Publish to PyPI
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
environment: pypi
|
|
13
|
+
permissions:
|
|
14
|
+
contents: read
|
|
15
|
+
id-token: write # PyPI Trusted Publishing, no API token
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v7
|
|
18
|
+
- uses: astral-sh/setup-uv@v10.3.3
|
|
19
|
+
- name: Build
|
|
20
|
+
run: uv build
|
|
21
|
+
- name: Publish
|
|
22
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
name: tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- master
|
|
7
|
+
- 'dev/**'
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
unittest:
|
|
11
|
+
name: Python ${{ matrix.python }}
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
strategy:
|
|
14
|
+
fail-fast: false
|
|
15
|
+
matrix:
|
|
16
|
+
python: ['3.11', '3.12', '3.13', '3.14']
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v7
|
|
19
|
+
- name: Run unit tests
|
|
20
|
+
run: >
|
|
21
|
+
docker run --rm -v "$PWD:/src" -w /src -e PYTHONDONTWRITEBYTECODE=1
|
|
22
|
+
python:${{ matrix.python }}-slim
|
|
23
|
+
sh -c 'pip install -q --root-user-action=ignore . && python3 -m unittest discover -s tests -v'
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Adel-S (https://github.com/adel-s/frigate-auth-mcp)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: frigate-auth-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server with authentication for Frigate NVR (authenticated port, viewer role)
|
|
5
|
+
Project-URL: Homepage, https://github.com/adel-s/frigate-auth-mcp
|
|
6
|
+
Project-URL: Issues, https://github.com/adel-s/frigate-auth-mcp/issues
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Python: >=3.11
|
|
10
|
+
Requires-Dist: httpx>=0.27
|
|
11
|
+
Requires-Dist: mcp<2,>=1.2
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
[](https://github.com/adel-s/frigate-auth-mcp/actions/workflows/tests.yml)
|
|
15
|
+
[](https://github.com/adel-s/frigate-auth-mcp/actions/workflows/publish.yml)
|
|
16
|
+
|
|
17
|
+
# frigate-auth-mcp
|
|
18
|
+
|
|
19
|
+
## What
|
|
20
|
+
|
|
21
|
+
An [MCP](https://modelcontextprotocol.io) server with authentication for [Frigate NVR](https://frigate.video): lets an AI
|
|
22
|
+
assistant (Claude Code, Claude Desktop, Hermes or any other MCP client) look at your cameras, events and reviews -
|
|
23
|
+
and nothing more.
|
|
24
|
+
|
|
25
|
+
It logs in to Frigate's authenticated port as a user with the `viewer` role, so Frigate itself enforces what the
|
|
26
|
+
assistant may see. Tested with Frigate 0.17.x.
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
Python 3.11+ and [uv](https://docs.astral.sh/uv/). Nothing to install by hand: the MCP client starts the server
|
|
31
|
+
with
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
uvx --from git+https://github.com/adel-s/frigate-auth-mcp frigate-auth-mcp
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Setup
|
|
38
|
+
|
|
39
|
+
1. In Frigate, create a user for the assistant (Settings -> Users) with the role `viewer`. If the role lets you
|
|
40
|
+
limit cameras, give it only the cameras the assistant should see.
|
|
41
|
+
2. Add the server to your MCP client. For Claude Code, in `.mcp.json`:
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"mcpServers": {
|
|
46
|
+
"frigate": {
|
|
47
|
+
"command": "uvx",
|
|
48
|
+
"args": ["--from", "git+https://github.com/adel-s/frigate-auth-mcp", "frigate-auth-mcp"],
|
|
49
|
+
"env": {
|
|
50
|
+
"FRIGATE_URL": "https://frigate.example.com:8971",
|
|
51
|
+
"FRIGATE_USER": "assistant",
|
|
52
|
+
"FRIGATE_PASSWD": "${FRIGATE_PASSWD}"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Keep the password out of the file: put it in the environment the client runs in.
|
|
60
|
+
|
|
61
|
+
## Tools
|
|
62
|
+
|
|
63
|
+
| Tool | What it returns | Frigate API |
|
|
64
|
+
|------------------|--------------------------------------------------------------------------|----------------------------------------------|
|
|
65
|
+
| `frigate_status` | version, uptime, the cameras this user may see, per-camera fps | `/api/stats`, `/api/profile`, `/api/version` |
|
|
66
|
+
| `list_events` | tracked-object events, filters by camera, label, time, GenAI description | `/api/events` |
|
|
67
|
+
| `get_event` | one event in detail, with its GenAI description | `/api/events/<id>` |
|
|
68
|
+
| `event_snapshot` | snapshot of an event with the bounding box, optionally cropped | `/api/events/<id>/snapshot.jpg` |
|
|
69
|
+
| `latest_frame` | the current frame of a camera | `/api/<camera>/latest.jpg` |
|
|
70
|
+
| `list_reviews` | review items as in the Frigate UI, last 7 days by default | `/api/review` |
|
|
71
|
+
| `review_summary` | alert and detection counts for the last 24 hours and per day | `/api/review/summary` |
|
|
72
|
+
|
|
73
|
+
## Environment
|
|
74
|
+
|
|
75
|
+
| Variable | Meaning |
|
|
76
|
+
|-------------------------|---------------------------------------------------------------------|
|
|
77
|
+
| `FRIGATE_IMAGE_CAMERAS` | cameras allowed to return images, comma-separated; default all |
|
|
78
|
+
| `FRIGATE_PASSWD` | password of the Frigate user, required (secret) |
|
|
79
|
+
| `FRIGATE_TIMEOUT` | seconds per request, default `30` |
|
|
80
|
+
| `FRIGATE_URL` | base URL of the authenticated port, default `http://localhost:8971` |
|
|
81
|
+
| `FRIGATE_USER` | Frigate username, required |
|
|
82
|
+
|
|
83
|
+
## Safety
|
|
84
|
+
|
|
85
|
+
- Talks to the authenticated port (`8971` by default) only, never to the unauthenticated internal port `5000`,
|
|
86
|
+
which gives full API access to anyone who can reach it.
|
|
87
|
+
- Logs in as a Frigate user with the role `viewer`; the only non-GET call is `POST /api/login`.
|
|
88
|
+
- No tool deletes, exports, edits the config or restarts anything.
|
|
89
|
+
- The raw Frigate config is not exposed: it carries camera stream URLs with credentials.
|
|
90
|
+
- Optional image filter `FRIGATE_IMAGE_CAMERAS`: only these cameras return images (snapshots, frames). This is the
|
|
91
|
+
second layer of protection, after the camera limit of the Frigate user's role; by default all cameras are
|
|
92
|
+
allowed. Use it when the MCP client sends images to a third-party model and some frames, such as indoor
|
|
93
|
+
cameras, must not leave your home.
|
|
94
|
+
|
|
95
|
+
## Why
|
|
96
|
+
|
|
97
|
+
The Frigate MCP server I found talks to port `5000` without authentication and exposes tools that delete events,
|
|
98
|
+
start exports, save the config or restart Frigate. Handing that to an AI model is more trust than a camera system
|
|
99
|
+
deserves. This server gives the assistant eyes, not hands.
|
|
100
|
+
|
|
101
|
+
## Notes
|
|
102
|
+
|
|
103
|
+
- Without `after`, Frigate's `/api/review` returns only the last 24 hours; `list_reviews` asks for the last 7 days
|
|
104
|
+
instead, so "what happened this week" gets a full answer.
|
|
105
|
+
- `list_events` with `has_description` filters on the GenAI description, which the Frigate API cannot do itself.
|
|
106
|
+
|
|
107
|
+
## Tests
|
|
108
|
+
|
|
109
|
+
```sh
|
|
110
|
+
uv run python -m unittest discover -s tests
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The tests use `unittest` and a fake Frigate API on `httpx.MockTransport`, so they need no running Frigate. They
|
|
114
|
+
also check that no tool sends anything but `GET`, apart from the login.
|
|
115
|
+
|
|
116
|
+
GitHub Actions runs them on Python 3.11 to 3.14 on each push to `master` and `dev/*` branches.
|
|
117
|
+
|
|
118
|
+
## Development
|
|
119
|
+
|
|
120
|
+
I don't accept pull requests, sorry: my code, my rules. But if something doesn't work or you need a new feature,
|
|
121
|
+
feel free to open an issue and I'll fix it ASAP.
|
|
122
|
+
|
|
123
|
+
## License
|
|
124
|
+
|
|
125
|
+
MIT, see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
[](https://github.com/adel-s/frigate-auth-mcp/actions/workflows/tests.yml)
|
|
2
|
+
[](https://github.com/adel-s/frigate-auth-mcp/actions/workflows/publish.yml)
|
|
3
|
+
|
|
4
|
+
# frigate-auth-mcp
|
|
5
|
+
|
|
6
|
+
## What
|
|
7
|
+
|
|
8
|
+
An [MCP](https://modelcontextprotocol.io) server with authentication for [Frigate NVR](https://frigate.video): lets an AI
|
|
9
|
+
assistant (Claude Code, Claude Desktop, Hermes or any other MCP client) look at your cameras, events and reviews -
|
|
10
|
+
and nothing more.
|
|
11
|
+
|
|
12
|
+
It logs in to Frigate's authenticated port as a user with the `viewer` role, so Frigate itself enforces what the
|
|
13
|
+
assistant may see. Tested with Frigate 0.17.x.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
Python 3.11+ and [uv](https://docs.astral.sh/uv/). Nothing to install by hand: the MCP client starts the server
|
|
18
|
+
with
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
uvx --from git+https://github.com/adel-s/frigate-auth-mcp frigate-auth-mcp
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Setup
|
|
25
|
+
|
|
26
|
+
1. In Frigate, create a user for the assistant (Settings -> Users) with the role `viewer`. If the role lets you
|
|
27
|
+
limit cameras, give it only the cameras the assistant should see.
|
|
28
|
+
2. Add the server to your MCP client. For Claude Code, in `.mcp.json`:
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"mcpServers": {
|
|
33
|
+
"frigate": {
|
|
34
|
+
"command": "uvx",
|
|
35
|
+
"args": ["--from", "git+https://github.com/adel-s/frigate-auth-mcp", "frigate-auth-mcp"],
|
|
36
|
+
"env": {
|
|
37
|
+
"FRIGATE_URL": "https://frigate.example.com:8971",
|
|
38
|
+
"FRIGATE_USER": "assistant",
|
|
39
|
+
"FRIGATE_PASSWD": "${FRIGATE_PASSWD}"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Keep the password out of the file: put it in the environment the client runs in.
|
|
47
|
+
|
|
48
|
+
## Tools
|
|
49
|
+
|
|
50
|
+
| Tool | What it returns | Frigate API |
|
|
51
|
+
|------------------|--------------------------------------------------------------------------|----------------------------------------------|
|
|
52
|
+
| `frigate_status` | version, uptime, the cameras this user may see, per-camera fps | `/api/stats`, `/api/profile`, `/api/version` |
|
|
53
|
+
| `list_events` | tracked-object events, filters by camera, label, time, GenAI description | `/api/events` |
|
|
54
|
+
| `get_event` | one event in detail, with its GenAI description | `/api/events/<id>` |
|
|
55
|
+
| `event_snapshot` | snapshot of an event with the bounding box, optionally cropped | `/api/events/<id>/snapshot.jpg` |
|
|
56
|
+
| `latest_frame` | the current frame of a camera | `/api/<camera>/latest.jpg` |
|
|
57
|
+
| `list_reviews` | review items as in the Frigate UI, last 7 days by default | `/api/review` |
|
|
58
|
+
| `review_summary` | alert and detection counts for the last 24 hours and per day | `/api/review/summary` |
|
|
59
|
+
|
|
60
|
+
## Environment
|
|
61
|
+
|
|
62
|
+
| Variable | Meaning |
|
|
63
|
+
|-------------------------|---------------------------------------------------------------------|
|
|
64
|
+
| `FRIGATE_IMAGE_CAMERAS` | cameras allowed to return images, comma-separated; default all |
|
|
65
|
+
| `FRIGATE_PASSWD` | password of the Frigate user, required (secret) |
|
|
66
|
+
| `FRIGATE_TIMEOUT` | seconds per request, default `30` |
|
|
67
|
+
| `FRIGATE_URL` | base URL of the authenticated port, default `http://localhost:8971` |
|
|
68
|
+
| `FRIGATE_USER` | Frigate username, required |
|
|
69
|
+
|
|
70
|
+
## Safety
|
|
71
|
+
|
|
72
|
+
- Talks to the authenticated port (`8971` by default) only, never to the unauthenticated internal port `5000`,
|
|
73
|
+
which gives full API access to anyone who can reach it.
|
|
74
|
+
- Logs in as a Frigate user with the role `viewer`; the only non-GET call is `POST /api/login`.
|
|
75
|
+
- No tool deletes, exports, edits the config or restarts anything.
|
|
76
|
+
- The raw Frigate config is not exposed: it carries camera stream URLs with credentials.
|
|
77
|
+
- Optional image filter `FRIGATE_IMAGE_CAMERAS`: only these cameras return images (snapshots, frames). This is the
|
|
78
|
+
second layer of protection, after the camera limit of the Frigate user's role; by default all cameras are
|
|
79
|
+
allowed. Use it when the MCP client sends images to a third-party model and some frames, such as indoor
|
|
80
|
+
cameras, must not leave your home.
|
|
81
|
+
|
|
82
|
+
## Why
|
|
83
|
+
|
|
84
|
+
The Frigate MCP server I found talks to port `5000` without authentication and exposes tools that delete events,
|
|
85
|
+
start exports, save the config or restart Frigate. Handing that to an AI model is more trust than a camera system
|
|
86
|
+
deserves. This server gives the assistant eyes, not hands.
|
|
87
|
+
|
|
88
|
+
## Notes
|
|
89
|
+
|
|
90
|
+
- Without `after`, Frigate's `/api/review` returns only the last 24 hours; `list_reviews` asks for the last 7 days
|
|
91
|
+
instead, so "what happened this week" gets a full answer.
|
|
92
|
+
- `list_events` with `has_description` filters on the GenAI description, which the Frigate API cannot do itself.
|
|
93
|
+
|
|
94
|
+
## Tests
|
|
95
|
+
|
|
96
|
+
```sh
|
|
97
|
+
uv run python -m unittest discover -s tests
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
The tests use `unittest` and a fake Frigate API on `httpx.MockTransport`, so they need no running Frigate. They
|
|
101
|
+
also check that no tool sends anything but `GET`, apart from the login.
|
|
102
|
+
|
|
103
|
+
GitHub Actions runs them on Python 3.11 to 3.14 on each push to `master` and `dev/*` branches.
|
|
104
|
+
|
|
105
|
+
## Development
|
|
106
|
+
|
|
107
|
+
I don't accept pull requests, sorry: my code, my rules. But if something doesn't work or you need a new feature,
|
|
108
|
+
feel free to open an issue and I'll fix it ASAP.
|
|
109
|
+
|
|
110
|
+
## License
|
|
111
|
+
|
|
112
|
+
MIT, see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "frigate-auth-mcp"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "MCP server with authentication for Frigate NVR (authenticated port, viewer role)"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
requires-python = ">=3.11"
|
|
9
|
+
dependencies = ["mcp>=1.2,<2", "httpx>=0.27"]
|
|
10
|
+
|
|
11
|
+
[project.urls]
|
|
12
|
+
Homepage = "https://github.com/adel-s/frigate-auth-mcp"
|
|
13
|
+
Issues = "https://github.com/adel-s/frigate-auth-mcp/issues"
|
|
14
|
+
|
|
15
|
+
[project.scripts]
|
|
16
|
+
frigate-auth-mcp = "frigate_auth_mcp.server:main"
|
|
17
|
+
|
|
18
|
+
[build-system]
|
|
19
|
+
requires = ["hatchling"]
|
|
20
|
+
build-backend = "hatchling.build"
|
|
21
|
+
|
|
22
|
+
[tool.hatch.build.targets.wheel]
|
|
23
|
+
packages = ["src/frigate_auth_mcp"]
|
|
File without changes
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""MCP server with authentication for Frigate NVR (0.17.x).
|
|
2
|
+
|
|
3
|
+
Talks to the AUTHENTICATED port (8971) only, logs in as a Frigate user (role `viewer`) and exposes
|
|
4
|
+
GET calls only: there is no tool that deletes, exports, edits the config or restarts anything.
|
|
5
|
+
The raw Frigate config is not exposed either - it carries camera stream URLs with credentials.
|
|
6
|
+
|
|
7
|
+
Environment:
|
|
8
|
+
FRIGATE_URL base URL of the authenticated port (default http://localhost:8971)
|
|
9
|
+
FRIGATE_USER Frigate username (required)
|
|
10
|
+
FRIGATE_PASSWD its password (required, secret, never logged)
|
|
11
|
+
FRIGATE_TIMEOUT seconds per request (default 30)
|
|
12
|
+
FRIGATE_IMAGE_CAMERAS comma-separated cameras whose IMAGES may be returned (snapshots, frames);
|
|
13
|
+
unset or empty = all. Text data (events, reviews) is not affected. Use it when the
|
|
14
|
+
MCP client sends images to a third-party model and indoor frames must not leave home.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
import os
|
|
19
|
+
from datetime import datetime, timedelta, timezone
|
|
20
|
+
|
|
21
|
+
import httpx
|
|
22
|
+
from mcp.server.fastmcp import FastMCP, Image
|
|
23
|
+
|
|
24
|
+
URL = os.environ.get("FRIGATE_URL", "http://localhost:8971").rstrip("/")
|
|
25
|
+
TIMEOUT = float(os.environ.get("FRIGATE_TIMEOUT", "30"))
|
|
26
|
+
IMAGE_CAMERAS = {c.strip() for c in os.environ.get("FRIGATE_IMAGE_CAMERAS", "").split(",") if c.strip()}
|
|
27
|
+
|
|
28
|
+
mcp = FastMCP("frigate")
|
|
29
|
+
logging.getLogger("httpx").setLevel(logging.WARNING) # one INFO line per request otherwise
|
|
30
|
+
logging.getLogger("mcp.server.lowlevel.server").setLevel(logging.WARNING) # same for every MCP request
|
|
31
|
+
_client = httpx.Client(base_url=URL, timeout=TIMEOUT)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _login():
|
|
35
|
+
# The only non-GET call: POST /api/login sets the `frigate_token` cookie (JWT) on the client.
|
|
36
|
+
user, password = os.environ.get("FRIGATE_USER"), os.environ.get("FRIGATE_PASSWD")
|
|
37
|
+
if not user or not password:
|
|
38
|
+
raise RuntimeError("FRIGATE_USER and FRIGATE_PASSWD must be set")
|
|
39
|
+
r = _client.post("/api/login", json={"user": user, "password": password})
|
|
40
|
+
r.raise_for_status()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _get(path, params=None):
|
|
44
|
+
params = {k: v for k, v in (params or {}).items() if v is not None}
|
|
45
|
+
r = _client.get(path, params=params)
|
|
46
|
+
if r.status_code == 401:
|
|
47
|
+
_login()
|
|
48
|
+
r = _client.get(path, params=params)
|
|
49
|
+
r.raise_for_status()
|
|
50
|
+
return r
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _image_allowed(camera):
|
|
54
|
+
if IMAGE_CAMERAS and camera not in IMAGE_CAMERAS:
|
|
55
|
+
raise ValueError(f"Images from camera '{camera}' are disabled for this client (FRIGATE_IMAGE_CAMERAS)")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _ts(value):
|
|
59
|
+
# ISO 8601 (local or with offset) or unix seconds -> unix seconds for the Frigate API
|
|
60
|
+
if value is None:
|
|
61
|
+
return None
|
|
62
|
+
try:
|
|
63
|
+
return float(value)
|
|
64
|
+
except ValueError:
|
|
65
|
+
dt = datetime.fromisoformat(value)
|
|
66
|
+
return (dt if dt.tzinfo else dt.astimezone()).timestamp()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _iso(ts):
|
|
70
|
+
return datetime.fromtimestamp(ts, timezone.utc).astimezone().isoformat(timespec="seconds") if ts else None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _event(e):
|
|
74
|
+
data = e.get("data") or {}
|
|
75
|
+
return {
|
|
76
|
+
"id": e["id"],
|
|
77
|
+
"camera": e["camera"],
|
|
78
|
+
"label": e["label"],
|
|
79
|
+
"sub_label": e.get("sub_label"),
|
|
80
|
+
"start": _iso(e.get("start_time")),
|
|
81
|
+
"end": _iso(e.get("end_time")),
|
|
82
|
+
"score": data.get("top_score") or e.get("top_score"),
|
|
83
|
+
"zones": e.get("zones"),
|
|
84
|
+
"has_clip": e.get("has_clip"),
|
|
85
|
+
"has_snapshot": e.get("has_snapshot"),
|
|
86
|
+
"description": data.get("description"),
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@mcp.tool()
|
|
91
|
+
def frigate_status() -> dict:
|
|
92
|
+
"""Frigate version, uptime, the cameras this user may see and per-camera fps / detection fps."""
|
|
93
|
+
stats = _get("/api/stats").json()
|
|
94
|
+
profile = _get("/api/profile").json()
|
|
95
|
+
cams = {name: {k: c.get(k) for k in ("camera_fps", "detection_fps", "process_fps", "skipped_fps")}
|
|
96
|
+
for name, c in (stats.get("cameras") or {}).items()}
|
|
97
|
+
return {
|
|
98
|
+
"version": _get("/api/version").text,
|
|
99
|
+
"uptime_s": (stats.get("service") or {}).get("uptime"),
|
|
100
|
+
"user": profile.get("username"),
|
|
101
|
+
"role": profile.get("role"),
|
|
102
|
+
"allowed_cameras": profile.get("allowed_cameras"),
|
|
103
|
+
"cameras": cams,
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@mcp.tool()
|
|
108
|
+
def list_events(camera: str | None = None, label: str | None = None, after: str | None = None,
|
|
109
|
+
before: str | None = None, limit: int = 20, has_description: bool = False) -> list[dict]:
|
|
110
|
+
"""Tracked-object events, newest first. after/before: ISO 8601 or unix seconds.
|
|
111
|
+
label e.g. person, cat, squirrel. has_description=True keeps only events with a GenAI description."""
|
|
112
|
+
# the API cannot filter on a description, so over-fetch and trim to `limit` here
|
|
113
|
+
events = _get("/api/events", {
|
|
114
|
+
"cameras": camera, "labels": label, "after": _ts(after), "before": _ts(before),
|
|
115
|
+
"limit": limit * 10 if has_description else limit, "include_thumbnails": 0,
|
|
116
|
+
}).json()
|
|
117
|
+
out = [_event(e) for e in events]
|
|
118
|
+
return [e for e in out if e["description"]][:limit] if has_description else out
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@mcp.tool()
|
|
122
|
+
def get_event(event_id: str) -> dict:
|
|
123
|
+
"""One event in detail, including the GenAI description when there is one."""
|
|
124
|
+
return _event(_get(f"/api/events/{event_id}").json())
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@mcp.tool()
|
|
128
|
+
def event_snapshot(event_id: str, crop: bool = True) -> Image:
|
|
129
|
+
"""Snapshot of an event with its bounding box; crop=True zooms on the object."""
|
|
130
|
+
_image_allowed(_get(f"/api/events/{event_id}").json()["camera"])
|
|
131
|
+
r = _get(f"/api/events/{event_id}/snapshot.jpg", {"bbox": 1, "crop": int(crop), "height": 500})
|
|
132
|
+
return Image(data=r.content, format="jpeg")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@mcp.tool()
|
|
136
|
+
def latest_frame(camera: str, height: int = 720) -> Image:
|
|
137
|
+
"""The current frame of a camera."""
|
|
138
|
+
_image_allowed(camera)
|
|
139
|
+
r = _get(f"/api/{camera}/latest.jpg", {"height": height})
|
|
140
|
+
return Image(data=r.content, format="jpeg")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@mcp.tool()
|
|
144
|
+
def list_reviews(camera: str | None = None, severity: str | None = None, after: str | None = None,
|
|
145
|
+
before: str | None = None, limit: int = 20) -> list[dict]:
|
|
146
|
+
"""Review items (what the Frigate UI shows), newest first. severity: alert or detection.
|
|
147
|
+
after/before: ISO 8601 or unix seconds; after defaults to 7 days ago (Frigate itself returns only the
|
|
148
|
+
last 24 hours when it is not set)."""
|
|
149
|
+
since = _ts(after) if after is not None else (datetime.now(timezone.utc) - timedelta(days=7)).timestamp()
|
|
150
|
+
items = _get("/api/review", {
|
|
151
|
+
"cameras": camera, "severity": severity, "after": since, "before": _ts(before), "limit": limit,
|
|
152
|
+
}).json()
|
|
153
|
+
return [{
|
|
154
|
+
"id": i["id"],
|
|
155
|
+
"camera": i["camera"],
|
|
156
|
+
"severity": i.get("severity"),
|
|
157
|
+
"start": _iso(i.get("start_time")),
|
|
158
|
+
"end": _iso(i.get("end_time")),
|
|
159
|
+
"objects": (i.get("data") or {}).get("objects"),
|
|
160
|
+
"zones": (i.get("data") or {}).get("zones"),
|
|
161
|
+
"audio": (i.get("data") or {}).get("audio"),
|
|
162
|
+
"reviewed": i.get("has_been_reviewed"),
|
|
163
|
+
} for i in items]
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@mcp.tool()
|
|
167
|
+
def review_summary(days: int = 7) -> dict:
|
|
168
|
+
"""Counts of alerts and detections for the last 24 hours and per day for the last `days` days
|
|
169
|
+
(reviewed / not reviewed), as in the Frigate UI."""
|
|
170
|
+
summary = _get("/api/review/summary").json()
|
|
171
|
+
day_keys = sorted((k for k in summary if k != "last24Hours"), reverse=True)[:days]
|
|
172
|
+
return {k: summary[k] for k in ["last24Hours", *day_keys] if k in summary}
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def main():
|
|
176
|
+
mcp.run()
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
if __name__ == "__main__":
|
|
180
|
+
main()
|