v1.0
This commit is contained in:
1 parent
0246cbb6ba
commit
0ca4b4bf7a
89 files changed
+7042
-2480
No files matched your search
@@ -0,0 +1,419 @@
|
||||
# Homey, Emby, DuckDuckGo & Spotify MCP Servers — Code Audit
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **Scope** | `homey-mcp` (Python, ~1.6k lines), `emby-mcp` (Python, ~0.6k), `duckduckgo-mcp` (Python, ~0.2k), `spotify-mcp-server` (TypeScript, ~2.5k) |
|
||||
| **Method** | Every file read by hand. Each finding was reproduced: offline with mocked HTTP transports, or with **read-only** live requests against the configured Homey, Emby and Spotify accounts. Nothing was switched, played, deleted or changed during the audit. |
|
||||
| **Date** | 2026-10-03 |
|
||||
| **Companion audits** | [AUDIT.md](AUDIT.md) (Javis), [AUDIT_SLAVE.md](AUDIT_SLAVE.md) (satellite + slave MCP) |
|
||||
|
||||
---
|
||||
|
||||
## 1. How Javis uses these servers
|
||||
|
||||
All four run as **stdio** subprocesses of the Javis MCP host (`mcp_servers.json`).
|
||||
`homey-mcp` can additionally run as a LAN HTTP server (streamable-http / SSE + a REST
|
||||
API) for other clients such as Odysseus.
|
||||
|
||||
| Server | Approval in `mcp_servers.json` | Credential it holds |
|
||||
|---|---|---|
|
||||
| homey | `ask` (default) | Homey Pro personal access token |
|
||||
| emby | `allow` | Emby **admin** API key |
|
||||
| duckduckgo | `allow` | — |
|
||||
| spotify | `ask` | OAuth client secret + refresh token |
|
||||
|
||||
The Javis host treats a tool result as failed only when MCP reports `isError: true`, which
|
||||
it turns into an `ERROR:` prefix. Failures that are returned as ordinary text therefore count
|
||||
as **successes**: they are cached, recorded as quick commands and reported as done.
|
||||
|
||||
---
|
||||
|
||||
## 2. Findings summary
|
||||
|
||||
| ID | Severity | Server | Finding | Status |
|
||||
|---|---|---|---|---|
|
||||
| [EMB-01](#emb-01) | Critical | emby | All Emby tools fail with Javis' config: the scheme defaults to `https` while the server speaks HTTP | Fixed |
|
||||
| [EMB-02](#emb-02) | High | emby | `search_items` only looks at the first 60 of 68,932 library items | Fixed |
|
||||
| [EMB-03](#emb-03) | High | emby | `play_item`, `get_item`, `playback_url` call endpoints that don't exist; `list_devices` always returns `[]` | Fixed |
|
||||
| [EMB-04](#emb-04) | High | emby / security | IDs inserted unencoded into URL paths, with the admin key (`123/Delete`, `../System/Restart`) | Fixed |
|
||||
| [HOM-01](#hom-01) | High | homey / security | Path injection through IDs: `trigger_flow('../../system/reboot#')` reboots the Homey | Fixed |
|
||||
| [HOM-02](#hom-02) | High | homey LAN / security | `AUTH_MODE=bearer` leaves the SSE endpoints `/sse` and `/messages/` unauthenticated | Fixed |
|
||||
| [HOM-03](#hom-03) | Medium | homey LAN / security | DNS-rebinding protection disabled although the docs promise a Host check | Fixed |
|
||||
| [HOM-04](#hom-04) | Medium | homey | Realtime events never arrive (wrong Socket.IO protocol) | Fixed |
|
||||
| [HOM-05](#hom-05) | Medium | homey | Zone names resolve to the first substring hit (`Wohnzimmer` can pick `Wohnzimmer Ecke`) | Fixed |
|
||||
| [HOM-06](#hom-06) | Medium | homey | `find_devices` matches ANY query word, so "esszimmer licht" returns every light | Fixed |
|
||||
| [DDG-01](#ddg-01) | Medium | duckduckgo | Blocking searches run on the server's event loop | Fixed |
|
||||
| [DDG-02](#ddg-02) | Medium | duckduckgo | Search failures are returned as normal text, so Javis caches them as results | Fixed |
|
||||
| [SPO-01](#spo-01) | Medium | spotify | Failures are returned without `isError` in all 28 tools | Fixed |
|
||||
| [SPO-02](#spo-02) | Medium | spotify | `playMusic(uri="spotify:track:…")` is sent as a context URI and rejected by Spotify | Fixed |
|
||||
| [SPO-03](#spo-03) | Medium | spotify | Token refresh ignores rotated refresh tokens, has no expiry margin and can run in parallel | Fixed |
|
||||
| [EMB-05](#emb-05) | Medium | emby | Failures are returned as `{"ok": false}` data instead of tool errors | Fixed |
|
||||
| [HOM-07](#hom-07) | Low | homey | systemd unit cannot work (`homey-mcp@user` enabled, but not a template; `%h` is `/root`) | Fixed |
|
||||
| [HOM-08](#hom-08) | Low | homey | Errors returned as data; REST returns 500 on bad JSON; non-ASCII token crashes the auth check | Fixed |
|
||||
| [EMB-06](#emb-06) | Low | emby | Wrong field mapping (runtime, year, episode numbers); documented `type` filter is missing | Fixed |
|
||||
| [DDG-03](#ddg-03) | Low | duckduckgo | No input validation | Fixed |
|
||||
| [SPO-04](#spo-04) | Low | spotify | Credentials file written non-atomically with default permissions | Fixed |
|
||||
| [SPO-05](#spo-05) | Low | spotify | IDs unencoded in raw API paths | Fixed |
|
||||
| [SPO-06](#spo-06) | Low | spotify | `adjustVolume` reads one device's volume and sets another's | Fixed |
|
||||
| [SPO-07](#spo-07) | Low | spotify | `3:60` durations, playlist search lines, saved-track numbering | Fixed |
|
||||
| [X-01](#x-01) | Low | all | No MCP tool annotations (e.g. `control_all` can switch off every socket) | Fixed |
|
||||
| [X-02](#x-02) | Low | Python servers | `mcp>=1.2` allows mcp 2.x, which removed `mcp.server.fastmcp` | Fixed (pin) |
|
||||
| [X-03](#x-03) | Info | all | READMEs out of date (tool lists, defaults, paths) | Fixed |
|
||||
| [X-04](#x-04) | Info | homey / emby / spotify | Broad credentials (admin Emby key, full Homey token, unused Spotify scopes) | Open (recommendation) |
|
||||
| [X-05](#x-05) | Info | spotify | Biome reports format errors (CRLF line endings) — present before the audit | Open |
|
||||
|
||||
---
|
||||
|
||||
## 3. Detailed findings
|
||||
|
||||
### Emby
|
||||
|
||||
#### EMB-01
|
||||
**All Emby tools fail with Javis' configuration** — *Critical*
|
||||
|
||||
`mcp_servers.json` sets `EMBY_HOST=10.10.10.50:9096` without a scheme. `Settings.scheme`
|
||||
defaulted to `https`, so the base URL became `https://10.10.10.50:9096`. That port speaks
|
||||
plain HTTP:
|
||||
|
||||
```
|
||||
https://10.10.10.50:9096/System/Info/Public → ConnectError [SSL: WRONG_VERSION_NUMBER]
|
||||
http://10.10.10.50:9096/System/Info/Public → 200 {"ServerName": …}
|
||||
```
|
||||
|
||||
`emby-mcp/.env` (scheme `https`, port 443) is **not** read when Javis launches the server,
|
||||
because pydantic-settings resolves `.env` relative to the working directory, which is
|
||||
`F:\Javis`.
|
||||
|
||||
**Fix:** `scheme` defaults to `http` (Emby's own default). `EMBY_HOST` may now be
|
||||
`host:port` or a full URL, and `EMBY_PORT` is ignored when the host already has a port.
|
||||
The README explains the `.env` lookup.
|
||||
|
||||
#### EMB-02
|
||||
**Search only sees the first 60 items** — *High*
|
||||
|
||||
`search_items` fetched `limit*3` items sorted by name and filtered them in Python. The library
|
||||
holds 68,932 items, so almost every title was out of reach. **Fix:** server-side
|
||||
`SearchTerm` plus `IncludeItemTypes`. Without the type filter Emby also returns People,
|
||||
Studios and other non-media items. Verified live: the fixed search finds titles across the
|
||||
whole library.
|
||||
|
||||
#### EMB-03
|
||||
**Endpoints that don't exist** — *High*
|
||||
|
||||
Checked against the server's own `/openapi.json` and live requests:
|
||||
|
||||
| Tool | Called | Reality |
|
||||
|---|---|---|
|
||||
| `get_item` | `GET /Items/{id}` | only `DELETE` exists → 404 |
|
||||
| `play_item` | `POST /Items/{id}/Play` | no such route; remote play is `POST /Sessions/{id}/Playing?ItemIds=…&PlayCommand=PlayNow` |
|
||||
| `playback_url` | `GET /Items/{id}/StreamUrl` | no such route; streams are `/Videos/{id}/stream`, `/Audio/{id}/stream` |
|
||||
| `list_devices` | `GET /Devices` expecting a list | returns `{"Items": [...]}` → tool always `[]` |
|
||||
|
||||
**Fix:**
|
||||
- `get_item` uses `/Items?Ids=…` with detail fields.
|
||||
- `play_item` picks a remote-controllable session by `session_id` or a `device` name, or the
|
||||
only one there is, and errors with the list of candidates otherwise.
|
||||
- `list_sessions` exposes `id`, `supports_remote_control` and `now_playing`.
|
||||
- `playback_url` builds the static stream URL **without** the API key.
|
||||
- `list_devices` unwraps `Items`.
|
||||
|
||||
#### EMB-04
|
||||
**IDs inserted unencoded into URL paths** — *High (security)*
|
||||
|
||||
The server authenticates with the **admin** API key, and Javis approves Emby tools
|
||||
automatically (`approval: allow`). The spec lists `POST /Items/{Id}/Delete`,
|
||||
`/System/Restart` and `/System/Shutdown`. An item ID like `123/Delete?` in `play_item`
|
||||
became `POST /Items/123/Delete`, deleting the media item. `../System/Restart#` restarted the
|
||||
server. Such an ID could come from prompt injection, for example via web search results or
|
||||
media metadata. **Fix:** every path parameter is percent-encoded as a single segment, and
|
||||
`get_item` passes the ID as a query value. Verified: `123/Delete` now yields a harmless
|
||||
lookup error.
|
||||
|
||||
#### EMB-05
|
||||
**Failures returned as data** — *Medium*
|
||||
|
||||
`ping`, `playback_url`, `playback_info` and `play_item` caught exceptions and returned
|
||||
`{"ok": false, "error": …}`. MCP reports those as successful results. **Fix:** they raise,
|
||||
so FastMCP returns `isError: true`. `ping` keeps its `ok` flag because it is a probe.
|
||||
|
||||
#### EMB-06
|
||||
**Field mapping** — *Low*
|
||||
|
||||
`RunTimeNumeric`, `SeasonNumber`, `EpisodeNumber` and `Year` are not Emby fields, so
|
||||
runtime, year and episode numbers were always empty. The instructions advertised
|
||||
`list_items(type='Movie')`, but no such parameter existed. A `None` name crashed the pydantic
|
||||
model. **Fix:**
|
||||
- Runtime is computed from `RunTimeTicks`.
|
||||
- Season and episode numbers come from `ParentIndexNumber` and `IndexNumber`.
|
||||
- `ProductionYear` is requested through `Fields`.
|
||||
- New `item_type` parameter.
|
||||
- Aliases removed.
|
||||
|
||||
### Homey
|
||||
|
||||
#### HOM-01
|
||||
**Path injection through IDs** — *High (security)*
|
||||
|
||||
`homey_client` interpolated `device_id`, `flow_id`, `variable_id` and `capability_id` into
|
||||
URL paths. httpx normalises dot segments and drops `#…` fragments. Reproduced offline:
|
||||
|
||||
```
|
||||
trigger_flow('../../system/reboot#') → POST /api/manager/system/reboot
|
||||
set_variable('../../../system/reboot#', 1) → PUT /api/system/reboot
|
||||
```
|
||||
|
||||
**Fix:** IDs are percent-encoded single path segments (`_seg`).
|
||||
|
||||
#### HOM-02
|
||||
**Bearer mode did not protect the SSE transport** — *High (security, LAN mode)*
|
||||
|
||||
The auth middleware only checked `/mcp` and the root alias. With
|
||||
`HOMEY_MCP_TRANSPORT=sse` and `HOMEY_MCP_AUTH_MODE=bearer`, both `GET /sse` and
|
||||
`POST /messages/` went through unauthenticated. **Fix:** in bearer mode every route
|
||||
requires the token except `GET /` (discovery) and `/openapi.json`. The token is compared as
|
||||
bytes, so a non-ASCII header no longer raises inside `compare_digest`.
|
||||
|
||||
#### HOM-03
|
||||
**No DNS-rebinding protection** — *Medium (security, LAN mode)*
|
||||
|
||||
FastMCP's Host/Origin validation was switched off (`enable_dns_rebinding_protection=False`),
|
||||
yet README and UBUNTU_SETUP promise a Host check (421) via
|
||||
`HOMEY_MCP_ADVERTISED_HOST`. In the default `AUTH_MODE=none`, any web page opened in **any
|
||||
browser on the LAN** could rebind its domain to the server and control the smart home over
|
||||
MCP. Without a token the REST API was open the same way. **Fix:** a Host-header allow-list
|
||||
covers MCP **and** REST and answers `421 Misdirected Request`. It is built from the
|
||||
advertised host, `localhost`, `127.0.0.1` and `HOMEY_MCP_ALLOWED_HOSTS` (`*` turns it off).
|
||||
If the bind address is a wildcard and nothing is configured, the server logs a warning.
|
||||
|
||||
#### HOM-04
|
||||
**Realtime events never arrived** — *Medium*
|
||||
|
||||
The client connected with `auth={"token"}` and listened for `device.update` on the root
|
||||
namespace. Athom's `homey-api` (`SocketSession.js`, `SubscriptionRegistry.js`) shows the real
|
||||
protocol:
|
||||
1. `handshakeClient {token, homeyId}` returns a namespace (`/api`).
|
||||
2. Connect to that namespace.
|
||||
3. `subscribe` to URIs such as `homey:manager:devices` and `homey:device:<id>`.
|
||||
4. Events arrive named by their URI, with `(event, data)` as payload.
|
||||
|
||||
Verified live (read-only): the old code received 0 events. **Fix:** the client implements the
|
||||
handshake and subscriptions, rebuilds them on reconnect, and buffers compact entries. The
|
||||
fixed client captured 18 `capability` events in 25 s.
|
||||
|
||||
#### HOM-05
|
||||
**Zone resolution by first substring hit** — *Medium*
|
||||
|
||||
`control_zone`, `get_zone_status` and the REST `zones/control` took the first zone whose
|
||||
name *contained* the query. That depends on dict order: "Wohnzimmer" could switch
|
||||
"Wohnzimmer Ecke". `control_zone` also matched devices of *other* zones by name substring
|
||||
when the zone was embedded as an object. **Fix:** a shared `_resolve_zone` prefers an exact
|
||||
match, then a prefix, then a substring (shortest name wins). Devices are matched by zone ID
|
||||
only.
|
||||
|
||||
#### HOM-06
|
||||
**`find_devices` too broad** — *Medium*
|
||||
|
||||
The query was split into words and a device matched if **any** word hit its name or zone.
|
||||
"esszimmer licht" therefore returned every light in the house plus every Esszimmer device,
|
||||
and the model then acts on one of them. **Fix:** all words must match. Only if nothing does
|
||||
are partial matches returned.
|
||||
|
||||
#### HOM-07
|
||||
**systemd unit** — *Low*
|
||||
|
||||
`homey-mcp.service` uses `User=%i`, but `setup-ubuntu.sh` copies it as `homey-mcp.service`
|
||||
and enables `homey-mcp@$USER.service`, which fails because the template doesn't exist. In
|
||||
addition, `%h` resolves to `/root` for system units: specifiers ignore `User=`. **Fix:** the
|
||||
unit is documented and installed as the template `homey-mcp@.service` and uses
|
||||
`/home/%i/…`.
|
||||
|
||||
#### HOM-08
|
||||
**Error handling** — *Low*
|
||||
|
||||
The following returned errors as data instead of raising:
|
||||
- `control_all`'s safety refusal (`{"error": …}`)
|
||||
- `get_temperatures` (`[{"error": …}]`)
|
||||
- `send_notification` (`{"ok": false}`)
|
||||
|
||||
In addition, REST handlers returned 500 on invalid JSON. **Fix:** the tools raise, and REST
|
||||
returns 400.
|
||||
|
||||
### DuckDuckGo
|
||||
|
||||
#### DDG-01
|
||||
**Blocking calls on the event loop** — *Medium*
|
||||
|
||||
The tools were plain `def` functions. FastMCP calls those directly on its event loop, and a
|
||||
search took 0.5–2 s (measured). During that time the server could not answer anything else,
|
||||
including cancellations. **Fix:** the tools are `async` and run `ddgs` in
|
||||
`asyncio.to_thread`.
|
||||
|
||||
#### DDG-02
|
||||
**Failures look like results** — *Medium*
|
||||
|
||||
Rate limits and network errors came back as `"Search failed: …"` text. The Javis host
|
||||
caches non-`ERROR:` results of cacheable tools, so the failure was replayed for later
|
||||
identical queries. **Fix:** failures raise `ToolError` (`isError: true`).
|
||||
|
||||
#### DDG-03
|
||||
**Input validation** — *Low*
|
||||
|
||||
An empty query raised `DDGSException: query is mandatory` from deep inside the library, and
|
||||
`safesearch` accepted any value. **Fix:** both are validated up front.
|
||||
|
||||
### Spotify
|
||||
|
||||
#### SPO-01
|
||||
**No `isError`** — *Medium*
|
||||
|
||||
Every handler caught exceptions and returned `"Error …"` text as a normal result. For Javis,
|
||||
"Error starting playback: 404 No active device" was a success. **Fix:** a single wrapper in
|
||||
`index.ts` marks results whose text starts with `Error` and turns thrown errors into
|
||||
`isError: true`. `adjustVolume`'s "No active device" messages now follow the same
|
||||
convention. `verify_tools.mjs` counts `isError` as a failure.
|
||||
|
||||
#### SPO-02
|
||||
**Track URIs played as context** — *Medium*
|
||||
|
||||
`playMusic` checked only `type === 'track'`. With just `uri: "spotify:track:…"`, which LLMs
|
||||
often send, the track went into `context_uri` and Spotify rejected it. **Fix:** the kind is
|
||||
taken from the URI; tracks and episodes go to `uris`.
|
||||
|
||||
#### SPO-03
|
||||
**Token refresh** — *Medium*
|
||||
|
||||
Spotify's refresh response may contain a new `refresh_token`
|
||||
([docs](https://developer.spotify.com/documentation/web-api/tutorials/refreshing-tokens)),
|
||||
and the code discarded it. Refresh happened only *after* expiry, so a token could expire
|
||||
mid-request. Concurrent tool calls each refreshed and wrote the file. An expired 6-month
|
||||
refresh token (`invalid_grant`) produced an opaque error. **Fix:**
|
||||
- Single-flight refresh, 60 s before expiry.
|
||||
- A rotated refresh token is saved.
|
||||
- `invalid_grant` produces the message "refresh token expired or revoked — run
|
||||
`npm run auth`".
|
||||
|
||||
#### SPO-04 – SPO-07
|
||||
- **SPO-04** `spotify-config.json` (client secret and refresh token) is now written via
|
||||
temp-file + rename with mode `0600`.
|
||||
- **SPO-05** Playlist and user IDs are `encodeURIComponent`-ed in raw API paths. Verified:
|
||||
`../me` stays inside `playlists/…`.
|
||||
- **SPO-06** `adjustVolume` reads the volume of the device it is about to change.
|
||||
- **SPO-07** Display fixes:
|
||||
- `formatDuration(179600)` used to print `2:60`.
|
||||
- Playlist search lines printed the description as a track count.
|
||||
- Saved-track numbering ignored the offset for removed items.
|
||||
|
||||
### Cross-cutting
|
||||
|
||||
#### X-01
|
||||
**Tool annotations** — *Low*
|
||||
All tools now carry MCP annotations:
|
||||
- `readOnlyHint` for lookups.
|
||||
- `destructiveHint` for:
|
||||
- Homey's `control_zone`, `control_all` and `trigger_flow`.
|
||||
- Spotify's removals, playlist edits and reordering.
|
||||
- `openWorldHint` for web and Spotify calls.
|
||||
|
||||
The Javis host now asks before any tool marked `destructiveHint` (and not `readOnlyHint`),
|
||||
even on servers with `approval: allow`. In practice this covers the satellite power tools.
|
||||
Turn it off with `mcp.confirm_destructive: false`.
|
||||
|
||||
#### X-02
|
||||
**Dependency pins** — *Low*
|
||||
`mcp[cli]>=1.2` would install mcp 2.x, which no longer ships `mcp.server.fastmcp`. All three
|
||||
servers now pin `mcp[cli]>=1.13,<2`. `ddgs>=9.0` is pinned for the renamed package.
|
||||
|
||||
#### X-03
|
||||
**Docs** — *Info*
|
||||
The READMEs listed 8 of 21 Homey tools, claimed DuckDuckGo "instant answers", gave Emby's
|
||||
default scheme as https and referenced `d:\Javis` paths. All four READMEs are updated.
|
||||
|
||||
#### X-04
|
||||
**Least privilege** — *Info*
|
||||
- The Emby admin API key can delete media and restart or shut down the server.
|
||||
- The Homey personal access token usually carries all scopes.
|
||||
- Spotify requests `user-read-email`, which no tool uses.
|
||||
|
||||
📌 See §6.
|
||||
|
||||
#### X-05
|
||||
**Biome formatting** — *Info*
|
||||
`npx biome check` reports 8 format errors caused by CRLF line endings. These existed before
|
||||
the audit. `biome lint` and `tsc` are clean.
|
||||
|
||||
---
|
||||
|
||||
## 4. Reviewed and rejected
|
||||
|
||||
| Claim | Why rejected |
|
||||
|---|---|
|
||||
| Spotify `handleSpotifyRequest` swallows real errors ("Unexpected token") | The SDK validates the HTTP status before parsing; only successful non-JSON bodies (player endpoints) reach the parser. |
|
||||
| Homey `ping` should raise | It is a connectivity probe; `ok: false` with the reason is the intended result. |
|
||||
|
||||
---
|
||||
|
||||
## 5. Verification
|
||||
|
||||
| Check | Result |
|
||||
|---|---|
|
||||
| `homey-mcp/.venv` → `pytest tests/test_homey_mcp.py` | 9 passed |
|
||||
| `emby-mcp/.venv` → `pytest tests/test_emby_mcp.py` | 8 passed |
|
||||
| `duckduckgo-mcp/.venv` → `pytest tests/test_duckduckgo_mcp.py` | 4 passed |
|
||||
| `ruff check …/src` + new tests (`F,E9,B`) | clean |
|
||||
| Spotify `tsc --noEmit`, `biome lint`, `npm run build` | clean |
|
||||
| Live, read-only: Emby | ping, search, `get_item`, `playback_url` (no key in URL), users, sessions, devices all OK; crafted ID `123/Delete` → harmless lookup error |
|
||||
| Live, read-only: Homey realtime | handshake + subscriptions, 18 events in 25 s (before: 0) |
|
||||
| Live, read-only: Spotify over stdio | 28 tools (12 read-only, 5 destructive); search OK; bad and crafted playlist IDs → `isError: true`, path stays encoded |
|
||||
|
||||
The new tests:
|
||||
- **Homey:**
|
||||
- Encoded IDs.
|
||||
- Zone ranking.
|
||||
- All-words search.
|
||||
- `control_zone` scoped by ID.
|
||||
- `control_all` refusal.
|
||||
- Compact events.
|
||||
- Bearer protection of `/sse` and `/messages/` and the root alias.
|
||||
- Host guard (421).
|
||||
- Annotations.
|
||||
- **Emby:**
|
||||
- Base-URL forms.
|
||||
- Encoded IDs.
|
||||
- Server-side search parameters.
|
||||
- `get_item` via `/Items?Ids`.
|
||||
- Session-targeted play.
|
||||
- Device unwrapping.
|
||||
- Key-free stream URL.
|
||||
- Annotations.
|
||||
- **DuckDuckGo:**
|
||||
- Search runs in a worker thread.
|
||||
- Rate limits become `ToolError`.
|
||||
- Input validation.
|
||||
- Annotations.
|
||||
|
||||
---
|
||||
|
||||
## 6. Improvements and best practices
|
||||
|
||||
✅ = applied, 📌 = recommendation.
|
||||
|
||||
| Source | Guidance | Status |
|
||||
|---|---|---|
|
||||
| [MCP spec — Tools](https://modelcontextprotocol.io/specification/2025-06-18/server/tools) | Tool execution failures belong in the result with `isError: true`. | ✅ All four servers (EMB-05, HOM-08, DDG-02, SPO-01) |
|
||||
| MCP spec — Tools | Annotations describe behaviour; a human should be able to deny invocations. | ✅ Annotations everywhere. ✅ The Javis host asks before `destructiveHint` tools even on `approval: allow` servers (`mcp.confirm_destructive`, default on). |
|
||||
| [MCP spec — Transports, security warning](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports) | Servers **MUST** validate `Origin`/Host against DNS rebinding, **SHOULD** bind to localhost when local, **SHOULD** authenticate all connections. | ✅ Host guard + auth on every MCP route (HOM-02/03). 📌 Prefer `HOMEY_MCP_AUTH_MODE=bearer` wherever the client can send a header. |
|
||||
| [MCP — Security best practices](https://modelcontextprotocol.io/specification/2025-06-18/basic/security_best_practices) | Local servers: use stdio or require a token on HTTP. Scope minimisation: least-privilege tokens. | ✅ Javis uses stdio for all four. 📌 Homey: create a PAT with only the scopes the tools need (devices, zones, flows, logic, notifications). Emby: consider a restricted user's token instead of the admin API key, or keep Emby on `approval: ask`. Spotify: drop the unused `user-read-email` scope at the next `npm run auth`. |
|
||||
| OWASP Injection / path traversal guidance | Never build URLs by concatenating untrusted input; encode path parameters. | ✅ Homey, Emby, Spotify (HOM-01, EMB-04, SPO-05) |
|
||||
| [Spotify — Refreshing tokens](https://developer.spotify.com/documentation/web-api/tutorials/refreshing-tokens) | Store a rotated refresh token; `invalid_grant` means re-authorize; refresh tokens last 6 months. | ✅ SPO-03 |
|
||||
| Athom `homey-api` (`SocketSession.js`, `SubscriptionRegistry.js`) | Handshake → namespace → URI subscriptions; re-subscribe after reconnect. | ✅ HOM-04 |
|
||||
| Emby server OpenAPI (`/openapi.json`) | Use documented routes (`/Sessions/{Id}/Playing`, `/Items?Ids=`, `SearchTerm`). | ✅ EMB-02/03 |
|
||||
| Python asyncio / FastMCP | Never block the event loop; offload sync I/O to threads. | ✅ DDG-01 |
|
||||
| OWASP LLM prompt-injection guidance | Treat tool output from the web as untrusted data. | ✅ DuckDuckGo server instructions say so. 📌 Same idea for media and device names returned by Emby and Homey. |
|
||||
| systemd.exec(5) | Specifiers like `%h` describe the service manager's user, not `User=`. | ✅ HOM-07 |
|
||||
|
||||
Observations:
|
||||
- `mcp` 2.x removed FastMCP, so these servers stay on 1.x until they are ported to the new
|
||||
`MCPServer` API.
|
||||
- Still open:
|
||||
- Least-privilege credentials (X-04).
|
||||
- CRLF line endings in the Spotify sources (X-05).
|
||||
Reference in new issue
Block a user