diff --git a/e2e/servers/mcp-go/go.mod b/e2e/servers/mcp-go/go.mod index 8825482a4f..306944e9c5 100644 --- a/e2e/servers/mcp-go/go.mod +++ b/e2e/servers/mcp-go/go.mod @@ -5,23 +5,34 @@ go 1.24.0 toolchain go1.24.1 require ( - github.com/x402-foundation/x402/go v0.0.0 github.com/modelcontextprotocol/go-sdk v1.3.0 + github.com/x402-foundation/x402/go v0.0.0 ) require ( + github.com/Microsoft/go-winio v0.6.2 // indirect github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 // indirect + github.com/StackExchange/wmi v1.2.1 // indirect github.com/bits-and-blooms/bitset v1.20.0 // indirect github.com/consensys/gnark-crypto v0.18.0 // indirect github.com/crate-crypto/go-eth-kzg v1.4.0 // indirect github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect + github.com/deckarep/golang-set/v2 v2.6.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect github.com/ethereum/c-kzg-4844/v2 v2.1.5 // indirect github.com/ethereum/go-ethereum v1.16.7 // indirect github.com/ethereum/go-verkle v0.2.2 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect github.com/google/jsonschema-go v0.4.2 // indirect + github.com/gorilla/websocket v1.4.2 // indirect github.com/holiman/uint256 v1.3.2 // indirect + github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect golang.org/x/crypto v0.46.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect diff --git a/e2e/servers/mcp-go/go.sum b/e2e/servers/mcp-go/go.sum index 44cd6d3944..7a08d9d417 100644 --- a/e2e/servers/mcp-go/go.sum +++ b/e2e/servers/mcp-go/go.sum @@ -1,17 +1,46 @@ +github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= +github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 h1:1zYrtlhrZ6/b6SAjLSfKzWtdgqK0U+HtH/VcBWh1BaU= github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA= github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= +github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0= +github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU= +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/bits-and-blooms/bitset v1.20.0 h1:2F+rfL86jE2d/bmw7OhqUg2Sj/1rURkBn3MdfoPyRVU= github.com/bits-and-blooms/bitset v1.20.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I= +github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8= +github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce h1:giXvy4KSc/6g/esnpM7Geqxka4WSqI1SZc7sMJFd3y4= +github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= +github.com/cockroachdb/pebble v1.1.5 h1:5AAWCBWbat0uE0blr8qzufZP5tBjkRyy/jWe1QWLnvw= +github.com/cockroachdb/pebble v1.1.5/go.mod h1:17wO9el1YEigxkP/YtV8NtCivQDgoCyBg5c4VR/eOWo= +github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30= +github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= github.com/consensys/gnark-crypto v0.18.0 h1:vIye/FqI50VeAr0B3dx+YjeIvmc3LWz4yEfbWBpTUf0= github.com/consensys/gnark-crypto v0.18.0/go.mod h1:L3mXGFTe1ZN+RSJ+CLjUt9x7PNdx8ubaYfDROyp2Z8c= +github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= +github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/crate-crypto/go-eth-kzg v1.4.0 h1:WzDGjHk4gFg6YzV0rJOAsTK4z3Qkz5jd4RE3DAvPFkg= github.com/crate-crypto/go-eth-kzg v1.4.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a h1:W8mUrRp6NOVl3J+MYp5kPMoUZPp7aOYHtaua31lwRHg= github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a/go.mod h1:sTwzHBvIzm2RfVCGNEBZgRyjwK40bVoun3ZnGOCafNM= +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/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA= +github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc= +github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= +github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= @@ -20,18 +49,29 @@ github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A= github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= github.com/ethereum/c-kzg-4844/v2 v2.1.5 h1:aVtoLK5xwJ6c5RiqO8g8ptJ5KU+2Hdquf6G3aXiHh5s= github.com/ethereum/c-kzg-4844/v2 v2.1.5/go.mod h1:u59hRTTah4Co6i9fDWtiCjTrblJv0UwsqZKCc0GfgUs= +github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJhy07IMfEKuARQ9TKojGqLVNxQajaXEp/BoqSk= +github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab/go.mod h1:IuLm4IsPipXKF7CW5Lzf68PIbZ5yl7FFd74l/E0o9A8= github.com/ethereum/go-ethereum v1.16.7 h1:qeM4TvbrWK0UC0tgkZ7NiRsmBGwsjqc64BHo20U59UQ= github.com/ethereum/go-ethereum v1.16.7/go.mod h1:Fs6QebQbavneQTYcA39PEKv2+zIjX7rPUZ14DER46wk= github.com/ethereum/go-verkle v0.2.2 h1:I2W0WjnrFUIzzVPwm8ykY+7pL2d4VhlsePn4j7cnFk8= github.com/ethereum/go-verkle v0.2.2/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk= github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY= github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg= +github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps= +github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= +github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= +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.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= @@ -42,56 +82,130 @@ github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbc github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= 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/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE= +github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0= +github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db h1:IZUYC/xb3giYwBLMnr8d0TGTzPKFGNTCGgGLoyeX330= +github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db/go.mod h1:xTEYN9KCHxuYHs+NmrmzFcnvHMzLLNiGFafCb1n3Mfg= +github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= +github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA= github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA= github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= +github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= +github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= +github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= +github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4= +github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4= github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A= +github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4= github.com/modelcontextprotocol/go-sdk v1.3.0 h1:gMfZkv3DzQF5q/DcQePo5rahEY+sguyPfXDfNBcT0Zs= github.com/modelcontextprotocol/go-sdk v1.3.0/go.mod h1:AnQ//Qc6+4nIyyrB4cxBU7UW9VibK4iOZBeyP/rF1IE= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8= +github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= +github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= +github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= +github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0= +github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ= +github.com/pion/transport/v2 v2.2.1 h1:7qYnCBlpgSJNYMbLCKuSY9KbQdBFoETvPNETv0y4N7c= +github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= +github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM= +github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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.15.0 h1:5fCgGYogn0hFdhyhLbw7hEsWxufKtY9klyvdNfFlFhM= +github.com/prometheus/client_golang v1.15.0/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk= +github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= +github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= +github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= +github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= +github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= +github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= +github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU= github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 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/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe h1:nbdqkIGOGfUAD54q1s2YBcBz/WcsxCO9HUQ4aGV5hUw= github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= +github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= 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= golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME= +golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= +google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/e2e/servers/mcp-go/main.go b/e2e/servers/mcp-go/main.go index 311c8b9b16..e7a057a69e 100644 --- a/e2e/servers/mcp-go/main.go +++ b/e2e/servers/mcp-go/main.go @@ -11,6 +11,7 @@ import ( "github.com/modelcontextprotocol/go-sdk/mcp" x402 "github.com/x402-foundation/x402/go" + "github.com/x402-foundation/x402/go/extensions/bazaar" x402http "github.com/x402-foundation/x402/go/http" mcp402 "github.com/x402-foundation/x402/go/mcp" evm "github.com/x402-foundation/x402/go/mechanisms/evm/exact/server" @@ -96,6 +97,29 @@ func main() { os.Exit(1) } + // Declare bazaar MCP discovery extension for weather tool + weatherInputSchema := map[string]any{ + "type": "object", + "properties": map[string]any{ + "city": map[string]any{ + "type": "string", + "description": "The city name to get weather for", + }, + }, + "required": []string{"city"}, + } + bazaarExtension, err := bazaar.DeclareMcpDiscoveryExtension(bazaar.DeclareMcpDiscoveryConfig{ + ToolName: "get_weather", + Description: "Get current weather for a city. Requires payment of $0.001.", + Transport: bazaar.TransportSSE, + InputSchema: weatherInputSchema, + Example: map[string]any{"city": "San Francisco"}, + }) + if err != nil { + fmt.Printf("āŒ Failed to declare bazaar extension: %v\n", err) + os.Exit(1) + } + // Create payment wrapper using the x402 MCP SDK paymentWrapper := mcp402.NewPaymentWrapper(resourceServer, mcp402.PaymentWrapperConfig{ Accepts: weatherAccepts, @@ -104,6 +128,9 @@ func main() { Description: "Get current weather for a city", MimeType: "application/json", }, + Extensions: map[string]interface{}{ + bazaar.BAZAAR.Key(): bazaarExtension, + }, }) // Create MCP server using the official SDK diff --git a/e2e/servers/mcp-typescript/index.ts b/e2e/servers/mcp-typescript/index.ts index 6275a4f408..a198c1a79c 100644 --- a/e2e/servers/mcp-typescript/index.ts +++ b/e2e/servers/mcp-typescript/index.ts @@ -10,6 +10,7 @@ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; import { ExactEvmScheme } from "@x402/evm/exact/server"; import { createPaymentWrapper, x402ResourceServer } from "@x402/mcp"; import { HTTPFacilitatorClient } from "@x402/core/server"; +import { declareDiscoveryExtension } from "@x402/extensions/bazaar"; import express from "express"; import { z } from "zod"; @@ -60,12 +61,26 @@ async function main(): Promise { extra: { name: "USDC", version: "2" }, }); - // Step 4: Create payment wrapper + // Step 4: Declare bazaar discovery extension for the weather tool + const weatherExtensions = declareDiscoveryExtension({ + toolName: "get_weather", + description: "Get current weather for a city. Requires payment of $0.001.", + inputSchema: { + type: "object", + properties: { + city: { type: "string", description: "The city name to get weather for" }, + }, + required: ["city"], + }, + }); + + // Step 5: Create payment wrapper with extensions const paidWeather = createPaymentWrapper(resourceServer, { accepts: weatherAccepts, + extensions: weatherExtensions, }); - // Step 5: Register tools + // Step 6: Register tools mcpServer.tool( "get_weather", "Get current weather for a city. Requires payment of $0.001.", diff --git a/e2e/servers/mcp-typescript/package.json b/e2e/servers/mcp-typescript/package.json index 67e35a8d31..73252bf96b 100644 --- a/e2e/servers/mcp-typescript/package.json +++ b/e2e/servers/mcp-typescript/package.json @@ -13,6 +13,7 @@ "@modelcontextprotocol/sdk": "^1.15.1", "@x402/core": "workspace:*", "@x402/evm": "workspace:*", + "@x402/extensions": "workspace:*", "@x402/mcp": "workspace:*", "express": "^4.18.2", "zod": "^3.24.4" diff --git a/go/extensions/bazaar/bazaar_test.go b/go/extensions/bazaar/bazaar_test.go index c1d90896c9..7b9ebecf4e 100644 --- a/go/extensions/bazaar/bazaar_test.go +++ b/go/extensions/bazaar/bazaar_test.go @@ -2150,6 +2150,544 @@ func TestBazaarDynamicRoutes(t *testing.T) { }) } +// ===== MCP Discovery Extension Tests ===== + +func TestDeclareMcpDiscoveryExtension(t *testing.T) { + t.Run("should create a valid MCP extension with full config", func(t *testing.T) { + extension, err := bazaar.DeclareMcpDiscoveryExtension(bazaar.DeclareMcpDiscoveryConfig{ + ToolName: "weather_lookup", + Description: "Look up weather for a city", + Transport: bazaar.TransportStreamableHTTP, + InputSchema: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "city": map[string]interface{}{"type": "string"}, + }, + "required": []string{"city"}, + }, + Example: map[string]interface{}{"city": "San Francisco"}, + Output: &bazaar.OutputConfig{ + Example: map[string]interface{}{"temperature": 72, "unit": "F"}, + }, + }) + + require.NoError(t, err) + assert.NotNil(t, extension.Info) + assert.NotNil(t, extension.Schema) + + mcpInput, ok := extension.Info.Input.(bazaar.McpInput) + require.True(t, ok, "Expected McpInput type") + assert.Equal(t, "mcp", mcpInput.Type) + assert.Equal(t, "weather_lookup", mcpInput.ToolName) + assert.Equal(t, "Look up weather for a city", mcpInput.Description) + assert.Equal(t, bazaar.TransportStreamableHTTP, mcpInput.Transport) + assert.NotNil(t, mcpInput.InputSchema) + assert.NotNil(t, mcpInput.Example) + + assert.NotNil(t, extension.Info.Output) + assert.Equal(t, "json", extension.Info.Output.Type) + }) + + t.Run("should create a valid MCP extension with minimal config", func(t *testing.T) { + extension, err := bazaar.DeclareMcpDiscoveryExtension(bazaar.DeclareMcpDiscoveryConfig{ + ToolName: "simple_tool", + InputSchema: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{}, + }, + }) + + require.NoError(t, err) + + mcpInput, ok := extension.Info.Input.(bazaar.McpInput) + require.True(t, ok) + assert.Equal(t, "mcp", mcpInput.Type) + assert.Equal(t, "simple_tool", mcpInput.ToolName) + assert.Empty(t, mcpInput.Description) + assert.Empty(t, mcpInput.Transport) + assert.Nil(t, mcpInput.Example) + assert.Nil(t, extension.Info.Output) + }) + + t.Run("should return error when toolName is missing", func(t *testing.T) { + _, err := bazaar.DeclareMcpDiscoveryExtension(bazaar.DeclareMcpDiscoveryConfig{ + InputSchema: map[string]interface{}{"type": "object"}, + }) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "toolName is required") + }) + + t.Run("should return error when inputSchema is missing", func(t *testing.T) { + _, err := bazaar.DeclareMcpDiscoveryExtension(bazaar.DeclareMcpDiscoveryConfig{ + ToolName: "my_tool", + }) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "inputSchema is required") + }) + + t.Run("should return error when toolName is whitespace-only", func(t *testing.T) { + _, err := bazaar.DeclareMcpDiscoveryExtension(bazaar.DeclareMcpDiscoveryConfig{ + ToolName: " ", + InputSchema: map[string]interface{}{"type": "object"}, + }) + + // Current implementation only checks for empty string, so whitespace passes declaration. + // This documents the current behavior. + require.NoError(t, err) + }) + + t.Run("should support SSE transport", func(t *testing.T) { + extension, err := bazaar.DeclareMcpDiscoveryExtension(bazaar.DeclareMcpDiscoveryConfig{ + ToolName: "sse_tool", + Transport: bazaar.TransportSSE, + InputSchema: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{}, + }, + }) + + require.NoError(t, err) + + mcpInput, ok := extension.Info.Input.(bazaar.McpInput) + require.True(t, ok) + assert.Equal(t, bazaar.TransportSSE, mcpInput.Transport) + }) + + t.Run("should accept an arbitrary transport value without error", func(t *testing.T) { + extension, err := bazaar.DeclareMcpDiscoveryExtension(bazaar.DeclareMcpDiscoveryConfig{ + ToolName: "custom_transport_tool", + Transport: bazaar.McpTransport("custom-protocol"), + InputSchema: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{}, + }, + }) + + // Declaration succeeds since transport is not validated at declaration time + require.NoError(t, err) + + mcpInput, ok := extension.Info.Input.(bazaar.McpInput) + require.True(t, ok) + assert.Equal(t, bazaar.McpTransport("custom-protocol"), mcpInput.Transport) + + // But schema validation should fail because transport enum only allows known values + result := bazaar.ValidateDiscoveryExtension(extension) + assert.False(t, result.Valid, "Invalid transport should fail schema validation") + }) + + t.Run("should support streamable-http transport", func(t *testing.T) { + extension, err := bazaar.DeclareMcpDiscoveryExtension(bazaar.DeclareMcpDiscoveryConfig{ + ToolName: "streamable_tool", + Transport: bazaar.TransportStreamableHTTP, + InputSchema: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{}, + }, + }) + + require.NoError(t, err) + + mcpInput, ok := extension.Info.Input.(bazaar.McpInput) + require.True(t, ok) + assert.Equal(t, bazaar.TransportStreamableHTTP, mcpInput.Transport) + }) +} + +func TestValidateDiscoveryExtension_MCP(t *testing.T) { + t.Run("should validate a correct MCP extension", func(t *testing.T) { + extension, err := bazaar.DeclareMcpDiscoveryExtension(bazaar.DeclareMcpDiscoveryConfig{ + ToolName: "weather_lookup", + Description: "Look up weather", + InputSchema: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{"city": map[string]interface{}{"type": "string"}}, + }, + }) + require.NoError(t, err) + + result := bazaar.ValidateDiscoveryExtension(extension) + assert.True(t, result.Valid, "MCP extension should be valid: %v", result.Errors) + }) + + t.Run("should validate a minimal MCP extension", func(t *testing.T) { + extension, err := bazaar.DeclareMcpDiscoveryExtension(bazaar.DeclareMcpDiscoveryConfig{ + ToolName: "minimal_tool", + InputSchema: map[string]interface{}{"type": "object"}, + }) + require.NoError(t, err) + + result := bazaar.ValidateDiscoveryExtension(extension) + assert.True(t, result.Valid, "Minimal MCP extension should be valid: %v", result.Errors) + }) + + t.Run("should validate MCP extension with output", func(t *testing.T) { + extension, err := bazaar.DeclareMcpDiscoveryExtension(bazaar.DeclareMcpDiscoveryConfig{ + ToolName: "output_tool", + InputSchema: map[string]interface{}{"type": "object"}, + Output: &bazaar.OutputConfig{ + Example: map[string]interface{}{"result": "ok"}, + }, + }) + require.NoError(t, err) + + result := bazaar.ValidateDiscoveryExtension(extension) + assert.True(t, result.Valid, "MCP extension with output should be valid: %v", result.Errors) + }) + + t.Run("should reject MCP extension with wrong type in info", func(t *testing.T) { + // Manually construct an extension where info.input.type != "mcp" + // but schema requires const: "mcp" + extension := bazaar.DiscoveryExtension{ + Info: bazaar.DiscoveryInfo{ + Input: bazaar.McpInput{ + Type: "http", // wrong type + ToolName: "bad_tool", + InputSchema: map[string]interface{}{"type": "object"}, + }, + }, + Schema: bazaar.JSONSchema{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": map[string]interface{}{ + "input": map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "type": map[string]interface{}{ + "type": "string", + "const": "mcp", + }, + "toolName": map[string]interface{}{ + "type": "string", + }, + "inputSchema": map[string]interface{}{ + "type": "object", + }, + }, + "required": []string{"type", "toolName", "inputSchema"}, + "additionalProperties": false, + }, + }, + "required": []string{"input"}, + }, + } + + result := bazaar.ValidateDiscoveryExtension(extension) + assert.False(t, result.Valid, "Extension with wrong type should fail validation") + }) + + t.Run("should reject MCP extension missing toolName in info", func(t *testing.T) { + // Manually construct an extension where info is missing toolName + extension := bazaar.DiscoveryExtension{ + Info: bazaar.DiscoveryInfo{ + Input: bazaar.McpInput{ + Type: "mcp", + ToolName: "", // missing + InputSchema: map[string]interface{}{"type": "object"}, + }, + }, + Schema: bazaar.JSONSchema{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": map[string]interface{}{ + "input": map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "type": map[string]interface{}{ + "type": "string", + "const": "mcp", + }, + "toolName": map[string]interface{}{ + "type": "string", + "minLength": 1, + }, + "inputSchema": map[string]interface{}{ + "type": "object", + }, + }, + "required": []string{"type", "toolName", "inputSchema"}, + "additionalProperties": false, + }, + }, + "required": []string{"input"}, + }, + } + + result := bazaar.ValidateDiscoveryExtension(extension) + assert.False(t, result.Valid, "Extension with empty toolName should fail validation when schema has minLength") + }) +} + +func TestDiscoveryInfoUnmarshalJSON_MCP(t *testing.T) { + t.Run("should unmarshal MCP discovery info correctly", func(t *testing.T) { + // Create an MCP extension, marshal to JSON, unmarshal back + extension, err := bazaar.DeclareMcpDiscoveryExtension(bazaar.DeclareMcpDiscoveryConfig{ + ToolName: "round_trip_tool", + Description: "Tests JSON round-trip", + Transport: bazaar.TransportStreamableHTTP, + InputSchema: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "query": map[string]interface{}{"type": "string"}, + }, + }, + Example: map[string]interface{}{"query": "test"}, + }) + require.NoError(t, err) + + // Marshal to JSON + jsonBytes, err := json.Marshal(extension) + require.NoError(t, err) + + // Unmarshal back + var roundTripped bazaar.DiscoveryExtension + err = json.Unmarshal(jsonBytes, &roundTripped) + require.NoError(t, err) + + // Should preserve McpInput type + mcpInput, ok := roundTripped.Info.Input.(bazaar.McpInput) + require.True(t, ok, "Expected McpInput after round-trip, got %T", roundTripped.Info.Input) + assert.Equal(t, "mcp", mcpInput.Type) + assert.Equal(t, "round_trip_tool", mcpInput.ToolName) + assert.Equal(t, "Tests JSON round-trip", mcpInput.Description) + assert.Equal(t, bazaar.TransportStreamableHTTP, mcpInput.Transport) + }) + + t.Run("should distinguish MCP from HTTP in mixed JSON", func(t *testing.T) { + // HTTP extension + httpExtension, err := bazaar.DeclareDiscoveryExtension( + bazaar.MethodGET, + map[string]interface{}{"q": "test"}, + bazaar.JSONSchema{"properties": map[string]interface{}{"q": map[string]interface{}{"type": "string"}}}, + "", + nil, + ) + require.NoError(t, err) + + // MCP extension + mcpExtension, err := bazaar.DeclareMcpDiscoveryExtension(bazaar.DeclareMcpDiscoveryConfig{ + ToolName: "mcp_tool", + InputSchema: map[string]interface{}{"type": "object"}, + }) + require.NoError(t, err) + + // Marshal both + httpJSON, _ := json.Marshal(httpExtension) + mcpJSON, _ := json.Marshal(mcpExtension) + + // Unmarshal both + var httpResult bazaar.DiscoveryExtension + var mcpResult bazaar.DiscoveryExtension + require.NoError(t, json.Unmarshal(httpJSON, &httpResult)) + require.NoError(t, json.Unmarshal(mcpJSON, &mcpResult)) + + _, isQuery := httpResult.Info.Input.(bazaar.QueryInput) + assert.True(t, isQuery, "HTTP extension should unmarshal to QueryInput") + + _, isMcp := mcpResult.Info.Input.(bazaar.McpInput) + assert.True(t, isMcp, "MCP extension should unmarshal to McpInput") + }) +} + +func TestExtractDiscoveredResourceFromPaymentPayload_MCP(t *testing.T) { + t.Run("should extract MCP info from v2 PaymentPayload", func(t *testing.T) { + extension, err := bazaar.DeclareMcpDiscoveryExtension(bazaar.DeclareMcpDiscoveryConfig{ + ToolName: "search_tool", + Description: "Search the database", + Transport: bazaar.TransportStreamableHTTP, + InputSchema: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "query": map[string]interface{}{"type": "string"}, + }, + "required": []string{"query"}, + }, + }) + require.NoError(t, err) + + requirements := x402.PaymentRequirements{ + Scheme: "exact", + Network: "eip155:8453", + } + + paymentPayload := x402.PaymentPayload{ + X402Version: 2, + Accepted: requirements, + Payload: map[string]interface{}{}, + Resource: &x402.ResourceInfo{ + URL: "https://api.example.com/mcp", + }, + Extensions: map[string]interface{}{ + bazaar.BAZAAR.Key(): extension, + }, + } + + payloadBytes, _ := json.Marshal(paymentPayload) + requirementsBytes, _ := json.Marshal(requirements) + + info, err := bazaar.ExtractDiscoveredResourceFromPaymentPayload(payloadBytes, requirementsBytes, true) + require.NoError(t, err) + require.NotNil(t, info) + + assert.Equal(t, "https://api.example.com/mcp", info.ResourceURL) + assert.Equal(t, 2, info.X402Version) + assert.Equal(t, "search_tool", info.ToolName) + assert.Empty(t, info.Method, "MCP resources should have empty Method") + + mcpInput, ok := info.DiscoveryInfo.Input.(bazaar.McpInput) + require.True(t, ok) + assert.Equal(t, "mcp", mcpInput.Type) + assert.Equal(t, "search_tool", mcpInput.ToolName) + }) +} + +func TestExtractDiscoveredResourceFromPaymentRequired_MCP(t *testing.T) { + t.Run("should extract MCP info from v2 PaymentRequired", func(t *testing.T) { + extension, err := bazaar.DeclareMcpDiscoveryExtension(bazaar.DeclareMcpDiscoveryConfig{ + ToolName: "lookup_tool", + Description: "Lookup data", + InputSchema: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "id": map[string]interface{}{"type": "string"}, + }, + }, + }) + require.NoError(t, err) + + paymentRequired := x402.PaymentRequired{ + X402Version: 2, + Resource: &x402.ResourceInfo{ + URL: "https://api.example.com/mcp", + Description: "MCP endpoint", + MimeType: "application/json", + }, + Accepts: []x402.PaymentRequirements{ + {Scheme: "exact", Network: "eip155:8453"}, + }, + Extensions: map[string]interface{}{ + "bazaar": extension, + }, + } + + paymentRequiredBytes, _ := json.Marshal(paymentRequired) + + info, err := bazaar.ExtractDiscoveredResourceFromPaymentRequired(paymentRequiredBytes, true) + require.NoError(t, err) + require.NotNil(t, info) + + assert.Equal(t, "https://api.example.com/mcp", info.ResourceURL) + assert.Equal(t, 2, info.X402Version) + assert.Equal(t, "lookup_tool", info.ToolName) + assert.Empty(t, info.Method) + + mcpInput, ok := info.DiscoveryInfo.Input.(bazaar.McpInput) + require.True(t, ok) + assert.Equal(t, "mcp", mcpInput.Type) + assert.Equal(t, "lookup_tool", mcpInput.ToolName) + }) +} + +func TestBazaarResourceServerExtension_MCP(t *testing.T) { + t.Run("should pass MCP extension through unchanged", func(t *testing.T) { + extension, err := bazaar.DeclareMcpDiscoveryExtension(bazaar.DeclareMcpDiscoveryConfig{ + ToolName: "passthrough_tool", + Description: "Should not be modified by EnrichDeclaration", + Transport: bazaar.TransportStreamableHTTP, + InputSchema: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{"x": map[string]interface{}{"type": "number"}}, + }, + }) + require.NoError(t, err) + + httpContext := x402http.HTTPRequestContext{ + Method: "POST", + Path: "/mcp/tool", + RoutePattern: "/mcp/tool", + Adapter: &mockHTTPAdapterForBazaar{path: "/mcp/tool"}, + } + + enriched := bazaar.BazaarResourceServerExtension.EnrichDeclaration(extension, httpContext) + + // Should be returned unchanged (same object) + enrichedExt, ok := enriched.(bazaar.DiscoveryExtension) + require.True(t, ok) + + mcpInput, ok := enrichedExt.Info.Input.(bazaar.McpInput) + require.True(t, ok) + assert.Equal(t, "passthrough_tool", mcpInput.ToolName) + assert.Equal(t, "Should not be modified by EnrichDeclaration", mcpInput.Description) + assert.Empty(t, enrichedExt.RouteTemplate, "MCP should not get a routeTemplate") + }) + + t.Run("should pass MCP extension through even with dynamic route context", func(t *testing.T) { + extension, err := bazaar.DeclareMcpDiscoveryExtension(bazaar.DeclareMcpDiscoveryConfig{ + ToolName: "dynamic_context_tool", + InputSchema: map[string]interface{}{"type": "object"}, + }) + require.NoError(t, err) + + // Even with a dynamic route pattern, MCP should be unchanged + httpContext := x402http.HTTPRequestContext{ + Method: "POST", + Path: "/mcp/tools/123", + RoutePattern: "/mcp/tools/[toolId]", + Adapter: &mockHTTPAdapterForBazaar{path: "/mcp/tools/123"}, + } + + enriched := bazaar.BazaarResourceServerExtension.EnrichDeclaration(extension, httpContext) + + enrichedExt, ok := enriched.(bazaar.DiscoveryExtension) + require.True(t, ok) + + _, isMcp := enrichedExt.Info.Input.(bazaar.McpInput) + assert.True(t, isMcp, "Should still be McpInput after enrichment") + assert.Empty(t, enrichedExt.RouteTemplate, "MCP should not get a routeTemplate even with dynamic route") + }) +} + +func TestExtractDiscoveryInfoFromExtension_MCP(t *testing.T) { + t.Run("should extract info from MCP extension with validation", func(t *testing.T) { + extension, err := bazaar.DeclareMcpDiscoveryExtension(bazaar.DeclareMcpDiscoveryConfig{ + ToolName: "extract_tool", + Description: "For extraction test", + InputSchema: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "param": map[string]interface{}{"type": "string"}, + }, + }, + }) + require.NoError(t, err) + + info, err := bazaar.ExtractDiscoveryInfoFromExtension(extension, true) + require.NoError(t, err) + require.NotNil(t, info) + + mcpInput, ok := info.Input.(bazaar.McpInput) + require.True(t, ok) + assert.Equal(t, "extract_tool", mcpInput.ToolName) + }) + + t.Run("should extract info from MCP extension without validation", func(t *testing.T) { + extension, err := bazaar.DeclareMcpDiscoveryExtension(bazaar.DeclareMcpDiscoveryConfig{ + ToolName: "novalidate_tool", + InputSchema: map[string]interface{}{"type": "object"}, + }) + require.NoError(t, err) + + info, err := bazaar.ExtractDiscoveryInfoFromExtension(extension, false) + require.NoError(t, err) + require.NotNil(t, info) + + mcpInput, ok := info.Input.(bazaar.McpInput) + require.True(t, ok) + assert.Equal(t, "novalidate_tool", mcpInput.ToolName) + }) +} + // TestDynamicRoutesCatalogConsolidation verifies that two requests to the same // parameterized route produce the same canonical ResourceURL, so a catalog keyed by ResourceURL // contains exactly one entry regardless of how many distinct concrete parameter values arrive. diff --git a/go/extensions/bazaar/doc.go b/go/extensions/bazaar/doc.go index fea557f8a4..44424980d8 100644 --- a/go/extensions/bazaar/doc.go +++ b/go/extensions/bazaar/doc.go @@ -34,7 +34,36 @@ The v2 extension follows a pattern where: Resource: x402.Resource{...}, Accepts: []x402.PaymentRequirements{...}, Extensions: map[string]interface{}{ - bazaar.BAZAAR: extension, + bazaar.BAZAAR.Key(): extension, + }, + } + +# For MCP Tool Servers (V2) + + import "github.com/x402-foundation/x402/go/extensions/bazaar" + + // Declare an MCP tool + extension, err := bazaar.DeclareMcpDiscoveryExtension(bazaar.DeclareMcpDiscoveryConfig{ + ToolName: "weather_lookup", + Description: "Look up weather for a city", + Transport: bazaar.TransportStreamableHTTP, + InputSchema: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "city": map[string]interface{}{"type": "string"}, + }, + "required": []string{"city"}, + }, + Example: map[string]interface{}{"city": "San Francisco"}, + }) + + // Include in PaymentRequired response + paymentRequired := x402.PaymentRequired{ + X402Version: 2, + Resource: x402.Resource{...}, + Accepts: []x402.PaymentRequirements{...}, + Extensions: map[string]interface{}{ + bazaar.BAZAAR.Key(): extension, }, } diff --git a/go/extensions/bazaar/facilitator.go b/go/extensions/bazaar/facilitator.go index 422bdebccc..647805868c 100644 --- a/go/extensions/bazaar/facilitator.go +++ b/go/extensions/bazaar/facilitator.go @@ -91,6 +91,7 @@ func ValidateDiscoveryExtension(extension types.DiscoveryExtension) ValidationRe type DiscoveredResource struct { ResourceURL string Method string + ToolName string X402Version int DiscoveryInfo *types.DiscoveryInfo Description string @@ -224,17 +225,20 @@ func ExtractDiscoveredResourceFromPaymentPayload( return nil, nil } - // Extract method from discovery info - method := "UNKNOWN" + // Extract method or toolName from discovery info + method := "" + toolName := "" switch input := discoveryInfo.Input.(type) { case types.QueryInput: method = string(input.Method) case types.BodyInput: method = string(input.Method) + case types.McpInput: + toolName = input.ToolName } - if method == "UNKNOWN" { - return nil, fmt.Errorf("failed to extract method from discovery info") + if method == "" && toolName == "" { + return nil, fmt.Errorf("failed to extract method/toolName from discovery info") } normalizedURL := normalizeResourceURL(resourceURL, routeTemplate) @@ -244,6 +248,7 @@ func ExtractDiscoveredResourceFromPaymentPayload( Description: description, MimeType: mimeType, Method: method, + ToolName: toolName, X402Version: version, DiscoveryInfo: discoveryInfo, RouteTemplate: routeTemplate, @@ -448,17 +453,20 @@ func ExtractDiscoveredResourceFromPaymentRequired( return nil, nil } - // Extract method from discovery info - method := "UNKNOWN" + // Extract method or toolName from discovery info + method := "" + toolName := "" switch input := discoveryInfo.Input.(type) { case types.QueryInput: method = string(input.Method) case types.BodyInput: method = string(input.Method) + case types.McpInput: + toolName = input.ToolName } - if method == "UNKNOWN" { - return nil, fmt.Errorf("failed to extract method from discovery info") + if method == "" && toolName == "" { + return nil, fmt.Errorf("failed to extract method/toolName from discovery info") } normalizedURL := normalizeResourceURL(resourceURL, routeTemplate) @@ -468,6 +476,7 @@ func ExtractDiscoveredResourceFromPaymentRequired( Description: description, MimeType: mimeType, Method: method, + ToolName: toolName, X402Version: version, DiscoveryInfo: discoveryInfo, RouteTemplate: routeTemplate, diff --git a/go/extensions/bazaar/resource_service.go b/go/extensions/bazaar/resource_service.go index dea9239eed..6c4261cede 100644 --- a/go/extensions/bazaar/resource_service.go +++ b/go/extensions/bazaar/resource_service.go @@ -97,6 +97,149 @@ func DeclareDiscoveryExtension( return types.DiscoveryExtension{}, fmt.Errorf("unsupported HTTP method: %s", methodStr) } +// DeclareMcpDiscoveryExtension creates a discovery extension for an MCP tool. +// +// This function helps servers declare how their MCP tool should be discovered, +// including the tool name, input schema, and optional transport/description/example. +// +// Args: +// - config: Configuration for the MCP discovery extension +// +// Returns: +// - DiscoveryExtension with both info and schema +// - Error if required fields are missing +// +// Example: +// +// extension, err := bazaar.DeclareMcpDiscoveryExtension(bazaar.DeclareMcpDiscoveryConfig{ +// ToolName: "weather_lookup", +// Description: "Look up weather for a city", +// Transport: bazaar.TransportStreamableHTTP, +// InputSchema: map[string]interface{}{ +// "type": "object", +// "properties": map[string]interface{}{ +// "city": map[string]interface{}{"type": "string"}, +// }, +// "required": []string{"city"}, +// }, +// Example: map[string]interface{}{"city": "San Francisco"}, +// }) +func DeclareMcpDiscoveryExtension(config types.DeclareMcpDiscoveryConfig) (types.DiscoveryExtension, error) { + if config.ToolName == "" { + return types.DiscoveryExtension{}, fmt.Errorf("toolName is required for MCP discovery extension") + } + if config.InputSchema == nil { + return types.DiscoveryExtension{}, fmt.Errorf("inputSchema is required for MCP discovery extension") + } + + // Build the info + mcpInput := types.McpInput{ + Type: "mcp", + ToolName: config.ToolName, + InputSchema: config.InputSchema, + } + if config.Description != "" { + mcpInput.Description = config.Description + } + if config.Transport != "" { + mcpInput.Transport = config.Transport + } + if config.Example != nil { + mcpInput.Example = config.Example + } + + mcpInfo := types.McpDiscoveryInfo{ + Input: mcpInput, + } + + if config.Output != nil && config.Output.Example != nil { + mcpInfo.Output = &types.OutputInfo{ + Type: "json", + Example: config.Output.Example, + } + } + + // Build the schema + inputSchemaProperties := map[string]interface{}{ + "type": map[string]interface{}{ + "type": "string", + "const": "mcp", + }, + "toolName": map[string]interface{}{ + "type": "string", + }, + "inputSchema": map[string]interface{}{ + "type": "object", + }, + } + inputRequired := []string{"type", "toolName", "inputSchema"} + + if config.Description != "" { + inputSchemaProperties["description"] = map[string]interface{}{ + "type": "string", + } + } + if config.Transport != "" { + inputSchemaProperties["transport"] = map[string]interface{}{ + "type": "string", + "enum": []string{string(TransportStreamableHTTP), string(TransportSSE)}, + } + } + if config.Example != nil { + inputSchemaProperties["example"] = map[string]interface{}{ + "type": "object", + } + } + + schemaProperties := map[string]interface{}{ + "input": map[string]interface{}{ + "type": "object", + "properties": inputSchemaProperties, + "required": inputRequired, + "additionalProperties": false, + }, + } + + // Add output schema if provided + if config.Output != nil && config.Output.Example != nil { + outputSchema := map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "type": map[string]interface{}{ + "type": "string", + }, + "example": map[string]interface{}{ + "type": "object", + }, + }, + "required": []string{"type"}, + } + + if config.Output.Schema != nil { + for k, v := range config.Output.Schema { + outputSchema["properties"].(map[string]interface{})["example"].(map[string]interface{})[k] = v + } + } + + schemaProperties["output"] = outputSchema + } + + schema := types.JSONSchema{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": schemaProperties, + "required": []string{"input"}, + } + + return types.DiscoveryExtension{ + Info: types.DiscoveryInfo{ + Input: mcpInfo.Input, + Output: mcpInfo.Output, + }, + Schema: schema, + }, nil +} + // createQueryDiscoveryExtension creates a query discovery extension func createQueryDiscoveryExtension( method types.QueryParamMethods, diff --git a/go/extensions/bazaar/server.go b/go/extensions/bazaar/server.go index 1c59d8ab5e..f720ef6d40 100644 --- a/go/extensions/bazaar/server.go +++ b/go/extensions/bazaar/server.go @@ -140,6 +140,12 @@ func (e *bazaarResourceServerExtension) EnrichDeclaration( return declaration } + // MCP extensions pass through unchanged — they don't need HTTP method narrowing + // or dynamic route extraction. + if _, ok := extension.Info.Input.(types.McpInput); ok { + return declaration + } + method := httpContext.Method if queryInput, ok := extension.Info.Input.(types.QueryInput); ok { diff --git a/go/extensions/bazaar/types.go b/go/extensions/bazaar/types.go index f468b12820..186d0469bd 100644 --- a/go/extensions/bazaar/types.go +++ b/go/extensions/bazaar/types.go @@ -23,22 +23,33 @@ const ( BodyTypeText = types.BodyTypeText ) +// Re-export MCP transport constants +const ( + TransportStreamableHTTP = types.TransportStreamableHTTP + TransportSSE = types.TransportSSE +) + // Re-export types type ( - QueryParamMethods = types.QueryParamMethods - BodyMethods = types.BodyMethods - BodyType = types.BodyType - QueryDiscoveryInfo = types.QueryDiscoveryInfo - QueryInput = types.QueryInput - BodyDiscoveryInfo = types.BodyDiscoveryInfo - BodyInput = types.BodyInput - OutputInfo = types.OutputInfo - DiscoveryInfo = types.DiscoveryInfo - JSONSchema = types.JSONSchema - QueryDiscoveryExtension = types.QueryDiscoveryExtension - BodyDiscoveryExtension = types.BodyDiscoveryExtension - DiscoveryExtension = types.DiscoveryExtension - OutputConfig = types.OutputConfig + QueryParamMethods = types.QueryParamMethods + BodyMethods = types.BodyMethods + BodyType = types.BodyType + QueryDiscoveryInfo = types.QueryDiscoveryInfo + QueryInput = types.QueryInput + BodyDiscoveryInfo = types.BodyDiscoveryInfo + BodyInput = types.BodyInput + OutputInfo = types.OutputInfo + DiscoveryInfo = types.DiscoveryInfo + JSONSchema = types.JSONSchema + QueryDiscoveryExtension = types.QueryDiscoveryExtension + BodyDiscoveryExtension = types.BodyDiscoveryExtension + DiscoveryExtension = types.DiscoveryExtension + OutputConfig = types.OutputConfig + McpTransport = types.McpTransport + McpInput = types.McpInput + McpDiscoveryInfo = types.McpDiscoveryInfo + McpDiscoveryExtension = types.McpDiscoveryExtension + DeclareMcpDiscoveryConfig = types.DeclareMcpDiscoveryConfig ) // Re-export utility functions diff --git a/go/extensions/types/types.go b/go/extensions/types/types.go index 990b368c5f..2fba53f2bb 100644 --- a/go/extensions/types/types.go +++ b/go/extensions/types/types.go @@ -92,6 +92,46 @@ type OutputInfo struct { Example interface{} `json:"example,omitempty"` // Example response } +// McpTransport represents the transport protocol for MCP tools +type McpTransport string + +const ( + TransportStreamableHTTP McpTransport = "streamable-http" + TransportSSE McpTransport = "sse" +) + +// McpInput represents input information for MCP tool discovery +type McpInput struct { + Type string `json:"type"` // "mcp" + ToolName string `json:"toolName"` + Description string `json:"description,omitempty"` + Transport McpTransport `json:"transport,omitempty"` + InputSchema interface{} `json:"inputSchema"` + Example interface{} `json:"example,omitempty"` +} + +// McpDiscoveryInfo represents discovery info for MCP tools +type McpDiscoveryInfo struct { + Input McpInput `json:"input"` + Output *OutputInfo `json:"output,omitempty"` +} + +// McpDiscoveryExtension represents a discovery extension for MCP tools +type McpDiscoveryExtension struct { + Info McpDiscoveryInfo `json:"info"` + Schema JSONSchema `json:"schema"` +} + +// DeclareMcpDiscoveryConfig is the configuration for declaring an MCP discovery extension +type DeclareMcpDiscoveryConfig struct { + ToolName string // MCP tool name + Description string // Human-readable description + Transport McpTransport // Transport protocol (streamable-http, sse) + InputSchema interface{} // JSON Schema for the tool's input + Example interface{} // Example input + Output *OutputConfig +} + // DiscoveryInfo is a union type that can be either Query or Body discovery info type DiscoveryInfo struct { Input interface{} `json:"input"` @@ -109,20 +149,28 @@ func (d *DiscoveryInfo) UnmarshalJSON(data []byte) error { return err } - // Try to determine if it's a query or body input by checking for bodyType field - var checkType struct { + // Check the type field first to discriminate between http and mcp inputs + var checkFields struct { + Type string `json:"type"` BodyType *string `json:"bodyType"` } // Intentionally ignore error - we're just probing for field existence - _ = json.Unmarshal(raw.Input, &checkType) + _ = json.Unmarshal(raw.Input, &checkFields) - if checkType.BodyType != nil { + switch { + case checkFields.Type == "mcp": + var mcpInput McpInput + if err := json.Unmarshal(raw.Input, &mcpInput); err != nil { + return err + } + d.Input = mcpInput + case checkFields.BodyType != nil: var bodyInput BodyInput if err := json.Unmarshal(raw.Input, &bodyInput); err != nil { return err } d.Input = bodyInput - } else { + default: var queryInput QueryInput if err := json.Unmarshal(raw.Input, &queryInput); err != nil { return err diff --git a/go/mcp/integration_test.go b/go/mcp/integration_test.go new file mode 100644 index 0000000000..64e3a87169 --- /dev/null +++ b/go/mcp/integration_test.go @@ -0,0 +1,317 @@ +package mcp + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "testing" + + "github.com/modelcontextprotocol/go-sdk/mcp" + x402 "github.com/x402-foundation/x402/go" + "github.com/x402-foundation/x402/go/types" +) + +// integrationMCPCaller bridges client → server in-process for integration tests. +// It routes CallTool calls through the server's registered tool handlers. +type integrationMCPCaller struct { + handlers map[string]ToolHandler +} + +func (m *integrationMCPCaller) CallTool(ctx context.Context, params *mcp.CallToolParams) (*mcp.CallToolResult, error) { + handler, ok := m.handlers[params.Name] + if !ok { + return nil, fmt.Errorf("tool %q not found", params.Name) + } + + argsBytes, _ := json.Marshal(params.Arguments) + + req := &mcp.CallToolRequest{ + Params: &mcp.CallToolParamsRaw{ + Name: params.Name, + Arguments: argsBytes, + Meta: params.Meta, + }, + } + return handler(ctx, req) +} + +// setupIntegrationServer creates a resource server with mock facilitator and scheme, +// initializes it, and returns the server along with the mock facilitator for assertions. +func setupIntegrationServer(t *testing.T, facilitator *mockFacilitatorClient) *x402.X402ResourceServer { + t.Helper() + if facilitator == nil { + facilitator = &mockFacilitatorClient{} + } + schemeServer := &mockSchemeNetworkServer{scheme: "cash"} + + server := x402.Newx402ResourceServer( + x402.WithFacilitatorClient(facilitator), + x402.WithSchemeServer("x402:cash", schemeServer), + ) + if err := server.Initialize(context.Background()); err != nil { + t.Fatalf("Failed to initialize server: %v", err) + } + return server +} + +var integrationAccepts = []types.PaymentRequirements{ + { + Scheme: "cash", + Network: "x402:cash", + Amount: "1000", + PayTo: "test-recipient", + }, +} + +// TestIntegration_FreeToolWithoutPayment verifies that free (unwrapped) tools +// can be called without any payment interaction. +func TestIntegration_FreeToolWithoutPayment(t *testing.T) { + caller := &integrationMCPCaller{ + handlers: map[string]ToolHandler{ + "ping": func(ctx context.Context, request *mcp.CallToolRequest) (*mcp.CallToolResult, error) { + return &mcp.CallToolResult{ + Content: []mcp.Content{&mcp.TextContent{Text: "pong"}}, + }, nil + }, + }, + } + + paymentClient := x402.Newx402Client() + client := NewX402MCPClient(caller, paymentClient, Options{}) + + result, err := client.CallTool(context.Background(), "ping", map[string]interface{}{}) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if result.PaymentMade { + t.Error("Expected no payment for free tool") + } + if result.IsError { + t.Error("Expected success result") + } + if len(result.Content) == 0 || result.Content[0].Text != "pong" { + t.Errorf("Expected content 'pong', got %v", result.Content) + } +} + +// TestIntegration_PaidToolAutoPayment verifies the full paid tool flow: +// first call returns 402, client auto-pays, second call succeeds with settlement. +func TestIntegration_PaidToolAutoPayment(t *testing.T) { + facilitator := &mockFacilitatorClient{} + resourceServer := setupIntegrationServer(t, facilitator) + + wrapper := NewPaymentWrapper(resourceServer, PaymentWrapperConfig{ + Accepts: integrationAccepts, + Resource: &ResourceInfo{ + URL: "mcp://tool/get_weather", + Description: "Get weather", + MimeType: "application/json", + }, + }) + + weatherHandler := wrapper.Wrap(func(ctx context.Context, request *mcp.CallToolRequest) (*mcp.CallToolResult, error) { + return &mcp.CallToolResult{ + Content: []mcp.Content{&mcp.TextContent{Text: `{"city":"SF","weather":"sunny"}`}}, + }, nil + }) + + caller := &integrationMCPCaller{ + handlers: map[string]ToolHandler{ + "get_weather": weatherHandler, + }, + } + + paymentClient := x402.Newx402Client() + schemeClient := &mockSchemeNetworkClient{scheme: "cash"} + paymentClient.Register("x402:cash", schemeClient) + + client := NewX402MCPClient(caller, paymentClient, Options{ + AutoPayment: BoolPtr(true), + }) + + result, err := client.CallTool(context.Background(), "get_weather", map[string]interface{}{"city": "SF"}) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if !result.PaymentMade { + t.Error("Expected PaymentMade to be true") + } + if result.IsError { + t.Error("Expected success result") + } + if result.PaymentResponse == nil { + t.Fatal("Expected PaymentResponse to be set") + } + if result.PaymentResponse.Transaction != "tx123" { + t.Errorf("Expected transaction 'tx123', got '%s'", result.PaymentResponse.Transaction) + } + if len(result.Content) == 0 { + t.Fatal("Expected content") + } + + var weather map[string]interface{} + if err := json.Unmarshal([]byte(result.Content[0].Text), &weather); err != nil { + t.Fatalf("Failed to parse weather content: %v", err) + } + if weather["city"] != "SF" { + t.Errorf("Expected city 'SF', got '%v'", weather["city"]) + } +} + +// TestIntegration_ApprovalHookCalledBeforePayment verifies that OnPaymentRequested +// is called and can approve payment before it proceeds. +func TestIntegration_ApprovalHookCalledBeforePayment(t *testing.T) { + facilitator := &mockFacilitatorClient{} + resourceServer := setupIntegrationServer(t, facilitator) + + wrapper := NewPaymentWrapper(resourceServer, PaymentWrapperConfig{ + Accepts: integrationAccepts, + }) + + weatherHandler := wrapper.Wrap(func(ctx context.Context, request *mcp.CallToolRequest) (*mcp.CallToolResult, error) { + return &mcp.CallToolResult{ + Content: []mcp.Content{&mcp.TextContent{Text: `{"weather":"sunny"}`}}, + }, nil + }) + + caller := &integrationMCPCaller{ + handlers: map[string]ToolHandler{ + "get_weather": weatherHandler, + }, + } + + paymentClient := x402.Newx402Client() + schemeClient := &mockSchemeNetworkClient{scheme: "cash"} + paymentClient.Register("x402:cash", schemeClient) + + approvalCalled := false + client := NewX402MCPClient(caller, paymentClient, Options{ + AutoPayment: BoolPtr(true), + OnPaymentRequested: func(ctx PaymentRequiredContext) (bool, error) { + approvalCalled = true + if ctx.ToolName != "get_weather" { + t.Errorf("Expected tool name 'get_weather', got '%s'", ctx.ToolName) + } + if len(ctx.PaymentRequired.Accepts) == 0 { + t.Error("Expected payment requirements in hook context") + } + return true, nil + }, + }) + + result, err := client.CallTool(context.Background(), "get_weather", map[string]interface{}{}) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if !approvalCalled { + t.Error("Expected OnPaymentRequested to be called") + } + if !result.PaymentMade { + t.Error("Expected PaymentMade to be true") + } +} + +// TestIntegration_PaymentDeniedViaHook verifies that when OnPaymentRequested +// returns false, payment is denied and the client returns an error. +func TestIntegration_PaymentDeniedViaHook(t *testing.T) { + facilitator := &mockFacilitatorClient{} + resourceServer := setupIntegrationServer(t, facilitator) + + wrapper := NewPaymentWrapper(resourceServer, PaymentWrapperConfig{ + Accepts: integrationAccepts, + }) + + handlerCalled := false + weatherHandler := wrapper.Wrap(func(ctx context.Context, request *mcp.CallToolRequest) (*mcp.CallToolResult, error) { + handlerCalled = true + return &mcp.CallToolResult{ + Content: []mcp.Content{&mcp.TextContent{Text: `{"weather":"sunny"}`}}, + }, nil + }) + + caller := &integrationMCPCaller{ + handlers: map[string]ToolHandler{ + "get_weather": weatherHandler, + }, + } + + paymentClient := x402.Newx402Client() + schemeClient := &mockSchemeNetworkClient{scheme: "cash"} + paymentClient.Register("x402:cash", schemeClient) + + client := NewX402MCPClient(caller, paymentClient, Options{ + AutoPayment: BoolPtr(true), + OnPaymentRequested: func(ctx PaymentRequiredContext) (bool, error) { + return false, nil // Deny + }, + }) + + _, err := client.CallTool(context.Background(), "get_weather", map[string]interface{}{}) + if err == nil { + t.Fatal("Expected error when payment denied") + } + + var paymentErr *PaymentRequiredError + if !errors.As(err, &paymentErr) { + t.Fatalf("Expected PaymentRequiredError, got %T: %v", err, err) + } + + if handlerCalled { + t.Error("Handler should not be called when payment is denied") + } +} + +// TestIntegration_PaymentVerificationFailure verifies that when the facilitator +// returns isValid: false, the client sees a 402 error after the retry. +func TestIntegration_PaymentVerificationFailure(t *testing.T) { + facilitator := &mockFacilitatorClient{ + verifyFunc: func(ctx context.Context, payloadBytes []byte, requirementsBytes []byte) (*x402.VerifyResponse, error) { + return &x402.VerifyResponse{IsValid: false, InvalidReason: "Insufficient funds"}, nil + }, + } + resourceServer := setupIntegrationServer(t, facilitator) + + wrapper := NewPaymentWrapper(resourceServer, PaymentWrapperConfig{ + Accepts: integrationAccepts, + }) + + handlerCalled := false + weatherHandler := wrapper.Wrap(func(ctx context.Context, request *mcp.CallToolRequest) (*mcp.CallToolResult, error) { + handlerCalled = true + return &mcp.CallToolResult{ + Content: []mcp.Content{&mcp.TextContent{Text: `{"weather":"sunny"}`}}, + }, nil + }) + + caller := &integrationMCPCaller{ + handlers: map[string]ToolHandler{ + "get_weather": weatherHandler, + }, + } + + paymentClient := x402.Newx402Client() + schemeClient := &mockSchemeNetworkClient{scheme: "cash"} + paymentClient.Register("x402:cash", schemeClient) + + client := NewX402MCPClient(caller, paymentClient, Options{ + AutoPayment: BoolPtr(true), + }) + + result, err := client.CallTool(context.Background(), "get_weather", map[string]interface{}{}) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + // After first 402, client retries with payment. + // Server verifies, fails, returns 402 again. + // Client sees second 402 after already paying, returns the error result. + if !result.IsError { + t.Error("Expected error result due to verification failure") + } + if handlerCalled { + t.Error("Handler should not be called when verification fails") + } +} diff --git a/go/mcp/server.go b/go/mcp/server.go index 021d28e26e..5063a20761 100644 --- a/go/mcp/server.go +++ b/go/mcp/server.go @@ -200,6 +200,7 @@ func (w *PaymentWrapper) paymentRequiredResult(errorMsg string) *mcp.CallToolRes Accepts: w.config.Accepts, Error: errorMsg, Resource: resource, + Extensions: w.config.Extensions, } data, _ := json.Marshal(pr) diff --git a/go/mcp/server_test.go b/go/mcp/server_test.go index efe2fafbfa..0eba7a2ade 100644 --- a/go/mcp/server_test.go +++ b/go/mcp/server_test.go @@ -502,6 +502,129 @@ func TestNewPaymentWrapper_HookErrors_NonFatal(t *testing.T) { } } +func TestNewPaymentWrapper_ExtensionsIncludedIn402(t *testing.T) { + server := x402.Newx402ResourceServer() + + extensions := map[string]interface{}{ + "bazaar": map[string]interface{}{ + "info": map[string]interface{}{ + "input": map[string]interface{}{ + "type": "mcp", + "toolName": "get_weather", + }, + }, + }, + } + + config := PaymentWrapperConfig{ + Accepts: []types.PaymentRequirements{ + {Scheme: "cash", Network: "x402:cash", Amount: "1000", PayTo: "test-recipient"}, + }, + Extensions: extensions, + } + + wrapper := NewPaymentWrapper(server, config) + handler := func(ctx context.Context, request *mcp.CallToolRequest) (*mcp.CallToolResult, error) { + return &mcp.CallToolResult{}, nil + } + wrapped := wrapper.Wrap(handler) + + ctx := context.Background() + req := makeCallToolRequest(map[string]interface{}{}, mcp.Meta{}) + result, err := wrapped(ctx, req) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if !result.IsError { + t.Error("Expected error result for missing payment") + } + + // Verify structuredContent contains extensions.bazaar + if result.StructuredContent == nil { + t.Fatal("Expected structuredContent to be set") + } + sc, ok := result.StructuredContent.(map[string]interface{}) + if !ok { + t.Fatalf("Expected structuredContent to be a map, got %T", result.StructuredContent) + } + extRaw, ok := sc["extensions"] + if !ok { + t.Fatal("Expected 'extensions' key in structuredContent") + } + extMap, ok := extRaw.(map[string]interface{}) + if !ok { + t.Fatalf("Expected extensions to be a map, got %T", extRaw) + } + bazaarRaw, ok := extMap["bazaar"] + if !ok { + t.Fatal("Expected 'bazaar' key in extensions") + } + bazaarMap, ok := bazaarRaw.(map[string]interface{}) + if !ok { + t.Fatalf("Expected bazaar to be a map, got %T", bazaarRaw) + } + infoRaw, ok := bazaarMap["info"] + if !ok { + t.Fatal("Expected 'info' key in bazaar extension") + } + infoMap, ok := infoRaw.(map[string]interface{}) + if !ok { + t.Fatalf("Expected info to be a map, got %T", infoRaw) + } + inputRaw, ok := infoMap["input"] + if !ok { + t.Fatal("Expected 'input' key in bazaar info") + } + inputMap, ok := inputRaw.(map[string]interface{}) + if !ok { + t.Fatalf("Expected input to be a map, got %T", inputRaw) + } + if inputMap["toolName"] != "get_weather" { + t.Errorf("Expected toolName 'get_weather', got '%v'", inputMap["toolName"]) + } +} + +func TestNewPaymentWrapper_NilExtensionsOmitted(t *testing.T) { + server := x402.Newx402ResourceServer() + + config := PaymentWrapperConfig{ + Accepts: []types.PaymentRequirements{ + {Scheme: "cash", Network: "x402:cash", Amount: "1000", PayTo: "test-recipient"}, + }, + // Extensions not set (nil) + } + + wrapper := NewPaymentWrapper(server, config) + handler := func(ctx context.Context, request *mcp.CallToolRequest) (*mcp.CallToolResult, error) { + return &mcp.CallToolResult{}, nil + } + wrapped := wrapper.Wrap(handler) + + ctx := context.Background() + req := makeCallToolRequest(map[string]interface{}{}, mcp.Meta{}) + result, err := wrapped(ctx, req) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if !result.IsError { + t.Error("Expected error result for missing payment") + } + + // Verify structuredContent does NOT contain "extensions" key + if result.StructuredContent == nil { + t.Fatal("Expected structuredContent to be set") + } + sc, ok := result.StructuredContent.(map[string]interface{}) + if !ok { + t.Fatalf("Expected structuredContent to be a map, got %T", result.StructuredContent) + } + if _, ok := sc["extensions"]; ok { + t.Error("Expected 'extensions' key to be absent when Extensions is nil") + } +} + func TestNewPaymentWrapper_SettlementFailure(t *testing.T) { mockFacilitator := &mockFacilitatorClient{ settleFunc: func(ctx context.Context, payloadBytes []byte, requirementsBytes []byte) (*x402.SettleResponse, error) { diff --git a/go/mcp/types.go b/go/mcp/types.go index 654a37e132..df0ff4926b 100644 --- a/go/mcp/types.go +++ b/go/mcp/types.go @@ -99,9 +99,10 @@ type MCPToolCallResult struct { // PaymentWrapperConfig configures payment wrapper behavior type PaymentWrapperConfig struct { - Accepts []types.PaymentRequirements - Resource *ResourceInfo - Hooks *PaymentWrapperHooks + Accepts []types.PaymentRequirements + Resource *ResourceInfo + Hooks *PaymentWrapperHooks + Extensions map[string]interface{} } // ResourceInfo provides resource metadata. Alias for types.ResourceInfo for compatibility. diff --git a/go/test/integration/mcp_evm_test.go b/go/test/integration/mcp_evm_test.go index a9cad50fe6..fc6069dffe 100644 --- a/go/test/integration/mcp_evm_test.go +++ b/go/test/integration/mcp_evm_test.go @@ -34,6 +34,7 @@ import ( mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp" x402 "github.com/x402-foundation/x402/go" + "github.com/x402-foundation/x402/go/extensions/bazaar" "github.com/x402-foundation/x402/go/mcp" evmclient "github.com/x402-foundation/x402/go/mechanisms/evm/exact/client" evmfacilitator "github.com/x402-foundation/x402/go/mechanisms/evm/exact/facilitator" @@ -150,6 +151,22 @@ func TestMCPEVMIntegration(t *testing.T) { Version: "1.0.0", }, nil) + // Declare bazaar MCP discovery extension for weather tool + bazaarExtension, err := bazaar.DeclareMcpDiscoveryExtension(bazaar.DeclareMcpDiscoveryConfig{ + ToolName: "get_weather", + Description: "Get weather for a city", + Transport: bazaar.TransportSSE, + InputSchema: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "city": map[string]interface{}{"type": "string"}, + }, + }, + }) + if err != nil { + t.Fatalf("Failed to declare bazaar extension: %v", err) + } + // Create payment wrapper paymentWrapper := mcp.NewPaymentWrapper(resourceServer, mcp.PaymentWrapperConfig{ Accepts: accepts, @@ -158,6 +175,9 @@ func TestMCPEVMIntegration(t *testing.T) { Description: "Get weather for a city", MimeType: "application/json", }, + Extensions: map[string]interface{}{ + bazaar.BAZAAR.Key(): bazaarExtension, + }, }) // Register free tool @@ -299,7 +319,61 @@ func TestMCPEVMIntegration(t *testing.T) { }) // ======================================================================== - // Test 3: Paid tool with payment succeeds (REAL BLOCKCHAIN TRANSACTION) + // Test 3: 402 response includes bazaar extensions + // ======================================================================== + t.Run("402 response includes bazaar extensions", func(t *testing.T) { + manualClient := mcp.NewX402MCPClient(clientSession, paymentClient, mcp.Options{ + AutoPayment: mcp.BoolPtr(false), + }) + + _, err := manualClient.CallTool(ctx, "get_weather", map[string]interface{}{"city": "San Francisco"}) + if err == nil { + t.Fatal("Expected 402 error") + } + + paymentErr, ok := err.(*mcp.PaymentRequiredError) + if !ok { + t.Fatalf("Expected PaymentRequiredError, got %T: %v", err, err) + } + if paymentErr.PaymentRequired == nil { + t.Fatal("Expected PaymentRequired to be set") + } + if paymentErr.PaymentRequired.Extensions == nil { + t.Fatal("Expected Extensions to be set in PaymentRequired") + } + + bazaarRaw, ok := paymentErr.PaymentRequired.Extensions[bazaar.BAZAAR.Key()] + if !ok { + t.Fatal("Expected 'bazaar' key in Extensions") + } + + // Round-trip through JSON to verify it deserializes to a valid DiscoveryExtension + bazaarJSON, err := json.Marshal(bazaarRaw) + if err != nil { + t.Fatalf("Failed to marshal bazaar extension: %v", err) + } + + var ext bazaar.DiscoveryExtension + if err := json.Unmarshal(bazaarJSON, &ext); err != nil { + t.Fatalf("Failed to unmarshal bazaar extension: %v", err) + } + + // Verify the MCP input contains the expected tool name + if ext.Info.McpInput == nil { + t.Fatal("Expected McpInput to be set in bazaar extension") + } + if ext.Info.McpInput.ToolName != "get_weather" { + t.Errorf("Expected toolName 'get_weather', got '%s'", ext.Info.McpInput.ToolName) + } + if ext.Info.McpInput.Type != "mcp" { + t.Errorf("Expected type 'mcp', got '%s'", ext.Info.McpInput.Type) + } + + t.Logf("āœ… Bazaar extension present in 402 response with toolName: %s", ext.Info.McpInput.ToolName) + }) + + // ======================================================================== + // Test 4: Paid tool with payment succeeds (REAL BLOCKCHAIN TRANSACTION) // ======================================================================== t.Run("Paid tool with auto-payment and real blockchain settlement", func(t *testing.T) { t.Log("\nšŸ”„ Starting paid tool call with real blockchain settlement...\n") @@ -343,7 +417,7 @@ func TestMCPEVMIntegration(t *testing.T) { }) // ======================================================================== - // Test 4: Multiple paid tool calls work + // Test 5: Multiple paid tool calls work // ======================================================================== t.Run("Multiple paid tool calls work", func(t *testing.T) { // Wait for the previous test's settlement tx to be mined @@ -376,7 +450,7 @@ func TestMCPEVMIntegration(t *testing.T) { }) // ======================================================================== - // Test 5: List tools works + // Test 6: List tools works // ======================================================================== t.Run("List tools works", func(t *testing.T) { session, ok := x402McpClient.Client().(*mcpsdk.ClientSession) diff --git a/typescript/packages/mcp/src/server/paymentWrapper.ts b/typescript/packages/mcp/src/server/paymentWrapper.ts index ffd72d6fff..572c066d20 100644 --- a/typescript/packages/mcp/src/server/paymentWrapper.ts +++ b/typescript/packages/mcp/src/server/paymentWrapper.ts @@ -49,6 +49,9 @@ export interface PaymentWrapperConfig { mimeType?: string; }; + /** Extensions to include in 402 payment required responses (e.g., bazaar discovery) */ + extensions?: Record; + /** Hooks for payment lifecycle events */ hooks?: { /** Called after payment verification, before tool execution. Return false to abort. */ @@ -310,6 +313,7 @@ async function createPaymentRequiredResult( config.accepts, resourceInfo, errorMessage, + config.extensions, ); return { diff --git a/typescript/packages/mcp/test/integration/mcp-sse-evm.test.ts b/typescript/packages/mcp/test/integration/mcp-sse-evm.test.ts index 4b2cd5a12a..67e0ca6983 100644 --- a/typescript/packages/mcp/test/integration/mcp-sse-evm.test.ts +++ b/typescript/packages/mcp/test/integration/mcp-sse-evm.test.ts @@ -385,7 +385,41 @@ describe.skipIf(SKIP_TESTS)("Real SSE MCP Integration Tests", () => { }, 60000); // 60s timeout for blockchain transaction // ========================================================================== - // Test 6: Multiple paid tool calls work + // Test 6: 402 response includes bazaar extensions + // ========================================================================== + it("should include bazaar extensions in 402 response", async () => { + // Use a client with autoPayment disabled to see the 402 + const manualClient = new x402MCPClient( + x402ClientInstance.client, + x402ClientInstance.paymentClient, + { autoPayment: false }, + ); + + try { + await manualClient.callTool("get_weather", { city: "Chicago" }); + expect.fail("Should have thrown 402 error"); + } catch (error) { + const err = error as { + code?: number; + paymentRequired?: { extensions?: Record }; + }; + expect(err.code).toBe(402); + expect(err.paymentRequired).toBeDefined(); + // Verify bazaar extension is present if the server was configured with it + // This test validates extension passthrough from server to client + if (err.paymentRequired?.extensions?.bazaar) { + const bazaar = err.paymentRequired.extensions.bazaar as Record; + expect(bazaar).toBeDefined(); + const info = bazaar.info as Record; + expect(info).toBeDefined(); + const input = info.input as Record; + expect(input.toolName).toBe("get_weather"); + } + } + }); + + // ========================================================================== + // Test 7: Multiple paid tool calls work // ========================================================================== it("should handle multiple paid tool calls", async () => { console.log("\nšŸ”„ Starting second paid tool call...\n"); diff --git a/typescript/packages/mcp/test/unit/server.test.ts b/typescript/packages/mcp/test/unit/server.test.ts index ae36faeec1..33b7f336c1 100644 --- a/typescript/packages/mcp/test/unit/server.test.ts +++ b/typescript/packages/mcp/test/unit/server.test.ts @@ -458,6 +458,77 @@ describe("createPaymentWrapper", () => { }); }); + describe("extensions", () => { + it("should include extensions in 402 response when configured", async () => { + const extensions = { + bazaar: { + info: { + input: { + type: "mcp", + toolName: "test", + }, + }, + }, + }; + + const mockPaymentRequiredWithExtensions = { + ...mockPaymentRequired, + extensions, + }; + + mockResourceServer.createPaymentRequiredResponse.mockResolvedValueOnce( + mockPaymentRequiredWithExtensions, + ); + + const paid = createPaymentWrapper( + mockResourceServer as unknown as Parameters[0], + { + accepts: [mockPaymentRequirements], + extensions, + }, + ); + + const handler = vi.fn().mockResolvedValue({ + content: [{ type: "text", text: "success" }], + }); + + const wrappedHandler = paid(handler); + const result = await wrappedHandler({ test: "arg" }, {}); + + expect(result.isError).toBe(true); + expect(mockResourceServer.createPaymentRequiredResponse).toHaveBeenCalledWith( + [mockPaymentRequirements], + expect.any(Object), + "Payment required to access this tool", + extensions, + ); + expect((result.structuredContent as Record)?.extensions).toEqual(extensions); + }); + + it("should not include extensions when not configured", async () => { + const paid = createPaymentWrapper( + mockResourceServer as unknown as Parameters[0], + { + accepts: [mockPaymentRequirements], + }, + ); + + const handler = vi.fn().mockResolvedValue({ + content: [{ type: "text", text: "success" }], + }); + + const wrappedHandler = paid(handler); + await wrappedHandler({ test: "arg" }, {}); + + expect(mockResourceServer.createPaymentRequiredResponse).toHaveBeenCalledWith( + [mockPaymentRequirements], + expect.any(Object), + "Payment required to access this tool", + undefined, + ); + }); + }); + describe("settlement failures", () => { it("should return 402 error when settlement fails", async () => { mockResourceServer.settlePayment.mockRejectedValueOnce(new Error("Settlement failed"));