Compare commits

...

6 Commits

Author SHA1 Message Date
dependabot[bot]
4f02fcd018 chore(deps): bump k8s.io/apimachinery from 0.34.3 to 0.36.0
Bumps [k8s.io/apimachinery](https://github.com/kubernetes/apimachinery) from 0.34.3 to 0.36.0.
- [Commits](https://github.com/kubernetes/apimachinery/compare/v0.34.3...v0.36.0)

---
updated-dependencies:
- dependency-name: k8s.io/apimachinery
  dependency-version: 0.36.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-27 23:22:56 +00:00
Kevin Wan
5b74b9ab7b feat(mcp): add opt-in request metadata bridge for tool handlers (#5550) 2026-04-25 17:11:04 +08:00
Kevin Wan
4a67261b7b fix(discov): move etcd hosts from URI authority to path for Go 1.26 compatibility (#5548) 2026-04-25 10:48:28 +08:00
dependabot[bot]
22bdae0787 chore(deps): bump codecov/codecov-action from 5 to 6 (#5521) 2026-04-11 15:41:58 +08:00
dependabot[bot]
e8675d6a9a chore(deps): bump google.golang.org/grpc from 1.79.3 to 1.80.0 (#5523) 2026-04-11 15:06:08 +08:00
dependabot[bot]
e441c44975 chore(deps): bump google.golang.org/grpc from 1.79.3 to 1.80.0 in /tools/goctl (#5524) 2026-04-11 10:46:11 +08:00
17 changed files with 740 additions and 61 deletions

View File

@@ -40,7 +40,7 @@ jobs:
run: go test -race -coverprofile=coverage.txt -covermode=atomic ./...
- name: Codecov
uses: codecov/codecov-action@v5
uses: codecov/codecov-action@v6
with:
files: ./coverage.txt
flags: unittests

26
go.mod
View File

@@ -1,6 +1,6 @@
module github.com/zeromicro/go-zero
go 1.24.0
go 1.26.0
require (
github.com/DATA-DOG/go-sqlmock v1.5.2
@@ -38,13 +38,13 @@ require (
golang.org/x/sys v0.41.0
golang.org/x/time v0.14.0
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409
google.golang.org/grpc v1.79.3
google.golang.org/protobuf v1.36.11
google.golang.org/grpc v1.80.0
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af
gopkg.in/cheggaaa/pb.v1 v1.0.28
gopkg.in/h2non/gock.v1 v1.1.2
gopkg.in/yaml.v2 v2.4.0
k8s.io/api v0.34.3
k8s.io/apimachinery v0.34.3
k8s.io/apimachinery v0.36.0
k8s.io/client-go v0.34.3
k8s.io/utils v0.0.0-20260319190234-28399d86e0b5
)
@@ -57,7 +57,7 @@ require (
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect
github.com/coreos/go-semver v0.3.1 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/emicklei/go-restful/v3 v3.12.2 // indirect
github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect
@@ -71,7 +71,6 @@ require (
github.com/go-openapi/swag v0.23.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/gnostic-models v0.7.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/jsonschema-go v0.4.2 // indirect
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect
@@ -93,9 +92,8 @@ require (
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/openzipkin/zipkin-go v0.4.3 // indirect
github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.66.1 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
@@ -119,7 +117,7 @@ require (
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
go.uber.org/zap v1.24.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/oauth2 v0.34.0 // indirect
@@ -127,13 +125,13 @@ require (
golang.org/x/term v0.40.0 // indirect
golang.org/x/text v0.34.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
k8s.io/klog/v2 v2.140.0 // indirect
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
)

59
go.sum
View File

@@ -26,8 +26,9 @@ github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=
@@ -59,9 +60,6 @@ github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+Gr
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
@@ -78,8 +76,6 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8=
github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grafana/pyroscope-go v1.2.8 h1:UvCwIhlx9DeV7F6TW/z8q1Mi4PIm3vuUJ2ZlCEvmA4M=
@@ -143,10 +139,6 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4=
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM=
github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
github.com/openzipkin/zipkin-go v0.4.3 h1:9EGwpqkgnwdEIJ+Od7QVSEIH+ocmm5nPat0G7sjsSdg=
github.com/openzipkin/zipkin-go v0.4.3/go.mod h1:M9wCJZFWCo2RiY+o1eBCEMe0Dp2S5LDHcMZmk3RmK7c=
github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM=
@@ -157,8 +149,9 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
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/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
@@ -183,8 +176,8 @@ github.com/segmentio/encoding v0.5.3 h1:OjMgICtcSFuNvQCdwqMCv9Tg7lEOXGwm1J5RPQcc
github.com/segmentio/encoding v0.5.3/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo=
github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -264,8 +257,8 @@ go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
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=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -327,23 +320,23 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M=
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI=
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/cheggaaa/pb.v1 v1.0.28 h1:n1tBJnnK2r7g9OW2btFH91V92STTUevLXYFb8gy9EMk=
gopkg.in/cheggaaa/pb.v1 v1.0.28/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=
gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY=
gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
@@ -357,21 +350,21 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/api v0.34.3 h1:D12sTP257/jSH2vHV2EDYrb16bS7ULlHpdNdNhEw2S4=
k8s.io/api v0.34.3/go.mod h1:PyVQBF886Q5RSQZOim7DybQjAbVs8g7gwJNhGtY5MBk=
k8s.io/apimachinery v0.34.3 h1:/TB+SFEiQvN9HPldtlWOTp0hWbJ+fjU+wkxysf/aQnE=
k8s.io/apimachinery v0.34.3/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
k8s.io/apimachinery v0.36.0 h1:jZyPzhd5Z+3h9vJLt0z9XdzW9VzNzWAUw+P1xZ9PXtQ=
k8s.io/apimachinery v0.36.0/go.mod h1:FklypaRJt6n5wUIwWXIP6GJlIpUizTgfo1T/As+Tyxc=
k8s.io/client-go v0.34.3 h1:wtYtpzy/OPNYf7WyNBTj3iUA0XaBHVqhv4Iv3tbrF5A=
k8s.io/client-go v0.34.3/go.mod h1:OxxeYagaP9Kdf78UrKLa3YZixMCfP6bgPwPwNBQBzpM=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA=
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts=
k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc=
k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0=
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a h1:xCeOEAOoGYl2jnJoHkC3hkbPJgdATINPMAxaynU2Ovg=
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0=
k8s.io/utils v0.0.0-20260319190234-28399d86e0b5 h1:kBawHLSnx/mYHmRnNUf9d4CpjREbeZuxoSGOX/J+aYM=
k8s.io/utils v0.0.0-20260319190234-28399d86e0b5/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=
sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
sigs.k8s.io/structured-merge-diff/v6 v6.3.2 h1:kwVWMx5yS1CrnFWA/2QHyRVJ8jM6dBA80uLmm0wJkk8=
sigs.k8s.io/structured-merge-diff/v6 v6.3.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=

33
mcp/options.go Normal file
View File

@@ -0,0 +1,33 @@
package mcp
import "net/http"
// RequestMetadataExtractor extracts request metadata for downstream handlers.
type RequestMetadataExtractor func(*http.Request) RequestMetadata
// McpOption customizes MCP server construction.
type McpOption interface {
apply(*serverOptions)
}
type mcpOptionFunc func(*serverOptions)
func (f mcpOptionFunc) apply(opts *serverOptions) {
f(opts)
}
type serverOptions struct {
requestMetadataExtractor RequestMetadataExtractor
}
func defaultServerOptions() serverOptions {
return serverOptions{}
}
// WithRequestMetadataExtractor installs an extractor that runs for each incoming
// MCP HTTP request, and stores the extracted metadata into handler context.
func WithRequestMetadataExtractor(extractor RequestMetadataExtractor) McpOption {
return mcpOptionFunc(func(opts *serverOptions) {
opts.requestMetadataExtractor = extractor
})
}

View File

@@ -15,6 +15,7 @@ This package provides a go-zero integration for the [Model Context Protocol (MCP
- **CORS Support**: Configurable CORS settings for cross-origin requests
- **Type-Safe Tool Handlers**: Generic tool handlers with automatic JSON schema generation
- **Prompts and Resources**: Full support for MCP prompts and resources
- **Request Metadata Bridge**: Optional request metadata extraction into handler context
## Quick Start
@@ -220,6 +221,35 @@ mcp:
messageEndpoint: /message
```
## Request Metadata Bridge
For multi-tenant or request-context-aware tools, you can extract selected HTTP request metadata once at the transport boundary and read it from `context.Context` in handlers.
```go
server := mcp.NewMcpServerWithOptions(c,
mcp.WithRequestMetadataExtractor(mcp.DefaultRequestMetadataExtractor),
)
handler := func(ctx context.Context, req *mcp.CallToolRequest, args SomeArgs) (*mcp.CallToolResult, any, error) {
tenant, _ := mcp.HeaderFromContext(ctx, "X-Tenant-Id")
traceID, _ := mcp.QueryFromContext(ctx, "trace")
scope, _ := mcp.PathFromContext(ctx, "scope")
_ = tenant
_ = traceID
_ = scope
return &mcp.CallToolResult{}, nil, nil
}
```
Available helpers:
- `RequestMetadataFromContext(ctx)`
- `HeaderFromContext(ctx, key)`
- `QueryFromContext(ctx, key)`
- `PathFromContext(ctx, key)`
## Configuration Options
| Field | Type | Default | Description |

150
mcp/request_metadata.go Normal file
View File

@@ -0,0 +1,150 @@
package mcp
import (
"context"
"net/http"
"github.com/zeromicro/go-zero/rest/pathvar"
)
// RequestMetadata carries selected request-scoped values into MCP handlers.
type RequestMetadata struct {
Headers map[string][]string
Query map[string][]string
Path map[string]string
}
type requestMetadataCtxKey struct{}
// RequestMetadataFromContext returns metadata extracted at the transport boundary.
func RequestMetadataFromContext(ctx context.Context) (RequestMetadata, bool) {
metadata, ok := requestMetadataFromContext(ctx)
if !ok {
return RequestMetadata{}, false
}
return normalizeRequestMetadata(metadata), true
}
// HeaderFromContext returns the first header value for key.
func HeaderFromContext(ctx context.Context, key string) (string, bool) {
metadata, ok := requestMetadataFromContext(ctx)
if !ok {
return "", false
}
vals := metadata.Headers[http.CanonicalHeaderKey(key)]
if len(vals) == 0 {
return "", false
}
return vals[0], true
}
// QueryFromContext returns the first query value for key.
func QueryFromContext(ctx context.Context, key string) (string, bool) {
metadata, ok := requestMetadataFromContext(ctx)
if !ok {
return "", false
}
vals := metadata.Query[key]
if len(vals) == 0 {
return "", false
}
return vals[0], true
}
// PathFromContext returns the path variable value for key.
func PathFromContext(ctx context.Context, key string) (string, bool) {
metadata, ok := requestMetadataFromContext(ctx)
if !ok {
return "", false
}
val, ok := metadata.Path[key]
if !ok {
return "", false
}
return val, true
}
func requestMetadataFromContext(ctx context.Context) (RequestMetadata, bool) {
metadata, ok := ctx.Value(requestMetadataCtxKey{}).(RequestMetadata)
if !ok {
return RequestMetadata{}, false
}
return metadata, true
}
// DefaultRequestMetadataExtractor extracts headers, query values, and path variables.
func DefaultRequestMetadataExtractor(r *http.Request) RequestMetadata {
metadata := RequestMetadata{
Headers: make(map[string][]string, len(r.Header)),
Query: make(map[string][]string),
Path: clonePathVars(pathvar.Vars(r)),
}
for key, vals := range r.Header {
metadata.Headers[http.CanonicalHeaderKey(key)] = append([]string(nil), vals...)
}
if r.URL != nil {
for key, vals := range r.URL.Query() {
metadata.Query[key] = append([]string(nil), vals...)
}
}
return metadata
}
func normalizeRequestMetadata(metadata RequestMetadata) RequestMetadata {
return RequestMetadata{
Headers: cloneCanonicalHeaderValues(metadata.Headers),
Query: cloneHeaderValues(metadata.Query),
Path: clonePathVars(metadata.Path),
}
}
func cloneHeaderValues(values map[string][]string) map[string][]string {
if len(values) == 0 {
return nil
}
cloned := make(map[string][]string, len(values))
for key, vals := range values {
cloned[key] = append([]string(nil), vals...)
}
return cloned
}
func cloneCanonicalHeaderValues(values map[string][]string) map[string][]string {
if len(values) == 0 {
return nil
}
cloned := make(map[string][]string, len(values))
for key, vals := range values {
canonical := http.CanonicalHeaderKey(key)
cloned[canonical] = append(cloned[canonical], vals...)
}
return cloned
}
func clonePathVars(values map[string]string) map[string]string {
if len(values) == 0 {
return nil
}
cloned := make(map[string]string, len(values))
for key, val := range values {
cloned[key] = val
}
return cloned
}

View File

@@ -0,0 +1,185 @@
package mcp
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/zeromicro/go-zero/rest/pathvar"
)
func TestDefaultRequestMetadataExtractor(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/sse?tenant=t1&trace=abc", nil)
req.Header.Add("X-Tenant-Id", "tenant-from-header")
req = pathvar.WithVars(req, map[string]string{"tool": "sum"})
metadata := DefaultRequestMetadataExtractor(req)
header, ok := metadata.Headers["X-Tenant-Id"]
assert.True(t, ok)
assert.Equal(t, []string{"tenant-from-header"}, header)
assert.Equal(t, []string{"t1"}, metadata.Query["tenant"])
assert.Equal(t, "sum", metadata.Path["tool"])
}
func TestRequestMetadataContextHelpers(t *testing.T) {
ctx := context.WithValue(context.Background(), requestMetadataCtxKey{}, RequestMetadata{
Headers: map[string][]string{"X-Trace-Id": {"trace-1"}},
Query: map[string][]string{"tenant": {"foo"}},
Path: map[string]string{"scope": "prod"},
})
metadata, ok := RequestMetadataFromContext(ctx)
assert.True(t, ok)
assert.Equal(t, []string{"trace-1"}, metadata.Headers["X-Trace-Id"])
header, ok := HeaderFromContext(ctx, "x-trace-id")
assert.True(t, ok)
assert.Equal(t, "trace-1", header)
query, ok := QueryFromContext(ctx, "tenant")
assert.True(t, ok)
assert.Equal(t, "foo", query)
path, ok := PathFromContext(ctx, "scope")
assert.True(t, ok)
assert.Equal(t, "prod", path)
}
func TestRequestMetadataContextHelpersMissingKeys(t *testing.T) {
ctx := context.WithValue(context.Background(), requestMetadataCtxKey{}, RequestMetadata{
Headers: map[string][]string{"X-Trace-Id": {"trace-1"}},
Query: map[string][]string{"tenant": {"foo"}},
Path: map[string]string{"scope": "prod"},
})
_, ok := HeaderFromContext(ctx, "x-missing")
assert.False(t, ok)
_, ok = QueryFromContext(ctx, "missing")
assert.False(t, ok)
_, ok = PathFromContext(ctx, "missing")
assert.False(t, ok)
}
func TestRequestMetadataFromContextNotFound(t *testing.T) {
_, ok := RequestMetadataFromContext(context.Background())
assert.False(t, ok)
_, ok = HeaderFromContext(context.Background(), "x-test")
assert.False(t, ok)
_, ok = QueryFromContext(context.Background(), "tenant")
assert.False(t, ok)
_, ok = PathFromContext(context.Background(), "tenant")
assert.False(t, ok)
}
func TestWrapRequestMetadata(t *testing.T) {
s := &mcpServerImpl{
options: serverOptions{
requestMetadataExtractor: DefaultRequestMetadataExtractor,
},
}
called := false
handler := s.wrapRequestMetadata(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
called = true
header, ok := HeaderFromContext(r.Context(), "x-tenant-id")
assert.True(t, ok)
assert.Equal(t, "tenant-1", header)
query, ok := QueryFromContext(r.Context(), "tenant")
assert.True(t, ok)
assert.Equal(t, "q-tenant", query)
}))
req := httptest.NewRequest(http.MethodGet, "/sse?tenant=q-tenant", nil)
req.Header.Set("X-Tenant-Id", "tenant-1")
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
assert.True(t, called)
}
func TestWrapRequestMetadataNoExtractor(t *testing.T) {
s := &mcpServerImpl{}
called := false
handler := s.wrapRequestMetadata(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
called = true
_, ok := RequestMetadataFromContext(r.Context())
assert.False(t, ok)
}))
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/sse", nil))
assert.True(t, called)
}
func TestWrapRequestMetadataCanonicalizesCustomHeaders(t *testing.T) {
s := &mcpServerImpl{
options: serverOptions{
requestMetadataExtractor: func(*http.Request) RequestMetadata {
return RequestMetadata{
Headers: map[string][]string{
"x-tenant-id": {"tenant-lower"},
},
}
},
},
}
called := false
handler := s.wrapRequestMetadata(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
called = true
header, ok := HeaderFromContext(r.Context(), "X-Tenant-Id")
assert.True(t, ok)
assert.Equal(t, "tenant-lower", header)
}))
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/sse", nil))
assert.True(t, called)
}
func TestRequestMetadataFromContextReturnsCopy(t *testing.T) {
ctx := context.WithValue(context.Background(), requestMetadataCtxKey{}, RequestMetadata{
Headers: map[string][]string{"X-Trace-Id": {"trace-1"}},
})
metadata, ok := RequestMetadataFromContext(ctx)
assert.True(t, ok)
metadata.Headers["X-Trace-Id"][0] = "mutated"
metadata.Headers["X-New"] = []string{"new"}
fresh, ok := RequestMetadataFromContext(ctx)
assert.True(t, ok)
assert.Equal(t, []string{"trace-1"}, fresh.Headers["X-Trace-Id"])
assert.Nil(t, fresh.Headers["X-New"])
}
func TestRequestMetadataFromContextWithEmptyAndCanonicalizedHeaders(t *testing.T) {
emptyCtx := context.WithValue(context.Background(), requestMetadataCtxKey{}, RequestMetadata{})
empty, ok := RequestMetadataFromContext(emptyCtx)
assert.True(t, ok)
assert.Nil(t, empty.Headers)
assert.Nil(t, empty.Query)
assert.Nil(t, empty.Path)
ctx := context.WithValue(context.Background(), requestMetadataCtxKey{}, RequestMetadata{
Headers: map[string][]string{
"x-tenant-id": {"a"},
"X-Tenant-Id": {"b"},
},
})
metadata, ok := RequestMetadataFromContext(ctx)
assert.True(t, ok)
assert.Equal(t, []string{"a", "b"}, metadata.Headers["X-Tenant-Id"])
}

View File

@@ -1,6 +1,7 @@
package mcp
import (
"context"
"net/http"
sdkmcp "github.com/modelcontextprotocol/go-sdk/mcp"
@@ -20,10 +21,23 @@ type mcpServerImpl struct {
conf McpConf
httpServer *rest.Server
mcpServer *sdkmcp.Server
options serverOptions
}
// NewMcpServer creates a new MCP server using the official SDK
func NewMcpServer(c McpConf) McpServer {
return NewMcpServerWithOptions(c)
}
// NewMcpServerWithOptions creates a new MCP server with optional customizations.
func NewMcpServerWithOptions(c McpConf, opts ...McpOption) McpServer {
serverOpts := defaultServerOptions()
for _, opt := range opts {
if opt != nil {
opt.apply(&serverOpts)
}
}
// Create the underlying rest HTTP server
var httpServer *rest.Server
if len(c.Mcp.Cors) == 0 {
@@ -52,6 +66,7 @@ func NewMcpServer(c McpConf) McpServer {
conf: c,
httpServer: httpServer,
mcpServer: mcpServer,
options: serverOpts,
}
// Choose transport based on configuration
@@ -85,7 +100,7 @@ func (s *mcpServerImpl) setupSSETransport() {
return s.mcpServer
}, nil)
s.registerRoutes(handler, s.conf.Mcp.SseEndpoint)
s.registerRoutes(s.wrapRequestMetadata(handler), s.conf.Mcp.SseEndpoint)
}
// setupStreamableTransport configures the server to use Streamable HTTP transport (2025-03-26 spec)
@@ -96,7 +111,7 @@ func (s *mcpServerImpl) setupStreamableTransport() {
return s.mcpServer
}, nil)
s.registerRoutes(handler, s.conf.Mcp.MessageEndpoint)
s.registerRoutes(s.wrapRequestMetadata(handler), s.conf.Mcp.MessageEndpoint)
}
func (s *mcpServerImpl) registerRoutes(handler http.Handler, endpoint string) {
@@ -113,3 +128,16 @@ func (s *mcpServerImpl) registerRoutes(handler http.Handler, endpoint string) {
Handler: handler.ServeHTTP,
}, rest.WithTimeout(s.conf.Mcp.MessageTimeout))
}
func (s *mcpServerImpl) wrapRequestMetadata(next http.Handler) http.Handler {
extractor := s.options.requestMetadataExtractor
if extractor == nil {
return next
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
metadata := normalizeRequestMetadata(extractor(r))
ctx := context.WithValue(r.Context(), requestMetadataCtxKey{}, metadata)
next.ServeHTTP(w, r.WithContext(ctx))
})
}

View File

@@ -3,11 +3,14 @@ package mcp
import (
"bytes"
"context"
"fmt"
"net"
"net/http"
"net/http/httptest"
"testing"
"time"
sdkmcp "github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/stretchr/testify/assert"
"github.com/zeromicro/go-zero/core/conf"
)
@@ -391,3 +394,148 @@ func TestAddToolWithCustomServer(t *testing.T) {
return nil, nil, nil
})
}
func TestRequestMetadataIntegrationSSEToolCall(t *testing.T) {
port := getFreePort(t)
c := McpConf{}
c.Host = "127.0.0.1"
c.Port = port
c.Mcp.Name = "metadata-integration-test"
c.Mcp.UseStreamable = false
c.Mcp.SseEndpoint = "/sse/:scope"
c.Mcp.MessageTimeout = 2 * time.Second
c.Mcp.SseTimeout = 2 * time.Second
server := NewMcpServerWithOptions(c, WithRequestMetadataExtractor(DefaultRequestMetadataExtractor))
tool := &Tool{
Name: "inspect_metadata",
Description: "Inspect metadata in handler context",
}
type Args struct{}
AddTool(server, tool, func(ctx context.Context, req *CallToolRequest, args Args) (*CallToolResult, any, error) {
header, ok := HeaderFromContext(ctx, "x-tenant-id")
if !ok || header != "tenant-header" {
return nil, nil, fmt.Errorf("unexpected header from context: %q", header)
}
query, ok := QueryFromContext(ctx, "tenant")
if !ok || query != "tenant-query" {
return nil, nil, fmt.Errorf("unexpected query from context: %q", query)
}
scope, ok := PathFromContext(ctx, "scope")
if !ok || scope != "prod" {
return nil, nil, fmt.Errorf("unexpected path from context: %q", scope)
}
return &CallToolResult{
Content: []Content{&TextContent{Text: "metadata-ok"}},
}, nil, nil
})
go server.Start()
t.Cleanup(server.Stop)
baseURL := fmt.Sprintf("http://127.0.0.1:%d/sse/prod?tenant=tenant-query", port)
waitForServerReady(t, baseURL, 2*time.Second)
client := sdkmcp.NewClient(&sdkmcp.Implementation{
Name: "metadata-client",
Version: "1.0.0",
}, nil)
httpClient := &http.Client{
Timeout: 2 * time.Second,
Transport: metadataHeaderRoundTripper{
next: http.DefaultTransport,
},
}
transport := &sdkmcp.SSEClientTransport{
Endpoint: baseURL,
HTTPClient: httpClient,
}
session, err := client.Connect(context.Background(), transport, nil)
if !assert.NoError(t, err) {
return
}
t.Cleanup(func() {
_ = session.Close()
})
res, err := session.CallTool(context.Background(), &sdkmcp.CallToolParams{
Name: "inspect_metadata",
Arguments: map[string]any{},
})
if !assert.NoError(t, err) {
return
}
if !assert.NotNil(t, res) {
return
}
assert.False(t, res.IsError)
}
type metadataHeaderRoundTripper struct {
next http.RoundTripper
}
func (r metadataHeaderRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
next := r.next
if next == nil {
next = http.DefaultTransport
}
clone := req.Clone(req.Context())
clone.Header.Set("X-Tenant-Id", "tenant-header")
return next.RoundTrip(clone)
}
func getFreePort(t *testing.T) int {
t.Helper()
listener, err := net.Listen("tcp", "127.0.0.1:0")
if !assert.NoError(t, err) {
return 0
}
defer listener.Close()
addr, ok := listener.Addr().(*net.TCPAddr)
if !assert.True(t, ok) {
return 0
}
return addr.Port
}
func waitForServerReady(t *testing.T, endpoint string, timeout time.Duration) {
t.Helper()
client := &http.Client{Timeout: 200 * time.Millisecond}
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
req, err := http.NewRequest(http.MethodGet, endpoint, nil)
if err != nil {
t.Fatalf("failed to build readiness request: %v", err)
}
req.Header.Set("Accept", "text/event-stream")
resp, err := client.Do(req)
if err == nil {
_ = resp.Body.Close()
if resp.StatusCode > 0 {
return
}
}
time.Sleep(20 * time.Millisecond)
}
t.Fatalf("server did not become ready for %s within %s", endpoint, timeout)
}

View File

@@ -18,7 +18,7 @@ require (
github.com/zeromicro/ddl-parser v1.0.5
github.com/zeromicro/go-zero v1.10.1
golang.org/x/text v0.34.0
google.golang.org/grpc v1.79.3
google.golang.org/grpc v1.80.0
google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v2 v2.4.0
)

View File

@@ -282,14 +282,14 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M=
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@@ -13,10 +13,10 @@ type discovBuilder struct{}
func (b *discovBuilder) Build(target resolver.Target, cc resolver.ClientConn, _ resolver.BuildOptions) (
resolver.Resolver, error) {
hosts := strings.FieldsFunc(targets.GetAuthority(target), func(r rune) bool {
hosts := strings.FieldsFunc(targets.GetHosts(target), func(r rune) bool {
return r == EndpointSepChar
})
sub, err := discov.NewSubscriber(hosts, targets.GetEndpoints(target))
sub, err := discov.NewSubscriber(hosts, targets.GetKey(target))
if err != nil {
return nil, err
}

View File

@@ -28,7 +28,7 @@ func TestDiscovBuilder_Build(t *testing.T) {
for _, server := range servers.Servers {
addrs = append(addrs, server.Address)
}
u, err := url.Parse(fmt.Sprintf("%s://%s", DiscovScheme, strings.Join(addrs, ",")))
u, err := url.Parse(fmt.Sprintf("%s:///%s?key=test", DiscovScheme, strings.Join(addrs, ",")))
assert.NoError(t, err)
var b discovBuilder

View File

@@ -17,3 +17,29 @@ func GetAuthority(target resolver.Target) string {
func GetEndpoints(target resolver.Target) string {
return strings.Trim(target.URL.Path, slashSeparator)
}
// GetHosts returns the comma-separated etcd hosts from the target URL.
// It supports two formats:
// - New format (etcd:///h1:port,h2:port?key=k): hosts are in the URL path (empty authority)
// - Legacy format (etcd://h1:port/key): host is in the URL authority
func GetHosts(target resolver.Target) string {
if target.URL.Host == "" {
// New format: hosts encoded in URL path to avoid RFC 3986 authority issues
return GetEndpoints(target)
}
// Legacy format: single host in authority
return target.URL.Host
}
// GetKey returns the etcd key from the target URL.
// It supports two formats:
// - New format (etcd:///h1:port,h2:port?key=k): key is in the "key" query parameter
// - Legacy format (etcd://h1:port/key): key is in the URL path
func GetKey(target resolver.Target) string {
if target.URL.Host == "" {
// New format: key is in the query parameter
return target.URL.Query().Get("key")
}
// Legacy format: key is in the path
return strings.Trim(target.URL.Path, slashSeparator)
}

View File

@@ -87,3 +87,83 @@ func TestGetEndpoints(t *testing.T) {
})
}
}
func TestGetHosts(t *testing.T) {
tests := []struct {
name string
url string
want string
}{
{
name: "single host",
url: "etcd:///localhost:2379?key=foo",
want: "localhost:2379",
},
{
name: "multiple hosts",
url: "etcd:///host1:2379,host2:2379,host3:2379?key=foo",
want: "host1:2379,host2:2379,host3:2379",
},
{
name: "legacy single host in authority",
url: "etcd://localhost:2379/my-service",
want: "localhost:2379",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
uri, err := url.Parse(test.url)
assert.Nil(t, err)
target := resolver.Target{
URL: *uri,
}
assert.Equal(t, test.want, GetHosts(target))
})
}
}
func TestGetKey(t *testing.T) {
tests := []struct {
name string
url string
want string
}{
{
name: "simple key",
url: "etcd:///localhost:2379?key=my-service",
want: "my-service",
},
{
name: "key with slashes",
url: "etcd:///localhost:2379?key=%2Fgrpc%2Fmy-service",
want: "/grpc/my-service",
},
{
name: "no key",
url: "etcd:///localhost:2379",
want: "",
},
{
name: "legacy key in path",
url: "etcd://localhost:2379/my-service",
want: "my-service",
},
{
name: "legacy key with leading slash",
url: "etcd://localhost:2379/grpc/my-service",
want: "grpc/my-service",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
uri, err := url.Parse(test.url)
assert.Nil(t, err)
target := resolver.Target{
URL: *uri,
}
assert.Equal(t, test.want, GetKey(target))
})
}
}

View File

@@ -2,6 +2,7 @@ package resolver
import (
"fmt"
"net/url"
"strings"
"github.com/zeromicro/go-zero/zrpc/resolver/internal"
@@ -14,7 +15,9 @@ func BuildDirectTarget(endpoints []string) string {
}
// BuildDiscovTarget returns a string that represents the given endpoints with discov schema.
// The format is etcd:///host1:port,host2:port?key=<etcd-key> to avoid placing comma-separated
// hosts in the URI authority, which Go 1.26+ rejects per RFC 3986.
func BuildDiscovTarget(endpoints []string, key string) string {
return fmt.Sprintf("%s://%s/%s", internal.EtcdScheme,
strings.Join(endpoints, internal.EndpointSep), key)
return fmt.Sprintf("%s:///%s?key=%s", internal.EtcdScheme,
strings.Join(endpoints, internal.EndpointSep), url.QueryEscape(key))
}

View File

@@ -13,5 +13,10 @@ func TestBuildDirectTarget(t *testing.T) {
func TestBuildDiscovTarget(t *testing.T) {
target := BuildDiscovTarget([]string{"localhost:123", "localhost:456"}, "foo")
assert.Equal(t, "etcd://localhost:123,localhost:456/foo", target)
assert.Equal(t, "etcd:///localhost:123,localhost:456?key=foo", target)
}
func TestBuildDiscovTargetWithSlashKey(t *testing.T) {
target := BuildDiscovTarget([]string{"localhost:2379"}, "/grpc/my-service")
assert.Equal(t, "etcd:///localhost:2379?key=%2Fgrpc%2Fmy-service", target)
}