Skip to content

Commit a0b81fe

Browse files
xaionaro@dx.centerxaionaro@dx.center
authored andcommitted
feat(mcp): add battery, location, and display workflow tools
1 parent bdff7aa commit a0b81fe

2 files changed

Lines changed: 316 additions & 1 deletion

File tree

mcp/server.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,5 @@ func (s *Server) Run(ctx context.Context) error {
5353
return s.mcp.Run(ctx, &gomcp.StdioTransport{})
5454
}
5555

56-
func (s *Server) registerWorkflowTools() {}
5756
func (s *Server) registerGenericTool() {}
5857
func (s *Server) registerRawJNITool() {}

mcp/tools_workflow.go

Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
package mcp
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
8+
batteryclient "github.com/AndroidGoLab/jni-proxy/grpc/client/battery"
9+
displayclient "github.com/AndroidGoLab/jni-proxy/grpc/client/display"
10+
locationclient "github.com/AndroidGoLab/jni-proxy/grpc/client/location"
11+
powerclient "github.com/AndroidGoLab/jni-proxy/grpc/client/power"
12+
displaypb "github.com/AndroidGoLab/jni-proxy/proto/display"
13+
handlepb "github.com/AndroidGoLab/jni-proxy/proto/handlestore"
14+
locationpb "github.com/AndroidGoLab/jni-proxy/proto/location"
15+
gomcp "github.com/modelcontextprotocol/go-sdk/mcp"
16+
)
17+
18+
func boolPtr(b bool) *bool { return &b }
19+
20+
// jsonResult marshals v as indented JSON and wraps it in a CallToolResult.
21+
func jsonResult(v any) (*gomcp.CallToolResult, error) {
22+
data, err := json.MarshalIndent(v, "", " ")
23+
if err != nil {
24+
return nil, fmt.Errorf("marshal result: %w", err)
25+
}
26+
return &gomcp.CallToolResult{
27+
Content: []gomcp.Content{&gomcp.TextContent{Text: string(data)}},
28+
}, nil
29+
}
30+
31+
func (s *Server) registerWorkflowTools() {
32+
s.registerBatteryTools()
33+
s.registerLocationTools()
34+
s.registerDisplayTools()
35+
}
36+
37+
// Android BatteryManager property constants.
38+
const (
39+
batteryPropertyChargeCounter int32 = 1
40+
batteryPropertyCurrentNow int32 = 2
41+
batteryPropertyCurrentAverage int32 = 3
42+
batteryPropertyCapacity int32 = 4
43+
batteryPropertyEnergyCounter int32 = 5
44+
batteryPropertyStatus int32 = 6
45+
)
46+
47+
type batteryInput struct{}
48+
49+
type batteryOutput struct {
50+
Capacity int32 `json:"capacity"`
51+
Status int32 `json:"status"`
52+
CurrentNowUA int32 `json:"current_now_ua"`
53+
CurrentAvgUA int32 `json:"current_avg_ua"`
54+
ChargeCounter int32 `json:"charge_counter_uah"`
55+
EnergyCounter int32 `json:"energy_counter_nwh"`
56+
IsCharging bool `json:"is_charging"`
57+
IsInteractive bool `json:"is_interactive"`
58+
IsPowerSave bool `json:"is_power_save_mode"`
59+
IsDeviceIdle bool `json:"is_device_idle_mode"`
60+
ChargeTimeLeft int64 `json:"charge_time_remaining_ms"`
61+
}
62+
63+
func (s *Server) registerBatteryTools() {
64+
gomcp.AddTool(s.mcp, &gomcp.Tool{
65+
Name: "get_battery_status",
66+
Description: "Get battery status: capacity %, charging state, current draw, power save mode, and related power information.",
67+
Annotations: &gomcp.ToolAnnotations{
68+
ReadOnlyHint: true,
69+
DestructiveHint: boolPtr(false),
70+
Title: "Get Battery Status",
71+
},
72+
}, func(ctx context.Context, req *gomcp.CallToolRequest, _ batteryInput) (*gomcp.CallToolResult, batteryOutput, error) {
73+
bat := batteryclient.NewClient(s.conn)
74+
pwr := powerclient.NewClient(s.conn)
75+
76+
var out batteryOutput
77+
var err error
78+
79+
out.Capacity, err = bat.GetIntProperty(ctx, batteryPropertyCapacity)
80+
if err != nil {
81+
return nil, out, fmt.Errorf("get capacity: %w", err)
82+
}
83+
84+
out.Status, err = bat.GetIntProperty(ctx, batteryPropertyStatus)
85+
if err != nil {
86+
return nil, out, fmt.Errorf("get status: %w", err)
87+
}
88+
89+
out.CurrentNowUA, err = bat.GetIntProperty(ctx, batteryPropertyCurrentNow)
90+
if err != nil {
91+
return nil, out, fmt.Errorf("get current_now: %w", err)
92+
}
93+
94+
out.CurrentAvgUA, err = bat.GetIntProperty(ctx, batteryPropertyCurrentAverage)
95+
if err != nil {
96+
return nil, out, fmt.Errorf("get current_avg: %w", err)
97+
}
98+
99+
out.ChargeCounter, err = bat.GetIntProperty(ctx, batteryPropertyChargeCounter)
100+
if err != nil {
101+
return nil, out, fmt.Errorf("get charge_counter: %w", err)
102+
}
103+
104+
out.EnergyCounter, err = bat.GetIntProperty(ctx, batteryPropertyEnergyCounter)
105+
if err != nil {
106+
return nil, out, fmt.Errorf("get energy_counter: %w", err)
107+
}
108+
109+
out.IsCharging, err = bat.IsCharging(ctx)
110+
if err != nil {
111+
return nil, out, fmt.Errorf("is_charging: %w", err)
112+
}
113+
114+
out.ChargeTimeLeft, err = bat.ComputeChargeTimeRemaining(ctx)
115+
if err != nil {
116+
return nil, out, fmt.Errorf("charge_time_remaining: %w", err)
117+
}
118+
119+
out.IsInteractive, err = pwr.IsInteractive(ctx)
120+
if err != nil {
121+
return nil, out, fmt.Errorf("is_interactive: %w", err)
122+
}
123+
124+
out.IsPowerSave, err = pwr.IsPowerSaveMode(ctx)
125+
if err != nil {
126+
return nil, out, fmt.Errorf("is_power_save: %w", err)
127+
}
128+
129+
out.IsDeviceIdle, err = pwr.IsDeviceIdleMode(ctx)
130+
if err != nil {
131+
return nil, out, fmt.Errorf("is_device_idle: %w", err)
132+
}
133+
134+
return nil, out, nil
135+
})
136+
}
137+
138+
type locationInput struct {
139+
Provider string `json:"provider" jsonschema:"default=gps,description=Location provider: gps or network"`
140+
}
141+
142+
type locationOutput struct {
143+
Provider string `json:"provider"`
144+
Latitude float64 `json:"latitude"`
145+
Longitude float64 `json:"longitude"`
146+
Altitude float64 `json:"altitude"`
147+
Accuracy float32 `json:"accuracy"`
148+
Speed float32 `json:"speed"`
149+
Bearing float32 `json:"bearing"`
150+
Time int64 `json:"time"`
151+
}
152+
153+
func (s *Server) registerLocationTools() {
154+
gomcp.AddTool(s.mcp, &gomcp.Tool{
155+
Name: "get_location",
156+
Description: "Get last known GPS or network location: latitude, longitude, altitude, accuracy, speed, bearing, and timestamp.",
157+
Annotations: &gomcp.ToolAnnotations{
158+
ReadOnlyHint: true,
159+
DestructiveHint: boolPtr(false),
160+
Title: "Get Location",
161+
},
162+
}, func(ctx context.Context, req *gomcp.CallToolRequest, in locationInput) (*gomcp.CallToolResult, locationOutput, error) {
163+
provider := in.Provider
164+
if provider == "" {
165+
provider = "gps"
166+
}
167+
168+
locMgr := locationclient.NewClient(s.conn)
169+
handle, err := locMgr.GetLastKnownLocation(ctx, provider)
170+
if err != nil {
171+
return nil, locationOutput{}, fmt.Errorf("get last known location: %w", err)
172+
}
173+
if handle == 0 {
174+
return nil, locationOutput{}, fmt.Errorf("no location available for provider %q (null handle)", provider)
175+
}
176+
177+
// Release the handle when done.
178+
handles := handlepb.NewHandleStoreServiceClient(s.conn)
179+
defer func() {
180+
_, _ = handles.ReleaseHandle(ctx, &handlepb.ReleaseHandleRequest{Handle: handle})
181+
}()
182+
183+
// Query location properties via the LocationService.
184+
// The server maps object-level RPCs to the handle returned above.
185+
locSvc := locationpb.NewLocationServiceClient(s.conn)
186+
187+
var out locationOutput
188+
out.Provider = provider
189+
190+
latResp, err := locSvc.GetLatitude(ctx, &locationpb.GetLatitudeRequest{})
191+
if err != nil {
192+
return nil, out, fmt.Errorf("get latitude: %w", err)
193+
}
194+
out.Latitude = latResp.GetResult()
195+
196+
lngResp, err := locSvc.GetLongitude(ctx, &locationpb.GetLongitudeRequest{})
197+
if err != nil {
198+
return nil, out, fmt.Errorf("get longitude: %w", err)
199+
}
200+
out.Longitude = lngResp.GetResult()
201+
202+
altResp, err := locSvc.GetAltitude(ctx, &locationpb.GetAltitudeRequest{})
203+
if err != nil {
204+
return nil, out, fmt.Errorf("get altitude: %w", err)
205+
}
206+
out.Altitude = altResp.GetResult()
207+
208+
accResp, err := locSvc.GetAccuracy(ctx, &locationpb.GetAccuracyRequest{})
209+
if err != nil {
210+
return nil, out, fmt.Errorf("get accuracy: %w", err)
211+
}
212+
out.Accuracy = accResp.GetResult()
213+
214+
speedResp, err := locSvc.GetSpeed(ctx, &locationpb.GetSpeedRequest{})
215+
if err != nil {
216+
return nil, out, fmt.Errorf("get speed: %w", err)
217+
}
218+
out.Speed = speedResp.GetResult()
219+
220+
bearingResp, err := locSvc.GetBearing(ctx, &locationpb.GetBearingRequest{})
221+
if err != nil {
222+
return nil, out, fmt.Errorf("get bearing: %w", err)
223+
}
224+
out.Bearing = bearingResp.GetResult()
225+
226+
timeResp, err := locSvc.GetTime(ctx, &locationpb.GetTimeRequest{})
227+
if err != nil {
228+
return nil, out, fmt.Errorf("get time: %w", err)
229+
}
230+
out.Time = timeResp.GetResult()
231+
232+
return nil, out, nil
233+
})
234+
}
235+
236+
type displayInput struct{}
237+
238+
type displayOutput struct {
239+
Width int32 `json:"width"`
240+
Height int32 `json:"height"`
241+
Rotation int32 `json:"rotation"`
242+
RefreshRate float32 `json:"refresh_rate"`
243+
State int32 `json:"state"`
244+
Name string `json:"name"`
245+
}
246+
247+
func (s *Server) registerDisplayTools() {
248+
gomcp.AddTool(s.mcp, &gomcp.Tool{
249+
Name: "get_display_info",
250+
Description: "Get display information: screen resolution, rotation, refresh rate, state, and name.",
251+
Annotations: &gomcp.ToolAnnotations{
252+
ReadOnlyHint: true,
253+
DestructiveHint: boolPtr(false),
254+
Title: "Get Display Info",
255+
},
256+
}, func(ctx context.Context, req *gomcp.CallToolRequest, _ displayInput) (*gomcp.CallToolResult, displayOutput, error) {
257+
dispMgr := displayclient.NewClient(s.conn)
258+
handle, err := dispMgr.GetDefaultDisplay(ctx)
259+
if err != nil {
260+
return nil, displayOutput{}, fmt.Errorf("get default display: %w", err)
261+
}
262+
if handle == 0 {
263+
return nil, displayOutput{}, fmt.Errorf("no default display available (null handle)")
264+
}
265+
266+
// Release the handle when done.
267+
handles := handlepb.NewHandleStoreServiceClient(s.conn)
268+
defer func() {
269+
_, _ = handles.ReleaseHandle(ctx, &handlepb.ReleaseHandleRequest{Handle: handle})
270+
}()
271+
272+
// Query display properties via the DisplayService.
273+
// The server maps object-level RPCs to the handle returned above.
274+
dispSvc := displaypb.NewDisplayServiceClient(s.conn)
275+
276+
var out displayOutput
277+
278+
wResp, err := dispSvc.GetWidth(ctx, &displaypb.GetWidthRequest{})
279+
if err != nil {
280+
return nil, out, fmt.Errorf("get width: %w", err)
281+
}
282+
out.Width = wResp.GetResult()
283+
284+
hResp, err := dispSvc.GetHeight(ctx, &displaypb.GetHeightRequest{})
285+
if err != nil {
286+
return nil, out, fmt.Errorf("get height: %w", err)
287+
}
288+
out.Height = hResp.GetResult()
289+
290+
rotResp, err := dispSvc.GetRotation(ctx, &displaypb.GetRotationRequest{})
291+
if err != nil {
292+
return nil, out, fmt.Errorf("get rotation: %w", err)
293+
}
294+
out.Rotation = rotResp.GetResult()
295+
296+
rrResp, err := dispSvc.GetRefreshRate(ctx, &displaypb.GetRefreshRateRequest{})
297+
if err != nil {
298+
return nil, out, fmt.Errorf("get refresh rate: %w", err)
299+
}
300+
out.RefreshRate = rrResp.GetResult()
301+
302+
stResp, err := dispSvc.GetState(ctx, &displaypb.GetStateRequest{})
303+
if err != nil {
304+
return nil, out, fmt.Errorf("get state: %w", err)
305+
}
306+
out.State = stResp.GetResult()
307+
308+
nameResp, err := dispSvc.GetName(ctx, &displaypb.GetNameRequest{})
309+
if err != nil {
310+
return nil, out, fmt.Errorf("get name: %w", err)
311+
}
312+
out.Name = nameResp.GetResult()
313+
314+
return nil, out, nil
315+
})
316+
}

0 commit comments

Comments
 (0)