Skip to content

Commit 4302ad7

Browse files
xaionaro@dx.centerxaionaro@dx.center
authored andcommitted
feat: binder-mcp P0 tools — power, display, packages, audio, camera, network, battery
Add 22 new MCP tools covering device state inspection and control: Binder-based (via raw parcel transactions): - is_screen_on, wake_screen, sleep_screen (IPowerManager) - get_brightness, set_brightness (IDisplayManager) - get_display_size (IWindowManager) - list_packages, get_package_info (IPackageManager) - stop_app (IActivityManager.forceStopPackage) - list_cameras (ICameraService.getNumberOfCameras) - get_media_volume, set_media_volume (IAudioService) - get_bluetooth_state (IBluetoothManager.getState) Shell-based (dumpsys/sysfs fallbacks for complex parcelables): - get_clipboard, set_clipboard - launch_app (monkey/am start) - get_current_app, get_focused_activity (dumpsys activity) - get_wifi_state (cmd wifi status) - get_telephony_info (dumpsys telephony.registry) - list_notifications (dumpsys notification) - get_battery_info (dumpsys battery / sysfs) All tools verified on-device via MCP JSON-RPC.
1 parent 975f141 commit 4302ad7

23 files changed

Lines changed: 2023 additions & 0 deletions
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
//go:build linux
2+
3+
package main
4+
5+
import (
6+
"context"
7+
"encoding/json"
8+
"fmt"
9+
"strings"
10+
11+
"github.com/facebookincubator/go-belt/tool/logger"
12+
"github.com/mark3labs/mcp-go/mcp"
13+
"github.com/mark3labs/mcp-go/server"
14+
)
15+
16+
// BatteryInfoResult holds the get_battery_info response.
17+
type BatteryInfoResult struct {
18+
Level string `json:"level"`
19+
Status string `json:"status"`
20+
Temperature string `json:"temperature,omitempty"`
21+
Voltage string `json:"voltage,omitempty"`
22+
Technology string `json:"technology,omitempty"`
23+
Health string `json:"health,omitempty"`
24+
Error string `json:"error,omitempty"`
25+
}
26+
27+
func registerGetBatteryInfo(s *server.MCPServer) {
28+
tool := mcp.NewTool("get_battery_info",
29+
mcp.WithDescription(
30+
"Get battery information (level, status, temperature, voltage) "+
31+
"from /sys/class/power_supply/ sysfs entries. More reliable "+
32+
"than binder for shell UID.",
33+
),
34+
mcp.WithReadOnlyHintAnnotation(true),
35+
mcp.WithDestructiveHintAnnotation(false),
36+
mcp.WithIdempotentHintAnnotation(true),
37+
)
38+
39+
s.AddTool(tool, handleGetBatteryInfo)
40+
}
41+
42+
func handleGetBatteryInfo(
43+
ctx context.Context,
44+
_ mcp.CallToolRequest,
45+
) (*mcp.CallToolResult, error) {
46+
logger.Tracef(ctx, "handleGetBatteryInfo")
47+
defer func() { logger.Tracef(ctx, "/handleGetBatteryInfo") }()
48+
49+
result := BatteryInfoResult{}
50+
51+
// Try dumpsys battery first (most reliable on Android).
52+
out, err := shellExec("dumpsys battery")
53+
if err == nil {
54+
result = parseBatteryDumpsys(out)
55+
} else {
56+
// Fall back to sysfs.
57+
result = readBatterySysfs()
58+
}
59+
60+
data, err := json.Marshal(result)
61+
if err != nil {
62+
return nil, fmt.Errorf("marshaling battery info: %w", err)
63+
}
64+
65+
return mcp.NewToolResultText(string(data)), nil
66+
}
67+
68+
func parseBatteryDumpsys(output string) BatteryInfoResult {
69+
result := BatteryInfoResult{}
70+
for _, line := range strings.Split(output, "\n") {
71+
line = strings.TrimSpace(line)
72+
parts := strings.SplitN(line, ":", 2)
73+
if len(parts) != 2 {
74+
continue
75+
}
76+
key := strings.TrimSpace(parts[0])
77+
val := strings.TrimSpace(parts[1])
78+
switch key {
79+
case "level":
80+
result.Level = val
81+
case "status":
82+
result.Status = batteryStatusName(val)
83+
case "temperature":
84+
result.Temperature = val
85+
case "voltage":
86+
result.Voltage = val
87+
case "technology":
88+
result.Technology = val
89+
case "health":
90+
result.Health = batteryHealthName(val)
91+
}
92+
}
93+
return result
94+
}
95+
96+
func readBatterySysfs() BatteryInfoResult {
97+
result := BatteryInfoResult{}
98+
readSysfs := func(path string) string {
99+
out, err := shellExec("cat " + path + " 2>/dev/null")
100+
if err != nil {
101+
return ""
102+
}
103+
return strings.TrimSpace(out)
104+
}
105+
106+
result.Level = readSysfs("/sys/class/power_supply/battery/capacity")
107+
result.Status = readSysfs("/sys/class/power_supply/battery/status")
108+
result.Temperature = readSysfs("/sys/class/power_supply/battery/temp")
109+
result.Voltage = readSysfs("/sys/class/power_supply/battery/voltage_now")
110+
result.Technology = readSysfs("/sys/class/power_supply/battery/technology")
111+
result.Health = readSysfs("/sys/class/power_supply/battery/health")
112+
113+
return result
114+
}
115+
116+
func batteryStatusName(code string) string {
117+
switch code {
118+
case "1":
119+
return "unknown"
120+
case "2":
121+
return "charging"
122+
case "3":
123+
return "discharging"
124+
case "4":
125+
return "not_charging"
126+
case "5":
127+
return "full"
128+
default:
129+
return code
130+
}
131+
}
132+
133+
func batteryHealthName(code string) string {
134+
switch code {
135+
case "1":
136+
return "unknown"
137+
case "2":
138+
return "good"
139+
case "3":
140+
return "overheat"
141+
case "4":
142+
return "dead"
143+
case "5":
144+
return "over_voltage"
145+
case "6":
146+
return "failure"
147+
case "7":
148+
return "cold"
149+
default:
150+
return code
151+
}
152+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
//go:build linux
2+
3+
package main
4+
5+
import (
6+
"context"
7+
"encoding/json"
8+
"fmt"
9+
10+
"github.com/facebookincubator/go-belt/tool/logger"
11+
"github.com/mark3labs/mcp-go/mcp"
12+
"github.com/mark3labs/mcp-go/server"
13+
14+
"github.com/AndroidGoLab/binder/binder"
15+
"github.com/AndroidGoLab/binder/parcel"
16+
"github.com/AndroidGoLab/binder/servicemanager"
17+
)
18+
19+
const bluetoothManagerDescriptor = "android.bluetooth.IBluetoothManager"
20+
21+
// BluetoothState maps the int32 returned by IBluetoothManager.getState().
22+
type BluetoothState int32
23+
24+
const (
25+
BluetoothStateOff BluetoothState = 10
26+
BluetoothStateTurningOn BluetoothState = 11
27+
BluetoothStateOn BluetoothState = 12
28+
BluetoothStateTurningOff BluetoothState = 13
29+
BluetoothStateBLETurningOn BluetoothState = 14
30+
BluetoothStateBLEOn BluetoothState = 15
31+
BluetoothStateBLETurnOff BluetoothState = 16
32+
)
33+
34+
// BluetoothStateResult holds the get_bluetooth_state response.
35+
type BluetoothStateResult struct {
36+
StateCode int32 `json:"state_code"`
37+
State string `json:"state"`
38+
Error string `json:"error,omitempty"`
39+
}
40+
41+
func (ts *ToolSet) registerGetBluetoothState(s *server.MCPServer) {
42+
tool := mcp.NewTool("get_bluetooth_state",
43+
mcp.WithDescription(
44+
"Get the Bluetooth adapter state using IBluetoothManager.getState(). "+
45+
"Returns state_code and human-readable state (off/turning_on/on/turning_off).",
46+
),
47+
mcp.WithReadOnlyHintAnnotation(true),
48+
mcp.WithDestructiveHintAnnotation(false),
49+
mcp.WithIdempotentHintAnnotation(true),
50+
)
51+
52+
s.AddTool(tool, ts.handleGetBluetoothState)
53+
}
54+
55+
func (ts *ToolSet) handleGetBluetoothState(
56+
ctx context.Context,
57+
_ mcp.CallToolRequest,
58+
) (*mcp.CallToolResult, error) {
59+
logger.Tracef(ctx, "handleGetBluetoothState")
60+
defer func() { logger.Tracef(ctx, "/handleGetBluetoothState") }()
61+
62+
svc, err := ts.sm.CheckService(ctx, servicemanager.ServiceName("bluetooth_manager"))
63+
if err != nil || svc == nil {
64+
return mcp.NewToolResultError("bluetooth_manager service unavailable"), nil
65+
}
66+
67+
code, err := svc.ResolveCode(ctx, bluetoothManagerDescriptor, "getState")
68+
if err != nil {
69+
return mcp.NewToolResultError(fmt.Sprintf("resolving getState: %v", err)), nil
70+
}
71+
72+
data := parcel.New()
73+
defer data.Recycle()
74+
data.WriteInterfaceToken(bluetoothManagerDescriptor)
75+
76+
reply, err := svc.Transact(ctx, code, 0, data)
77+
if err != nil {
78+
return mcp.NewToolResultError(fmt.Sprintf("getState: %v", err)), nil
79+
}
80+
defer reply.Recycle()
81+
82+
if err := binder.ReadStatus(reply); err != nil {
83+
return mcp.NewToolResultError(fmt.Sprintf("getState status: %v", err)), nil
84+
}
85+
86+
stateCode, err := reply.ReadInt32()
87+
if err != nil {
88+
return mcp.NewToolResultError(fmt.Sprintf("reading state: %v", err)), nil
89+
}
90+
91+
result := BluetoothStateResult{
92+
StateCode: stateCode,
93+
State: bluetoothStateString(BluetoothState(stateCode)),
94+
}
95+
96+
out, err := json.Marshal(result)
97+
if err != nil {
98+
return nil, fmt.Errorf("marshaling bluetooth state: %w", err)
99+
}
100+
101+
return mcp.NewToolResultText(string(out)), nil
102+
}
103+
104+
func bluetoothStateString(state BluetoothState) string {
105+
switch state {
106+
case BluetoothStateOff:
107+
return "off"
108+
case BluetoothStateTurningOn:
109+
return "turning_on"
110+
case BluetoothStateOn:
111+
return "on"
112+
case BluetoothStateTurningOff:
113+
return "turning_off"
114+
case BluetoothStateBLETurningOn:
115+
return "ble_turning_on"
116+
case BluetoothStateBLEOn:
117+
return "ble_on"
118+
case BluetoothStateBLETurnOff:
119+
return "ble_turning_off"
120+
default:
121+
return fmt.Sprintf("unknown(%d)", state)
122+
}
123+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
//go:build linux
2+
3+
package main
4+
5+
import (
6+
"context"
7+
"encoding/json"
8+
"fmt"
9+
10+
"github.com/facebookincubator/go-belt/tool/logger"
11+
"github.com/mark3labs/mcp-go/mcp"
12+
"github.com/mark3labs/mcp-go/server"
13+
14+
"github.com/AndroidGoLab/binder/binder"
15+
"github.com/AndroidGoLab/binder/parcel"
16+
"github.com/AndroidGoLab/binder/servicemanager"
17+
)
18+
19+
// BrightnessResult holds the get_brightness response.
20+
type BrightnessResult struct {
21+
Brightness float32 `json:"brightness"`
22+
Error string `json:"error,omitempty"`
23+
}
24+
25+
func (ts *ToolSet) registerGetBrightness(s *server.MCPServer) {
26+
tool := mcp.NewTool("get_brightness",
27+
mcp.WithDescription(
28+
"Get the current display brightness level (0.0 to 1.0) from "+
29+
"IDisplayManager.getBrightness().",
30+
),
31+
mcp.WithReadOnlyHintAnnotation(true),
32+
mcp.WithDestructiveHintAnnotation(false),
33+
mcp.WithIdempotentHintAnnotation(true),
34+
)
35+
36+
s.AddTool(tool, ts.handleGetBrightness)
37+
}
38+
39+
func (ts *ToolSet) handleGetBrightness(
40+
ctx context.Context,
41+
_ mcp.CallToolRequest,
42+
) (*mcp.CallToolResult, error) {
43+
logger.Tracef(ctx, "handleGetBrightness")
44+
defer func() { logger.Tracef(ctx, "/handleGetBrightness") }()
45+
46+
svc, err := ts.sm.CheckService(ctx, servicemanager.ServiceName("display"))
47+
if err != nil || svc == nil {
48+
return mcp.NewToolResultError("display service unavailable"), nil
49+
}
50+
51+
code, err := svc.ResolveCode(ctx, displayManagerDescriptor, "getBrightness")
52+
if err != nil {
53+
return mcp.NewToolResultError(fmt.Sprintf("resolving getBrightness: %v", err)), nil
54+
}
55+
56+
data := parcel.New()
57+
defer data.Recycle()
58+
data.WriteInterfaceToken(displayManagerDescriptor)
59+
// getBrightness(int displayId) -- use display 0 (default).
60+
data.WriteInt32(0)
61+
62+
reply, err := svc.Transact(ctx, code, 0, data)
63+
if err != nil {
64+
return mcp.NewToolResultError(fmt.Sprintf("getBrightness: %v", err)), nil
65+
}
66+
defer reply.Recycle()
67+
68+
if err := binder.ReadStatus(reply); err != nil {
69+
return mcp.NewToolResultError(fmt.Sprintf("getBrightness status: %v", err)), nil
70+
}
71+
72+
brightness, err := reply.ReadFloat32()
73+
if err != nil {
74+
return mcp.NewToolResultError(fmt.Sprintf("reading brightness: %v", err)), nil
75+
}
76+
77+
out, err := json.Marshal(BrightnessResult{Brightness: brightness})
78+
if err != nil {
79+
return nil, fmt.Errorf("marshaling brightness: %w", err)
80+
}
81+
82+
return mcp.NewToolResultText(string(out)), nil
83+
}

0 commit comments

Comments
 (0)