Skip to content

Commit 181d46e

Browse files
committed
feat: save large results to temp files, add iwdp_status tool
- Screenshots saved as PNG files instead of inline base64 (fixes token limit) - Large tool results (>50K chars) automatically saved to temp JSON files - New iwdp_status tool checks/auto-starts ios-webkit-debug-proxy - Updated SKILL.md to use iwdp_status instead of manual curl commands
1 parent 79fae10 commit 181d46e

4 files changed

Lines changed: 144 additions & 52 deletions

File tree

.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "iwdp-mcp",
3-
"version": "0.2.3",
3+
"version": "0.2.4",
44
"description": "iOS Safari debugging via ios-webkit-debug-proxy — MCP server with full WebKit Inspector Protocol support",
55
"mcpServers": {
66
"iwdp-mcp": {

.gitignore

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,4 @@ dist/
1818
*.swo
1919

2020
# OS
21-
.DS_Store
22-
iwdp-mcp
21+
.DS_Store

cmd/iwdp-mcp/main.go

Lines changed: 130 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77
"fmt"
88
"log"
99
"net/url"
10+
"os"
11+
"path/filepath"
1012
"strings"
1113
"sync"
1214

@@ -39,7 +41,7 @@ func getClient(ctx context.Context) (*webkit.Client, error) {
3941
func main() {
4042
server := mcp.NewServer(&mcp.Implementation{
4143
Name: "iwdp-mcp",
42-
Version: "0.2.3",
44+
Version: "0.2.4",
4345
}, nil)
4446

4547
registerTools(server)
@@ -54,6 +56,10 @@ func main() {
5456
type EmptyInput struct{}
5557

5658
type (
59+
IWDPStatusInput struct {
60+
AutoStart *bool `json:"auto_start,omitempty" jsonschema:"if true, automatically start iwdp when not running (default: true)"`
61+
}
62+
5763
ListDevicesInput struct{}
5864
ListDevicesOutput struct {
5965
Devices []webkit.DeviceEntry `json:"devices"`
@@ -82,7 +88,7 @@ type NavigateInput struct {
8288
type (
8389
TakeScreenshotInput struct{}
8490
TakeScreenshotOutput struct {
85-
DataURL string `json:"data_url"`
91+
FilePath string `json:"file_path"`
8692
}
8793
)
8894

@@ -391,32 +397,92 @@ type NodeIDOutput struct {
391397

392398
func ok() OKOutput { return OKOutput{OK: true} }
393399

394-
// imageResultFromDataURL converts a data URL (data:image/png;base64,...) to an
395-
// MCP ImageContent result so the image is delivered natively instead of as a
396-
// multi-megabyte text string.
397-
func imageResultFromDataURL(dataURL string) (*mcp.CallToolResult, error) {
398-
// Expected format: data:image/png;base64,<data>
400+
// saveScreenshot saves a data URL (data:image/png;base64,...) to a temp PNG
401+
// file. iOS retina screenshots are too large for inline MCP results — saving
402+
// to disk lets Claude Code read the image directly with its Read tool.
403+
func saveScreenshot(dataURL string) (filePath string, result *mcp.CallToolResult, err error) {
399404
const prefix = "data:image/png;base64,"
400405
if !strings.HasPrefix(dataURL, prefix) {
401-
// Fallback: return as text
402-
return &mcp.CallToolResult{
406+
return "", &mcp.CallToolResult{
403407
Content: []mcp.Content{&mcp.TextContent{Text: dataURL}},
404408
}, nil
405409
}
406410
b64Data := dataURL[len(prefix):]
407411
rawBytes, err := base64.StdEncoding.DecodeString(b64Data)
408412
if err != nil {
409-
return nil, fmt.Errorf("decoding screenshot base64: %w", err)
413+
return "", nil, fmt.Errorf("decoding screenshot base64: %w", err)
414+
}
415+
tmpDir := filepath.Join(os.TempDir(), "iwdp-mcp")
416+
if err := os.MkdirAll(tmpDir, 0o755); err != nil {
417+
return "", nil, fmt.Errorf("creating temp dir: %w", err)
418+
}
419+
f, err := os.CreateTemp(tmpDir, "screenshot-*.png")
420+
if err != nil {
421+
return "", nil, fmt.Errorf("creating temp file: %w", err)
422+
}
423+
defer f.Close()
424+
if _, err := f.Write(rawBytes); err != nil {
425+
return "", nil, fmt.Errorf("writing screenshot: %w", err)
426+
}
427+
return f.Name(), &mcp.CallToolResult{
428+
Content: []mcp.Content{&mcp.TextContent{
429+
Text: fmt.Sprintf("Screenshot saved to %s — use the Read tool to view it.", f.Name()),
430+
}},
431+
}, nil
432+
}
433+
434+
// maxInlineResultSize is the maximum number of characters for a tool result
435+
// before it gets saved to a temp file instead. Claude Code truncates results
436+
// above ~60K characters, so we save anything larger to disk.
437+
const maxInlineResultSize = 50_000
438+
439+
// largeResultToFile checks if the JSON representation of result exceeds
440+
// maxInlineResultSize. If so, it saves the JSON to a temp file and returns a
441+
// *mcp.CallToolResult pointing to the file. Otherwise returns nil (use inline).
442+
func largeResultToFile(result any, prefix string) (*mcp.CallToolResult, error) {
443+
data, err := json.MarshalIndent(result, "", " ")
444+
if err != nil || len(data) <= maxInlineResultSize {
445+
return nil, err
446+
}
447+
tmpDir := filepath.Join(os.TempDir(), "iwdp-mcp")
448+
if err := os.MkdirAll(tmpDir, 0o755); err != nil {
449+
return nil, fmt.Errorf("creating temp dir: %w", err)
450+
}
451+
f, err := os.CreateTemp(tmpDir, prefix+"-*.json")
452+
if err != nil {
453+
return nil, fmt.Errorf("creating temp file: %w", err)
454+
}
455+
defer f.Close()
456+
if _, err := f.Write(data); err != nil {
457+
return nil, fmt.Errorf("writing result: %w", err)
410458
}
411459
return &mcp.CallToolResult{
412-
Content: []mcp.Content{&mcp.ImageContent{
413-
Data: rawBytes,
414-
MIMEType: "image/png",
460+
Content: []mcp.Content{&mcp.TextContent{
461+
Text: fmt.Sprintf("Result too large for inline display (%d bytes). Saved to %s — use the Read tool to view it.", len(data), f.Name()),
415462
}},
416463
}, nil
417464
}
418465

419466
func registerTools(server *mcp.Server) {
467+
// --- iwdp status ---
468+
mcp.AddTool(server, &mcp.Tool{
469+
Name: "iwdp_status", Description: "Check if ios-webkit-debug-proxy is running and optionally start it. Call this first before any other tool to ensure iwdp is available.",
470+
}, func(ctx context.Context, req *mcp.CallToolRequest, input IWDPStatusInput) (*mcp.CallToolResult, any, error) {
471+
running := proxy.IsRunning()
472+
if running {
473+
return nil, map[string]any{"running": true, "message": "ios-webkit-debug-proxy is running on port 9221"}, nil
474+
}
475+
// Default auto_start to true when not explicitly set
476+
autoStart := input.AutoStart == nil || *input.AutoStart
477+
if !autoStart {
478+
return nil, map[string]any{"running": false, "message": "ios-webkit-debug-proxy is not running. Start it with: ios_webkit_debug_proxy --no-frontend"}, nil
479+
}
480+
if err := proxy.Start(ctx); err != nil {
481+
return nil, map[string]any{"running": false, "message": fmt.Sprintf("failed to start iwdp: %v", err)}, nil
482+
}
483+
return nil, map[string]any{"running": true, "started": true, "message": "ios-webkit-debug-proxy was not running — started it automatically"}, nil
484+
})
485+
420486
// --- Device/Page management ---
421487
mcp.AddTool(server, &mcp.Tool{
422488
Name: "list_devices", Description: "List connected iOS devices (from iwdp listing port 9221). Each device's URL shows which port to use for list_pages.",
@@ -515,7 +581,7 @@ func registerTools(server *mcp.Server) {
515581
})
516582

517583
mcp.AddTool(server, &mcp.Tool{
518-
Name: "take_screenshot", Description: "Capture page screenshot as base64 PNG",
584+
Name: "take_screenshot", Description: "Capture page screenshot as PNG file. Returns the file path — use the Read tool to view it.",
519585
}, func(ctx context.Context, req *mcp.CallToolRequest, _ TakeScreenshotInput) (*mcp.CallToolResult, any, error) {
520586
c, err := getClient(ctx)
521587
if err != nil {
@@ -525,15 +591,15 @@ func registerTools(server *mcp.Server) {
525591
if err != nil {
526592
return nil, TakeScreenshotOutput{}, err
527593
}
528-
result, err := imageResultFromDataURL(dataURL)
594+
path, result, err := saveScreenshot(dataURL)
529595
if err != nil {
530596
return nil, TakeScreenshotOutput{}, err
531597
}
532-
return result, TakeScreenshotOutput{DataURL: dataURL}, nil
598+
return result, TakeScreenshotOutput{FilePath: path}, nil
533599
})
534600

535601
mcp.AddTool(server, &mcp.Tool{
536-
Name: "snapshot_node", Description: "Capture a specific DOM node as PNG",
602+
Name: "snapshot_node", Description: "Capture a specific DOM node as PNG file. Returns the file path — use the Read tool to view it.",
537603
}, func(ctx context.Context, req *mcp.CallToolRequest, input SnapshotNodeInput) (*mcp.CallToolResult, any, error) {
538604
c, err := getClient(ctx)
539605
if err != nil {
@@ -543,11 +609,11 @@ func registerTools(server *mcp.Server) {
543609
if err != nil {
544610
return nil, TakeScreenshotOutput{}, err
545611
}
546-
result, err := imageResultFromDataURL(dataURL)
612+
path, result, err := saveScreenshot(dataURL)
547613
if err != nil {
548614
return nil, TakeScreenshotOutput{}, err
549615
}
550-
return result, TakeScreenshotOutput{DataURL: dataURL}, nil
616+
return result, TakeScreenshotOutput{FilePath: path}, nil
551617
})
552618

553619
// --- Runtime ---
@@ -562,7 +628,11 @@ func registerTools(server *mcp.Server) {
562628
if err != nil {
563629
return nil, EvaluateScriptOutput{}, err
564630
}
565-
return nil, EvaluateScriptOutput{Result: result.Result, Type: result.Result.Type}, nil
631+
out := EvaluateScriptOutput{Result: result.Result, Type: result.Result.Type}
632+
if fileResult, err := largeResultToFile(out, "eval"); err == nil && fileResult != nil {
633+
return fileResult, out, nil
634+
}
635+
return nil, out, nil
566636
})
567637

568638
mcp.AddTool(server, &mcp.Tool{
@@ -576,7 +646,11 @@ func registerTools(server *mcp.Server) {
576646
if err != nil {
577647
return nil, RawOutput{}, err
578648
}
579-
return nil, RawOutput{Result: result.Result}, nil
649+
out := RawOutput{Result: result.Result}
650+
if fileResult, err := largeResultToFile(out, "call-fn"); err == nil && fileResult != nil {
651+
return fileResult, out, nil
652+
}
653+
return nil, out, nil
580654
})
581655

582656
mcp.AddTool(server, &mcp.Tool{
@@ -590,7 +664,11 @@ func registerTools(server *mcp.Server) {
590664
if err != nil {
591665
return nil, RawOutput{}, err
592666
}
593-
return nil, RawOutput{Result: props}, nil
667+
out := RawOutput{Result: props}
668+
if fileResult, err := largeResultToFile(out, "props"); err == nil && fileResult != nil {
669+
return fileResult, out, nil
670+
}
671+
return nil, out, nil
594672
})
595673

596674
// --- DOM ---
@@ -605,7 +683,11 @@ func registerTools(server *mcp.Server) {
605683
if err != nil {
606684
return nil, RawOutput{}, err
607685
}
608-
return nil, RawOutput{Result: doc}, nil
686+
out := RawOutput{Result: doc}
687+
if fileResult, err := largeResultToFile(out, "dom"); err == nil && fileResult != nil {
688+
return fileResult, out, nil
689+
}
690+
return nil, out, nil
609691
})
610692

611693
mcp.AddTool(server, &mcp.Tool{
@@ -647,7 +729,11 @@ func registerTools(server *mcp.Server) {
647729
if err != nil {
648730
return nil, GetOuterHTMLOutput{}, err
649731
}
650-
return nil, GetOuterHTMLOutput{OuterHTML: html}, nil
732+
out := GetOuterHTMLOutput{OuterHTML: html}
733+
if fileResult, err := largeResultToFile(out, "html"); err == nil && fileResult != nil {
734+
return fileResult, out, nil
735+
}
736+
return nil, out, nil
651737
})
652738

653739
mcp.AddTool(server, &mcp.Tool{
@@ -835,7 +921,11 @@ func registerTools(server *mcp.Server) {
835921
if nm == nil {
836922
return nil, RawOutput{Result: []any{}}, nil
837923
}
838-
return nil, RawOutput{Result: nm.GetRequests()}, nil
924+
out := RawOutput{Result: nm.GetRequests()}
925+
if fileResult, err := largeResultToFile(out, "network"); err == nil && fileResult != nil {
926+
return fileResult, out, nil
927+
}
928+
return nil, out, nil
839929
})
840930

841931
mcp.AddTool(server, &mcp.Tool{
@@ -849,7 +939,11 @@ func registerTools(server *mcp.Server) {
849939
if err != nil {
850940
return nil, GetResponseBodyOutput{}, err
851941
}
852-
return nil, GetResponseBodyOutput{Body: body, Base64Encoded: b64}, nil
942+
out := GetResponseBodyOutput{Body: body, Base64Encoded: b64}
943+
if fileResult, err := largeResultToFile(out, "response"); err == nil && fileResult != nil {
944+
return fileResult, out, nil
945+
}
946+
return nil, out, nil
853947
})
854948

855949
mcp.AddTool(server, &mcp.Tool{
@@ -1128,7 +1222,11 @@ func registerTools(server *mcp.Server) {
11281222
if cc == nil {
11291223
return nil, RawOutput{Result: []any{}}, nil
11301224
}
1131-
return nil, RawOutput{Result: cc.GetMessages()}, nil
1225+
out := RawOutput{Result: cc.GetMessages()}
1226+
if fileResult, err := largeResultToFile(out, "console"); err == nil && fileResult != nil {
1227+
return fileResult, out, nil
1228+
}
1229+
return nil, out, nil
11321230
})
11331231

11341232
mcp.AddTool(server, &mcp.Tool{
@@ -1405,7 +1503,11 @@ func registerTools(server *mcp.Server) {
14051503
if tc == nil {
14061504
return nil, RawOutput{Result: []any{}}, nil
14071505
}
1408-
return nil, RawOutput{Result: tc.GetEvents()}, nil
1506+
out := RawOutput{Result: tc.GetEvents()}
1507+
if fileResult, err := largeResultToFile(out, "timeline"); err == nil && fileResult != nil {
1508+
return fileResult, out, nil
1509+
}
1510+
return nil, out, nil
14091511
})
14101512

14111513
// --- Memory & Heap ---

skills/ios-safari-debug/SKILL.md

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,9 @@ Use this skill to inspect and debug Safari tabs on a connected iOS device throug
99

1010
## Prerequisites
1111

12-
1. **Check if iwdp is running** (check the listing port 9221, NOT a device port):
13-
```bash
14-
curl -s http://localhost:9221/json
15-
```
16-
A JSON array of connected devices means it is running.
17-
18-
2. **If not running, start it:**
19-
```bash
20-
ios_webkit_debug_proxy --no-frontend &
21-
```
22-
Wait 1 second, then verify:
23-
```bash
24-
sleep 1 && curl -s http://localhost:9221/json
25-
```
26-
27-
3. **An iOS device must be connected** via USB with Safari open and Web Inspector enabled (Settings > Safari > Advanced > Web Inspector).
12+
1. **Check iwdp status** — call `iwdp_status` first. It checks if ios-webkit-debug-proxy is running and auto-starts it if needed (no manual shell commands required).
13+
14+
2. **An iOS device must be connected** via USB with Safari open and Web Inspector enabled (Settings > Safari > Advanced > Web Inspector).
2815

2916
## Port Layout
3017

@@ -36,16 +23,18 @@ Each device entry from port 9221 includes a URL field (e.g., `localhost:9222`) i
3623

3724
## Workflow
3825

39-
1. **List devices** — use `list_devices` to see connected iOS devices and their port assignments.
26+
1. **Ensure iwdp is running** — call `iwdp_status` to verify (it auto-starts if needed).
27+
28+
2. **List devices** — use `list_devices` to see connected iOS devices and their port assignments.
4029

41-
2. **List pages** — use `list_pages` (optionally with `port` for a specific device) to discover open Safari tabs. Each entry includes a title, URL, and `webSocketDebuggerUrl`.
30+
3. **List pages** — use `list_pages` (optionally with `port` for a specific device) to discover open Safari tabs. Each entry includes a title, URL, and `webSocketDebuggerUrl`.
4231

43-
3. **Select a page** — use `select_page` with the WebSocket URL from the listing to connect to the target tab.
32+
4. **Select a page** — use `select_page` with the WebSocket URL from the listing to connect to the target tab.
4433

45-
4. **Use debugging tools** for the task at hand. Available tools include:
34+
5. **Use debugging tools** for the task at hand. Available tools include:
4635
- `navigate` — go to a URL
4736
- `evaluate_script` — run JavaScript in the page
48-
- `take_screenshot` — capture the page as a base64 PNG
37+
- `take_screenshot` — capture the page as a PNG file (returns path — use Read to view)
4938
- `get_document` / `query_selector` / `get_outer_html` — inspect the DOM
5039
- `click` / `fill` / `type_text` — interact with page elements
5140
- `get_cookies` / `set_cookie` / `delete_cookie` — manage cookies (incl. httpOnly)
@@ -102,3 +91,5 @@ View all cookies including httpOnly ones that JavaScript cannot access:
10291
- `get_cookies` returns httpOnly and secure cookies that `document.cookie` cannot access.
10392
- Network/console monitoring only captures events while active — enable before triggering traffic.
10493
- `evaluate_script` runs arbitrary JS in the page context — use it for anything the specialized tools don't cover.
94+
- Screenshots are saved as PNG files — use the Read tool to view the returned file path.
95+
- Large results (DOM trees, network logs, JS output) are automatically saved to temp files when they exceed the inline size limit.

0 commit comments

Comments
 (0)