# Reported issues for gaggiuino-mcp

Pod holds 24 of 88 problems reported by people outside the maintainer team. Issues filed by the project's own owners, members and collaborators are excluded entirely — a maintainer's release checklist is not a warning to a prospective user.

Back to [gaggiuino-mcp](/mcp/gaggiuino-mcp).

## Most discussed

### fix(server): make the consent token stateless so /oauth/authorize cannot be evicted

Found while writing the tests for #108/#109 (PR #118). Not a vulnerability that grants anything — it is an availability defect on the login path, and the fix removes a store rather than adding a guard.

#### What is wrong

`handleGet` in `apps/server/src/oauth/authorize.ts` calls `codes.remember(...)` to park the validated authorization request **before any authentication happens** — it has to, because the CSRF token in the form is the key to that parked request. `createCodeStore`'s `pending` ma

[Read the thread](https://github.com/ljcl/gaggiuino-mcp/issues/119) · 2026-08-05 · closed · 2 comments

### feat(server): zero-click Tailscale identity at /oauth/authorize

`/oauth/authorize` is the only endpoint a human's browser touches. On a tailnet, Tailscale already knows who that human is — so the owner passphrase (#109) could be skipped entirely, and the consent step would become physically uncompletable from the public internet. `tsidp` does exactly this: `server/authorize.go` refuses the route over Funnel with *"they are not part of the tailnet that they are trying to be authenticated for."*

The mechanism is real. Verified in `tailscale/tailscale`, `ipn/i

[Read the thread](https://github.com/ljcl/gaggiuino-mcp/issues/115) · 2026-08-05 · open · 2 comments

### chore(server): drop the MCP_AUTH_TOKEN startup tombstone

#111 kept `env.MCP_AUTH_TOKEN` read solely so a deployment that still sets it fails loudly at startup rather than silently serving an open `/mcp`. One release after 2.0.0, remove it:

- the startup check and its `ConfigError`;
- the `.env.example` tombstone line and its explanatory comment — `envExample.test.ts` enforces this direction too, so the code and the template must move in the same commit;
- any remaining mention in README, SECURITY.md, AGENTS.md, `server.json`, `turbo.json`.

No behavi

[Read the thread](https://github.com/ljcl/gaggiuino-mcp/issues/114) · 2026-08-05 · closed · 2 comments

### feat(server)!: remove MCP_AUTH_TOKEN in favour of OAuth

**This is the breaking change.** It lands only after OAuth works end to end against the owner's real connector on iOS (#113's probe is the gate). Nothing before this point removes anything.

#### Why the token goes rather than staying alongside

It is a control the owner cannot configure, and one nobody on a personal plan can. Keeping it means two auth mechanisms, two sets of gate ordering to reason about, and a `writeToolDisabled` that answers a per-request question by reading the process envir

[Read the thread](https://github.com/ljcl/gaggiuino-mcp/issues/111) · 2026-08-05 · closed · 2 comments

### feat(server): delegate to an external issuer with MCP_OAUTH_ISSUER

The built-in AS (#108, #109) is the default because it keeps the one-container promise. It should not be the only path — plenty of self-hosters already run Authentik, Authelia, Keycloak, Zitadel or Kanidm, and a Tailscale user may prefer `tsidp`. This makes that a one-variable choice against code that already exists.

Anthropic supports it with no extra ceremony: *"A cross-host authorization server doesn't need anything special on its own. The `authorization_servers` field in your protected reso

[Read the thread](https://github.com/ljcl/gaggiuino-mcp/issues/110) · 2026-08-05 · closed · 2 comments

### feat(server): delete_profile tool, gated on explicit user confirmation

`upload_profile` (#101) gives the server a way to create a profile and no way to remove one. A model that uploads a malformed or duplicate profile leaves the user to clean it up on the machine's own screen, which is the surface the whole server exists to avoid.

`DELETE /api/profile-select/{id}` is the endpoint. PR #104 deliberately left it on the not-called list (`client.ts`, "endpoints this client does not call") on the grounds that it was unverified and differs from the selector by HTTP metho

[Read the thread](https://github.com/ljcl/gaggiuino-mcp/issues/105) · 2026-08-05 · closed · 2 comments

### feat: adopt MCP spec 2026-07-28 via SDK v2 (`@modelcontextprotocol/server`), dual-era

## Why

MCP spec [2026-07-28](https://modelcontextprotocol.io/specification/2026-07-28/changelog) shipped on July 28 ([announcement](https://blog.modelcontextprotocol.io/posts/2026-07-28/)). Anthropic says support is "[rolling out across Claude products soon](https://claude.com/blog/bringing-mcp-2026-07-28-to-claude)" — today's Claude clients still speak the 2025-era protocol, so there is no urgency, but once Claude's client update lands there are two concrete wins for this server:

- **`ttlMs`/

[Read the thread](https://github.com/ljcl/gaggiuino-mcp/issues/165) · 2026-08-12 · closed · 1 comment

### epic: derived shot diagnostics

Batch from the 2026-08-08 assessment of [mxkissnr/gaggiuino-local-profiler](https://github.com/mxkissnr/gaggiuino-local-profiler) — a Home Assistant espresso profiler covering the same hardware, read in source for ideas that fit a stateless MCP server.

## Why these belong together

All four are `apps/server/src/analysis.ts`, and they are the part of that project that transfers without a database: **pure functions over datapoints this server already fetches**. Nothing here needs a store, user-en

[Read the thread](https://github.com/ljcl/gaggiuino-mcp/issues/145) · 2026-08-08 · closed · 1 comment

## Most recent

### epic: loose-boundary tolerance

Batch 3 of the 2026-08-22 codebase improvement audit.

## Why these belong together

Four places where a malformed or hostile input takes down more than it should. The repo already has a posture here, stated in AGENTS.md: upstream payloads are validated at the client boundary with deliberately loose schemas so a firmware revision cannot take the server down, `maintenance.ts` routes uninterpretable keys to `extras` rather than failing, and expected failures are results rather than exceptions. The

[Read the thread](https://github.com/ljcl/gaggiuino-mcp/issues/200) · 2026-08-23 · open · 0 comments

### epic: the /mcp gate and the OAuth path

Batch 1 of the 2026-08-22 codebase improvement audit.

## Why these belong together

The first two are not adjacent issues, they are one edit. Both restructure the same block of `apps/server/src/http.ts` (`:203-239`):

- #176 moves `authenticate` to run *after* `checkRequest`, restoring the ordering `mcpAuth.ts` documents and this file currently defeats.
- #185 wraps the gate's rejection in `withCors` so an allowlisted browser origin can read the 401 challenge.

They also interact in a way neith

[Read the thread](https://github.com/ljcl/gaggiuino-mcp/issues/198) · 2026-08-23 · open · 0 comments

### chore(repo): add a CONTRIBUTING pointer to the local verification gates

## Why

Dev commands live accurately in README §Development, but nothing onboards a contributor to the actual working loop: `check` vs `check:affected`, the coverage-ratchet rule (commit the raised numbers; never hand-edit `vitest.config.ts`), story screenshots for visual review, `fake-machine` for machineless development, and the fact that the squash-merged PR title is the only thing release-please reads.

## Fix

A thin CONTRIBUTING.md that points at the relevant AGENTS.md sections rather than

[Read the thread](https://github.com/ljcl/gaggiuino-mcp/issues/196) · 2026-08-22 · open · 0 comments

### ci: pin the mcp-publisher binary by release checksum

## Why

`publish-mcp.yml` downloads mcp-publisher from `releases/latest` unpinned (`curl | tar xz`), then hands that binary a GitHub OIDC token two steps later. Latest is a documented requirement — the registry rejects stale publisher binaries — but the download is currently unverified, an acknowledged supply-chain exposure directly on the publish path.

## Fix

Keep resolving latest at run time, but verify the tarball against the release's published checksum before execution (checksum fetched w

[Read the thread](https://github.com/ljcl/gaggiuino-mcp/issues/195) · 2026-08-22 · open · 0 comments

### refactor(server): read GAGGIUINO_URL lazily so resetClient stays a full seam

## Why

`MACHINE_URL` is read once at module load (`client.ts:979-980`: `process.env.GAGGIUINO_URL?.trim() || DEFAULT_MACHINE_URL`) and `getClient()` builds from that frozen constant (`client.ts:984-989`). `resetClient(config)` — the labelled test seam — can override every knob except baseUrl, so a test (or runtime config reload) changing the env after import observes nothing. The seam exists precisely to make config-dependent client behavior exercisable.

## Fix

Read the environment inside `ge

[Read the thread](https://github.com/ljcl/gaggiuino-mcp/issues/189) · 2026-08-22 · open · 0 comments

### refactor(server): scope cache keys by HTTP method to remove the as-T cast

## Why

`perform` returns cache hits via `hit as T` (`client.ts:699`) against a `createCache<unknown>`; soundness rests entirely on a docblock invariant that GET paths and write-ack paths never collide on a key. Nothing structural enforces it — a future cached GET on a path another verb touches would silently return the wrong payload type, and zod would catch it several layers away or not at all for loose schemas.

## Fix

Key cache entries by method as well as path (`${method}:${path}`), or sco

[Read the thread](https://github.com/ljcl/gaggiuino-mcp/issues/188) · 2026-08-22 · open · 0 comments

### refactor(server): extract the shared target-series pairing used by events and analysis

## Why

`events.ts:87-107` (`toSamples`) and `analysis.ts:131-148` (`meanDeviationFromTarget`) independently encode "pair the measured and target series index-wise, treat commanded ≤ 0 as not-commanded, normalize via SCALE_BY_10" — and already drift subtly: events drops instants where a reading is missing, analysis skips them mid-loop. Any future change to that convention must be made twice and can silently disagree.

## Fix

Extract `pairedSamples(measured, target, fieldName)` into `normalize.t

[Read the thread](https://github.com/ljcl/gaggiuino-mcp/issues/187) · 2026-08-22 · open · 0 comments

### refactor: give the x10 wire scaling one home shared by server and shot-graph

## Why

Two ×10-scaling homes exist and have diverged:

- `apps/server/src/normalize.ts` `SCALE_BY_10` covers pressure, targetPressure, temperature, targetTemperature, pumpFlow, weightFlow, targetPumpFlow, shotWeight, waterPumped, timeInShot;
- `packages/shot-graph/src/normalize.ts` hand-divides only the seven charted fields plus inline duration/timeInShot handling.

If a firmware-scaled field ever reaches the chart or the CSV export (`targetTemperature` and `waterPumped` are live candidates), s

[Read the thread](https://github.com/ljcl/gaggiuino-mcp/issues/186) · 2026-08-22 · open · 0 comments

### fix(server): send CORS headers on early rejections for allowlisted origins

## Why

CORS headers decorate only responses that come back through `handleMcp`. The
401 refusal from the security gate is returned bare at `http.ts:233`, outside
the `withCors(...)` wrap at `http.ts:236-239`, so a browser-based client on an
allowlisted origin gets no `Access-Control-Allow-Origin` on the challenge and
cannot read `WWW-Authenticate` from JavaScript. Header reflection itself is
correct (`mcpAuth.ts:301-324`: origin echoed, never `*`, `Vary: Origin`,
expose-headers covers the sessi

[Read the thread](https://github.com/ljcl/gaggiuino-mcp/issues/185) · 2026-08-22 · open · 0 comments

### fix(server): bind refresh-token audience to the granted resource parameter

## Why

The authorization-code grant seals the refresh token's audience from the caller-supplied `resource` parameter via `audienceFor()` (`oauth/token.ts:345-355`), but `handleRefresh` verifies the presented refresh token against `deps.config.resource` only (`oauth/tokens.ts:244-260`). A grant issued with a non-default `resource` yields access tokens that authenticate fine while their refresh token is refused as `invalid_grant` — such deployments loop through re-consent every access-token lifet

[Read the thread](https://github.com/ljcl/gaggiuino-mcp/issues/184) · 2026-08-22 · open · 0 comments

### fix(server): negative-cache failed CIMD client-document resolutions

## Why

`resolveClient` caches only successful CIMD document fetches (`oauth/clients.ts:180-215`; the cache fill sits behind the success branch), so failed or rejected resolutions re-fetch on every request. An anonymous flood of `GET /oauth/authorize` carrying distinct public-https `client_id`s therefore drives one outbound fetch each (3s timeout, 64KB cap) at arbitrary public hosts — an unmetered outbound-fetch sink on an unauthenticated path. The DNS TOCTOU residue is documented and accepted; 

[Read the thread](https://github.com/ljcl/gaggiuino-mcp/issues/183) · 2026-08-22 · open · 0 comments

### fix(server): derive the scope gate fail-closed when readOnlyHint is absent

## Why

`protectedToolsIn` selects tools with `readOnlyHint === false` (`oauth/scopeGate.ts:29-33`), so a future write tool whose annotations omit `readOnlyHint` entirely (undefined) is *not* scope-gated: the runtime derivation falls open where this repo otherwise fails closed. Currently mitigated — `defineTool` requires annotations and `scopeGate.test.ts` pins `PROTECTED_TOOLS` to exactly the three writes, so an accidental loss fails CI — but the derivation direction is wrong for any future too

[Read the thread](https://github.com/ljcl/gaggiuino-mcp/issues/182) · 2026-08-22 · open · 0 comments

### fix(server): answer unknown tools with JSON-RPC -32602 instead of an isError result

## Why

`handleToolCall` throws `Error("Unknown tool: …")` for an unrecognized name and `callTool`'s catch-all wraps it as `{ isError: true, … }` over HTTP 200. The MCP spec directs servers to answer `tools/call` for an unknown name with Invalid params (-32602); today a host cannot distinguish a mistyped tool name from a server bug, and the caller's mistake logs at error level as though it were ours. An output-schema `.parse()` violation falls into the same generic channel instead of the typed I

[Read the thread](https://github.com/ljcl/gaggiuino-mcp/issues/181) · 2026-08-22 · open · 0 comments

### fix(mcp-app): validate tool-result JSON at readToolJson and stop NaN or raw TypeErrors reaching the UI

## Why

`readToolJson` does `JSON.parse(text) as T` with no runtime validation (`packages/ui/src/host/toolResult.ts`), and the shot-graph app trusts the result structurally:

- missing `duration` → `shot.duration / SCALE` is NaN → ShotHeader renders "NaN g in NaN s";
- missing `profile` object → `shot.profile.name` throws TypeError inside render, putting a raw JS message in front of the user — contradicting the shell rule that users see server-authored errors, never invented ones;
- string-numbe

[Read the thread](https://github.com/ljcl/gaggiuino-mcp/issues/180) · 2026-08-22 · open · 0 comments

### fix(server): tolerate a malformed element in /api/profiles/all instead of failing the whole list

## Why

`MachineProfilesSchema` accepts `/api/profiles/all` as an array of elements each requiring a string `name` (`client.ts:273-278`, element schema ~L218-224). One malformed element — a firmware revision omitting `name`, or a truncated body — fails the whole array parse: `MalformedUpstreamError`, `list_profiles` errors out entirely, and `profileCatalog.ts` degrades to documentation-only, hiding every real profile because of one bad record.

That sits badly next to this repo's own loose-bound

[Read the thread](https://github.com/ljcl/gaggiuino-mcp/issues/179) · 2026-08-22 · open · 0 comments

### fix(server): substitute guidance placeholders with replacer functions

## Why

`renderDialInGuidance` substitutes placeholders with two-argument `String.replace` (`guidance.ts:24-25`):

```
.replace("{user_context}", prompt.userContext ?? "")
.replace("{profiles_text}", getAllProfilesText())
```

In JS the replacement *string* interpolates `$&`, `` $` `` and `$'` sequences, so a `user_context` in `prompts.local.yaml` containing e.g. a bare `$&` corrupts the rendered guidance; profile text flowing through `getAllProfilesText()` has the same power. Both surfaces inhe

[Read the thread](https://github.com/ljcl/gaggiuino-mcp/issues/178) · 2026-08-22 · open · 0 comments

The remaining reports are on [the project's issue tracker](https://github.com/ljcl/gaggiuino-mcp/issues).
