mirror of
https://github.com/zeromicro/go-zero.git
synced 2026-05-13 01:40:00 +08:00
Compare commits
7 Commits
tools/goct
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47525987d3 | ||
|
|
3738be1945 | ||
|
|
5b74b9ab7b | ||
|
|
4a67261b7b | ||
|
|
22bdae0787 | ||
|
|
e8675d6a9a | ||
|
|
e441c44975 |
2
.github/workflows/go.yml
vendored
2
.github/workflows/go.yml
vendored
@@ -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
|
||||
|
||||
14
go.mod
14
go.mod
@@ -1,6 +1,6 @@
|
||||
module github.com/zeromicro/go-zero
|
||||
|
||||
go 1.24.0
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2
|
||||
@@ -14,7 +14,7 @@ require (
|
||||
github.com/grafana/pyroscope-go v1.2.8
|
||||
github.com/jackc/pgx/v5 v5.8.0
|
||||
github.com/jhump/protoreflect v1.18.0
|
||||
github.com/modelcontextprotocol/go-sdk v1.4.0
|
||||
github.com/modelcontextprotocol/go-sdk v1.6.0
|
||||
github.com/pelletier/go-toml/v2 v2.3.0
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/redis/go-redis/v9 v9.18.0
|
||||
@@ -23,7 +23,7 @@ require (
|
||||
github.com/titanous/json5 v1.0.0
|
||||
go.etcd.io/etcd/api/v3 v3.5.21
|
||||
go.etcd.io/etcd/client/v3 v3.5.21
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0
|
||||
go.mongodb.org/mongo-driver/v2 v2.6.0
|
||||
go.opentelemetry.io/otel v1.40.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0
|
||||
@@ -38,7 +38,7 @@ 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/grpc v1.80.0
|
||||
google.golang.org/protobuf v1.36.11
|
||||
gopkg.in/cheggaaa/pb.v1 v1.0.28
|
||||
gopkg.in/h2non/gock.v1 v1.1.2
|
||||
@@ -72,7 +72,7 @@ require (
|
||||
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/google/jsonschema-go v0.4.3 // indirect
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect
|
||||
@@ -101,7 +101,7 @@ require (
|
||||
github.com/prometheus/procfs v0.16.1 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/segmentio/asm v1.1.3 // indirect
|
||||
github.com/segmentio/encoding v0.5.3 // indirect
|
||||
github.com/segmentio/encoding v0.5.4 // indirect
|
||||
github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
@@ -122,7 +122,7 @@ require (
|
||||
go.yaml.in/yaml/v2 v2.4.2 // 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
|
||||
golang.org/x/oauth2 v0.35.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/term v0.40.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
|
||||
36
go.sum
36
go.sum
@@ -67,8 +67,8 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
|
||||
@@ -76,8 +76,8 @@ github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7O
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
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/jsonschema-go v0.4.3 h1:/DBOLZTfDow7pe2GmaJNhltueGTtDKICi8V8p+DQPd0=
|
||||
github.com/google/jsonschema-go v0.4.3/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=
|
||||
@@ -131,8 +131,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
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/modelcontextprotocol/go-sdk v1.6.0 h1:PPLS3kn7WtOEnR+Af4X5H96SG0qSab8R/ZQT/HkhPkY=
|
||||
github.com/modelcontextprotocol/go-sdk v1.6.0/go.mod h1:kzm3kzFL1/+AziGOE0nUs3gvPoNxMCvkxokMkuFapXQ=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
@@ -179,8 +179,8 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t
|
||||
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/segmentio/encoding v0.5.3 h1:OjMgICtcSFuNvQCdwqMCv9Tg7lEOXGwm1J5RPQccx6w=
|
||||
github.com/segmentio/encoding v0.5.3/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
|
||||
github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0=
|
||||
github.com/segmentio/encoding v0.5.4/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=
|
||||
@@ -226,8 +226,8 @@ go.etcd.io/etcd/client/pkg/v3 v3.5.21 h1:lPBu71Y7osQmzlflM9OfeIV2JlmpBjqBNlLtcoB
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.21/go.mod h1:BgqT/IXPjK9NkeSDjbzwsHySX3yIle2+ndz28nVsjUs=
|
||||
go.etcd.io/etcd/client/v3 v3.5.21 h1:T6b1Ow6fNjOLOtM0xSoKNQt1ASPCLWrF9XMHcH9pEyY=
|
||||
go.etcd.io/etcd/client/v3 v3.5.21/go.mod h1:mFYy67IOqmbRf/kRUvsHixzo3iG+1OF2W2+jVIQRAnU=
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||
go.mongodb.org/mongo-driver/v2 v2.6.0 h1:b9sJOYrkmt4l8bY43ZenFBcPlhYIjaOfYHLtbB/5qi8=
|
||||
go.mongodb.org/mongo-driver/v2 v2.6.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
|
||||
@@ -285,8 +285,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
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/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
|
||||
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -321,20 +321,20 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
||||
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
||||
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
||||
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
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=
|
||||
|
||||
33
mcp/options.go
Normal file
33
mcp/options.go
Normal 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
|
||||
})
|
||||
}
|
||||
@@ -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
150
mcp/request_metadata.go
Normal 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
|
||||
}
|
||||
185
mcp/request_metadata_test.go
Normal file
185
mcp/request_metadata_test.go
Normal 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"])
|
||||
}
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user