Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 16 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,17 +266,28 @@ to it yet.
clients of 2025-03-26 to 2025-11-25 that start with `initialize`, without
session ids or a GET stream. The read tools are `homes_list`, `status`,
`chargers_list`, `cars_list`, `plan_get`, `plan_explain`,
`history_query`, `health_get`, `logs_tail` (with before/after cursors),
`drivers_status`, `config_get` and `measurement_diagnostics`, each with
typed arguments and the scope its box operation requires.
`history_query`, `events_query`, `health_get`, `logs_tail` (with
before/after cursors), `drivers_status`, `config_get` and
`measurement_diagnostics`, each with typed arguments and the scope its box
operation requires.
`measurement_diagnostics` needs the owner's diagnostic grant. Both MCP
profiles offer its fixed, read-only views of measurement reasons, saved
charger associations, energy provenance and stored series. Names and
hardware identifiers become opaque selections; other private fields stay
out. Refresh the catalogue after a service restart. No arbitrary path,
settings write, control or credential export is accepted by this tool.
`diag_run` and `events_list` are not listed until Core serves them. Cached
calls get an explicit unsupported result and never reach Core.
`events_query` answers "what happened at 06:14" from the box's control
journal (`events.get`, scope `ftw.events.read`): each span in which a
device, a source, dispatch or the mode held one verdict, and each scored
plan slot, in the home's time zone from the box's `hello_ok`, with its
length, asked against measured watts and the latest verdict's numbers,
then the notices the box sent. The window is the last 24 hours unless
`since` and `until` say otherwise; when the box cuts the answer, `next`
names the `until` that reads on. A box from before the journal answers
`E_UNKNOWN_OP`, and the tool says to update FTW.
`diag_run` is not listed until Core serves it, and `events_list`, an
earlier name for events, is not listed at all. Cached calls to either get
an explicit unsupported result and never reach Core.
Full technical reads remain separate through `api_catalog` / `api_read`
on the technical profile. `status` keeps the box's `live` body as it
is, keyed by field id, and adds `readings`: each field by its registry
Expand Down
5 changes: 3 additions & 2 deletions docs/public-plugin.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ changing that table and this one.
| `setup_connect` | false | true | true | Starts a vendor sign-in (myUplink today) on the vendor's site. When it finishes, the vendor's token replaces the driver's token in the draft. Open world: a third-party service. |
| `setup_commit` | false | true | false | Replaces live settings with the draft the user confirmed. |
| `setup_restart` | false | true | false | Restarts Core to apply saved settings, which briefly stops control and readings. |
| `events_query` | true | false | false | Base `/mcp` only until it is reviewed for this profile. Reads the box's control journal; nothing on the box changes. |
| `api_catalog`, `api_read` | true | false | false | Base `/mcp` only. GET reads of the box's own diagnostic API, from the paths the box lists. |

## Limits
Expand All @@ -77,8 +78,8 @@ changing that table and this one.
profile excludes them because the directory's
[plugin guidelines](https://developers.openai.com/plugins/plugin-guidelines)
require each model-callable operation to appear as a separate reviewed tool.
The profile also excludes the `events_list` and `diag_run` operations that
Core does not yet serve.
The profile also excludes `diag_run`, which Core does not yet serve, and
`events_query`, which waits for its own review before this profile lists it.

`history_query` returns downsampled power readings. It does not establish
exact energy totals or savings. Skills and listing text must state that
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ require (
github.com/fxamacker/cbor/v2 v2.9.4
github.com/gorilla/websocket v1.5.3
github.com/quic-go/quic-go v0.63.0
github.com/srcfl/ftw/go/protocol v0.0.0-20261024093922-2d7c8eba41f0
github.com/srcfl/ftw/go/protocol v0.0.0-20261108080825-fd4426a107c1
golang.org/x/crypto v0.57.0
golang.org/x/net v0.58.0
golang.org/x/sys v0.48.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ github.com/quic-go/quic-go v0.63.0 h1:LIFGHI4PFUhhw2dDD1ARHdCff143ffMHwZtbnbuJ78
github.com/quic-go/quic-go v0.63.0/go.mod h1:RAro2j2yN9a9EiPACLHT9IB2NXCvGQmmo/alT0yYI0w=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/srcfl/ftw/go/protocol v0.0.0-20261024093922-2d7c8eba41f0 h1:iK/CxTZOsxEbkXT28eaX5JUx7J0u1abH4bscHpPmAYE=
github.com/srcfl/ftw/go/protocol v0.0.0-20261024093922-2d7c8eba41f0/go.mod h1:VEjr/o0yZijJ1nApuJK+nak6FxJ/fubAUBNOunqNrUU=
github.com/srcfl/ftw/go/protocol v0.0.0-20261108080825-fd4426a107c1 h1:jMTkobIdBd+6uLxjfuWrBXGLOroK7EhubT6g8giZCbg=
github.com/srcfl/ftw/go/protocol v0.0.0-20261108080825-fd4426a107c1/go.mod h1:VEjr/o0yZijJ1nApuJK+nak6FxJ/fubAUBNOunqNrUU=
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
Expand Down
26 changes: 24 additions & 2 deletions internal/mcp/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,10 @@ type stubBox struct {
apiStarted chan struct{}
apiRelease chan struct{}
requests []protocol.AgentRequest
// eventsReply answers events.get in place of the stub's journal, with
// a message type and its body; eventsAsked is each events.get asked.
eventsReply func(protocol.EventsGet) (string, any)
eventsAsked []protocol.EventsGet
// noNoise is a box from before the agent contract: its roster names no
// Noise key.
noNoise bool
Expand Down Expand Up @@ -649,8 +653,26 @@ func (b *stubBox) handle(s *boxSession, env appwire.Envelope) {
case protocol.MsgEventsGet:
var q protocol.EventsGet
_ = protocol.Unmarshal(env.B, &q)
ev, _ := protocol.Marshal(map[string]any{"kind": "box.updated", "ts": now.UnixMilli()})
b.reply(s, protocol.MsgEvents, env.ID, protocol.Events{Events: []cbor.RawMessage{ev}, More: q.BeforeMs == 0})
b.mu.Lock()
b.eventsAsked = append(b.eventsAsked, q)
fn := b.eventsReply
b.mu.Unlock()
if fn != nil {
t, body := fn(q)
b.reply(s, t, env.ID, body)
return
}
// The control journal: a battery that did not follow for a while
// this morning, a dispatch verdict that still holds, and the notice
// the box sent about the battery. Text is a device's to write.
start := now.Add(-3 * time.Hour).UnixMilli()
b.reply(s, protocol.MsgEvents, env.ID, protocol.Events{Events: []cbor.RawMessage{}, Spans: []protocol.EventSpan{
{Kind: "dispatch", Status: "ok", Severity: "info", Mode: "self_consumption", StartMs: start + 1_057_000, Samples: 640},
{Kind: "battery", Src: "s1", Status: "not_following", Reason: "readback_mismatch\u202e", Severity: "warning", Mode: "self_consumption",
StartMs: start, EndMs: start + 1_057_000, Samples: 212,
RequestedW: &protocol.PowerRange{Min: -3000, Max: -2000, Mean: -2500.4}, ActualW: &protocol.PowerRange{Min: -400, Max: 0, Mean: -120.6},
Last: map[string]float64{"sent_w": -3000, "readback_w": -2000, "battery_soc": 41.256, "command_at_ms": float64(start + 1_000_000)}},
}, Notices: []protocol.EventNotice{{Ts: start + 600_000, Type: "control.not_following", Src: "s1", Status: "sent", Priority: 4}}})
case protocol.MsgLogsGet:
var q protocol.LogsGet
_ = protocol.Unmarshal(env.B, &q)
Expand Down
Loading
Loading