Skip to content

Commit 7ca767f

Browse files
authored
Merge pull request #4 from nnemirovsky/fix/codex-schema-sanitize
fix(mcp): sanitize tool schemas for codex
2 parents 6b67751 + f69d8c4 commit 7ca767f

5 files changed

Lines changed: 87 additions & 19 deletions

File tree

.claude-plugin/marketplace.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.5.2",
3+
"version": "0.5.3",
44
"description": "iOS Safari debugging via ios-webkit-debug-proxy — MCP server with full WebKit Inspector Protocol support",
55
"owner": {
66
"name": "nnemirovsky"

.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.5.2",
3+
"version": "0.5.3",
44
"description": "iOS Safari debugging via ios-webkit-debug-proxy — MCP server with full WebKit Inspector Protocol support",
55
"mcpServers": {
66
"iwdp-mcp": {

cmd/iwdp-mcp/main.go

Lines changed: 56 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99
"net/url"
1010
"os"
1111
"path/filepath"
12-
"reflect"
1312
"strings"
1413
"sync"
1514

@@ -65,7 +64,7 @@ func lookupInterceptStage(requestID string) string {
6564
func main() {
6665
server := mcp.NewServer(&mcp.Implementation{
6766
Name: "iwdp-mcp",
68-
Version: "0.5.2",
67+
Version: "0.5.3",
6968
}, nil)
7069

7170
registerTools(server)
@@ -76,23 +75,67 @@ func main() {
7675
}
7776

7877
func addTool[In, Out any](s *mcp.Server, t *mcp.Tool, h mcp.ToolHandlerFor[In, Out]) {
79-
if t.InputSchema == nil && isEmptyStructType(reflect.TypeFor[In]()) {
80-
copy := *t
81-
copy.InputSchema = emptyObjectInputSchema()
82-
t = &copy
78+
copy := *t
79+
if copy.InputSchema == nil {
80+
schema, err := jsonschema.For[In](nil)
81+
if err != nil {
82+
panic(fmt.Sprintf("AddTool: tool %q: input schema: %v", copy.Name, err))
83+
}
84+
copy.InputSchema = codexCompatibleInputSchema(copy.Name, schema)
85+
} else {
86+
copy.InputSchema = codexCompatibleInputSchema(copy.Name, copy.InputSchema)
87+
}
88+
mcp.AddTool(s, &copy, h)
89+
}
90+
91+
func codexCompatibleInputSchema(toolName string, schema any) map[string]any {
92+
data, err := json.Marshal(schema)
93+
if err != nil {
94+
panic(fmt.Sprintf("AddTool: tool %q: marshal input schema: %v", toolName, err))
8395
}
84-
mcp.AddTool(s, t, h)
96+
var out map[string]any
97+
if err := json.Unmarshal(data, &out); err != nil {
98+
panic(fmt.Sprintf("AddTool: tool %q: unmarshal input schema: %v", toolName, err))
99+
}
100+
normalizeSchema(out)
101+
return out
85102
}
86103

87-
func isEmptyStructType(t reflect.Type) bool {
88-
return t.Kind() == reflect.Struct && t.NumField() == 0
104+
func normalizeSchema(schema any) {
105+
switch s := schema.(type) {
106+
case map[string]any:
107+
for _, value := range s {
108+
normalizeSchema(value)
109+
}
110+
if schemaTypeIncludes(s["type"], "object") {
111+
if _, ok := s["properties"].(map[string]any); !ok {
112+
s["properties"] = map[string]any{}
113+
}
114+
}
115+
if schemaTypeIncludes(s["type"], "array") {
116+
if _, ok := s["items"].(map[string]any); !ok {
117+
s["items"] = map[string]any{}
118+
}
119+
}
120+
case []any:
121+
for _, value := range s {
122+
normalizeSchema(value)
123+
}
124+
}
89125
}
90126

91-
func emptyObjectInputSchema() *jsonschema.Schema {
92-
return &jsonschema.Schema{
93-
Type: "object",
94-
Properties: map[string]*jsonschema.Schema{},
127+
func schemaTypeIncludes(value any, typ string) bool {
128+
switch v := value.(type) {
129+
case string:
130+
return v == typ
131+
case []any:
132+
for _, item := range v {
133+
if item == typ {
134+
return true
135+
}
136+
}
95137
}
138+
return false
96139
}
97140

98141
// --- Input/Output types for MCP tools ---

cmd/iwdp-mcp/mcp_test.go

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package main
33
import (
44
"context"
55
"encoding/json"
6+
"fmt"
67
"net/http"
78
"net/http/httptest"
89
"testing"
@@ -34,7 +35,7 @@ func TestServerCreationWithRegisterTools(t *testing.T) {
3435
}
3536
}
3637

37-
func TestToolInputSchemasIncludeProperties(t *testing.T) {
38+
func TestToolInputSchemasAreCodexCompatible(t *testing.T) {
3839
ctx := context.Background()
3940
impl := &mcp.Implementation{Name: "iwdp-mcp-test", Version: "0.1.0"}
4041
server := mcp.NewServer(impl, nil)
@@ -71,11 +72,31 @@ func TestToolInputSchemasIncludeProperties(t *testing.T) {
7172
if err := json.Unmarshal(data, &schema); err != nil {
7273
t.Fatalf("unmarshal input schema for %s: %v", tool.Name, err)
7374
}
74-
if schema["type"] == "object" {
75-
if _, ok := schema["properties"]; !ok {
76-
t.Fatalf("tool %s object input schema is missing properties: %s", tool.Name, data)
75+
assertCodexCompatibleInputSchema(t, tool.Name, schema, tool.Name)
76+
}
77+
}
78+
79+
func assertCodexCompatibleInputSchema(t *testing.T, toolName string, schema any, path string) {
80+
t.Helper()
81+
switch s := schema.(type) {
82+
case map[string]any:
83+
if schemaTypeIncludes(s["type"], "object") {
84+
if _, ok := s["properties"].(map[string]any); !ok {
85+
t.Fatalf("tool %s object input schema is missing properties at %s", toolName, path)
7786
}
7887
}
88+
if schemaTypeIncludes(s["type"], "array") {
89+
if _, ok := s["items"].(map[string]any); !ok {
90+
t.Fatalf("tool %s array input schema items is not an object at %s", toolName, path)
91+
}
92+
}
93+
for key, value := range s {
94+
assertCodexCompatibleInputSchema(t, toolName, value, path+"."+key)
95+
}
96+
case []any:
97+
for i, value := range s {
98+
assertCodexCompatibleInputSchema(t, toolName, value, fmt.Sprintf("%s[%d]", path, i))
99+
}
79100
}
80101
}
81102

e2e/simulator_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -986,6 +986,10 @@ func TestSim_SetEmulatedConditions(t *testing.T) {
986986
_ = monitor.Start(ctx, client)
987987

988988
if err := tools.SetEmulatedConditions(ctx, client, 100000); err != nil {
989+
if strings.Contains(err.Error(), "'Network.setEmulatedConditions' was not found") {
990+
t.Log("Network.setEmulatedConditions is not available in this WebKit/iwdp environment")
991+
return
992+
}
989993
t.Fatalf("SetEmulatedConditions: %v", err)
990994
}
991995
// Reset

0 commit comments

Comments
 (0)