From fceeeb1b501c32a131a92af6a84fbc4ec5d2b78f Mon Sep 17 00:00:00 2001 From: Khasbulat Abdullin Date: Sun, 1 Mar 2026 06:00:09 +0300 Subject: [PATCH 1/2] mcp: refactor --- cmd/mcp-smoke/main.go | 22 +- go.mod | 13 +- go.sum | 41 +- internal/mcpserver/easyp_config_schema.go | 295 ------------- internal/mcpserver/server.go | 3 +- internal/mcpserver/server_test.go | 84 ---- internal/mcpserver/tool_schemas.go | 146 ------ internal/mcpserver/tools_easyp_config.go | 515 ---------------------- internal/mcpserver/tools_plugins.go | 4 +- 9 files changed, 42 insertions(+), 1081 deletions(-) delete mode 100644 internal/mcpserver/easyp_config_schema.go delete mode 100644 internal/mcpserver/tools_easyp_config.go diff --git a/cmd/mcp-smoke/main.go b/cmd/mcp-smoke/main.go index 64fb815..4ead58d 100644 --- a/cmd/mcp-smoke/main.go +++ b/cmd/mcp-smoke/main.go @@ -18,7 +18,7 @@ func main() { var endpoint string var timeout time.Duration - flag.StringVar(&endpoint, "endpoint", "http://localhost:8083/mcp", "MCP streamable HTTP endpoint") + flag.StringVar(&endpoint, "endpoint", "http://localhost:23413/mcp", "MCP streamable HTTP endpoint") flag.DurationVar(&timeout, "timeout", 15*time.Second, "overall timeout") flag.Parse() @@ -41,26 +41,6 @@ func main() { } fmt.Println("MCP smoke check passed") - - list, err := session.ListTools(ctx, &mcp.ListToolsParams{}) - if err != nil { - exitf("failed to list tools: %v", err) - } - - fmt.Println("\nVerified tools:") - for _, tool := range list.Tools { - res, err := session.CallTool(ctx, &mcp.CallToolParams{ - Name: tool.Name, - }) - status := "OK" - if err != nil { - status = fmt.Sprintf("ERROR (transport): %v", err) - } else if res.IsError { - status = fmt.Sprintf("ERROR (tool): %s", toolText(res)) - } - fmt.Printf("- %s: %s\n", tool.Name, status) - fmt.Printf("result: %v\n", res.StructuredContent) - } } func runSmoke(ctx context.Context, session *mcp.ClientSession) error { diff --git a/go.mod b/go.mod index 924767c..335a5cf 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/easyp-tech/service go 1.26.0 require ( + github.com/easyp-tech/easyp v0.14.1-0.20260301022854-21e6e9dbe91e github.com/easyp-tech/protoc-gen-easydoc v0.4.0 github.com/gofrs/uuid/v5 v5.4.0 github.com/google/jsonschema-go v0.4.2 @@ -10,7 +11,6 @@ require ( github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 github.com/hellofresh/health-go/v5 v5.5.5 - github.com/invopop/jsonschema v0.13.0 github.com/jmoiron/sqlx v1.4.0 github.com/lib/pq v1.11.2 github.com/modelcontextprotocol/go-sdk v1.4.0 @@ -32,8 +32,10 @@ require ( ) require ( + github.com/a8m/envsubst v1.4.3 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect + github.com/bufbuild/protocompile v0.14.1 // indirect github.com/buger/jsonparser v1.1.1 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect @@ -43,21 +45,30 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect + github.com/invopop/jsonschema v0.13.0 // indirect github.com/klauspost/compress v1.18.4 // indirect github.com/mailru/easyjson v0.9.1 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/otiai10/copy v1.14.1 // indirect + github.com/otiai10/mint v1.6.3 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.67.5 // indirect github.com/prometheus/procfs v0.20.1 // indirect + github.com/samber/lo v1.52.0 // indirect github.com/segmentio/asm v1.2.1 // indirect github.com/segmentio/encoding v0.5.3 // indirect + github.com/tetratelabs/wazero v1.9.0 // indirect + github.com/wasilibs/wazero-helpers v0.0.0-20250123031827-cd30c44769bb // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect + github.com/yakwilikk/go-yamlvalidator v0.0.0-20260216223344-568790865548 // indirect + github.com/yoheimuta/go-protoparser/v4 v4.14.2 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect go.opentelemetry.io/proto/otlp v1.9.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect + golang.org/x/mod v0.32.0 // indirect golang.org/x/net v0.51.0 // indirect golang.org/x/oauth2 v0.35.0 // indirect golang.org/x/sys v0.41.0 // indirect diff --git a/go.sum b/go.sum index 8372130..e4d685a 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,13 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/a8m/envsubst v1.4.3 h1:kDF7paGK8QACWYaQo6KtyYBozY2jhQrTuNNuUxQkhJY= +github.com/a8m/envsubst v1.4.3/go.mod h1:4jjHWQlZoaXPoLQUb7H2qT4iLkZDdmEQiOUogdUmqVU= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= +github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= @@ -12,6 +16,8 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/easyp-tech/easyp v0.14.1-0.20260301022854-21e6e9dbe91e h1:6bPKqVs7zRYSbjuQN7JuFulkta7y6RKV0s78c7/qrrA= +github.com/easyp-tech/easyp v0.14.1-0.20260301022854-21e6e9dbe91e/go.mod h1:YIRgbpJBhj+txUP9EC/16L6QeZOgHhHgAYDeLpmWu6M= github.com/easyp-tech/protoc-gen-easydoc v0.4.0 h1:JJREL3C/+EKf9lzypTAGo6QeUWDtDKVxK1aq8EbRzzc= github.com/easyp-tech/protoc-gen-easydoc v0.4.0/go.mod h1:/NhDdfMihhuPWYUy74Hf0VaUtzOfEStv6UHHpPogQBg= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -49,7 +55,6 @@ github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcI github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -61,18 +66,18 @@ github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+ github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs= github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= -github.com/modelcontextprotocol/go-sdk v1.3.1 h1:TfqtNKOIWN4Z1oqmPAiWDC2Jq7K9OdJaooe0teoXASI= -github.com/modelcontextprotocol/go-sdk v1.3.1/go.mod h1:DgVX498dMD8UJlseK1S5i1T4tFz2fkBk4xogC3D15nw= github.com/modelcontextprotocol/go-sdk v1.4.0 h1:u0kr8lbJc1oBcawK7Df+/ajNMpIDFE41OEPxdeTLOn8= github.com/modelcontextprotocol/go-sdk v1.4.0/go.mod h1:Nxc2n+n/GdCebUaqCOhTetptS17SXXNu9IfNTaLDi1E= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/otiai10/copy v1.14.1 h1:5/7E6qsUMBaH5AnQ0sSLzzTg1oTECmcCmT6lvF45Na8= +github.com/otiai10/copy v1.14.1/go.mod h1:oQwrEDDOci3IM8dJF0d8+jnbfPDllW6vUjNc3DoZm9I= +github.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs= +github.com/otiai10/mint v1.6.3/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= @@ -81,26 +86,32 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= -github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= -github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc= -github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg= +github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw= +github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0= github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/segmentio/encoding v0.5.3 h1:OjMgICtcSFuNvQCdwqMCv9Tg7lEOXGwm1J5RPQccx6w= github.com/segmentio/encoding v0.5.3/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0= github.com/sethvargo/go-envconfig v1.3.0 h1:gJs+Fuv8+f05omTpwWIu6KmuseFAXKrIaOZSh8RMt0U= github.com/sethvargo/go-envconfig v1.3.0/go.mod h1:JLd0KFWQYzyENqnEPWWZ49i4vzZo/6nRidxI8YvGiHw= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= +github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I= +github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM= +github.com/wasilibs/wazero-helpers v0.0.0-20250123031827-cd30c44769bb h1:gQ+ZV4wJke/EBKYciZ2MshEouEHFuinB85dY3f5s1q8= +github.com/wasilibs/wazero-helpers v0.0.0-20250123031827-cd30c44769bb/go.mod h1:jMeV4Vpbi8osrE/pKUxRZkVaA0EX7NZN0A9/oRzgpgY= github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= +github.com/yakwilikk/go-yamlvalidator v0.0.0-20260216223344-568790865548 h1:wKohzDUpJJIQ+Tqbo1FvOsi/T3DNYOVceq6pXeGGh8o= +github.com/yakwilikk/go-yamlvalidator v0.0.0-20260216223344-568790865548/go.mod h1:JIBeXFmTJyghm8crrAhNGhVxsNpCFSmuPLjz2spSBVU= +github.com/yoheimuta/go-protoparser/v4 v4.14.2 h1:/P/LlX1CF9NaTWEltGcIZVvNlPbhABuAnBtAWpb3+74= +github.com/yoheimuta/go-protoparser/v4 v4.14.2/go.mod h1:AHNNnSWnb0UoL4QgHPiOAg2BniQceFscPI5X/BZNHl8= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= @@ -129,8 +140,8 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= -golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= -golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= +golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= +golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= @@ -145,12 +156,8 @@ golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/api v0.0.0-20260223185530-2f722ef697dc h1:ULD+ToGXUIU6Pkzr1ARxdyvwfHbelw+agoFDRbLg4TU= -google.golang.org/genproto/googleapis/api v0.0.0-20260223185530-2f722ef697dc/go.mod h1:M5krXqk4GhBKvB596udGL3UyjL4I1+cTbK0orROM9ng= google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 h1:tu/dtnW1o3wfaxCOjSLn5IRX4YDcJrtlpzYkhHhGaC4= google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171/go.mod h1:M5krXqk4GhBKvB596udGL3UyjL4I1+cTbK0orROM9ng= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260223185530-2f722ef697dc h1:51Wupg8spF+5FC6D+iMKbOddFjMckETnNnEiZ+HX37s= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260223185530-2f722ef697dc/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= diff --git a/internal/mcpserver/easyp_config_schema.go b/internal/mcpserver/easyp_config_schema.go deleted file mode 100644 index 5779d16..0000000 --- a/internal/mcpserver/easyp_config_schema.go +++ /dev/null @@ -1,295 +0,0 @@ -package mcpserver - -import ( - "encoding/json" - "sort" - - invjsonschema "github.com/invopop/jsonschema" -) - -func buildEasypConfigSchemaIndex() map[string]map[string]any { - reflector := &invjsonschema.Reflector{ - Anonymous: true, - DoNotReference: true, - } - - schema := reflector.Reflect(easypConfigSchemaRoot{}) - root := invSchemaToMap(schema) - if len(root) == 0 { - return map[string]map[string]any{} - } - - index := map[string]map[string]any{ - "$": root, - } - walkSchemaPaths(index, "$", root) - - return index -} - -func walkSchemaPaths(index map[string]map[string]any, basePath string, schema map[string]any) { - for _, key := range []string{"allOf", "anyOf", "oneOf"} { - branches, ok := asSchemaArray(schema[key]) - if !ok { - continue - } - for _, branch := range branches { - walkSchemaPaths(index, basePath, branch) - } - } - - props, ok := asSchemaMap(schema["properties"]) - if ok { - names := make([]string, 0, len(props)) - for name := range props { - names = append(names, name) - } - sort.Strings(names) - - for _, name := range names { - child, ok := asSchemaMap(props[name]) - if !ok { - continue - } - childPath := joinSchemaPath(basePath, name) - if _, exists := index[childPath]; !exists { - index[childPath] = child - } - walkSchemaPaths(index, childPath, child) - } - } - - if items, ok := asSchemaMap(schema["items"]); ok { - arrayPath := basePath + "[]" - if _, exists := index[arrayPath]; !exists { - index[arrayPath] = items - } - walkSchemaPaths(index, arrayPath, items) - } -} - -func joinSchemaPath(base, child string) string { - if base == "$" { - return child - } - return base + "." + child -} - -func asSchemaArray(v any) ([]map[string]any, bool) { - arr, ok := v.([]any) - if !ok { - return nil, false - } - out := make([]map[string]any, 0, len(arr)) - for _, item := range arr { - m, ok := asSchemaMap(item) - if !ok { - continue - } - out = append(out, m) - } - if len(out) == 0 { - return nil, false - } - return out, true -} - -func asSchemaMap(v any) (map[string]any, bool) { - m, ok := v.(map[string]any) - if !ok { - return nil, false - } - return m, true -} - -func invSchemaToMap(schema *invjsonschema.Schema) map[string]any { - if schema == nil { - return nil - } - - data, err := json.Marshal(schema) - if err != nil { - return map[string]any{} - } - - var out map[string]any - if err := json.Unmarshal(data, &out); err != nil { - return map[string]any{} - } - return out -} - -type easypConfigSchemaRoot struct { - Version string `json:"version,omitempty"` - Lint *easypConfigSchemaLint `json:"lint,omitempty"` - Deps []string `json:"deps,omitempty"` - Generate *easypConfigSchemaGenerate `json:"generate,omitempty"` - Breaking *easypConfigSchemaBreaking `json:"breaking,omitempty"` -} - -type easypConfigSchemaLint struct { - Use []string `json:"use,omitempty"` - EnumZeroValueSuffix string `json:"enum_zero_value_suffix,omitempty"` - ServiceSuffix string `json:"service_suffix,omitempty"` - Ignore []string `json:"ignore,omitempty"` - Except []string `json:"except,omitempty"` - AllowCommentIgnores bool `json:"allow_comment_ignores,omitempty"` - IgnoreOnly map[string][]string `json:"ignore_only,omitempty"` -} - -type easypConfigSchemaGenerate struct { - Inputs []easypConfigSchemaInput `json:"inputs"` - Plugins []easypConfigSchemaPlugin `json:"plugins"` - Managed *easypConfigSchemaManaged `json:"managed,omitempty"` -} - -func (easypConfigSchemaGenerate) JSONSchemaExtend(schema *invjsonschema.Schema) { - setMinItems(schema, "inputs", 1) - setMinItems(schema, "plugins", 1) -} - -type easypConfigSchemaInput struct { - Directory easypConfigSchemaInputDirectory `json:"directory,omitempty"` - GitRepo *easypConfigSchemaInputGitRepo `json:"git_repo,omitempty"` -} - -func (easypConfigSchemaInput) JSONSchemaExtend(schema *invjsonschema.Schema) { - schema.OneOf = []*invjsonschema.Schema{ - {Required: []string{"directory"}}, - {Required: []string{"git_repo"}}, - } -} - -type easypConfigSchemaInputDirectory struct{} - -func (easypConfigSchemaInputDirectory) JSONSchema() *invjsonschema.Schema { - reflector := &invjsonschema.Reflector{ - Anonymous: true, - DoNotReference: true, - } - objectSchema := reflector.Reflect(easypConfigSchemaInputDirectoryObject{}) - objectSchema.Version = "" - objectSchema.ID = "" - objectSchema.Definitions = nil - objectSchema.Title = "" - - return &invjsonschema.Schema{ - OneOf: []*invjsonschema.Schema{ - {Type: "string"}, - objectSchema, - }, - } -} - -type easypConfigSchemaInputDirectoryObject struct { - Path string `json:"path"` - Root string `json:"root,omitempty"` -} - -type easypConfigSchemaInputGitRepo struct { - URL string `json:"url"` - SubDirectory string `json:"sub_directory,omitempty"` - Root string `json:"root,omitempty"` -} - -type easypConfigSchemaPlugin struct { - Name string `json:"name,omitempty"` - Remote string `json:"remote,omitempty"` - Path string `json:"path,omitempty"` - Command []string `json:"command,omitempty"` - Out string `json:"out"` - Opts easypConfigSchemaPluginOps `json:"opts,omitempty"` - WithImports bool `json:"with_imports,omitempty"` -} - -func (easypConfigSchemaPlugin) JSONSchemaExtend(schema *invjsonschema.Schema) { - schema.OneOf = []*invjsonschema.Schema{ - {Required: []string{"name"}}, - {Required: []string{"remote"}}, - {Required: []string{"path"}}, - {Required: []string{"command"}}, - } -} - -type easypConfigSchemaPluginOps map[string]any - -func (easypConfigSchemaPluginOps) JSONSchema() *invjsonschema.Schema { - return &invjsonschema.Schema{ - Type: "object", - AdditionalProperties: &invjsonschema.Schema{ - OneOf: []*invjsonschema.Schema{ - {Type: "string"}, - { - Type: "array", - Items: &invjsonschema.Schema{Type: "string"}, - }, - }, - }, - } -} - -type easypConfigSchemaManaged struct { - Enabled bool `json:"enabled,omitempty"` - Disable []easypConfigSchemaManagedDisableRule `json:"disable,omitempty"` - Override []easypConfigSchemaManagedOverrideRule `json:"override,omitempty"` -} - -type easypConfigSchemaManagedDisableRule struct { - Module string `json:"module,omitempty"` - Path string `json:"path,omitempty"` - FileOption string `json:"file_option,omitempty"` - FieldOption string `json:"field_option,omitempty"` - Field string `json:"field,omitempty"` -} - -func (easypConfigSchemaManagedDisableRule) JSONSchemaExtend(schema *invjsonschema.Schema) { - schema.AnyOf = []*invjsonschema.Schema{ - {Required: []string{"module"}}, - {Required: []string{"path"}}, - {Required: []string{"file_option"}}, - {Required: []string{"field_option"}}, - {Required: []string{"field"}}, - } - schema.Not = &invjsonschema.Schema{Required: []string{"file_option", "field_option"}} - schema.DependentRequired = map[string][]string{ - "field": {"field_option"}, - } -} - -type easypConfigSchemaManagedOverrideRule struct { - FileOption string `json:"file_option,omitempty"` - FieldOption string `json:"field_option,omitempty"` - Value any `json:"value"` - Module string `json:"module,omitempty"` - Path string `json:"path,omitempty"` - Field string `json:"field,omitempty"` -} - -func (easypConfigSchemaManagedOverrideRule) JSONSchemaExtend(schema *invjsonschema.Schema) { - schema.AnyOf = []*invjsonschema.Schema{ - {Required: []string{"file_option"}}, - {Required: []string{"field_option"}}, - } - schema.Not = &invjsonschema.Schema{Required: []string{"file_option", "field_option"}} - schema.DependentRequired = map[string][]string{ - "field": {"field_option"}, - } -} - -type easypConfigSchemaBreaking struct { - Ignore []string `json:"ignore,omitempty"` - AgainstGitRef string `json:"against_git_ref,omitempty"` -} - -func setMinItems(schema *invjsonschema.Schema, fieldName string, min uint64) { - if schema == nil || schema.Properties == nil { - return - } - - itemSchema, ok := schema.Properties.Get(fieldName) - if !ok || itemSchema == nil { - return - } - - itemSchema.MinItems = &min -} diff --git a/internal/mcpserver/server.go b/internal/mcpserver/server.go index a53c422..54326ab 100644 --- a/internal/mcpserver/server.go +++ b/internal/mcpserver/server.go @@ -5,6 +5,7 @@ import ( "log/slog" "net/http" + easypmcp "github.com/easyp-tech/easyp/mcp/easypconfig" "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/easyp-tech/service/internal/core" @@ -35,7 +36,7 @@ func New(pluginService PluginService, logger *slog.Logger) *Server { }, opts) registerPluginTools(srv, pluginService) - registerEasypConfigTools(srv) + easypmcp.RegisterTool(srv) handler := mcp.NewStreamableHTTPHandler(func(*http.Request) *mcp.Server { return srv diff --git a/internal/mcpserver/server_test.go b/internal/mcpserver/server_test.go index 9aa9f8b..91285c1 100644 --- a/internal/mcpserver/server_test.go +++ b/internal/mcpserver/server_test.go @@ -7,7 +7,6 @@ import ( "log/slog" "net/http" "net/http/httptest" - "strings" "testing" "time" @@ -62,7 +61,6 @@ func TestMCPServer_RegistersToolsAndListsPlugins(t *testing.T) { toolNames = append(toolNames, tool.Name) } require.Contains(t, toolNames, pluginsListToolName) - require.Contains(t, toolNames, easypConfigDescribeToolName) args := map[string]any{ "group": "grpc", @@ -105,68 +103,6 @@ func TestMCPServer_RegistersToolsAndListsPlugins(t *testing.T) { }, fake.lastFilter) } -func TestMCPServer_EasypConfigDescribe(t *testing.T) { - t.Parallel() - - session, shutdown := newTestSession(t, &fakePluginService{}) - defer shutdown() - - res, err := session.CallTool(context.Background(), &mcp.CallToolParams{ - Name: easypConfigDescribeToolName, - Arguments: map[string]any{ - "path": "generate.inputs[].git_repo", - }, - }) - require.NoError(t, err) - require.False(t, res.IsError) - - var gitRepoOut struct { - SchemaVersion string `json:"schema_version"` - SelectedPath string `json:"selected_path"` - Fields []struct { - Path string `json:"path"` - } `json:"fields"` - Notes []string `json:"notes"` - } - decodeStructured(t, res, &gitRepoOut) - - require.Equal(t, "easyp-config-v1", gitRepoOut.SchemaVersion) - require.Equal(t, "generate.inputs[].git_repo", gitRepoOut.SelectedPath) - require.NotEmpty(t, gitRepoOut.Fields) - require.NotContains(t, fieldPaths(gitRepoOut.Fields), "generate.inputs[].git_repo.out") - require.Contains(t, strings.Join(gitRepoOut.Notes, "\n"), "git_repo.out") - - res, err = session.CallTool(context.Background(), &mcp.CallToolParams{ - Name: easypConfigDescribeToolName, - Arguments: map[string]any{ - "path": "generate.plugins[]", - }, - }) - require.NoError(t, err) - require.False(t, res.IsError) - - var pluginsOut struct { - Fields []struct { - Path string `json:"path"` - } `json:"fields"` - } - decodeStructured(t, res, &pluginsOut) - - paths := fieldPaths(pluginsOut.Fields) - require.Contains(t, paths, "generate.plugins[].remote") - require.NotContains(t, paths, "generate.plugins[].url") - - errRes, err := session.CallTool(context.Background(), &mcp.CallToolParams{ - Name: easypConfigDescribeToolName, - Arguments: map[string]any{ - "path": "unknown.section", - }, - }) - require.NoError(t, err) - require.True(t, errRes.IsError) - require.Contains(t, toolText(errRes), "unknown path") -} - func newTestSession(t *testing.T, pluginService PluginService) (*mcp.ClientSession, func()) { t.Helper() @@ -201,23 +137,3 @@ func decodeStructured(t *testing.T, res *mcp.CallToolResult, dst any) { require.NoError(t, err) require.NoError(t, json.Unmarshal(data, dst)) } - -func fieldPaths(fields []struct { - Path string `json:"path"` -}) []string { - out := make([]string, 0, len(fields)) - for _, field := range fields { - out = append(out, field.Path) - } - return out -} - -func toolText(res *mcp.CallToolResult) string { - parts := make([]string, 0, len(res.Content)) - for _, c := range res.Content { - if text, ok := c.(*mcp.TextContent); ok { - parts = append(parts, text.Text) - } - } - return strings.Join(parts, "\n") -} diff --git a/internal/mcpserver/tool_schemas.go b/internal/mcpserver/tool_schemas.go index 8f87500..82b06e2 100644 --- a/internal/mcpserver/tool_schemas.go +++ b/internal/mcpserver/tool_schemas.go @@ -78,149 +78,3 @@ func pluginsListOutputSchema() *jsonschema.Schema { Required: []string{"total", "plugins"}, } } - -func easypConfigDescribeInputSchema() *jsonschema.Schema { - return &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "path": { - Type: "string", - Description: "Dot path to a section of the schema. Empty means full schema", - }, - "include_schema": { - Type: "boolean", - Description: "Include JSON schema fragment in output. Default: true", - }, - "include_fields": { - Type: "boolean", - Description: "Include field documentation in output. Default: true", - }, - "include_examples": { - Type: "boolean", - Description: "Include examples in output. Default: true", - }, - "include_children": { - Type: "boolean", - Description: "Include descendants of selected path. Default: true", - }, - "examples_limit": { - Type: "integer", - Description: "Maximum number of examples to return. Default: 10, range 1..50", - Minimum: jsonschema.Ptr(1.0), - Maximum: jsonschema.Ptr(50.0), - }, - }, - } -} - -func easypConfigDescribeOutputSchema() *jsonschema.Schema { - fieldDocSchema := &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "path": { - Type: "string", - Description: "Field path", - }, - "type": { - Type: "string", - Description: "Value type", - }, - "required": { - Type: "boolean", - Description: "Whether field is required", - }, - "description": { - Type: "string", - Description: "Field purpose", - }, - "allowed_values": { - Type: "array", - Description: "Allowed values or enum options", - Items: &jsonschema.Schema{ - Type: "string", - }, - }, - "default_value": { - Type: "string", - Description: "Default value if omitted", - }, - "examples": { - Type: "array", - Description: "Value examples", - Items: &jsonschema.Schema{ - Type: "string", - }, - }, - "notes": { - Type: "array", - Description: "Extra constraints or caveats", - Items: &jsonschema.Schema{ - Type: "string", - }, - }, - }, - Required: []string{"path", "type", "required", "description"}, - } - - exampleSchema := &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "title": { - Type: "string", - Description: "Short example title", - }, - "description": { - Type: "string", - Description: "Example purpose", - }, - "yaml": { - Type: "string", - Description: "YAML snippet", - }, - "paths": { - Type: "array", - Description: "Schema paths covered by this example", - Items: &jsonschema.Schema{ - Type: "string", - }, - }, - }, - Required: []string{"title", "yaml"}, - } - - return &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "schema_version": { - Type: "string", - Description: "Schema metadata version", - }, - "selected_path": { - Type: "string", - Description: "Resolved path used for this response", - }, - "schema": { - Type: "object", - Description: "JSON schema fragment for selected path", - }, - "fields": { - Type: "array", - Description: "Field documentation", - Items: fieldDocSchema, - }, - "examples": { - Type: "array", - Description: "YAML examples", - Items: exampleSchema, - }, - "notes": { - Type: "array", - Description: "General notes and caveats", - Items: &jsonschema.Schema{ - Type: "string", - }, - }, - }, - Required: []string{"schema_version", "selected_path"}, - } -} diff --git a/internal/mcpserver/tools_easyp_config.go b/internal/mcpserver/tools_easyp_config.go deleted file mode 100644 index c51bc04..0000000 --- a/internal/mcpserver/tools_easyp_config.go +++ /dev/null @@ -1,515 +0,0 @@ -package mcpserver - -import ( - "context" - "fmt" - "sort" - "strings" - - "github.com/modelcontextprotocol/go-sdk/mcp" -) - -var ( - lintRuleGroups = []string{ - "MINIMAL", - "BASIC", - "DEFAULT", - "COMMENTS", - "UNARY_RPC", - } - lintRuleNames = []string{ - "DIRECTORY_SAME_PACKAGE", - "PACKAGE_DEFINED", - "PACKAGE_DIRECTORY_MATCH", - "PACKAGE_SAME_DIRECTORY", - "ENUM_FIRST_VALUE_ZERO", - "ENUM_NO_ALLOW_ALIAS", - "ENUM_PASCAL_CASE", - "ENUM_VALUE_UPPER_SNAKE_CASE", - "FIELD_LOWER_SNAKE_CASE", - "IMPORT_NO_PUBLIC", - "IMPORT_NO_WEAK", - "IMPORT_USED", - "MESSAGE_PASCAL_CASE", - "ONEOF_LOWER_SNAKE_CASE", - "PACKAGE_LOWER_SNAKE_CASE", - "PACKAGE_SAME_CSHARP_NAMESPACE", - "PACKAGE_SAME_GO_PACKAGE", - "PACKAGE_SAME_JAVA_MULTIPLE_FILES", - "PACKAGE_SAME_JAVA_PACKAGE", - "PACKAGE_SAME_PHP_NAMESPACE", - "PACKAGE_SAME_RUBY_PACKAGE", - "PACKAGE_SAME_SWIFT_PREFIX", - "RPC_PASCAL_CASE", - "SERVICE_PASCAL_CASE", - "ENUM_VALUE_PREFIX", - "ENUM_ZERO_VALUE_SUFFIX", - "FILE_LOWER_SNAKE_CASE", - "RPC_REQUEST_RESPONSE_UNIQUE", - "RPC_REQUEST_STANDARD_NAME", - "RPC_RESPONSE_STANDARD_NAME", - "PACKAGE_VERSION_SUFFIX", - "SERVICE_SUFFIX", - "COMMENT_ENUM", - "COMMENT_ENUM_VALUE", - "COMMENT_FIELD", - "COMMENT_MESSAGE", - "COMMENT_ONEOF", - "COMMENT_RPC", - "COMMENT_SERVICE", - "RPC_NO_CLIENT_STREAMING", - "RPC_NO_SERVER_STREAMING", - "PACKAGE_NO_IMPORT_CYCLE", - } -) - -type easypConfigDescribeInput struct { - Path string `json:"path,omitempty"` - IncludeSchema *bool `json:"include_schema,omitempty"` - IncludeFields *bool `json:"include_fields,omitempty"` - IncludeExamples *bool `json:"include_examples,omitempty"` - IncludeChildren *bool `json:"include_children,omitempty"` - ExamplesLimit *int `json:"examples_limit,omitempty"` -} - -type easypFieldDoc struct { - Path string `json:"path"` - Type string `json:"type"` - Required bool `json:"required"` - Description string `json:"description"` - AllowedValues []string `json:"allowed_values,omitempty"` - DefaultValue string `json:"default_value,omitempty"` - Examples []string `json:"examples,omitempty"` - Notes []string `json:"notes,omitempty"` -} - -type easypExample struct { - Title string `json:"title"` - Description string `json:"description,omitempty"` - YAML string `json:"yaml"` - Paths []string `json:"paths,omitempty"` -} - -type easypConfigDescribeOutput struct { - SchemaVersion string `json:"schema_version"` - SelectedPath string `json:"selected_path"` - Schema map[string]any `json:"schema,omitempty"` - Fields []easypFieldDoc `json:"fields,omitempty"` - Examples []easypExample `json:"examples,omitempty"` - Notes []string `json:"notes,omitempty"` -} - -type easypNodeDoc struct { - Fields []easypFieldDoc - Examples []easypExample - Notes []string -} - -type easypSpec struct { - SchemaVersion string - SchemaByPath map[string]map[string]any - DocsByPath map[string]easypNodeDoc -} - -const ( - easypConfigDescribeToolName = "easyp_config_describe" -) - -func registerEasypConfigTools(server *mcp.Server) { - spec := newEasypSpec() - - mcp.AddTool(server, &mcp.Tool{ - Name: easypConfigDescribeToolName, - Description: "Describe easyp.yaml schema and field usage. Supports full schema or a specific path with examples.", - InputSchema: easypConfigDescribeInputSchema(), - OutputSchema: easypConfigDescribeOutputSchema(), - }, func(_ context.Context, _ *mcp.CallToolRequest, input easypConfigDescribeInput) (*mcp.CallToolResult, easypConfigDescribeOutput, error) { - out, err := spec.describe(input) - if err != nil { - return nil, easypConfigDescribeOutput{}, err - } - return nil, out, nil - }) -} - -func (s easypSpec) describe(input easypConfigDescribeInput) (easypConfigDescribeOutput, error) { - selectedPath, ok := s.resolvePath(input.Path) - if !ok { - return easypConfigDescribeOutput{}, fmt.Errorf("unknown path %q", input.Path) - } - - includeSchema := boolOrDefault(input.IncludeSchema, true) - includeFields := boolOrDefault(input.IncludeFields, true) - includeExamples := boolOrDefault(input.IncludeExamples, true) - includeChildren := boolOrDefault(input.IncludeChildren, true) - examplesLimit := intOrDefault(input.ExamplesLimit, 10) - if examplesLimit < 1 { - examplesLimit = 1 - } - if examplesLimit > 50 { - examplesLimit = 50 - } - - paths := s.pathsFor(selectedPath, includeChildren) - - out := easypConfigDescribeOutput{ - SchemaVersion: s.SchemaVersion, - SelectedPath: selectedPath, - } - - if includeSchema { - out.Schema = s.SchemaByPath[selectedPath] - } - if includeFields { - out.Fields = s.collectFields(paths) - } - if includeExamples { - out.Examples = s.collectExamples(paths, examplesLimit) - } - out.Notes = s.collectNotes(paths) - - return out, nil -} - -func (s easypSpec) resolvePath(rawPath string) (string, bool) { - path := normalizePath(rawPath) - if s.hasPath(path) { - return path, true - } - - normPath := removeArrayMarkers(path) - for _, candidate := range s.allPaths() { - if removeArrayMarkers(candidate) == normPath { - return candidate, true - } - } - return "", false -} - -func (s easypSpec) pathsFor(selectedPath string, includeChildren bool) []string { - if !includeChildren { - return []string{selectedPath} - } - - allPaths := s.allPaths() - paths := make([]string, 0, len(allPaths)) - for _, p := range allPaths { - if isPathWithin(selectedPath, p) { - paths = append(paths, p) - } - } - return paths -} - -func (s easypSpec) collectFields(paths []string) []easypFieldDoc { - seen := make(map[string]struct{}) - out := make([]easypFieldDoc, 0) - for _, p := range paths { - doc, ok := s.DocsByPath[p] - if !ok { - continue - } - for _, f := range doc.Fields { - if _, exists := seen[f.Path]; exists { - continue - } - seen[f.Path] = struct{}{} - out = append(out, f) - } - } - - sort.Slice(out, func(i, j int) bool { - return out[i].Path < out[j].Path - }) - return out -} - -func (s easypSpec) collectExamples(paths []string, limit int) []easypExample { - out := make([]easypExample, 0, limit) - seen := make(map[string]struct{}) - for _, p := range paths { - doc, ok := s.DocsByPath[p] - if !ok { - continue - } - for _, ex := range doc.Examples { - if _, exists := seen[ex.Title]; exists { - continue - } - seen[ex.Title] = struct{}{} - out = append(out, ex) - if len(out) >= limit { - return out - } - } - } - return out -} - -func (s easypSpec) collectNotes(paths []string) []string { - seen := make(map[string]struct{}) - out := make([]string, 0) - for _, p := range paths { - doc, ok := s.DocsByPath[p] - if !ok { - continue - } - for _, note := range doc.Notes { - if _, exists := seen[note]; exists { - continue - } - seen[note] = struct{}{} - out = append(out, note) - } - } - return out -} - -func (s easypSpec) hasPath(path string) bool { - if _, ok := s.SchemaByPath[path]; ok { - return true - } - if _, ok := s.DocsByPath[path]; ok { - return true - } - return false -} - -func (s easypSpec) allPaths() []string { - seen := make(map[string]struct{}) - paths := make([]string, 0, len(s.SchemaByPath)+len(s.DocsByPath)) - - for p := range s.SchemaByPath { - if _, ok := seen[p]; ok { - continue - } - seen[p] = struct{}{} - paths = append(paths, p) - } - for p := range s.DocsByPath { - if _, ok := seen[p]; ok { - continue - } - seen[p] = struct{}{} - paths = append(paths, p) - } - - sort.Strings(paths) - return paths -} - -func boolOrDefault(v *bool, def bool) bool { - if v == nil { - return def - } - return *v -} - -func intOrDefault(v *int, def int) int { - if v == nil { - return def - } - return *v -} - -func normalizePath(path string) string { - path = strings.TrimSpace(path) - if path == "" || path == "$" || strings.EqualFold(path, "root") { - return "$" - } - - path = strings.TrimPrefix(path, "$.") - path = strings.TrimPrefix(path, ".") - path = strings.ReplaceAll(path, "[*]", "[]") - path = strings.ReplaceAll(path, "[0]", "[]") - path = strings.TrimSuffix(path, ".") - - return path -} - -func removeArrayMarkers(path string) string { - return strings.ReplaceAll(path, "[]", "") -} - -func isPathWithin(base, candidate string) bool { - if base == "$" { - return true - } - if base == candidate { - return true - } - if strings.HasPrefix(candidate, base+".") { - return true - } - if strings.HasPrefix(candidate, base+"[].") { - return true - } - if candidate == base+"[]" { - return true - } - return false -} - -func newEasypSpec() easypSpec { - docs := map[string]easypNodeDoc{ - "$": { - Fields: []easypFieldDoc{ - {Path: "version", Type: "string", Required: false, Description: "Legacy compatibility field.", DefaultValue: "omitted", Examples: []string{"v1alpha"}}, - {Path: "lint", Type: "object", Required: false, Description: "Linter configuration and rule selection."}, - {Path: "deps", Type: "array", Required: false, Description: "Dependency repositories in format @."}, - {Path: "generate", Type: "object", Required: false, Description: "Code generation configuration."}, - {Path: "breaking", Type: "object", Required: false, Description: "Breaking changes check configuration."}, - }, - Examples: []easypExample{ - { - Title: "minimal_config", - Description: "Small valid configuration with local input and one plugin.", - YAML: "lint:\n use:\n - DIRECTORY_SAME_PACKAGE\ngenerate:\n inputs:\n - directory: proto\n plugins:\n - name: go\n out: .\n opts:\n paths: source_relative\n", - Paths: []string{"$", "lint", "generate"}, - }, - }, - Notes: []string{ - "`generate.inputs[].git_repo.out` is intentionally excluded: it is treated as invalid and must not be used.", - "`generate.plugins[].url` is not a valid field in current schema; use `generate.plugins[].remote`.", - "Exactly one plugin source must be set per plugin item: name, remote, path, or command.", - }, - }, - "lint": { - Fields: []easypFieldDoc{ - {Path: "lint.use", Type: "array", Required: false, Description: "Rule groups and/or individual lint rule names.", AllowedValues: append(append([]string{}, lintRuleGroups...), lintRuleNames...), DefaultValue: "[]"}, - {Path: "lint.enum_zero_value_suffix", Type: "string", Required: false, Description: "Required suffix for enum zero value.", DefaultValue: "UNSPECIFIED (runtime default)"}, - {Path: "lint.service_suffix", Type: "string", Required: false, Description: "Required suffix for service names.", DefaultValue: "Service (runtime default)"}, - {Path: "lint.ignore", Type: "array", Required: false, Description: "Paths to exclude from linting.", DefaultValue: "[]"}, - {Path: "lint.except", Type: "array", Required: false, Description: "Rules to disable globally.", DefaultValue: "[]"}, - {Path: "lint.allow_comment_ignores", Type: "boolean", Required: false, Description: "Allow inline ignore comments in proto files.", DefaultValue: "false"}, - {Path: "lint.ignore_only", Type: "map>", Required: false, Description: "Disable specific rules only for selected paths.", DefaultValue: "{}"}, - }, - }, - "deps": { - Fields: []easypFieldDoc{ - {Path: "deps[]", Type: "string", Required: false, Description: "Dependency in format @.", Examples: []string{"github.com/googleapis/googleapis@v1.0.0", "github.com/bufbuild/protoc-gen-validate"}}, - }, - }, - "generate": { - Fields: []easypFieldDoc{ - {Path: "generate.inputs", Type: "array", Required: true, Description: "Input sources for proto files.", DefaultValue: "must be provided"}, - {Path: "generate.plugins", Type: "array", Required: true, Description: "Plugin definitions for generation.", DefaultValue: "must be provided"}, - {Path: "generate.managed", Type: "object", Required: false, Description: "Managed mode rules for file/field options.", DefaultValue: "{}"}, - }, - Examples: []easypExample{ - { - Title: "generate_local_and_remote_plugin", - Description: "Local directory input with remote plugin execution.", - YAML: "generate:\n inputs:\n - directory:\n path: api\n root: .\n plugins:\n - remote: api.easyp.tech/protobuf/go:v1.36.10\n out: .\n opts:\n paths: source_relative\n", - Paths: []string{"generate", "generate.inputs", "generate.plugins"}, - }, - }, - }, - "generate.inputs": { - Fields: []easypFieldDoc{ - {Path: "generate.inputs[].directory", Type: "string | object", Required: false, Description: "Local input directory. Shorthand string or object with path/root."}, - {Path: "generate.inputs[].git_repo", Type: "object", Required: false, Description: "Remote git repository input."}, - }, - Notes: []string{ - "Each input item must contain exactly one of `directory` or `git_repo`.", - }, - }, - "generate.inputs[].directory": { - Fields: []easypFieldDoc{ - {Path: "generate.inputs[].directory.path", Type: "string", Required: true, Description: "Directory with .proto files (relative to config root unless absolute).", Examples: []string{"proto", "api/proto"}}, - {Path: "generate.inputs[].directory.root", Type: "string", Required: false, Description: "Import root for path normalization.", DefaultValue: "."}, - }, - }, - "generate.inputs[].git_repo": { - Fields: []easypFieldDoc{ - {Path: "generate.inputs[].git_repo.url", Type: "string", Required: true, Description: "Git repo URL with optional revision.", Examples: []string{"github.com/acme/common@v1.0.0"}}, - {Path: "generate.inputs[].git_repo.sub_directory", Type: "string", Required: false, Description: "Subdirectory inside checked-out repository."}, - {Path: "generate.inputs[].git_repo.root", Type: "string", Required: false, Description: "Import root under repository contents.", DefaultValue: "\"\""}, - }, - Notes: []string{ - "`generate.inputs[].git_repo.out` is not part of valid schema and must not be used.", - }, - }, - "generate.plugins": { - Fields: []easypFieldDoc{ - {Path: "generate.plugins[]", Type: "object", Required: true, Description: "Plugin item with exactly one source and required output directory."}, - }, - }, - "generate.plugins[]": { - Fields: []easypFieldDoc{ - {Path: "generate.plugins[].name", Type: "string", Required: false, Description: "Built-in/local plugin name (one source option).", Examples: []string{"go", "go-grpc"}}, - {Path: "generate.plugins[].remote", Type: "string", Required: false, Description: "Remote plugin endpoint (one source option).", Examples: []string{"api.easyp.tech/protobuf/go:v1.36.10"}}, - {Path: "generate.plugins[].path", Type: "string", Required: false, Description: "Explicit path to plugin binary (one source option)."}, - {Path: "generate.plugins[].command", Type: "array", Required: false, Description: "Command invocation for plugin (one source option).", Examples: []string{`["go","run","github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@v2.25.1"]`}}, - {Path: "generate.plugins[].out", Type: "string", Required: true, Description: "Output directory for generated files.", Examples: []string{".", "gen/go"}}, - {Path: "generate.plugins[].opts", Type: "map", Required: false, Description: "Plugin options; value can be scalar or array of scalars."}, - {Path: "generate.plugins[].with_imports", Type: "boolean", Required: false, Description: "Include dependency protos in generation.", DefaultValue: "false"}, - }, - Notes: []string{ - "Use only one of `name`, `remote`, `path`, or `command` per item.", - "`generate.plugins[].url` is invalid in current schema; use `remote`.", - }, - Examples: []easypExample{ - { - Title: "plugin_remote", - Description: "Remote plugin source.", - YAML: "generate:\n plugins:\n - remote: api.easyp.tech/grpc/go:v1.5.1\n out: .\n opts:\n paths: source_relative\n", - Paths: []string{"generate.plugins[]"}, - }, - { - Title: "plugin_command", - Description: "Command-based plugin source.", - YAML: "generate:\n plugins:\n - command: [\"go\", \"run\", \"github.com/bufbuild/protoc-gen-validate@v0.10.1\"]\n out: gen/go\n", - Paths: []string{"generate.plugins[]"}, - }, - }, - }, - "generate.managed": { - Fields: []easypFieldDoc{ - {Path: "generate.managed.enabled", Type: "boolean", Required: false, Description: "Enable managed mode option rewriting.", DefaultValue: "false"}, - {Path: "generate.managed.disable", Type: "array", Required: false, Description: "Disable managed mode per module/path/option."}, - {Path: "generate.managed.override", Type: "array", Required: false, Description: "Override file/field options with values."}, - }, - }, - "generate.managed.disable": { - Fields: []easypFieldDoc{ - {Path: "generate.managed.disable[].module", Type: "string", Required: false, Description: "Apply disable to module."}, - {Path: "generate.managed.disable[].path", Type: "string", Required: false, Description: "Apply disable to path."}, - {Path: "generate.managed.disable[].file_option", Type: "string", Required: false, Description: "Disable this file option."}, - {Path: "generate.managed.disable[].field_option", Type: "string", Required: false, Description: "Disable this field option."}, - {Path: "generate.managed.disable[].field", Type: "string", Required: false, Description: "Field selector for field_option."}, - }, - Notes: []string{ - "At least one key in each disable item is required.", - "`file_option` and `field_option` cannot be used together.", - "`field` requires `field_option`.", - }, - }, - "generate.managed.override": { - Fields: []easypFieldDoc{ - {Path: "generate.managed.override[].file_option", Type: "string", Required: false, Description: "Target file option to override."}, - {Path: "generate.managed.override[].field_option", Type: "string", Required: false, Description: "Target field option to override."}, - {Path: "generate.managed.override[].value", Type: "any", Required: true, Description: "Override value."}, - {Path: "generate.managed.override[].module", Type: "string", Required: false, Description: "Optional module selector."}, - {Path: "generate.managed.override[].path", Type: "string", Required: false, Description: "Optional path selector."}, - {Path: "generate.managed.override[].field", Type: "string", Required: false, Description: "Optional field selector (for field_option)."}, - }, - Notes: []string{ - "Each override item requires exactly one of file_option or field_option.", - "`field` can only be used with `field_option`.", - }, - }, - "breaking": { - Fields: []easypFieldDoc{ - {Path: "breaking.ignore", Type: "array", Required: false, Description: "Paths excluded from breaking-change checks.", DefaultValue: "[]"}, - {Path: "breaking.against_git_ref", Type: "string", Required: false, Description: "Branch/tag/commit used for comparison."}, - }, - }, - } - - return easypSpec{ - SchemaVersion: "easyp-config-v1", - SchemaByPath: buildEasypConfigSchemaIndex(), - DocsByPath: docs, - } -} diff --git a/internal/mcpserver/tools_plugins.go b/internal/mcpserver/tools_plugins.go index 3049d7f..8b6b06c 100644 --- a/internal/mcpserver/tools_plugins.go +++ b/internal/mcpserver/tools_plugins.go @@ -49,7 +49,9 @@ func registerPluginTools(server *mcp.Server, pluginService PluginService) { Version: strings.TrimSpace(input.Version), Tags: compactStrings(input.Tags), } - + if pluginService == nil { + return &mcp.CallToolResult{}, pluginsListOutput{}, nil + } plugins, err := pluginService.ListPlugins(ctx, filter) if err != nil { return nil, pluginsListOutput{}, fmt.Errorf("list plugins: %w", err) From f1fbeadacd248935bbccb0ab319d4f6fd7e3b14b Mon Sep 17 00:00:00 2001 From: Khasbulat Abdullin Date: Tue, 10 Mar 2026 06:41:14 +0300 Subject: [PATCH 2/2] upd: workflows/easyp.yml --- .github/workflows/easyp.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/easyp.yml b/.github/workflows/easyp.yml index d2cc24e..43cabc6 100644 --- a/.github/workflows/easyp.yml +++ b/.github/workflows/easyp.yml @@ -18,9 +18,9 @@ jobs: steps: - uses: actions/checkout@v4 - name: EasyP Lint - uses: easyp-tech/actions/lint@v1 + uses: easyp-tech/actions/lint@v1.1.1 with: - version: v0.12.2 + version: v0.16.0 config: easyp.yaml directory: api @@ -32,9 +32,9 @@ jobs: with: fetch-depth: 0 - name: EasyP Breaking - uses: easyp-tech/actions/breaking@v1 + uses: easyp-tech/actions/breaking@v1.1.1 with: - version: v0.12.2 + version: v0.16.0 config: easyp.yaml directory: api against: master