Skip to content

Commit e262549

Browse files
xaionaro@dx.centerxaionaro@dx.center
authored andcommitted
test(mcp): add integration test for tool and resource registration
Fix jsonschema struct tags that used unsupported `WORD=` prefix syntax (e.g. `description=...`, `default=...`, `enum=...`). The google/jsonschema-go library treats the tag value as a plain description string and rejects tags beginning with `WORD=`. Add server_test.go that uses the SDK in-memory transport to verify: - All 54 tools are registered (52 workflow + 1 generic + 1 raw JNI) - The jni://services static resource is listed - Reading jni://services returns valid JSON with service names
1 parent 055cbbd commit e262549

2 files changed

Lines changed: 125 additions & 41 deletions

File tree

mcp/server_test.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package mcp_test
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"log/slog"
7+
"testing"
8+
9+
mcpserver "github.com/AndroidGoLab/jni-proxy/mcp"
10+
gomcp "github.com/modelcontextprotocol/go-sdk/mcp"
11+
"github.com/stretchr/testify/require"
12+
"google.golang.org/grpc"
13+
"google.golang.org/grpc/credentials/insecure"
14+
)
15+
16+
func TestServerToolsAndResources(t *testing.T) {
17+
// Create dummy gRPC conn (not used for tool/resource listing).
18+
conn, err := grpc.NewClient("passthrough:///dummy",
19+
grpc.WithTransportCredentials(insecure.NewCredentials()))
20+
require.NoError(t, err)
21+
defer conn.Close()
22+
23+
// Create our MCP server wrapper.
24+
log := slog.Default()
25+
srv := mcpserver.NewServer(conn, log)
26+
27+
// Create in-memory transports. Server must be connected first.
28+
clientTransport, serverTransport := gomcp.NewInMemoryTransports()
29+
30+
ctx := context.Background()
31+
32+
// Connect the MCP server to the server-side transport.
33+
serverSession, err := srv.MCPServer().Connect(ctx, serverTransport, nil)
34+
require.NoError(t, err)
35+
defer serverSession.Close()
36+
37+
// Create and connect the MCP client to the client-side transport.
38+
client := gomcp.NewClient(
39+
&gomcp.Implementation{Name: "test-client", Version: "0.0.1"}, nil)
40+
clientSession, err := client.Connect(ctx, clientTransport, nil)
41+
require.NoError(t, err)
42+
defer clientSession.Close()
43+
44+
// --- List tools ---
45+
toolsResult, err := clientSession.ListTools(ctx, nil)
46+
require.NoError(t, err)
47+
48+
// 52 workflow + 1 generic (call_android_api) + 1 raw (jni_raw) = 54
49+
require.Len(t, toolsResult.Tools, 54,
50+
"expected 54 tools (52 workflow + 1 generic + 1 raw)")
51+
52+
toolNames := make(map[string]bool, len(toolsResult.Tools))
53+
for _, tool := range toolsResult.Tools {
54+
toolNames[tool.Name] = true
55+
}
56+
require.True(t, toolNames["get_battery_status"],
57+
"workflow tool get_battery_status must be registered")
58+
require.True(t, toolNames["call_android_api"],
59+
"generic tool call_android_api must be registered")
60+
require.True(t, toolNames["jni_raw"],
61+
"raw JNI tool jni_raw must be registered")
62+
63+
// --- List resources ---
64+
resourcesResult, err := clientSession.ListResources(ctx, nil)
65+
require.NoError(t, err)
66+
require.Len(t, resourcesResult.Resources, 1,
67+
"expected exactly 1 static resource (jni://services)")
68+
require.Equal(t, "jni://services", resourcesResult.Resources[0].URI)
69+
70+
// --- Read the services resource ---
71+
readResult, err := clientSession.ReadResource(ctx,
72+
&gomcp.ReadResourceParams{URI: "jni://services"})
73+
require.NoError(t, err)
74+
require.Len(t, readResult.Contents, 1, "expected 1 content block")
75+
require.NotEmpty(t, readResult.Contents[0].Text,
76+
"services resource must return non-empty text")
77+
78+
// The text should be valid JSON (an array of service names).
79+
var services []string
80+
err = json.Unmarshal([]byte(readResult.Contents[0].Text), &services)
81+
require.NoError(t, err, "services resource must return valid JSON array")
82+
require.Greater(t, len(services), 0,
83+
"services list must contain at least one entry")
84+
}

mcp/tools_workflow.go

Lines changed: 41 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ func (s *Server) registerBatteryTools() {
182182
}
183183

184184
type locationInput struct {
185-
Provider string `json:"provider" jsonschema:"default=gps,description=Location provider: gps or network"`
185+
Provider string `json:"provider" jsonschema:"Location provider: gps or network (default: gps)"`
186186
}
187187

188188
type locationOutput struct {
@@ -440,11 +440,11 @@ type scanWifiOutput struct {
440440
}
441441

442442
type connectWifiInput struct {
443-
NetworkID int32 `json:"network_id" jsonschema:"description=Network ID to enable and connect to (from configured networks)"`
443+
NetworkID int32 `json:"network_id" jsonschema:"Network ID to enable and connect to (from configured networks)"`
444444
}
445445

446446
type setWifiEnabledInput struct {
447-
Enabled bool `json:"enabled" jsonschema:"description=true to enable WiFi or false to disable"`
447+
Enabled bool `json:"enabled" jsonschema:"true to enable WiFi or false to disable"`
448448
}
449449

450450
type setWifiEnabledOutput struct {
@@ -788,13 +788,13 @@ type audioOutput struct {
788788
}
789789

790790
type setVolumeInput struct {
791-
Stream int32 `json:"stream" jsonschema:"description=Audio stream type: 0=voice_call 1=system 2=ring 3=music 4=alarm 5=notification"`
792-
Volume int32 `json:"volume" jsonschema:"description=Volume level to set (0 to stream max)"`
793-
Flags int32 `json:"flags" jsonschema:"default=0,description=Flags: 0=none 1=show_ui 2=allow_ringer_modes 4=play_sound 8=remove_sound_and_vibrate 16=vibrate"`
791+
Stream int32 `json:"stream" jsonschema:"Audio stream type: 0=voice_call 1=system 2=ring 3=music 4=alarm 5=notification"`
792+
Volume int32 `json:"volume" jsonschema:"Volume level to set (0 to stream max)"`
793+
Flags int32 `json:"flags" jsonschema:"Flags: 0=none 1=show_ui 2=allow_ringer_modes 4=play_sound 8=remove_sound_and_vibrate 16=vibrate (default: 0)"`
794794
}
795795

796796
type setRingerModeInput struct {
797-
Mode int32 `json:"mode" jsonschema:"description=Ringer mode: 0=silent 1=vibrate 2=normal"`
797+
Mode int32 `json:"mode" jsonschema:"Ringer mode: 0=silent 1=vibrate 2=normal"`
798798
}
799799

800800
func (s *Server) registerAudioTools() {
@@ -958,7 +958,7 @@ type clipboardOutput struct {
958958
}
959959

960960
type setClipboardInput struct {
961-
Text string `json:"text" jsonschema:"description=Text to copy to the clipboard"`
961+
Text string `json:"text" jsonschema:"Text to copy to the clipboard"`
962962
}
963963

964964
func (s *Server) registerClipboardTools() {
@@ -1028,7 +1028,7 @@ func (s *Server) registerClipboardTools() {
10281028
// ---------------------------------------------------------------------------
10291029

10301030
type cancelNotifInput struct {
1031-
ID int32 `json:"id" jsonschema:"description=Notification ID to cancel"`
1031+
ID int32 `json:"id" jsonschema:"Notification ID to cancel"`
10321032
}
10331033

10341034
type notifStatusInput struct{}
@@ -1046,9 +1046,9 @@ type notifStatusOutput struct {
10461046
func (s *Server) registerNotificationTools() {
10471047
// send_notification — stub (requires Java object construction)
10481048
type sendNotifInput struct {
1049-
Title string `json:"title" jsonschema:"description=Notification title"`
1050-
Text string `json:"text" jsonschema:"description=Notification body text"`
1051-
Channel string `json:"channel" jsonschema:"description=Notification channel ID"`
1049+
Title string `json:"title" jsonschema:"Notification title"`
1050+
Text string `json:"text" jsonschema:"Notification body text"`
1051+
Channel string `json:"channel" jsonschema:"Notification channel ID"`
10521052
}
10531053
gomcp.AddTool(s.mcp, &gomcp.Tool{
10541054
Name: "send_notification",
@@ -1151,7 +1151,7 @@ func (s *Server) registerNotificationTools() {
11511151
// ---------------------------------------------------------------------------
11521152

11531153
type vibrateInput struct {
1154-
DurationMS int64 `json:"duration_ms" jsonschema:"description=Vibration duration in milliseconds"`
1154+
DurationMS int64 `json:"duration_ms" jsonschema:"Vibration duration in milliseconds"`
11551155
}
11561156

11571157
type vibratorStatusInput struct{}
@@ -1243,8 +1243,8 @@ type irStatusOutput struct {
12431243
func (s *Server) registerIRTools() {
12441244
// ir_transmit — stub (requires int[] handle for pattern)
12451245
type irTransmitInput struct {
1246-
Frequency int32 `json:"frequency" jsonschema:"description=IR carrier frequency in Hz"`
1247-
Pattern []int32 `json:"pattern" jsonschema:"description=Pattern of on/off durations in microseconds"`
1246+
Frequency int32 `json:"frequency" jsonschema:"IR carrier frequency in Hz"`
1247+
Pattern []int32 `json:"pattern" jsonschema:"Pattern of on/off durations in microseconds"`
12481248
}
12491249
gomcp.AddTool(s.mcp, &gomcp.Tool{
12501250
Name: "ir_transmit",
@@ -1339,7 +1339,7 @@ func (s *Server) registerCameraTools() {
13391339

13401340
// take_photo — stub: requires streaming RPC for camera capture pipeline
13411341
type takePhotoInput struct {
1342-
CameraID string `json:"camera_id" jsonschema:"description=Camera ID to capture from (from list_cameras)"`
1342+
CameraID string `json:"camera_id" jsonschema:"Camera ID to capture from (from list_cameras)"`
13431343
}
13441344
gomcp.AddTool(s.mcp, &gomcp.Tool{
13451345
Name: "take_photo",
@@ -1398,8 +1398,8 @@ func (s *Server) registerCameraTools() {
13981398
// ---------------------------------------------------------------------------
13991399

14001400
type setAlarmInput struct {
1401-
Type int32 `json:"type" jsonschema:"description=Alarm type: 0=RTC_WAKEUP 1=RTC 2=ELAPSED_REALTIME_WAKEUP 3=ELAPSED_REALTIME"`
1402-
TriggerMillis int64 `json:"trigger_millis" jsonschema:"description=Trigger time in milliseconds (RTC types: epoch millis; ELAPSED types: millis since boot)"`
1401+
Type int32 `json:"type" jsonschema:"Alarm type: 0=RTC_WAKEUP 1=RTC 2=ELAPSED_REALTIME_WAKEUP 3=ELAPSED_REALTIME"`
1402+
TriggerMillis int64 `json:"trigger_millis" jsonschema:"Trigger time in milliseconds (RTC types: epoch millis; ELAPSED types: millis since boot)"`
14031403
}
14041404

14051405
type getNextAlarmInput struct{}
@@ -1410,8 +1410,8 @@ type getNextAlarmOutput struct {
14101410
}
14111411

14121412
type manageJobsInput struct {
1413-
Action string `json:"action" jsonschema:"enum=list,enum=cancel,enum=cancel_all,description=Action to perform: list pending jobs, cancel a specific job by ID, or cancel all jobs"`
1414-
JobID int32 `json:"job_id,omitempty" jsonschema:"description=Job ID for cancel action (ignored for list and cancel_all)"`
1413+
Action string `json:"action" jsonschema:"Action to perform: list pending jobs, cancel a specific job by ID, or cancel all jobs (valid: list, cancel, cancel_all)"`
1414+
JobID int32 `json:"job_id,omitempty" jsonschema:"Job ID for cancel action (ignored for list and cancel_all)"`
14151415
}
14161416

14171417
func (s *Server) registerSchedulingTools() {
@@ -1712,8 +1712,8 @@ type getInputMethodsOutput struct {
17121712
}
17131713

17141714
type toggleKeyboardInput struct {
1715-
ShowFlags int32 `json:"show_flags" jsonschema:"default=0,description=Show flags for ToggleSoftInput: 0=implicit 1=forced 2=not_always"`
1716-
HideFlags int32 `json:"hide_flags" jsonschema:"default=0,description=Hide flags for ToggleSoftInput: 0=implicit 1=not_always"`
1715+
ShowFlags int32 `json:"show_flags" jsonschema:"Show flags for ToggleSoftInput: 0=implicit 1=forced 2=not_always (default: 0)"`
1716+
HideFlags int32 `json:"hide_flags" jsonschema:"Hide flags for ToggleSoftInput: 0=implicit 1=not_always (default: 0)"`
17171717
}
17181718

17191719
func (s *Server) registerInputTools() {
@@ -1990,8 +1990,8 @@ func (s *Server) registerStorageTools() {
19901990

19911991
// manage_downloads — mutation
19921992
type downloadInput struct {
1993-
Action string `json:"action" jsonschema:"enum=get_mime_type,enum=remove,description=Action to perform: get_mime_type or remove"`
1994-
DownloadID int64 `json:"download_id" jsonschema:"description=Download ID to operate on"`
1993+
Action string `json:"action" jsonschema:"Action to perform: get_mime_type or remove (valid: get_mime_type, remove)"`
1994+
DownloadID int64 `json:"download_id" jsonschema:"Download ID to operate on"`
19951995
}
19961996
gomcp.AddTool(s.mcp, &gomcp.Tool{
19971997
Name: "manage_downloads",
@@ -2045,7 +2045,7 @@ func (s *Server) registerStorageTools() {
20452045
func (s *Server) registerAppsTools() {
20462046
// get_app_usage — read-only
20472047
type appUsageInput struct {
2048-
PackageName string `json:"package_name,omitempty" jsonschema:"description=Package name to check inactivity for (optional)"`
2048+
PackageName string `json:"package_name,omitempty" jsonschema:"Package name to check inactivity for (optional)"`
20492049
}
20502050
type appUsageOutput struct {
20512051
StandbyBucket int32 `json:"standby_bucket"`
@@ -2086,7 +2086,7 @@ func (s *Server) registerAppsTools() {
20862086

20872087
// check_permissions — read-only
20882088
type checkPermInput struct {
2089-
RoleName string `json:"role_name" jsonschema:"description=Android role name to check (e.g. android.app.role.DIALER, android.app.role.SMS, android.app.role.BROWSER)"`
2089+
RoleName string `json:"role_name" jsonschema:"Android role name to check (e.g. android.app.role.DIALER, android.app.role.SMS, android.app.role.BROWSER)"`
20902090
}
20912091
type checkPermOutput struct {
20922092
RoleName string `json:"role_name"`
@@ -2130,7 +2130,7 @@ func (s *Server) registerAppsTools() {
21302130
func (s *Server) registerAccountsTools() {
21312131
// list_accounts — read-only
21322132
type listAccountsInput struct {
2133-
AccountType string `json:"account_type,omitempty" jsonschema:"description=Optional account type filter (e.g. com.google). If empty returns all accounts."`
2133+
AccountType string `json:"account_type,omitempty" jsonschema:"Optional account type filter (e.g. com.google). If empty returns all accounts."`
21342134
}
21352135
type listAccountsOutput struct {
21362136
AccountsHandle int64 `json:"accounts_handle"`
@@ -2181,9 +2181,9 @@ func (s *Server) registerAccountsTools() {
21812181
func (s *Server) registerCompanionTools() {
21822182
// manage_companions — read for list, mutation for disassociate
21832183
type companionInput struct {
2184-
Action string `json:"action" jsonschema:"enum=list,enum=disassociate,description=Action: list (get associations) or disassociate (remove by device ID)"`
2185-
DeviceID int32 `json:"device_id,omitempty" jsonschema:"description=Association ID for disassociate action"`
2186-
MacAddress string `json:"mac_address,omitempty" jsonschema:"description=MAC address string for disassociate action (alternative to device_id)"`
2184+
Action string `json:"action" jsonschema:"Action: list (get associations) or disassociate (remove by device ID) (valid: list, disassociate)"`
2185+
DeviceID int32 `json:"device_id,omitempty" jsonschema:"Association ID for disassociate action"`
2186+
MacAddress string `json:"mac_address,omitempty" jsonschema:"MAC address string for disassociate action (alternative to device_id)"`
21872187
}
21882188
gomcp.AddTool(s.mcp, &gomcp.Tool{
21892189
Name: "manage_companions",
@@ -2247,8 +2247,8 @@ func (s *Server) registerCompanionTools() {
22472247
func (s *Server) registerSettingsTools() {
22482248
// get_settings — stub
22492249
type getSettingsInput struct {
2250-
Namespace string `json:"namespace" jsonschema:"enum=system,enum=secure,enum=global,description=Settings namespace: system, secure, or global"`
2251-
Name string `json:"name" jsonschema:"description=Setting name (e.g. screen_brightness, screen_off_timeout)"`
2250+
Namespace string `json:"namespace" jsonschema:"Settings namespace: system, secure, or global (valid: system, secure, global)"`
2251+
Name string `json:"name" jsonschema:"Setting name (e.g. screen_brightness, screen_off_timeout)"`
22522252
}
22532253
gomcp.AddTool(s.mcp, &gomcp.Tool{
22542254
Name: "get_settings",
@@ -2275,9 +2275,9 @@ func (s *Server) registerSettingsTools() {
22752275

22762276
// set_settings — stub
22772277
type setSettingsInput struct {
2278-
Namespace string `json:"namespace" jsonschema:"enum=system,enum=secure,enum=global,description=Settings namespace: system, secure, or global"`
2279-
Name string `json:"name" jsonschema:"description=Setting name"`
2280-
Value string `json:"value" jsonschema:"description=Setting value to write"`
2278+
Namespace string `json:"namespace" jsonschema:"Settings namespace: system, secure, or global (valid: system, secure, global)"`
2279+
Name string `json:"name" jsonschema:"Setting name"`
2280+
Value string `json:"value" jsonschema:"Setting value to write"`
22812281
}
22822282
gomcp.AddTool(s.mcp, &gomcp.Tool{
22832283
Name: "set_settings",
@@ -2303,7 +2303,7 @@ func (s *Server) registerSettingsTools() {
23032303

23042304
// set_brightness — stub
23052305
type brightnessInput struct {
2306-
Level int32 `json:"level" jsonschema:"description=Brightness level 0-255"`
2306+
Level int32 `json:"level" jsonschema:"Brightness level 0-255"`
23072307
}
23082308
gomcp.AddTool(s.mcp, &gomcp.Tool{
23092309
Name: "set_brightness",
@@ -2347,7 +2347,7 @@ func settingsClassName(namespace string) string {
23472347

23482348
func (s *Server) registerBluetoothTools() {
23492349
type bluetoothInput struct {
2350-
Action string `json:"action" jsonschema:"enum=status,enum=enable,enum=disable,enum=start_discovery,enum=cancel_discovery,description=Bluetooth action to perform"`
2350+
Action string `json:"action" jsonschema:"Bluetooth action to perform (valid: status, enable, disable, start_discovery, cancel_discovery)"`
23512351
}
23522352
gomcp.AddTool(s.mcp, &gomcp.Tool{
23532353
Name: "bluetooth",
@@ -2441,10 +2441,10 @@ func (s *Server) registerPrintTools() {
24412441
func (s *Server) registerPowerTools() {
24422442
// set_power_mode — combined power query and wake lock stub
24432443
type powerInput struct {
2444-
Action string `json:"action" jsonschema:"enum=status,enum=new_wake_lock,description=Action: status (query power state) or new_wake_lock (create wake lock)"`
2445-
Level int32 `json:"level,omitempty" jsonschema:"description=Wake lock level for new_wake_lock (1=PARTIAL, 6=SCREEN_DIM, 10=SCREEN_BRIGHT, 26=FULL, 32=PROXIMITY_SCREEN_OFF)"`
2446-
Tag string `json:"tag,omitempty" jsonschema:"description=Wake lock tag for new_wake_lock"`
2447-
Package string `json:"package,omitempty" jsonschema:"description=Package name for battery optimization check"`
2444+
Action string `json:"action" jsonschema:"Action: status (query power state) or new_wake_lock (create wake lock) (valid: status, new_wake_lock)"`
2445+
Level int32 `json:"level,omitempty" jsonschema:"Wake lock level for new_wake_lock (1=PARTIAL, 6=SCREEN_DIM, 10=SCREEN_BRIGHT, 26=FULL, 32=PROXIMITY_SCREEN_OFF)"`
2446+
Tag string `json:"tag,omitempty" jsonschema:"Wake lock tag for new_wake_lock"`
2447+
Package string `json:"package,omitempty" jsonschema:"Package name for battery optimization check"`
24482448
}
24492449
gomcp.AddTool(s.mcp, &gomcp.Tool{
24502450
Name: "set_power_mode",

0 commit comments

Comments
 (0)