From 3716462cb835eb02998d05f8a8db5c3cf1e87a68 Mon Sep 17 00:00:00 2001 From: Nacorid Date: Sat, 18 Jul 2026 23:28:02 +0000 Subject: [PATCH 1/6] migrate middlewares --- internal/server/metricsMiddleware.go | 24 ++++++++------- internal/server/rateLimitMiddleware.go | 41 ++++++++++++++++---------- 2 files changed, 38 insertions(+), 27 deletions(-) diff --git a/internal/server/metricsMiddleware.go b/internal/server/metricsMiddleware.go index 1f30ed9..a62b14d 100644 --- a/internal/server/metricsMiddleware.go +++ b/internal/server/metricsMiddleware.go @@ -5,8 +5,7 @@ import ( "net/http" "time" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + "github.com/modelcontextprotocol/go-sdk/mcp" log "github.com/nacorid/logger" "github.com/nacorid/naco-api/internal/utils" ) @@ -23,15 +22,18 @@ func NewMetricsHook() *MetricsMiddleware { } } -func (mh *MetricsMiddleware) OnCall(next server.ToolHandlerFunc) server.ToolHandlerFunc { - return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - start := time.Now() - mh.requestsBucket.Increment() - mh.totalRequests.Increment() - result, err := next(ctx, request) - duration := time.Since(start) - log.DebugWithContext(ctx, "MetricsMiddleware: MCP Handler", "duration", duration) - return result, err +func (mh *MetricsMiddleware) OnCall(next mcp.MethodHandler) mcp.MethodHandler { + return func(ctx context.Context, method string, request mcp.Request) (mcp.Result, error) { + if method == "tools/call" { + start := time.Now() + mh.requestsBucket.Increment() + mh.totalRequests.Increment() + result, err := next(ctx, method, request) + duration := time.Since(start) + log.DebugWithContext(ctx, "MetricsMiddleware: MCP Handler", "duration", duration) + return result, err + } + return next(ctx, method, request) } } diff --git a/internal/server/rateLimitMiddleware.go b/internal/server/rateLimitMiddleware.go index e2cc58e..f8a79f3 100644 --- a/internal/server/rateLimitMiddleware.go +++ b/internal/server/rateLimitMiddleware.go @@ -8,8 +8,8 @@ import ( "strings" "sync" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + "github.com/modelcontextprotocol/go-sdk/mcp" + log "github.com/nacorid/logger" "golang.org/x/time/rate" ) @@ -45,23 +45,32 @@ func (m *RateLimitMiddleware) getLimiter(sessionID string) *rate.Limiter { return limiter } -func (m *RateLimitMiddleware) OnCall(next server.ToolHandlerFunc) server.ToolHandlerFunc { - return func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { - if slices.Contains(m.request, req.Params.Name) { - var sessionID string - session := server.ClientSessionFromContext(ctx) - if session == nil { - sessionID = getClientIP(req.Header) - } else { - sessionID = session.SessionID() - } - limiter := m.getLimiter(sessionID) +func (m *RateLimitMiddleware) OnCall(next mcp.MethodHandler) mcp.MethodHandler { + return func(ctx context.Context, method string, req mcp.Request) (mcp.Result, error) { + if method == "tools/call" { + callReq, ok := req.(*mcp.CallToolRequest) + if ok && slices.Contains(m.request, callReq.Params.Name) { + var sessionID string + session := req.GetSession() + if session == nil || session.ID() == "" { + if extra := req.GetExtra(); extra != nil { + sessionID = getClientIP(req.GetExtra().Header) + } else { + log.WarnWithContext(ctx, "No session or extra information found in request; using 'unknown' as session ID") + sessionID = "unknown" + } + } else { + sessionID = session.ID() + } + limiter := m.getLimiter(sessionID) - if !limiter.Allow() { - return nil, fmt.Errorf("rate limit exceeded for session %s\nRatelimit: %v requests per second", sessionID, limiter.Limit()) + if !limiter.Allow() { + return nil, fmt.Errorf("rate limit exceeded for session %s\nRatelimit: %v requests per second", sessionID, limiter.Limit()) + } } + return next(ctx, method, req) } - return next(ctx, req) + return next(ctx, method, req) } } From 9b33e9b7bbd4849b7828b873f36daf061ac004c6 Mon Sep 17 00:00:00 2001 From: Nacorid Date: Sun, 19 Jul 2026 00:27:56 +0000 Subject: [PATCH 2/6] Migrate internal x402 wrapper --- go.mod | 49 +++++++---- go.sum | 198 +++++++++++++++++++++++++++++++++--------- internal/x402/x402.go | 160 +++++++++++++++++++--------------- 3 files changed, 280 insertions(+), 127 deletions(-) diff --git a/go.mod b/go.mod index d7840fd..6cbf6e8 100644 --- a/go.mod +++ b/go.mod @@ -8,8 +8,10 @@ require ( github.com/lib/pq v1.10.9 github.com/mark3labs/mcp-go v0.43.2 github.com/mark3labs/x402-go v0.12.1 + github.com/modelcontextprotocol/go-sdk v1.6.1 github.com/nacorid/logger v0.0.0-20251221003040-586da7a03fa0 github.com/ollama/ollama v0.12.10 + github.com/x402-foundation/x402/go/v2 v2.19.0 golang.org/x/time v0.14.0 ) @@ -23,14 +25,12 @@ require ( github.com/blendle/zapdriver v1.3.1 // indirect github.com/buger/jsonparser v1.1.1 // indirect github.com/consensys/gnark-crypto v0.19.2 // 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/crate-crypto/go-eth-kzg v1.5.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/deckarep/golang-set/v2 v2.8.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect - github.com/ethereum/c-kzg-4844/v2 v2.1.5 // indirect - github.com/ethereum/go-ethereum v1.16.5 // indirect - github.com/ethereum/go-verkle v0.2.2 // indirect + github.com/ethereum/c-kzg-4844/v2 v2.1.6 // indirect + github.com/ethereum/go-ethereum v1.17.2 // indirect github.com/fatih/color v1.18.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/gagliardetto/binary v0.8.0 // indirect @@ -49,7 +49,6 @@ require ( github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/streamingfast/logging v0.0.0-20250918142248-ac5a1e292845 // indirect - github.com/stretchr/testify v1.11.1 // indirect github.com/supranational/blst v0.3.16 // indirect github.com/tyler-smith/go-bip32 v1.0.0 // indirect github.com/tyler-smith/go-bip39 v1.1.0 // indirect @@ -57,19 +56,26 @@ require ( github.com/yosida95/uritemplate/v3 v3.0.2 // indirect go.mongodb.org/mongo-driver v1.17.6 // indirect go.uber.org/ratelimit v0.3.1 // indirect - golang.org/x/sync v0.17.0 // indirect - golang.org/x/term v0.36.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/term v0.38.0 // indirect gopkg.in/square/go-jose.v2 v2.6.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) 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/carlmjohnson/versioninfo v0.22.5 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/jsonschema-go v0.4.3 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.1 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-retryablehttp v0.7.5 // indirect github.com/hashicorp/golang-lru v1.0.2 // indirect @@ -97,20 +103,31 @@ require ( github.com/multiformats/go-varint v0.0.7 // indirect github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f // indirect + github.com/segmentio/asm v1.2.0 // indirect + github.com/segmentio/encoding v0.5.4 // indirect + github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect github.com/sokkalf/slog-seq v0.5.1 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // 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 + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect - go.opentelemetry.io/otel v1.35.0 // indirect - go.opentelemetry.io/otel/metric v1.35.0 // indirect - go.opentelemetry.io/otel/sdk v1.35.0 // indirect - go.opentelemetry.io/otel/trace v1.35.0 // indirect + go.opentelemetry.io/otel v1.40.0 // indirect + go.opentelemetry.io/otel/metric v1.40.0 // indirect + go.opentelemetry.io/otel/sdk v1.40.0 // indirect + go.opentelemetry.io/otel/trace v1.40.0 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect - golang.org/x/crypto v0.43.0 // indirect - golang.org/x/sys v0.37.0 // indirect + golang.org/x/crypto v0.46.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/oauth2 v0.35.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/text v0.32.0 // indirect golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect lukechampine.com/blake3 v1.2.1 // indirect ) diff --git a/go.sum b/go.sum index 782cfc5..0ef9cfc 100644 --- a/go.sum +++ b/go.sum @@ -3,17 +3,27 @@ filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4 github.com/AlekSi/pointer v1.1.0 h1:SSDMPcXD9jSl8FPy9cRzoRaMJtm9g9ggGTxecRUbQoI= github.com/AlekSi/pointer v1.1.0/go.mod h1:y7BvfRI3wXPWKXEBhU71nbnIEEZX0QTSB2Bj48UJIZE= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= +github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= github.com/FactomProject/basen v0.0.0-20150613233007-fe3947df716e h1:ahyvB3q25YnZWly5Gq1ekg6jcmWaGj/vG/MhF4aisoc= github.com/FactomProject/basen v0.0.0-20150613233007-fe3947df716e/go.mod h1:kGUqhHd//musdITWjFvNTHn90WG9bMLBEPQZ17Cmlpw= github.com/FactomProject/btcutilecc v0.0.0-20130527213604-d3a63a5752ec h1:1Qb69mGp/UtRPn422BH4/Y4Q3SLUrD9KHuDkm8iodFc= github.com/FactomProject/btcutilecc v0.0.0-20130527213604-d3a63a5752ec/go.mod h1:CD8UlnlLDiqb36L110uqiP2iSflVjx9g/3U9hCI4q2U= +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/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +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.24.2 h1:M7/NzVbsytmtfHbumG+K2bremQPMJuqv1JD3vOaFxp0= github.com/bits-and-blooms/bitset v1.24.2/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/blendle/zapdriver v1.3.1 h1:C3dydBOWYRiOk+B8X9IVZ5IOe+7cl+tGOexN4QqHfpE= @@ -26,18 +36,34 @@ github.com/carlmjohnson/versioninfo v0.22.5 h1:O00sjOLUAFxYQjlN/bzYTuZiS0y6fWDQj github.com/carlmjohnson/versioninfo v0.22.5/go.mod h1:QT9mph3wcVfISUKd0i9sZfVrPviHuSF+cUtLjm2WSf8= github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk= github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= +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/cmars/basen v0.0.0-20150613233007-fe3947df716e h1:0XBUw73chJ1VYSsfvcPvVT7auykAJce9FpRr10L6Qhw= github.com/cmars/basen v0.0.0-20150613233007-fe3947df716e/go.mod h1:P13beTBKr5Q18lJe1rIoLUqjM+CB1zYrRg44ZqGuQSA= +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.19.2 h1:qrEAIXq3T4egxqiliFFoNrepkIWVEeIYwt3UL0fvS80= github.com/consensys/gnark-crypto v0.19.2/go.mod h1:rT23F0XSZqE0mUA0+pRtnL56IbPxs6gp4CeRsBk4XS0= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -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/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.5.0 h1:FYRiJMJG2iv+2Dy3fi14SVGjcPteZ5HAAUe4YWlJygc= +github.com/crate-crypto/go-eth-kzg v1.5.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= 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.8.0 h1:swm0rlPCmdWn9mESxKOjWk8hXSqoxOp+ZlfuyaAdFlQ= github.com/deckarep/golang-set/v2 v2.8.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8= @@ -46,12 +72,12 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvw github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= 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-ethereum v1.16.5 h1:GZI995PZkzP7ySCxEFaOPzS8+bd8NldE//1qvQDQpe0= -github.com/ethereum/go-ethereum v1.16.5/go.mod h1:kId9vOtlYg3PZk9VwKbGlQmSACB5ESPTBGT+M9zjmok= -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/ethereum/c-kzg-4844/v2 v2.1.6 h1:xQymkKCT5E2Jiaoqf3v4wsNgjZLY0lRSkZn27fRjSls= +github.com/ethereum/c-kzg-4844/v2 v2.1.6/go.mod h1:8HMkUZ5JRv4hpw/XUrYWSQNAUzhHMg2UDb/U+5m+XNw= +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.17.2 h1:ag6geu0kn8Hv5FLKTpH+Hm2DHD+iuFtuqKxEuwUsDOI= +github.com/ethereum/go-ethereum v1.17.2/go.mod h1:KHcRXfGOUfUmKg51IhQ0IowiqZ6PqZf08CMtk0g5K1o= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= @@ -70,11 +96,14 @@ github.com/gagliardetto/solana-go v1.14.0 h1:3WfAi70jOOjAJ0deFMjdhFYlLXATF4tOQXs github.com/gagliardetto/solana-go v1.14.0/go.mod h1:l/qqqIN6qJJPtxW/G1PF4JtcE3Zg2vD2EliZrr9Gn5k= github.com/gagliardetto/treeout v0.1.4 h1:ozeYerrLCmCubo1TcIjFiOWTTGteOOHND1twdFpgwaw= github.com/gagliardetto/treeout v0.1.4/go.mod h1:loUefvXTrlRG5rYmJmExNryyBRh8f89VZhmMOyCyqok= +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-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +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/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= @@ -82,16 +111,33 @@ 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 v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +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.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= 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= 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/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +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/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 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/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= +github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= +github.com/grafana/pyroscope-go v1.2.7 h1:VWBBlqxjyR0Cwk2W6UrE8CdcdD80GOFNutj0Kb1T8ac= +github.com/grafana/pyroscope-go v1.2.7/go.mod h1:o/bpSLiJYYP6HQtvcoVKiE9s5RiNgjYTj1DhiddP2Pc= +github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og= +github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU= +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/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI= @@ -100,8 +146,14 @@ github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +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/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs= @@ -131,6 +183,8 @@ github.com/ipfs/go-log/v2 v2.5.1 h1:1XdUzF7048prq4aBjDQQ4SL5RxftpRGdXhNRwKSAlcY= github.com/ipfs/go-log/v2 v2.5.1/go.mod h1:prSpmC1Gpllc9UYWxDiZDreBYw7zp4Iqp1kOLU9U5UI= github.com/ipfs/go-metrics-interface v0.0.1 h1:j+cpbjYvu4R8zbleSs36gvB7jR+wsL2fGD6n0jO4kdg= github.com/ipfs/go-metrics-interface v0.0.1/go.mod h1:6s6euYU4zowdslK0GKHmqaIZ3j/b/tL7HTWtJ4VPgWY= +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/jbenet/go-cienv v0.1.0/go.mod h1:TqNnHUmJgXau0nCzC7kXWeotg3J9W34CUv5Djy1+FlA= github.com/jbenet/goprocess v0.1.4 h1:DRGOFReOMqqDNXwW70QkacFW0YN9QnwLV0Vqk+3oU0o= github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4= @@ -174,14 +228,19 @@ github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stg github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 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.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= -github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= 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.6.1 h1:0zOSupjKUxPKSocPT1Wtago+mUHU2/uZ4xSOY0FGReU= +github.com/modelcontextprotocol/go-sdk v1.6.1/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= @@ -203,25 +262,49 @@ github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/n github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= github.com/nacorid/logger v0.0.0-20251221003040-586da7a03fa0 h1:vKVU0JbEOiG7xI+UNwb8M72mo98uHCiPNyPUd9oBC9c= github.com/nacorid/logger v0.0.0-20251221003040-586da7a03fa0/go.mod h1:hd9h4KEPU1f/Vv2qP0x/fZUwz2clFvOV/uIF0pkGo+s= -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/ollama/ollama v0.12.10 h1:Dd0/SeCc+nv+FffxmWuQTGiRreib7Gt3nBhIIFuKwZA= github.com/ollama/ollama v0.12.10/go.mod h1:RUSmYywUWx/YZMaHrqtnT1ZChu+iSz/7jx2aO9+Mgfg= github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= +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.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +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/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f h1:VXTQfuJj9vKR4TCkEuWIckKvdHFeJH/huIFJ9/cXOB0= github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw= -github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q= +github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY= +github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= +github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= +github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= +github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/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.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +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/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= +github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= +github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0= +github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0= 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/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= @@ -250,6 +333,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/supranational/blst v0.3.16 h1:bTDadT+3fK497EvLdWRQEjiGnUtzJ7jjIUMF0jqwYhE= github.com/supranational/blst v0.3.16/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/test-go/testify v1.1.4 h1:Tf9lntrKUMHiXQ07qBScBTSA0dhYQlu83hswqelv1iE= github.com/test-go/testify v1.1.4/go.mod h1:rH7cfJo/47vWGdi4GPj16x3/t1xGOj2YxzmNQzk2ghU= github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= @@ -260,13 +345,26 @@ github.com/tyler-smith/go-bip32 v1.0.0 h1:sDR9juArbUgX+bO/iblgZnMPeWY1KZMUC2AFUJ github.com/tyler-smith/go-bip32 v1.0.0/go.mod h1:onot+eHknzV4BVPwrzqY5OoVpyCvnwD7lMawL5aQupE= github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8= github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U= +github.com/urfave/cli v1.22.10 h1:p8Fspmz3iTctJstry1PYS3HVdllxnEzTEsgIgtxTrCk= github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +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/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ= github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw= github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e h1:28X54ciEwwUxyHn9yrZfl5ojgF4CBNLWX7LR0rvBkf4= github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so= github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= +github.com/x402-foundation/x402/go/v2 v2.19.0 h1:vyU9cKFfqw8oI6To66r+UH67vFwWpoMDRX9zGM8bCJI= +github.com/x402-foundation/x402/go/v2 v2.19.0/go.mod h1:OAsOuB+FYhCoKzijOGZ+PcJk9g4RBfCJvpABGxi+NcE= +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= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -274,18 +372,20 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss= go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +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/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo= -go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= -go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= -go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= -go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= -go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= -go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= -go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= -go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= +go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= +go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= +go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= +go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= +go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= +go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= +go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= +go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= +go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= @@ -315,8 +415,10 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= -golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +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-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= +golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -329,16 +431,19 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= -golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +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= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= -golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -346,17 +451,20 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= -golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +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.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= -golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= -golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +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.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -369,17 +477,23 @@ 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.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +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= golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +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= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +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/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/internal/x402/x402.go b/internal/x402/x402.go index 0e7ce28..f33f5cd 100644 --- a/internal/x402/x402.go +++ b/internal/x402/x402.go @@ -12,36 +12,39 @@ import ( "os" "strings" - "github.com/mark3labs/mcp-go/mcp" - mcpserver "github.com/mark3labs/mcp-go/server" - "github.com/mark3labs/x402-go" - x402http "github.com/mark3labs/x402-go/http" - "github.com/mark3labs/x402-go/mcp/server" + "github.com/modelcontextprotocol/go-sdk/mcp" log "github.com/nacorid/logger" + x402 "github.com/x402-foundation/x402/go/v2" + x402http "github.com/x402-foundation/x402/go/v2/http" + nethttp "github.com/x402-foundation/x402/go/v2/http/nethttp" + evm "github.com/x402-foundation/x402/go/v2/mechanisms/evm/exact/server" ) +type ToolHandlerFunc func(ctx context.Context, req *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, error) + type X402Middlewares struct { - MCPMiddleware []mcpserver.ToolHandlerMiddleware + MCPMiddleware []mcp.Middleware HTTPMiddleware []func(http.Handler) http.Handler } type HTTPMiddleware func(http.Handler) http.Handler type X402Server struct { - mcpHandler *mcpserver.MCPServer - mcpServer *server.X402Handler - x402Middlewares map[string]func(http.Handler) http.Handler - httpConfig *x402http.Config - mcpConfig *server.Config + mcpServer *mcp.Server + x402ResourceServer *x402.X402ResourceServer + facilitatorClient *x402http.HTTPFacilitatorClient + x402Middlewares map[string]func(http.Handler) http.Handler - toolMiddlewareChain mcpserver.ToolHandlerMiddleware - middlewareChain func(http.Handler) http.Handler - AllEndpoints []string - Handlers map[string]mcpserver.ToolHandlerFunc + middlewareChain func(http.Handler) http.Handler + AllEndpoints []string + Handlers map[string]ToolHandlerFunc } -func (s *X402Server) AddTool(tool mcp.Tool, handler mcpserver.ToolHandlerFunc) { - s.mcpHandler.AddTool(tool, handler) +func (s *X402Server) AddTool(tool mcp.Tool, handler ToolHandlerFunc) { + mcp.AddTool(s.mcpServer, &tool, func(ctx context.Context, req *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { + result, err := handler(ctx, req, args) + return result, nil, err + }) s.AllEndpoints = append(s.AllEndpoints, tool.Name) s.Handlers[tool.Name] = handler s.x402Middlewares[tool.Name] = noOpMiddleware @@ -49,22 +52,38 @@ func (s *X402Server) AddTool(tool mcp.Tool, handler mcpserver.ToolHandlerFunc) { func (s *X402Server) AddPayableTool( tool mcp.Tool, - handler mcpserver.ToolHandlerFunc, - requirements ...x402.PaymentRequirement, + handler ToolHandlerFunc, + requirements ...x402.PaymentRequirements, ) { - s.mcpHandler.AddTool(tool, handler) + s.AddTool(tool, handler) if len(requirements) == 0 { log.Fatalf("tool %s requires at least one payment requirement", tool.Name) } - s.mcpConfig.PaymentTools[tool.Name] = requirements + var options x402http.PaymentOptions + for _, req := range requirements { + options = append(options, x402http.PaymentOptions{x402http.PaymentOption{ + Scheme: req.Scheme, + PayTo: req.PayTo, + Price: req.Amount, + Network: x402.Network(req.Network), + }, + }...) + } + s.AllEndpoints = append(s.AllEndpoints, tool.Name) - s.Handlers[tool.Name] = handler - cfg := newMiddlewareConfig(s.httpConfig, func(c *x402http.Config) { - c.PaymentRequirements = requirements + s.x402Middlewares[tool.Name] = nethttp.X402Payment(nethttp.Config{ + Routes: x402http.RoutesConfig{ + "POST /" + tool.Name: { + Accepts: options, + }, + }, + Facilitator: s.facilitatorClient, + Schemes: []nethttp.SchemeConfig{ + {Network: "eip155:8453", Server: evm.NewExactEvmScheme()}, + }, }) - s.x402Middlewares[tool.Name] = x402http.NewX402Middleware(&cfg) } func noOpMiddleware(next http.Handler) http.Handler { @@ -73,36 +92,41 @@ func noOpMiddleware(next http.Handler) http.Handler { }) } -func NewX402Server(name, version string, httpConfig *x402http.Config, mws X402Middlewares) (*X402Server, error) { - toolChain := chainToolMiddlewares(mws.MCPMiddleware...) - httpChain := chainHTTPMiddlewares(mws.HTTPMiddleware...) - mcp := mcpserver.NewMCPServer(name, version, - mcpserver.WithToolHandlerMiddleware(toolChain), - mcpserver.WithRecovery(), +func NewX402Server(name, version string, facilitatorURL string, mws X402Middlewares) (*X402Server, error) { + mcpServer := mcp.NewServer( + &mcp.Implementation{ + Name: name, + Version: version, + }, + nil, + ) + for _, mw := range mws.MCPMiddleware { + mcpServer.AddReceivingMiddleware(mw) + } + + if facilitatorURL == "" { + facilitatorURL = "https://facilitator.mogami.tech" + } + facilitator := x402http.NewHTTPFacilitatorClient(&x402http.FacilitatorConfig{ + URL: facilitatorURL, + }) + + resourceServer := x402.Newx402ResourceServer( + x402.WithFacilitatorClient(facilitator), ) - return &X402Server{ - mcpHandler: mcp, - x402Middlewares: make(map[string]func(http.Handler) http.Handler), - httpConfig: httpConfig, - mcpConfig: &server.Config{ - FacilitatorURL: "https://facilitator.mogami.tech", - Verbose: true, - PaymentTools: make(map[string][]x402.PaymentRequirement), - }, - toolMiddlewareChain: toolChain, - middlewareChain: httpChain, - Handlers: make(map[string]mcpserver.ToolHandlerFunc), - }, nil -} + resourceServer.Register("eip155:8453", evm.NewExactEvmScheme()) -func chainToolMiddlewares(middlewares ...mcpserver.ToolHandlerMiddleware) mcpserver.ToolHandlerMiddleware { - return func(next mcpserver.ToolHandlerFunc) mcpserver.ToolHandlerFunc { - for i := len(middlewares) - 1; i >= 0; i-- { - next = middlewares[i](next) - } - return next - } + httpChain := chainHTTPMiddlewares(mws.HTTPMiddleware...) + + return &X402Server{ + mcpServer: mcpServer, + x402ResourceServer: resourceServer, + facilitatorClient: facilitator, + x402Middlewares: make(map[string]func(http.Handler) http.Handler), + middlewareChain: httpChain, + Handlers: make(map[string]ToolHandlerFunc), + }, nil } func chainHTTPMiddlewares(middlewares ...func(http.Handler) http.Handler) func(http.Handler) http.Handler { @@ -114,14 +138,6 @@ func chainHTTPMiddlewares(middlewares ...func(http.Handler) http.Handler) func(h } } -func newMiddlewareConfig(httpConfig *x402http.Config, overrides ...func(*x402http.Config)) x402http.Config { - cfg := *httpConfig - for _, override := range overrides { - override(&cfg) - } - return cfg -} - func (s *X402Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { toolName := strings.TrimPrefix(r.URL.Path, "/") addr := "@" @@ -135,7 +151,6 @@ func (s *X402Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { addr = realIP } } - log.InfoWithContext(r.Context(), "Request received", "host", r.Host, "method", r.Method, "path", r.URL.Path, "remoteAddr", addr) if r.URL.Path == "/healthz" { w.WriteHeader(http.StatusOK) @@ -143,6 +158,8 @@ func (s *X402Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } + log.InfoWithContext(r.Context(), "Request received", "host", r.Host, "method", r.Method, "path", r.URL.Path, "remoteAddr", addr) + if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return @@ -177,20 +194,28 @@ func (s *X402Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { functionHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { args := r.Context().Value(argsKey).(map[string]any) - callReq := mcp.CallToolRequest{ - Params: mcp.CallToolParams{ + argsJSON, err := json.Marshal(args) + if err != nil { + log.WarnWithContext(r.Context(), "Failed to marshal args to raw JSON", "error", err) + http.Error(w, "Failed to encode arguments", http.StatusInternalServerError) + return + } + + callReq := &mcp.CallToolRequest{ + Params: &mcp.CallToolParamsRaw{ Name: toolName, - Arguments: args, + Arguments: json.RawMessage(argsJSON), }, } - resp, err := s.toolMiddlewareChain(handler)(r.Context(), callReq) + + resp, err := handler(r.Context(), callReq, args) if err != nil { log.WarnWithContext(r.Context(), "Tool handler error in direct handler", "tool", toolName, "error", err) http.Error(w, err.Error(), http.StatusInternalServerError) return } - respBytes, err := json.Marshal(resp.Result) + respBytes, err := json.Marshal(resp) if err != nil { log.WarnWithContext(r.Context(), "Failed to marshal response", "error", err) http.Error(w, "Failed to marshal response", http.StatusInternalServerError) @@ -245,9 +270,6 @@ func (s *X402Server) Start(addr string, permissions os.FileMode) error { } defer listener.Close() - httpServer := mcpserver.NewStreamableHTTPServer(s.mcpHandler) - s.mcpServer = server.NewX402Handler(httpServer, s.mcpConfig) - srv := &http.Server{ Handler: s, ErrorLog: slog.NewLogLogger(slog.Default().Handler(), slog.LevelDebug), From d5b16cba3a5bc2f4395edde670b4f47b5b660236 Mon Sep 17 00:00:00 2001 From: Nacorid Date: Wed, 29 Jul 2026 00:03:11 +0000 Subject: [PATCH 3/6] rewrite wrapper migrate server from v1 to v2 --- go.mod | 12 +- go.sum | 18 - internal/server/auth.go | 84 +++++ internal/server/server.go | 679 +++++++++++++++++++++----------------- internal/server/types.go | 13 + internal/x402/x402.go | 452 +++++++++++++++---------- 6 files changed, 752 insertions(+), 506 deletions(-) create mode 100644 internal/server/auth.go diff --git a/go.mod b/go.mod index 6cbf6e8..0384f0e 100644 --- a/go.mod +++ b/go.mod @@ -4,9 +4,10 @@ go 1.25.4 require ( github.com/bluesky-social/indigo v0.0.0-20251009224519-09f107c1109e + github.com/golang-jwt/jwt/v5 v5.3.1 + github.com/google/jsonschema-go v0.4.3 github.com/joho/godotenv v1.5.1 github.com/lib/pq v1.10.9 - github.com/mark3labs/mcp-go v0.43.2 github.com/mark3labs/x402-go v0.12.1 github.com/modelcontextprotocol/go-sdk v1.6.1 github.com/nacorid/logger v0.0.0-20251221003040-586da7a03fa0 @@ -19,11 +20,9 @@ require ( filippo.io/edwards25519 v1.1.0 // indirect github.com/FactomProject/basen v0.0.0-20150613233007-fe3947df716e // indirect github.com/FactomProject/btcutilecc v0.0.0-20130527213604-d3a63a5752ec // indirect - github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/benbjohnson/clock v1.3.5 // indirect github.com/bits-and-blooms/bitset v1.24.2 // indirect github.com/blendle/zapdriver v1.3.1 // indirect - github.com/buger/jsonparser v1.1.1 // indirect github.com/consensys/gnark-crypto v0.19.2 // indirect github.com/crate-crypto/go-eth-kzg v1.5.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect @@ -37,29 +36,23 @@ require ( github.com/gagliardetto/solana-go v1.14.0 // indirect github.com/gagliardetto/treeout v0.1.4 // indirect github.com/holiman/uint256 v1.3.2 // indirect - github.com/invopop/jsonschema v0.13.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.18.1 // indirect github.com/logrusorgru/aurora v2.0.3+incompatible // indirect - github.com/mailru/easyjson v0.9.1 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 // indirect - github.com/spf13/cast v1.10.0 // indirect github.com/streamingfast/logging v0.0.0-20250918142248-ac5a1e292845 // indirect github.com/supranational/blst v0.3.16 // indirect github.com/tyler-smith/go-bip32 v1.0.0 // indirect github.com/tyler-smith/go-bip39 v1.1.0 // indirect - github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect go.mongodb.org/mongo-driver v1.17.6 // indirect go.uber.org/ratelimit v0.3.1 // indirect golang.org/x/sync v0.19.0 // indirect golang.org/x/term v0.38.0 // indirect - gopkg.in/square/go-jose.v2 v2.6.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) require ( @@ -73,7 +66,6 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/google/jsonschema-go v0.4.3 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.1 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect diff --git a/go.sum b/go.sum index 0ef9cfc..4135ce5 100644 --- a/go.sum +++ b/go.sum @@ -17,8 +17,6 @@ github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDO 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/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= -github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= @@ -30,8 +28,6 @@ github.com/blendle/zapdriver v1.3.1 h1:C3dydBOWYRiOk+B8X9IVZ5IOe+7cl+tGOexN4QqHf github.com/blendle/zapdriver v1.3.1/go.mod h1:mdXfREi6u5MArG4j9fewC+FGnXaBR+T4Ox4J2u4eHCc= github.com/bluesky-social/indigo v0.0.0-20251009224519-09f107c1109e h1:6oNYIMh+kbArCQiwZYPGgfxQq88tnMyiBIZtg5clq90= github.com/bluesky-social/indigo v0.0.0-20251009224519-09f107c1109e/go.mod h1:RuQVrCGm42QNsgumKaR6se+XkFKfCPNwdCiTvqKRUck= -github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= -github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/carlmjohnson/versioninfo v0.22.5 h1:O00sjOLUAFxYQjlN/bzYTuZiS0y6fWDQjMRvwtKgwwc= github.com/carlmjohnson/versioninfo v0.22.5/go.mod h1:QT9mph3wcVfISUKd0i9sZfVrPviHuSF+cUtLjm2WSf8= github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk= @@ -84,8 +80,6 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 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/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/gagliardetto/binary v0.8.0 h1:U9ahc45v9HW0d15LoN++vIXSJyqR/pWw8DDlhd7zvxg= @@ -154,8 +148,6 @@ 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/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= -github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs= github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0= github.com/ipfs/go-block-format v0.2.0 h1:ZqrkxBA2ICbDRbK8KJs/u0O3dlp6gmAuuXUJNiW1Ycs= @@ -217,10 +209,6 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= -github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= -github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= -github.com/mark3labs/mcp-go v0.43.2 h1:21PUSlWWiSbUPQwXIJ5WKlETixpFpq+WBpbMGDSVy/I= -github.com/mark3labs/mcp-go v0.43.2/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw= github.com/mark3labs/x402-go v0.12.1 h1:yWlT/mjF0P1j2sB0xAM4GJb+1/Ycv6p6EqUrT6RB2X0= github.com/mark3labs/x402-go v0.12.1/go.mod h1:srAvV9FosjBiqrclF15thrQbz0fVVfNXtMcqD0e1hKU= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= @@ -318,8 +306,6 @@ github.com/sokkalf/slog-seq v0.5.1 h1:4LicZGsMhuCtmZkbS++JOm3Wn8PLIrjFkuvEp0yeGH github.com/sokkalf/slog-seq v0.5.1/go.mod h1:B82pc/cMpdQQg6hkBbstHEL4vqI1eZ1MISuN1IK7h14= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= -github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091/go.mod h1:VlduQ80JcGJSargkRU4Sg9Xo63wZD/l8A5NC/Uo1/uU= github.com/streamingfast/logging v0.0.0-20250918142248-ac5a1e292845 h1:VMA0pZ3MI8BErRA3kh8dKJThP5d0Xh5vZVk5yFIgH/A= github.com/streamingfast/logging v0.0.0-20250918142248-ac5a1e292845/go.mod h1:BtDq81Tyc7H8up5aXNi/I95nPmG3C0PLEqGWY/iWQ2E= @@ -353,8 +339,6 @@ github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSD github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw= github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e h1:28X54ciEwwUxyHn9yrZfl5ojgF4CBNLWX7LR0rvBkf4= github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so= -github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= -github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/x402-foundation/x402/go/v2 v2.19.0 h1:vyU9cKFfqw8oI6To66r+UH67vFwWpoMDRX9zGM8bCJI= github.com/x402-foundation/x402/go/v2 v2.19.0/go.mod h1:OAsOuB+FYhCoKzijOGZ+PcJk9g4RBfCJvpABGxi+NcE= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= @@ -494,8 +478,6 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 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/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= -gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= diff --git a/internal/server/auth.go b/internal/server/auth.go new file mode 100644 index 0000000..94c75bc --- /dev/null +++ b/internal/server/auth.go @@ -0,0 +1,84 @@ +package server + +import ( + "context" + "crypto/ecdsa" + "crypto/rand" + "crypto/x509" + "encoding/hex" + "encoding/pem" + "fmt" + "time" + + "github.com/golang-jwt/jwt/v5" + x402http "github.com/x402-foundation/x402/go/v2/http" +) + +type CDPAuth struct { + keyName string + privateKey *ecdsa.PrivateKey + signingMethod jwt.SigningMethod +} + +func NewCDPAuth(keyName, privateKeyPEM string) (*CDPAuth, error) { + block, _ := pem.Decode([]byte(privateKeyPEM)) + if block == nil { + return nil, fmt.Errorf("failed to decode PEM block") + } + + var signingMethod jwt.SigningMethod + privKey, err := x509.ParseECPrivateKey(block.Bytes) + if err != nil { + parsed, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + return nil, fmt.Errorf("failed to parse private key: %w", err) + } + var ok bool + privKey, ok = parsed.(*ecdsa.PrivateKey) + if !ok { + return nil, fmt.Errorf("not an ECDSA private key") + } + signingMethod = jwt.SigningMethodEdDSA + } else { + signingMethod = jwt.SigningMethodES256 + } + return &CDPAuth{ + keyName: keyName, + privateKey: privKey, + signingMethod: signingMethod, + }, nil +} + +func (a *CDPAuth) generateBearerToken(method, path string) (string, error) { + nonceBytes := make([]byte, 16) + if _, err := rand.Read(nonceBytes); err != nil { + return "", err + } + nonce := hex.EncodeToString(nonceBytes) + uri := fmt.Sprintf("%s api.cdp.coinbase.com%s", method, path) + now := time.Now().Unix() + claims := jwt.MapClaims{ + "sub": a.keyName, + "iss": "cdp", + "nbf": now, + "exp": now + 120, + "uris": []string{uri}, + } + + token := jwt.NewWithClaims(a.signingMethod, claims) + token.Header["kid"] = a.keyName + token.Header["nonce"] = nonce + + return token.SignedString(a.privateKey) +} + +func (a *CDPAuth) GetAuthHeaders(ctx context.Context) (x402http.AuthHeaders, error) { + verify, _ := a.generateBearerToken("POST", "/platform/v2/x402/verify") + settle, _ := a.generateBearerToken("POST", "/platform/v2/x402/settle") + supported, _ := a.generateBearerToken("GET", "/platform/v2/x402/supported") + return x402http.AuthHeaders{ + Verify: map[string]string{"Authorization": "Bearer " + verify}, + Settle: map[string]string{"Authorization": "Bearer " + settle}, + Supported: map[string]string{"Authorization": "Bearer " + supported}, + }, nil +} diff --git a/internal/server/server.go b/internal/server/server.go index 9beda5f..cc824d0 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -2,6 +2,7 @@ package server import ( "context" + "encoding/json" "fmt" "net/http" "os" @@ -9,11 +10,7 @@ import ( "time" "github.com/bluesky-social/indigo/api/bsky" - "github.com/mark3labs/mcp-go/mcp" - mcpserver "github.com/mark3labs/mcp-go/server" - x402go "github.com/mark3labs/x402-go" - x402http "github.com/mark3labs/x402-go/http" - "github.com/mark3labs/x402-go/signers/coinbase" + "github.com/modelcontextprotocol/go-sdk/mcp" log "github.com/nacorid/logger" "github.com/nacorid/naco-api/internal/activitypub" "github.com/nacorid/naco-api/internal/bluesky" @@ -22,6 +19,8 @@ import ( "github.com/nacorid/naco-api/internal/store" "github.com/nacorid/naco-api/internal/utils" "github.com/nacorid/naco-api/internal/x402" + x402v2 "github.com/x402-foundation/x402/go/v2" + "github.com/x402-foundation/x402/go/v2/types" ) type Server struct { @@ -39,7 +38,7 @@ func New(ctx context.Context, db *store.Queries, ap *activitypub.APClient, bsky rateLimits := NewRateLimitMiddleware(float64(1.0/10.0), 2, []string{"getFeed"}) metrics := NewMetricsHook() toolChain := x402.X402Middlewares{ - MCPMiddleware: []mcpserver.ToolHandlerMiddleware{ + MCPMiddleware: []mcp.Middleware{ metrics.OnCall, rateLimits.OnCall, }, @@ -48,86 +47,82 @@ func New(ctx context.Context, db *store.Queries, ap *activitypub.APClient, bsky rateLimits.OnCallHTTP, }, } - auth, err := coinbase.NewCDPAuth(cfg.ApiKey, cfg.ApiSecret, "") + auth, err := NewCDPAuth(cfg.ApiKey, cfg.ApiSecret) if err != nil { log.FatalWithContext(ctx, "failed to create CDP auth", "error", err) } + srv, err := x402.NewX402Server( "x402 enabled Bluesky lookingglass for autonomous Agents", - "0.1.0", - &x402http.Config{ - FacilitatorURL: cfg.X402FacilitatorURL, - FacilitatorAuthorizationProvider: func(r *http.Request) string { - token, err := auth.GenerateBearerToken(r.Method, r.URL.Path) - if err != nil { - log.FatalWithContext(ctx, "failed to generate auth token", "error", err) - return "" - } - return "Bearer " + token - }, - FacilitatorOnAfterSettle: func(ctx context.Context, pp x402go.PaymentPayload, pr x402go.PaymentRequirement, sr *x402go.SettlementResponse, errVal error) { - var amount string - if errVal != nil { - log.ErrorWithContext(ctx, "Payment settlement failed", "error", errVal) - amount = "0" - } else { - if payload, ok := pp.Payload.(map[string]interface{}); ok { - if auth, ok := payload["Authorization"].(map[string]interface{}); ok { - if value, ok := auth["Value"].(string); ok { - amount = value - } - } - } - } - var to string - if payload, ok := pp.Payload.(map[string]interface{}); ok { - if auth, ok := payload["Authorization"].(map[string]interface{}); ok { - if value, ok := auth["To"].(string); ok { - to = value - } - } - } - var from string - if payload, ok := pp.Payload.(map[string]interface{}); ok { - if auth, ok := payload["Authorization"].(map[string]interface{}); ok { - if value, ok := auth["From"].(string); ok { - from = value - } - } - } - ret, err := db.SaveTransaction(ctx, store.SaveTransactionParams{ - X402TransactionHash: sr.Transaction, - Amount: amount, - Asset: pr.Asset, - Network: sr.Network, - Recipient: to, - Sender: from, - }) - var id int64 = 0 - if len(ret) > 0 { - id = ret[0].ID - } - fields := log.Fields{ - "transaction_id": id, - "X402_transaction_hash": sr.Transaction, - "amount": amount, - "asset": pr.Asset, - "network": sr.Network, - "recipient": to, - "sender": from, - } - if err != nil { - log.WithFields(fields).ErrorWithContext(ctx, "Failed to save transaction", "value", ret, "error", err) - } - log.WithFields(fields).DebugWithContext(ctx, "Saved transaction") - }, - }, + "0.2.0", + cfg.X402FacilitatorURL, + auth, toolChain, ) if err != nil { log.FatalWithContext(ctx, "failed to create x402 server", "error", err) } + srv.OnAfterSettle(func(settleCtx x402v2.SettleResultContext) error { + var amount, to, from, asset string + + var payloadMap map[string]any + if err := json.Unmarshal(settleCtx.PayloadBytes, &payloadMap); err == nil { + if payload, ok := payloadMap["payload"].(map[string]interface{}); ok { + if auth, ok := payload["Authorization"].(map[string]interface{}); ok { + if value, ok := auth["Value"].(string); ok { + amount = value + } + if value, ok := auth["To"].(string); ok { + to = value + } + if value, ok := auth["From"].(string); ok { + from = value + } + } + } + } + + var reqMap map[string]interface{} + if err := json.Unmarshal(settleCtx.RequirementsBytes, &reqMap); err == nil { + if value, ok := reqMap["asset"].(string); ok { + asset = value + } + } + + ret, err := db.SaveTransaction(settleCtx.Ctx, store.SaveTransactionParams{ + X402TransactionHash: settleCtx.Result.Transaction, + Amount: amount, + Asset: asset, + Network: string(settleCtx.Result.Network), + Recipient: to, + Sender: from, + }) + + var id int64 = 0 + if len(ret) > 0 { + id = ret[0].ID + } + + fields := log.Fields{ + "transaction_id": id, + "X402_transaction_hash": settleCtx.Result.Transaction, + "amount": amount, + "asset": asset, + "network": settleCtx.Result.Network, + "recipient": to, + "sender": from, + } + + if err != nil { + log.WithFields(fields).ErrorWithContext(settleCtx.Ctx, "Failed to save transaction", "value", ret, "error", err) + return err + } + + log.WithFields(fields).DebugWithContext(settleCtx.Ctx, "Saved transaction") + return nil + }) + s := &Server{ listenAddress: cfg.ListenAddress, permissions: cfg.UnixSocketPermissions, @@ -145,244 +140,245 @@ func New(ctx context.Context, db *store.Queries, ap *activitypub.APClient, bsky } func (s *Server) registerTools() { - chainConfig := x402go.BaseMainnet - - requirement, err := newUSDCPaymentRequirement(x402go.USDCRequirementConfig{ - Chain: chainConfig, - Amount: "0.01", - RecipientAddress: s.x402Wallet, - Description: "Get a specific post from a specific actor", + requirement := types.PaymentRequirements{ + Scheme: "exact", + Network: "eip155:8453", + Asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + Amount: "10000", + PayTo: s.x402Wallet, MaxTimeoutSeconds: 60, - }, - &x402go.OutputSchema{ - Input: x402go.InputSchema{ - Type: x402go.InputSchemaTypeHTTP, - Method: x402go.InputSchemaMethodPOST, - BodyType: x402go.InputSchemaBodyTypeJSON, - BodyFields: map[string]x402go.FieldDef{ - "actor": { - Type: "string", - Description: "Did or handle of an actor", - Required: true, - }, - "rkey": { - Type: "string", - Description: "Record key of the post", - Required: true, - }, - "collection": { - Type: "string", - Description: "Collection the post belongs to. Defaults to 'app.bsky.feed.post'", - Required: false, + Extra: map[string]any{ + "OutputSchema": x402.Schema{ + Input: x402.InputSchema{ + Type: "HTTP", + Method: "POST", + Discoverable: true, + BodyFields: map[string]any{ + "actor": Bodyfield{ + Type: "string", + Description: "Did or handle of an actor", + Required: true, + }, + "rkey": Bodyfield{ + Type: "string", + Description: "Record key of the post", + Required: true, + }, + "collection": Bodyfield{ + Type: "string", + Description: "Collection the post belongs to. Defaults to 'app.bsky.feed.post'", + Required: false, + }, }, }, - }, - Output: map[string]x402go.FieldDef{ - "actor": { - Type: "string", - Description: "Did of the actor", - }, - "post": { - Type: "object", - Description: "The post object. Includes the post content 'content', like 'likes', quote 'quotes', replies 'replies' and repost 'reposts' count", + Output: map[string]any{ + "actor": Bodyfield{ + Type: "string", + Description: "Did of the actor", + }, + "post": Bodyfield{ + Type: "object", + Description: "The post object. Includes the post content 'content', like 'likes', quote 'quotes', replies 'replies' and repost 'reposts' count", + }, }, }, }, - ) - if err != nil { - log.Fatal("failed to create payment requirement", "error", err) } - requirements := []x402go.PaymentRequirement{requirement} + requirements := []types.PaymentRequirements{requirement} s.mcpHandler.AddPayableTool( - mcp.NewTool("getPost", - mcp.WithDescription("Get a specific post from a specific actor"), - mcp.WithInputSchema[GetPostRequest](), - mcp.WithOutputSchema[GetPostResponsePost](), - ), - mcp.NewStructuredToolHandler(s.getPost), + mcp.Tool{Name: "getPost", + Description: "Get a specific post from a specific actor", + InputSchema: GetPostRequest{}, + OutputSchema: GetPostResponsePost{}, + }, + s.getPost, requirements..., ) - requirement, err = newUSDCPaymentRequirement(x402go.USDCRequirementConfig{ - Chain: chainConfig, - Amount: "0.10", - RecipientAddress: s.x402Wallet, - Description: "Get the latest n posts from a specific actor", + requirement = types.PaymentRequirements{ + Scheme: "exact", + Network: "eip155:8453", + Asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + Amount: "100000", + PayTo: s.x402Wallet, MaxTimeoutSeconds: 60, - }, - &x402go.OutputSchema{ - Input: x402go.InputSchema{ - Type: x402go.InputSchemaTypeHTTP, - Method: x402go.InputSchemaMethodPOST, - BodyType: x402go.InputSchemaBodyTypeJSON, - BodyFields: map[string]x402go.FieldDef{ - "actor": { - Type: "string", - Description: "Did or handle of an actor", - Required: true, + Extra: map[string]any{ + "OutputSchema": x402.Schema{ + Input: x402.InputSchema{ + Type: "HTTP", + Method: "POST", + Discoverable: true, + BodyFields: map[string]any{ + "actor": Bodyfield{ + Type: "string", + Description: "Did or handle of an actor", + Required: true, + }, + "n": Bodyfield{ + Type: "integer", + Description: "Number of posts to retrieve, maximum 50", + Required: true, + }, }, - "n": { + }, + Output: map[string]any{ + "actor": Bodyfield{ + Type: "string", + Description: "Did of an actor", + }, + "posts": Bodyfield{ + Type: "array", + Description: "Array of post objects. Each includes the post content 'content', like 'likes', quote 'quotes', replies 'replies' and repost 'reposts' count", + }, + }, + }, + }, + } + + requirements = []types.PaymentRequirements{requirement} + + s.mcpHandler.AddPayableTool( + mcp.Tool{Name: "getLatestPosts", + Description: "Get the latest n posts from a specific actor", + InputSchema: GetLatestPostsRequest{}, + OutputSchema: GetLatestPostsResponse{}, + }, + s.getLatestPosts, + requirements..., + ) + + requirement = types.PaymentRequirements{ + Scheme: "exact", + Network: "eip155:8453", + Asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + Amount: "100000", + PayTo: s.x402Wallet, + MaxTimeoutSeconds: 60, + Extra: map[string]any{ + "OutputSchema": x402.Schema{ + Input: x402.InputSchema{ + Type: "HTTP", + Method: "POST", + Discoverable: true, + BodyFields: map[string]any{ + "actor": Bodyfield{ + Type: "string", + Description: "Did or handle of an actor", + Required: true, + }, + "start": Bodyfield{ + Type: "string", + Description: "Start time in RFC3339 format", + Required: true, + }, + "end": Bodyfield{ + Type: "string", + Description: "End time in RFC3339 format", + Required: true, + }, + "n": Bodyfield{ + Type: "integer", + Description: "Maximum number of posts to retrieve, maximum 50", + Required: true, + }, + }, + }, + Output: map[string]any{ + "actor": Bodyfield{ + Type: "string", + Description: "Did of an actor", + }, + "posts": Bodyfield{ + Type: "array", + Description: "Array of post objects. Each includes the post content 'content', like 'likes', quote 'quotes', replies 'replies' and repost 'reposts' count", + }, + }, + }, + }, + } + + requirements = []types.PaymentRequirements{requirement} + + s.mcpHandler.AddPayableTool( + mcp.Tool{Name: "getPostsInTime", + Description: "Get posts from a specific actor in a time range", + InputSchema: GetPostsInTimeRequest{}, + OutputSchema: GetPostsInTimeResponse{}, + }, + s.getPostsInTime, + requirements..., + ) + + requirement = types.PaymentRequirements{ + Scheme: "exact", + Network: "eip155:8453", + Asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + Amount: "100000", + PayTo: s.x402Wallet, + MaxTimeoutSeconds: 60, + Extra: map[string]any{ + "OutputSchema": x402.Schema{ + Input: x402.InputSchema{ + Type: "HTTP", + Method: "POST", + Discoverable: true, + BodyFields: map[string]any{ + "actor": Bodyfield{ + Type: "string", + Description: "Did or handle of an actor", + Required: true, + }, + }, + }, + Output: map[string]any{ + "PersonaEmbedding": Bodyfield{ + Type: "array", + Description: "Embedding vector representing the actor's persona", + }, + "TopKeywords": Bodyfield{ + Type: "array", + Description: "Top words used by the actor", + }, + "StyleSummary": Bodyfield{ + Type: "string", + Description: "Summary of the actor's writing style", + }, + "PostCount": Bodyfield{ Type: "integer", - Description: "Number of posts to retrieve, maximum 50", - Required: true, + Description: "Number of posts analyzed", }, }, }, - Output: map[string]x402go.FieldDef{ - "actor": { - Type: "string", - Description: "Did of an actor", - }, - "posts": { - Type: "array", - Description: "Array of post objects. Each includes the post content 'content', like 'likes', quote 'quotes', replies 'replies' and repost 'reposts' count", - }, - }, }, - ) - if err != nil { - log.Fatal("failed to create payment requirement", "error", err) } - requirements = []x402go.PaymentRequirement{requirement} + + requirements = []types.PaymentRequirements{requirement} s.mcpHandler.AddPayableTool( - mcp.NewTool("getLatestPosts", - mcp.WithDescription("Get the latest n posts from a specific actor"), - mcp.WithInputSchema[GetLatestPostsRequest](), - mcp.WithOutputSchema[GetLatestPostsResponse](), - ), - mcp.NewStructuredToolHandler(s.getLatestPosts), - requirements..., - ) - - requirement, err = newUSDCPaymentRequirement(x402go.USDCRequirementConfig{ - Chain: chainConfig, - Amount: "0.10", - RecipientAddress: s.x402Wallet, - Description: "Get posts from a specific actor in a time range", - MaxTimeoutSeconds: 60, - }, - &x402go.OutputSchema{ - Input: x402go.InputSchema{ - Type: x402go.InputSchemaTypeHTTP, - Method: x402go.InputSchemaMethodPOST, - BodyType: x402go.InputSchemaBodyTypeJSON, - BodyFields: map[string]x402go.FieldDef{ - "actor": { - Type: "string", - Description: "Did or handle of an actor", - Required: true, - }, - "start": { - Type: "string", - Description: "Start time in RFC3339 format", - Required: true, - }, - "end": { - Type: "string", - Description: "End time in RFC3339 format", - Required: true, - }, - "n": { - Type: "integer", - Description: "Maximum number of posts to retrieve, maximum 50", - Required: true, - }, - }, - }, - Output: map[string]x402go.FieldDef{ - "actor": { - Type: "string", - Description: "Did of an actor", - }, - "posts": { - Type: "array", - Description: "Array of post objects. Each includes the post content 'content', like 'likes', quote 'quotes', replies 'replies' and repost 'reposts' count", - }, - }, + mcp.Tool{Name: "getStyleProfile", + Description: "Get an actor's style Profile", + InputSchema: GetStyleProfileRequest{}, + OutputSchema: GetStyleProfileResponse{}, }, - ) - if err != nil { - log.Fatal("failed to create payment requirement", "error", err) - } - requirements = []x402go.PaymentRequirement{requirement} - - s.mcpHandler.AddPayableTool( - mcp.NewTool("getPostsInTime", - mcp.WithDescription("Get posts from a specific actor in a time range"), - mcp.WithInputSchema[GetPostsInTimeRequest](), - mcp.WithOutputSchema[GetPostsInTimeResponse](), - ), - mcp.NewStructuredToolHandler(s.getPostsInTime), - requirements..., - ) - - requirement, err = newUSDCPaymentRequirement(x402go.USDCRequirementConfig{ - Chain: chainConfig, - Amount: "0.50", - RecipientAddress: s.x402Wallet, - Description: "Get an actor's style Profile", - MaxTimeoutSeconds: 60, - }, - &x402go.OutputSchema{ - Input: x402go.InputSchema{ - Type: x402go.InputSchemaTypeHTTP, - Method: x402go.InputSchemaMethodPOST, - BodyType: x402go.InputSchemaBodyTypeJSON, - BodyFields: map[string]x402go.FieldDef{ - "actor": { - Type: "string", - Description: "Did or handle of an actor", - Required: true, - }, - }, - }, - Output: map[string]x402go.FieldDef{ - "PersonaEmbedding": { - Type: "array", - Description: "Embedding vector representing the actor's persona", - }, - "TopKeywords": { - Type: "array", - Description: "Top words used by the actor", - }, - "StyleSummary": { - Type: "string", - Description: "Summary of the actor's writing style", - }, - "PostCount": { - Type: "integer", - Description: "Number of posts analyzed", - }, - }, - }, - ) - if err != nil { - log.Fatal("failed to create payment requirement", "error", err) - } - requirements = []x402go.PaymentRequirement{requirement} - - s.mcpHandler.AddPayableTool( - mcp.NewTool("getStyleProfile", - mcp.WithDescription("Get an actor's style Profile"), - mcp.WithInputSchema[GetStyleProfileRequest](), - mcp.WithOutputSchema[GetStyleProfileResponse](), - ), - mcp.NewStructuredToolHandler(s.getStyleProfile), + s.getStyleProfile, requirements..., ) s.mcpHandler.AddTool( - mcp.NewTool("getFeed", - mcp.WithDescription("Get a feed by its URI"), - mcp.WithInputSchema[GetFeedRequest](), - mcp.WithOutputSchema[GetFeedResponse](), - ), - mcp.NewStructuredToolHandler(s.getFeed), + mcp.Tool{Name: "getFeed", + Description: "Get a feed by its URI", + InputSchema: GetFeedRequest{}, + OutputSchema: GetFeedResponse{}, + }, + s.getFeed, ) } -func (s *Server) getPost(ctx context.Context, request mcp.CallToolRequest, args GetPostRequest) (*GetPostResponsePost, error) { +func (s *Server) getPost(ctx context.Context, request *mcp.CallToolRequest) (*mcp.CallToolResult, error) { + var args GetPostRequest + if err := json.Unmarshal(request.Params.Arguments, &args); err != nil { + return nil, fmt.Errorf("invalid arguments: %w", err) + } if args.Author == "" { return nil, fmt.Errorf("author must be a valid did or handle") } @@ -397,7 +393,7 @@ func (s *Server) getPost(ctx context.Context, request mcp.CallToolRequest, args if !ok { return nil, fmt.Errorf("record is not a feed post") } - return &GetPostResponsePost{ + resp := GetPostResponsePost{ Author: res.Author.Did, Content: post.Text, Likes: *res.LikeCount, @@ -405,10 +401,25 @@ func (s *Server) getPost(ctx context.Context, request mcp.CallToolRequest, args Quotes: *res.QuoteCount, Replies: *res.ReplyCount, CreatedAt: post.CreatedAt, + } + + respBytes, err := json.Marshal(resp) + if err != nil { + return nil, err + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: string(respBytes)}, + }, }, nil } -func (s *Server) getLatestPosts(ctx context.Context, request mcp.CallToolRequest, args GetLatestPostsRequest) (*GetLatestPostsResponse, error) { +func (s *Server) getLatestPosts(ctx context.Context, request *mcp.CallToolRequest) (*mcp.CallToolResult, error) { + var args GetLatestPostsRequest + if err := json.Unmarshal(request.Params.Arguments, &args); err != nil { + return nil, fmt.Errorf("invalid arguments: %w", err) + } if args.Author == "" { return nil, fmt.Errorf("author must be a valid did or handle") } @@ -442,13 +453,28 @@ func (s *Server) getLatestPosts(ctx context.Context, request mcp.CallToolRequest if author == "" { author = args.Author } - return &GetLatestPostsResponse{ + resp := GetLatestPostsResponse{ Author: author, Posts: posts, + } + + respBytes, err := json.Marshal(resp) + if err != nil { + return nil, err + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: string(respBytes)}, + }, }, nil } -func (s *Server) getPostsInTime(ctx context.Context, request mcp.CallToolRequest, args GetPostsInTimeRequest) (*GetPostsInTimeResponse, error) { +func (s *Server) getPostsInTime(ctx context.Context, request *mcp.CallToolRequest) (*mcp.CallToolResult, error) { + var args GetPostsInTimeRequest + if err := json.Unmarshal(request.Params.Arguments, &args); err != nil { + return nil, fmt.Errorf("invalid arguments: %w", err) + } if args.Author == "" { return nil, fmt.Errorf("author must be a valid did or handle") } @@ -493,13 +519,28 @@ func (s *Server) getPostsInTime(ctx context.Context, request mcp.CallToolRequest if author == "" { author = args.Author } - return &GetPostsInTimeResponse{ + resp := &GetPostsInTimeResponse{ Author: author, Posts: posts, + } + + respBytes, err := json.Marshal(resp) + if err != nil { + return nil, err + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: string(respBytes)}, + }, }, nil } -func (s *Server) getFeed(ctx context.Context, request mcp.CallToolRequest, args GetFeedRequest) (*GetFeedResponse, error) { +func (s *Server) getFeed(ctx context.Context, request *mcp.CallToolRequest) (*mcp.CallToolResult, error) { + var args GetFeedRequest + if err := json.Unmarshal(request.Params.Arguments, &args); err != nil { + return nil, fmt.Errorf("invalid arguments: %w", err) + } if args.URI == "" { return nil, fmt.Errorf("feedURI must be a valid feed URI") } @@ -524,13 +565,28 @@ func (s *Server) getFeed(ctx context.Context, request mcp.CallToolRequest, args CreatedAt: post.CreatedAt, }) } - return &GetFeedResponse{ + resp := GetFeedResponse{ URI: args.URI, Posts: posts, + } + + respBytes, err := json.Marshal(resp) + if err != nil { + return nil, err + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: string(respBytes)}, + }, }, nil } -func (s *Server) getStyleProfile(ctx context.Context, request mcp.CallToolRequest, args GetStyleProfileRequest) (*GetStyleProfileResponse, error) { +func (s *Server) getStyleProfile(ctx context.Context, request *mcp.CallToolRequest) (*mcp.CallToolResult, error) { + var args GetStyleProfileRequest + if err := json.Unmarshal(request.Params.Arguments, &args); err != nil { + return nil, fmt.Errorf("invalid arguments: %w", err) + } if args.Author == "" { return nil, fmt.Errorf("author must be a valid did or handle") } @@ -545,15 +601,34 @@ func (s *Server) getStyleProfile(ctx context.Context, request mcp.CallToolReques if err != nil { return nil, err } - return &GetStyleProfileResponse{ + resp := GetStyleProfileResponse{ Author: styleProfile.UserID, PersonaEmbedding: embedding, TopKeywords: styleProfile.TopKeywords, PostCount: int(styleProfile.PostCount), StyleSummary: styleProfile.StyleSummary, - }, err + } + + respBytes, err := json.Marshal(resp) + if err != nil { + return nil, err + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: string(respBytes)}, + }, + }, nil } + platform, err := determinePlatform(args.Author) + if err != nil { + return nil, err + } + switch platform { + case Bluesky: + + } res, err := s.bskyClient.GetLatestNPosts(ctx, args.Author, 50) if err != nil { return nil, err @@ -604,16 +679,16 @@ func (s *Server) getStyleProfile(ctx context.Context, request mcp.CallToolReques SourcePlatform: "bluesky", } - return profile, nil -} - -func newUSDCPaymentRequirement(config x402go.USDCRequirementConfig, schema *x402go.OutputSchema) (x402go.PaymentRequirement, error) { - req, err := x402go.NewUSDCPaymentRequirement(config) + profileBytes, err := json.Marshal(profile) if err != nil { - return x402go.PaymentRequirement{}, err + return nil, err } - req.OutputSchema = schema - return req, nil + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: string(profileBytes)}, + }, + }, nil } func determinePlatform(author string) (platform, error) { diff --git a/internal/server/types.go b/internal/server/types.go index b70f82b..8c55258 100644 --- a/internal/server/types.go +++ b/internal/server/types.go @@ -72,3 +72,16 @@ type DCAPMessage struct { TS int64 `json:"ts" jsonschema_description:"Unix timestamp of the message"` SID string `json:"sid" jsonschema_description:"Server ID, 8-12 characters"` } + +type platform int + +const ( + ActivityPub platform = iota + Bluesky + Nostr + Twitter +) + +func (p platform) String() string { + return [...]string{"ActivityPub", "Bluesky", "Nostr", "Twitter"}[p] +} diff --git a/internal/x402/x402.go b/internal/x402/x402.go index f33f5cd..8394c11 100644 --- a/internal/x402/x402.go +++ b/internal/x402/x402.go @@ -1,8 +1,6 @@ package x402 import ( - "bytes" - "context" "encoding/json" "fmt" "io" @@ -10,89 +8,44 @@ import ( "net" "net/http" "os" + "reflect" "strings" + "github.com/google/jsonschema-go/jsonschema" "github.com/modelcontextprotocol/go-sdk/mcp" log "github.com/nacorid/logger" x402 "github.com/x402-foundation/x402/go/v2" x402http "github.com/x402-foundation/x402/go/v2/http" nethttp "github.com/x402-foundation/x402/go/v2/http/nethttp" + x402mcp "github.com/x402-foundation/x402/go/v2/mcp" evm "github.com/x402-foundation/x402/go/v2/mechanisms/evm/exact/server" ) -type ToolHandlerFunc func(ctx context.Context, req *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, error) - type X402Middlewares struct { MCPMiddleware []mcp.Middleware HTTPMiddleware []func(http.Handler) http.Handler } -type HTTPMiddleware func(http.Handler) http.Handler +type ToolInfo struct { + Tool mcp.Tool + IsPayable bool + Requirements []x402.PaymentRequirements + Handler mcp.ToolHandler +} type X402Server struct { mcpServer *mcp.Server x402ResourceServer *x402.X402ResourceServer facilitatorClient *x402http.HTTPFacilitatorClient - x402Middlewares map[string]func(http.Handler) http.Handler + mux *http.ServeMux + name string + version string middlewareChain func(http.Handler) http.Handler - AllEndpoints []string - Handlers map[string]ToolHandlerFunc + tools []ToolInfo } -func (s *X402Server) AddTool(tool mcp.Tool, handler ToolHandlerFunc) { - mcp.AddTool(s.mcpServer, &tool, func(ctx context.Context, req *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) { - result, err := handler(ctx, req, args) - return result, nil, err - }) - s.AllEndpoints = append(s.AllEndpoints, tool.Name) - s.Handlers[tool.Name] = handler - s.x402Middlewares[tool.Name] = noOpMiddleware -} - -func (s *X402Server) AddPayableTool( - tool mcp.Tool, - handler ToolHandlerFunc, - requirements ...x402.PaymentRequirements, -) { - s.AddTool(tool, handler) - - if len(requirements) == 0 { - log.Fatalf("tool %s requires at least one payment requirement", tool.Name) - } - - var options x402http.PaymentOptions - for _, req := range requirements { - options = append(options, x402http.PaymentOptions{x402http.PaymentOption{ - Scheme: req.Scheme, - PayTo: req.PayTo, - Price: req.Amount, - Network: x402.Network(req.Network), - }, - }...) - } - - s.AllEndpoints = append(s.AllEndpoints, tool.Name) - s.x402Middlewares[tool.Name] = nethttp.X402Payment(nethttp.Config{ - Routes: x402http.RoutesConfig{ - "POST /" + tool.Name: { - Accepts: options, - }, - }, - Facilitator: s.facilitatorClient, - Schemes: []nethttp.SchemeConfig{ - {Network: "eip155:8453", Server: evm.NewExactEvmScheme()}, - }, - }) -} - -func noOpMiddleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - next.ServeHTTP(w, r) - }) -} - -func NewX402Server(name, version string, facilitatorURL string, mws X402Middlewares) (*X402Server, error) { +func NewX402Server(name, version string, facilitatorURL string, auth x402http.AuthProvider, mws X402Middlewares) (*X402Server, error) { mcpServer := mcp.NewServer( &mcp.Implementation{ Name: name, @@ -108,7 +61,8 @@ func NewX402Server(name, version string, facilitatorURL string, mws X402Middlewa facilitatorURL = "https://facilitator.mogami.tech" } facilitator := x402http.NewHTTPFacilitatorClient(&x402http.FacilitatorConfig{ - URL: facilitatorURL, + URL: facilitatorURL, + AuthProvider: auth, }) resourceServer := x402.Newx402ResourceServer( @@ -117,129 +71,219 @@ func NewX402Server(name, version string, facilitatorURL string, mws X402Middlewa resourceServer.Register("eip155:8453", evm.NewExactEvmScheme()) - httpChain := chainHTTPMiddlewares(mws.HTTPMiddleware...) - - return &X402Server{ + s := &X402Server{ mcpServer: mcpServer, x402ResourceServer: resourceServer, facilitatorClient: facilitator, - x402Middlewares: make(map[string]func(http.Handler) http.Handler), - middlewareChain: httpChain, - Handlers: make(map[string]ToolHandlerFunc), - }, nil -} - -func chainHTTPMiddlewares(middlewares ...func(http.Handler) http.Handler) func(http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - for i := len(middlewares) - 1; i >= 0; i-- { - next = middlewares[i](next) - } - return next - } -} - -func (s *X402Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { - toolName := strings.TrimPrefix(r.URL.Path, "/") - addr := "@" - forwarded := r.Header.Get("X-Forwarded-For") - if forwarded != "" { - ip := strings.Split(forwarded, ",")[0] - addr = strings.TrimSpace(ip) - } else { - realIP := r.Header.Get("X-Real-Ip") - if realIP != "" { - addr = realIP - } + mux: http.NewServeMux(), + name: name, + version: version, + middlewareChain: chainHTTPMiddlewares(mws.HTTPMiddleware...), + tools: make([]ToolInfo, 0), } - if r.URL.Path == "/healthz" { + s.mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte("ok")) - return - } + }) - log.InfoWithContext(r.Context(), "Request received", "host", r.Host, "method", r.Method, "path", r.URL.Path, "remoteAddr", addr) + s.mux.HandleFunc("GET /openapi.json", s.serveOpenAPI) - if r.Method != http.MethodPost { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } + mcpHandler := mcp.NewStreamableHTTPHandler(func(req *http.Request) *mcp.Server { + return s.mcpServer + }, nil) + s.mux.Handle("/mcp", mcpHandler) - if handler, ok := s.Handlers[toolName]; ok { - bodyBytes, err := io.ReadAll(r.Body) - if err != nil { - log.WarnWithContext(r.Context(), "Failed to read request body", "error", err) - http.Error(w, "Failed to read request body", http.StatusBadRequest) - return - } - var args map[string]any - if len(bodyBytes) > 0 { - if err := json.Unmarshal(bodyBytes, &args); err != nil { - log.WarnWithContext(r.Context(), "Failed to parse request body as JSON", "tool", toolName, "error", err) - http.Error(w, "Invalid JSON body", http.StatusBadRequest) - return - } - } else { - log.DebugWithContext(r.Context(), "Empty request body for tool", "tool", toolName) - args = make(map[string]any) - } - - // Store parsed args in context - type contextKey string - const argsKey contextKey = "parsed_args" - ctx := context.WithValue(r.Context(), argsKey, args) - r = r.WithContext(ctx) - - r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) // Restore body for further reading - functionHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - args := r.Context().Value(argsKey).(map[string]any) - - argsJSON, err := json.Marshal(args) - if err != nil { - log.WarnWithContext(r.Context(), "Failed to marshal args to raw JSON", "error", err) - http.Error(w, "Failed to encode arguments", http.StatusInternalServerError) - return - } - - callReq := &mcp.CallToolRequest{ - Params: &mcp.CallToolParamsRaw{ - Name: toolName, - Arguments: json.RawMessage(argsJSON), - }, - } - - resp, err := handler(r.Context(), callReq, args) - if err != nil { - log.WarnWithContext(r.Context(), "Tool handler error in direct handler", "tool", toolName, "error", err) - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - respBytes, err := json.Marshal(resp) - if err != nil { - log.WarnWithContext(r.Context(), "Failed to marshal response", "error", err) - http.Error(w, "Failed to marshal response", http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - w.Write(respBytes) - }) - - middleware, ok := s.x402Middlewares[toolName] - if !ok { - log.ErrorWithContext(r.Context(), "No X402 middleware found for tool", "tool", toolName) - http.Error(w, "Internal server error", http.StatusInternalServerError) - return - } - s.middlewareChain(middleware(functionHandler)).ServeHTTP(w, r) - } else { - http.NotFound(w, r) - } + return s, nil +} + +func (s *X402Server) AddTool(tool mcp.Tool, handler mcp.ToolHandler) { + tool.InputSchema = toJSONSchema(tool.InputSchema) + tool.OutputSchema = toJSONSchema(tool.OutputSchema) + + s.mcpServer.AddTool(&tool, handler) + + s.tools = append(s.tools, ToolInfo{ + Tool: tool, + IsPayable: false, + Handler: handler, + }) + + restHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + s.handleRESTCall(w, r, tool.Name, handler) + }) + s.mux.Handle("POST /v1/"+tool.Name, s.middlewareChain(restHandler)) +} + +func (s *X402Server) AddPayableTool( + tool mcp.Tool, + handler mcp.ToolHandler, + requirements ...x402.PaymentRequirements, +) { + s.AddPayableTool(tool, handler, requirements...) + + if len(requirements) == 0 { + log.Fatalf("tool %s requires at least one payment requirement", tool.Name) + } + + tool.InputSchema = toJSONSchema(tool.InputSchema) + tool.OutputSchema = toJSONSchema(tool.OutputSchema) + + wrapper := x402mcp.NewPaymentWrapper(s.x402ResourceServer, x402mcp.PaymentWrapperConfig{ + Accepts: requirements, + Resource: &x402mcp.ResourceInfo{ + URL: "mcp://tool/" + tool.Name, + Description: tool.Description, + }, + }) + + s.mcpServer.AddTool(&tool, wrapper.Wrap(handler)) + + s.tools = append(s.tools, ToolInfo{ + Tool: tool, + IsPayable: true, + Requirements: requirements, + Handler: handler, + }) + + var options x402http.PaymentOptions + for _, req := range requirements { + options = append(options, x402http.PaymentOption{ + Scheme: req.Scheme, + PayTo: req.PayTo, + Price: req.Amount, + Network: x402.Network(req.Network), + }) + } + + paymentMiddleware := nethttp.X402Payment(nethttp.Config{ + Routes: x402http.RoutesConfig{ + "POST /" + tool.Name: { + Accepts: options, + Description: tool.Description, + }, + }, + Facilitator: s.facilitatorClient, + Schemes: []nethttp.SchemeConfig{ + {Network: "eip155:8453", Server: evm.NewExactEvmScheme()}, + }, + }) + + restHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + s.handleRESTCall(w, r, tool.Name, handler) + }) + chained := s.middlewareChain(restHandler) + s.mux.Handle("POST /"+tool.Name, paymentMiddleware(chained)) +} + +func (s *X402Server) handleRESTCall(w http.ResponseWriter, r *http.Request, toolName string, handler mcp.ToolHandler) { + bodyBytes, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, "Failed to read body", http.StatusBadRequest) + return + } + + callReq := &mcp.CallToolRequest{ + Params: &mcp.CallToolParamsRaw{ + Name: toolName, + Arguments: json.RawMessage(bodyBytes), + }, + } + + resp, err := handler(r.Context(), callReq) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + if len(resp.Content) == 1 { + if textContent, ok := resp.Content[0].(*mcp.TextContent); ok { + var js json.RawMessage + if json.Unmarshal([]byte(textContent.Text), &js) == nil { + w.Write([]byte(textContent.Text)) + return + } + } + } + + json.NewEncoder(w).Encode(resp) +} + +func (s *X402Server) OnAfterSettle(hook func(ctx x402.SettleResultContext) error) { + s.x402ResourceServer.OnAfterSettle(hook) +} + +func noOpMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + next.ServeHTTP(w, r) + }) +} + +func (s *X402Server) serveOpenAPI(w http.ResponseWriter, r *http.Request) { + paths := make(map[string]any) + + for _, info := range s.tools { + inputSchema := parseSchema(info.Tool.InputSchema) + outputSchema := parseSchema(info.Tool.OutputSchema) + + postOp := map[string]any{ + "summary": info.Tool.Description, + "description": fmt.Sprintf("Exposes the MCP tool '%s' as a RESTful endpoint.", info.Tool.Name), + "requestBody": map[string]any{ + "required": true, + "content": map[string]any{ + "application/json": map[string]any{ + "schema": inputSchema, + }, + }, + }, + "responses": map[string]any{ + "200": map[string]any{ + "description": "Successful operation", + "content": map[string]any{ + "application/json": map[string]any{ + "schema": outputSchema, + }, + }, + }, + }, + } + + if info.IsPayable { + postOp["description"] = fmt.Sprintf("%s\n\n**Paid Route**: Requires payments via x402.", postOp["description"]) + postOp["responses"].(map[string]any)["402"] = map[string]any{ + "description": "Payment Required. Responds with the PAYMENT-REQUIRED header challenge.", + "headers": map[string]any{ + "PAYMENT-REQUIRED": map[string]any{ + "schema": map[string]any{"type": "string"}, + "description": "Base64 encoded PaymentRequired challenge matching the x402 specification.", + }, + }, + } + } + + paths["/"+info.Tool.Name] = map[string]any{ + "post": postOp, + } + } + + openapiDoc := map[string]any{ + "openapi": "3.0.3", + "info": map[string]any{ + "title": s.name, + "version": s.version, + "description": "Auto-generated REST API matching active Model Context Protocol (MCP) tools.", + }, + "paths": paths, + } + + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(openapiDoc) } -// Start starts the x402 server on the specified address func (s *X402Server) Start(addr string, permissions os.FileMode) error { var ( listener net.Listener @@ -258,10 +302,8 @@ func (s *X402Server) Start(addr string, permissions os.FileMode) error { } log.Info("Starting X402 MCP Server on unix socket", "socketPath", socketPath) - log.Info("MCP endpoint (unix socket)", "socketPath", socketPath) } else { log.Info("Starting X402 MCP Server on TCP", "addr", addr) - log.Infof("MCP endpoint: http://localhost%s", addr) listener, err = net.Listen("tcp", addr) if err != nil { @@ -271,8 +313,66 @@ func (s *X402Server) Start(addr string, permissions os.FileMode) error { defer listener.Close() srv := &http.Server{ - Handler: s, + Handler: s.mux, ErrorLog: slog.NewLogLogger(slog.Default().Handler(), slog.LevelDebug), } return srv.Serve(listener) } + +func toJSONSchema(val any) json.RawMessage { + if val == nil { + return nil + } + switch v := val.(type) { + case json.RawMessage: + return v + case []byte: + return json.RawMessage(v) + case string: + return json.RawMessage(v) + } + + t := reflect.TypeOf(val) + schema, err := jsonschema.ForType(t, &jsonschema.ForOptions{}) + if err != nil { + return nil + } + bytes, err := json.Marshal(schema) + if err != nil { + return nil + } + return bytes +} + +func parseSchema(schemaAny any) any { + if schemaAny == nil { + return map[string]any{"type": "object"} + } + switch v := schemaAny.(type) { + case json.RawMessage: + var m any + if err := json.Unmarshal(v, &m); err == nil { + return m + } + case []byte: + var m any + if err := json.Unmarshal(v, &m); err == nil { + return m + } + case string: + var m any + if err := json.Unmarshal([]byte(v), &m); err == nil { + return m + } + } + return schemaAny +} + +func chainHTTPMiddlewares(middlewares ...func(http.Handler) http.Handler) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + for i := len(middlewares) - 1; i >= 0; i-- { + next = middlewares[i](next) + } + return next + } +} From 779ddf981f8264f1466543158167534b38b1321e Mon Sep 17 00:00:00 2001 From: Nacorid Date: Wed, 29 Jul 2026 12:09:59 +0000 Subject: [PATCH 4/6] Remove obsolete types --- internal/server/server.go | 16 +++--- internal/server/types.go | 12 +++++ internal/x402/types.go | 110 -------------------------------------- mcpServer.service | 3 +- 4 files changed, 22 insertions(+), 119 deletions(-) delete mode 100644 internal/x402/types.go diff --git a/internal/server/server.go b/internal/server/server.go index cc824d0..8132317 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -148,8 +148,8 @@ func (s *Server) registerTools() { PayTo: s.x402Wallet, MaxTimeoutSeconds: 60, Extra: map[string]any{ - "OutputSchema": x402.Schema{ - Input: x402.InputSchema{ + "OutputSchema": Schema{ + Input: InputSchema{ Type: "HTTP", Method: "POST", Discoverable: true, @@ -203,8 +203,8 @@ func (s *Server) registerTools() { PayTo: s.x402Wallet, MaxTimeoutSeconds: 60, Extra: map[string]any{ - "OutputSchema": x402.Schema{ - Input: x402.InputSchema{ + "OutputSchema": Schema{ + Input: InputSchema{ Type: "HTTP", Method: "POST", Discoverable: true, @@ -255,8 +255,8 @@ func (s *Server) registerTools() { PayTo: s.x402Wallet, MaxTimeoutSeconds: 60, Extra: map[string]any{ - "OutputSchema": x402.Schema{ - Input: x402.InputSchema{ + "OutputSchema": Schema{ + Input: InputSchema{ Type: "HTTP", Method: "POST", Discoverable: true, @@ -317,8 +317,8 @@ func (s *Server) registerTools() { PayTo: s.x402Wallet, MaxTimeoutSeconds: 60, Extra: map[string]any{ - "OutputSchema": x402.Schema{ - Input: x402.InputSchema{ + "OutputSchema": Schema{ + Input: InputSchema{ Type: "HTTP", Method: "POST", Discoverable: true, diff --git a/internal/server/types.go b/internal/server/types.go index 8c55258..ec0e8d4 100644 --- a/internal/server/types.go +++ b/internal/server/types.go @@ -6,6 +6,18 @@ type Bodyfield struct { Required bool `json:"required,omitempty"` } +type InputSchema struct { + BodyFields map[string]any `json:"bodyFields,omitempty"` + Discoverable bool `json:"discoverable,omitempty"` + Method string `json:"method,omitempty"` + Type string `json:"type,omitempty"` +} + +type Schema struct { + Input InputSchema `json:"input"` + Output map[string]any `json:"output"` +} + type GetLatestPostsRequest struct { Author string `json:"author" jsonschema_description:"Did or handle of an author"` Limit int `json:"limit" jsonschema_description:"Number of posts to retrieve"` diff --git a/internal/x402/types.go b/internal/x402/types.go deleted file mode 100644 index 2ebb079..0000000 --- a/internal/x402/types.go +++ /dev/null @@ -1,110 +0,0 @@ -package x402 - -type InputSchema struct { - BodyFields map[string]any `json:"bodyFields,omitempty"` - Discoverable bool `json:"discoverable,omitempty"` - Method string `json:"method,omitempty"` - Type string `json:"type,omitempty"` -} - -type Schema struct { - Input InputSchema `json:"input"` - Output map[string]any `json:"output"` -} - -// PaymentRequirement defines payment requirements for a resource/tool -// as defined in the x402 specification section 5.1 -type PaymentRequirement struct { - Scheme string `json:"scheme"` - Network string `json:"network"` - MaxAmountRequired string `json:"maxAmountRequired"` - Asset string `json:"asset"` - PayTo string `json:"payTo"` - Resource string `json:"resource"` - Description string `json:"description"` - MimeType string `json:"mimeType"` - OutputSchema Schema `json:"outputSchema"` - MaxTimeoutSeconds int `json:"maxTimeoutSeconds"` - Extra map[string]string `json:"extra,omitempty"` -} - -// PaymentRequirements402Response is the HTTP 402 response body -type PaymentRequirements402Response struct { - X402Version int `json:"x402Version"` - Error string `json:"error"` - Accepts []PaymentRequirement `json:"accepts"` -} - -// PaymentPayload represents the X-PAYMENT header content -// as defined in the x402 specification section 5.2 -type PaymentPayload struct { - X402Version int `json:"x402Version"` - Scheme string `json:"scheme"` - Network string `json:"network"` - Payload struct { - Signature string `json:"signature"` - Authorization struct { - From string `json:"from"` - To string `json:"to"` - Value string `json:"value"` - ValidAfter string `json:"validAfter"` - ValidBefore string `json:"validBefore"` - Nonce string `json:"nonce"` - } `json:"authorization"` - } `json:"payload"` -} - -// SettlementResponse is included in X-PAYMENT-RESPONSE header -// as defined in the x402 specification section 5.3 -type SettleResponse struct { - Success bool `json:"success"` - Transaction string `json:"transaction"` - Network string `json:"network"` - Payer string `json:"payer"` - ErrorReason string `json:"errorReason,omitempty"` -} - -// VerifyRequest sent to facilitator /verify endpoint -// as defined in the x402 specification section 7.1 -// Note: x402Version added at root level for facilitator compatibility -type VerifyRequest struct { - X402Version int `json:"x402Version"` - PaymentPayload *PaymentPayload `json:"paymentPayload"` - PaymentRequirements *PaymentRequirement `json:"paymentRequirements"` -} - -// VerifyResponse from facilitator -// as defined in the x402 specification section 7.1 -type VerifyResponse struct { - IsValid bool `json:"isValid"` - Payer string `json:"payer"` - InvalidReason string `json:"invalidReason,omitempty"` -} - -// SettleRequest sent to facilitator /settle endpoint -// as defined in the x402 specification section 7.2 -// Note: x402Version added at root level for facilitator compatibility -type SettleRequest struct { - X402Version int `json:"x402Version"` - PaymentPayload *PaymentPayload `json:"paymentPayload"` - PaymentRequirements *PaymentRequirement `json:"paymentRequirements"` -} - -// Config for X402Server -type Config struct { - // FacilitatorURL is the base URL of the x402 facilitator service - FacilitatorURL string - - CbApiKey *string - CbApiSecret *string - // PaymentTools maps tool names to their payment requirements - // Each tool can have multiple payment options - PaymentTools map[string][]PaymentRequirement - AllTools []string - - // VerifyOnly if true, only verifies but doesn't settle payments - VerifyOnly bool - - // Verbose if true, logs detailed request and payment information - Verbose bool -} diff --git a/mcpServer.service b/mcpServer.service index bb9a498..d9e183e 100644 --- a/mcpServer.service +++ b/mcpServer.service @@ -4,12 +4,13 @@ After=network.target [Service] User=mcpserver -Group=www-data +Group=mcpserver WorkingDirectory=/home/ubuntu/gitSources/mcpServer ExecStart=/home/ubuntu/gitSources/mcpServer/bin/main RuntimeDirectory=mcpserver +RuntimeDirectoryMode=0750 Restart=on-failure RestartSec=3s From e0aeb8521ad87477a23c5936c0d8db1a6cc8e5ec Mon Sep 17 00:00:00 2001 From: Nacorid Date: Wed, 29 Jul 2026 12:39:26 +0000 Subject: [PATCH 5/6] critical: fix infinite recursion --- internal/x402/x402.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/x402/x402.go b/internal/x402/x402.go index 8394c11..bce92c0 100644 --- a/internal/x402/x402.go +++ b/internal/x402/x402.go @@ -120,7 +120,6 @@ func (s *X402Server) AddPayableTool( handler mcp.ToolHandler, requirements ...x402.PaymentRequirements, ) { - s.AddPayableTool(tool, handler, requirements...) if len(requirements) == 0 { log.Fatalf("tool %s requires at least one payment requirement", tool.Name) From 252eede9955755a907f76521fd00e0c397259112 Mon Sep 17 00:00:00 2001 From: Nacorid Date: Wed, 29 Jul 2026 14:09:49 +0000 Subject: [PATCH 6/6] add x402 unit test add test workflow --- .forgejo/workflows/test.yaml | 36 ++++ go.mod | 2 +- go.sum | 4 +- internal/server/server.go | 400 ++++++++++++++++++----------------- internal/x402/x402.go | 38 ++-- internal/x402/x402_test.go | 206 ++++++++++++++++++ 6 files changed, 472 insertions(+), 214 deletions(-) create mode 100644 .forgejo/workflows/test.yaml create mode 100644 internal/x402/x402_test.go diff --git a/.forgejo/workflows/test.yaml b/.forgejo/workflows/test.yaml new file mode 100644 index 0000000..92eccfb --- /dev/null +++ b/.forgejo/workflows/test.yaml @@ -0,0 +1,36 @@ +name: CI/CD Go Verification + +on: + push: + branches: [ main, dev ] + pull_request: + branches: [ main ] + +jobs: + build_and_test: + runs-on: node20 + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.25' + cache: true + + - name: Install dependencies + run: go mod download + + - name: Check code formatting + run: | + if [ -n "$(gofmt -l .)" ]; then + echo "Code is not formatted correctly. Run 'gofmt -w .'" + exit 1 + fi + + - name: Build Application + run: go build -v ./... + + - name: Run Tests (with Mocked Boundaries) + run: go test -v -cover ./... \ No newline at end of file diff --git a/go.mod b/go.mod index 0384f0e..a7a0b6e 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/lib/pq v1.10.9 github.com/mark3labs/x402-go v0.12.1 github.com/modelcontextprotocol/go-sdk v1.6.1 - github.com/nacorid/logger v0.0.0-20251221003040-586da7a03fa0 + github.com/nacorid/logger v0.0.0-20251221013547-5fc8fd94ed80 github.com/ollama/ollama v0.12.10 github.com/x402-foundation/x402/go/v2 v2.19.0 golang.org/x/time v0.14.0 diff --git a/go.sum b/go.sum index 4135ce5..f7de63b 100644 --- a/go.sum +++ b/go.sum @@ -248,8 +248,8 @@ github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7B github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= -github.com/nacorid/logger v0.0.0-20251221003040-586da7a03fa0 h1:vKVU0JbEOiG7xI+UNwb8M72mo98uHCiPNyPUd9oBC9c= -github.com/nacorid/logger v0.0.0-20251221003040-586da7a03fa0/go.mod h1:hd9h4KEPU1f/Vv2qP0x/fZUwz2clFvOV/uIF0pkGo+s= +github.com/nacorid/logger v0.0.0-20251221013547-5fc8fd94ed80 h1:nZwGInvJNQzPSFYpstVR/TLumqBEZt0+yUXchdyBZiI= +github.com/nacorid/logger v0.0.0-20251221013547-5fc8fd94ed80/go.mod h1:hd9h4KEPU1f/Vv2qP0x/fZUwz2clFvOV/uIF0pkGo+s= github.com/ollama/ollama v0.12.10 h1:Dd0/SeCc+nv+FffxmWuQTGiRreib7Gt3nBhIIFuKwZA= github.com/ollama/ollama v0.12.10/go.mod h1:RUSmYywUWx/YZMaHrqtnT1ZChu+iSz/7jx2aO9+Mgfg= github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= diff --git a/internal/server/server.go b/internal/server/server.go index 8132317..b88ab62 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -20,6 +20,7 @@ import ( "github.com/nacorid/naco-api/internal/utils" "github.com/nacorid/naco-api/internal/x402" x402v2 "github.com/x402-foundation/x402/go/v2" + x402http "github.com/x402-foundation/x402/go/v2/http/nethttp" "github.com/x402-foundation/x402/go/v2/types" ) @@ -58,69 +59,30 @@ func New(ctx context.Context, db *store.Queries, ap *activitypub.APClient, bsky cfg.X402FacilitatorURL, auth, toolChain, + func(w http.ResponseWriter, r *http.Request, resp *x402v2.SettleResponse) { + requirements, ok := x402http.RequirementsFromContext(r.Context()) + if !ok { + log.WarnWithContext(r.Context(), "failed to extract payment requirements from context during settlement hook") + return + } + + amount := requirements.Amount + asset := requirements.Asset + to := requirements.PayTo + _ = settleHook(r.Context(), db, resp.Transaction, amount, asset, string(resp.Network), to, resp.Payer) + }, ) if err != nil { log.FatalWithContext(ctx, "failed to create x402 server", "error", err) } srv.OnAfterSettle(func(settleCtx x402v2.SettleResultContext) error { - var amount, to, from, asset string + amount := settleCtx.Requirements.GetAmount() + asset := settleCtx.Requirements.GetAsset() + to := settleCtx.Requirements.GetPayTo() + from := settleCtx.Result.Payer - var payloadMap map[string]any - if err := json.Unmarshal(settleCtx.PayloadBytes, &payloadMap); err == nil { - if payload, ok := payloadMap["payload"].(map[string]interface{}); ok { - if auth, ok := payload["Authorization"].(map[string]interface{}); ok { - if value, ok := auth["Value"].(string); ok { - amount = value - } - if value, ok := auth["To"].(string); ok { - to = value - } - if value, ok := auth["From"].(string); ok { - from = value - } - } - } - } - - var reqMap map[string]interface{} - if err := json.Unmarshal(settleCtx.RequirementsBytes, &reqMap); err == nil { - if value, ok := reqMap["asset"].(string); ok { - asset = value - } - } - - ret, err := db.SaveTransaction(settleCtx.Ctx, store.SaveTransactionParams{ - X402TransactionHash: settleCtx.Result.Transaction, - Amount: amount, - Asset: asset, - Network: string(settleCtx.Result.Network), - Recipient: to, - Sender: from, - }) - - var id int64 = 0 - if len(ret) > 0 { - id = ret[0].ID - } - - fields := log.Fields{ - "transaction_id": id, - "X402_transaction_hash": settleCtx.Result.Transaction, - "amount": amount, - "asset": asset, - "network": settleCtx.Result.Network, - "recipient": to, - "sender": from, - } - - if err != nil { - log.WithFields(fields).ErrorWithContext(settleCtx.Ctx, "Failed to save transaction", "value", ret, "error", err) - return err - } - - log.WithFields(fields).DebugWithContext(settleCtx.Ctx, "Saved transaction") - return nil + return settleHook(settleCtx.Ctx, db, settleCtx.Result.Transaction, amount, asset, string(settleCtx.Result.Network), to, from) }) s := &Server{ @@ -140,6 +102,43 @@ func New(ctx context.Context, db *store.Queries, ap *activitypub.APClient, bsky } func (s *Server) registerTools() { + extra := map[string]any{ + "OutputSchema": Schema{ + Input: InputSchema{ + Type: "HTTP", + Method: "POST", + Discoverable: true, + BodyFields: map[string]any{ + "actor": Bodyfield{ + Type: "string", + Description: "Did or handle of an actor", + Required: true, + }, + "rkey": Bodyfield{ + Type: "string", + Description: "Record key of the post", + Required: true, + }, + "collection": Bodyfield{ + Type: "string", + Description: "Collection the post belongs to. Defaults to 'app.bsky.feed.post'", + Required: false, + }, + }, + }, + Output: map[string]any{ + "actor": Bodyfield{ + Type: "string", + Description: "Did of the actor", + }, + "post": Bodyfield{ + Type: "object", + Description: "The post object. Includes the post content 'content', like 'likes', quote 'quotes', replies 'replies' and repost 'reposts' count", + }, + }, + }, + } + requirement := types.PaymentRequirements{ Scheme: "exact", Network: "eip155:8453", @@ -147,42 +146,7 @@ func (s *Server) registerTools() { Amount: "10000", PayTo: s.x402Wallet, MaxTimeoutSeconds: 60, - Extra: map[string]any{ - "OutputSchema": Schema{ - Input: InputSchema{ - Type: "HTTP", - Method: "POST", - Discoverable: true, - BodyFields: map[string]any{ - "actor": Bodyfield{ - Type: "string", - Description: "Did or handle of an actor", - Required: true, - }, - "rkey": Bodyfield{ - Type: "string", - Description: "Record key of the post", - Required: true, - }, - "collection": Bodyfield{ - Type: "string", - Description: "Collection the post belongs to. Defaults to 'app.bsky.feed.post'", - Required: false, - }, - }, - }, - Output: map[string]any{ - "actor": Bodyfield{ - Type: "string", - Description: "Did of the actor", - }, - "post": Bodyfield{ - Type: "object", - Description: "The post object. Includes the post content 'content', like 'likes', quote 'quotes', replies 'replies' and repost 'reposts' count", - }, - }, - }, - }, + Extra: extra, } requirements := []types.PaymentRequirements{requirement} s.mcpHandler.AddPayableTool( @@ -195,6 +159,38 @@ func (s *Server) registerTools() { requirements..., ) + extra = map[string]any{ + "OutputSchema": Schema{ + Input: InputSchema{ + Type: "HTTP", + Method: "POST", + Discoverable: true, + BodyFields: map[string]any{ + "actor": Bodyfield{ + Type: "string", + Description: "Did or handle of an actor", + Required: true, + }, + "n": Bodyfield{ + Type: "integer", + Description: "Number of posts to retrieve, maximum 50", + Required: true, + }, + }, + }, + Output: map[string]any{ + "actor": Bodyfield{ + Type: "string", + Description: "Did of an actor", + }, + "posts": Bodyfield{ + Type: "array", + Description: "Array of post objects. Each includes the post content 'content', like 'likes', quote 'quotes', replies 'replies' and repost 'reposts' count", + }, + }, + }, + } + requirement = types.PaymentRequirements{ Scheme: "exact", Network: "eip155:8453", @@ -202,37 +198,7 @@ func (s *Server) registerTools() { Amount: "100000", PayTo: s.x402Wallet, MaxTimeoutSeconds: 60, - Extra: map[string]any{ - "OutputSchema": Schema{ - Input: InputSchema{ - Type: "HTTP", - Method: "POST", - Discoverable: true, - BodyFields: map[string]any{ - "actor": Bodyfield{ - Type: "string", - Description: "Did or handle of an actor", - Required: true, - }, - "n": Bodyfield{ - Type: "integer", - Description: "Number of posts to retrieve, maximum 50", - Required: true, - }, - }, - }, - Output: map[string]any{ - "actor": Bodyfield{ - Type: "string", - Description: "Did of an actor", - }, - "posts": Bodyfield{ - Type: "array", - Description: "Array of post objects. Each includes the post content 'content', like 'likes', quote 'quotes', replies 'replies' and repost 'reposts' count", - }, - }, - }, - }, + Extra: extra, } requirements = []types.PaymentRequirements{requirement} @@ -247,6 +213,48 @@ func (s *Server) registerTools() { requirements..., ) + extra = map[string]any{ + "OutputSchema": Schema{ + Input: InputSchema{ + Type: "HTTP", + Method: "POST", + Discoverable: true, + BodyFields: map[string]any{ + "actor": Bodyfield{ + Type: "string", + Description: "Did or handle of an actor", + Required: true, + }, + "start": Bodyfield{ + Type: "string", + Description: "Start time in RFC3339 format", + Required: true, + }, + "end": Bodyfield{ + Type: "string", + Description: "End time in RFC3339 format", + Required: true, + }, + "n": Bodyfield{ + Type: "integer", + Description: "Maximum number of posts to retrieve, maximum 50", + Required: true, + }, + }, + }, + Output: map[string]any{ + "actor": Bodyfield{ + Type: "string", + Description: "Did of an actor", + }, + "posts": Bodyfield{ + Type: "array", + Description: "Array of post objects. Each includes the post content 'content', like 'likes', quote 'quotes', replies 'replies' and repost 'reposts' count", + }, + }, + }, + } + requirement = types.PaymentRequirements{ Scheme: "exact", Network: "eip155:8453", @@ -254,47 +262,7 @@ func (s *Server) registerTools() { Amount: "100000", PayTo: s.x402Wallet, MaxTimeoutSeconds: 60, - Extra: map[string]any{ - "OutputSchema": Schema{ - Input: InputSchema{ - Type: "HTTP", - Method: "POST", - Discoverable: true, - BodyFields: map[string]any{ - "actor": Bodyfield{ - Type: "string", - Description: "Did or handle of an actor", - Required: true, - }, - "start": Bodyfield{ - Type: "string", - Description: "Start time in RFC3339 format", - Required: true, - }, - "end": Bodyfield{ - Type: "string", - Description: "End time in RFC3339 format", - Required: true, - }, - "n": Bodyfield{ - Type: "integer", - Description: "Maximum number of posts to retrieve, maximum 50", - Required: true, - }, - }, - }, - Output: map[string]any{ - "actor": Bodyfield{ - Type: "string", - Description: "Did of an actor", - }, - "posts": Bodyfield{ - Type: "array", - Description: "Array of post objects. Each includes the post content 'content', like 'likes', quote 'quotes', replies 'replies' and repost 'reposts' count", - }, - }, - }, - }, + Extra: extra, } requirements = []types.PaymentRequirements{requirement} @@ -309,6 +277,41 @@ func (s *Server) registerTools() { requirements..., ) + extra = map[string]any{ + "OutputSchema": Schema{ + Input: InputSchema{ + Type: "HTTP", + Method: "POST", + Discoverable: true, + BodyFields: map[string]any{ + "actor": Bodyfield{ + Type: "string", + Description: "Did or handle of an actor", + Required: true, + }, + }, + }, + Output: map[string]any{ + "PersonaEmbedding": Bodyfield{ + Type: "array", + Description: "Embedding vector representing the actor's persona", + }, + "TopKeywords": Bodyfield{ + Type: "array", + Description: "Top words used by the actor", + }, + "StyleSummary": Bodyfield{ + Type: "string", + Description: "Summary of the actor's writing style", + }, + "PostCount": Bodyfield{ + Type: "integer", + Description: "Number of posts analyzed", + }, + }, + }, + } + requirement = types.PaymentRequirements{ Scheme: "exact", Network: "eip155:8453", @@ -316,40 +319,7 @@ func (s *Server) registerTools() { Amount: "100000", PayTo: s.x402Wallet, MaxTimeoutSeconds: 60, - Extra: map[string]any{ - "OutputSchema": Schema{ - Input: InputSchema{ - Type: "HTTP", - Method: "POST", - Discoverable: true, - BodyFields: map[string]any{ - "actor": Bodyfield{ - Type: "string", - Description: "Did or handle of an actor", - Required: true, - }, - }, - }, - Output: map[string]any{ - "PersonaEmbedding": Bodyfield{ - Type: "array", - Description: "Embedding vector representing the actor's persona", - }, - "TopKeywords": Bodyfield{ - Type: "array", - Description: "Top words used by the actor", - }, - "StyleSummary": Bodyfield{ - Type: "string", - Description: "Summary of the actor's writing style", - }, - "PostCount": Bodyfield{ - Type: "integer", - Description: "Number of posts analyzed", - }, - }, - }, - }, + Extra: extra, } requirements = []types.PaymentRequirements{requirement} @@ -729,6 +699,40 @@ func determinePlatform(author string) (platform, error) { return -1, fmt.Errorf("unknown author format: %s", author) } +func settleHook(ctx context.Context, db *store.Queries, transaction, amount, asset, network, to, from string) error { + ret, err := db.SaveTransaction(ctx, store.SaveTransactionParams{ + X402TransactionHash: transaction, + Amount: amount, + Asset: asset, + Network: network, + Recipient: to, + Sender: from, + }) + + var id int64 = 0 + if len(ret) > 0 { + id = ret[0].ID + } + + fields := log.Fields{ + "transaction_id": id, + "X402_transaction_hash": transaction, + "amount": amount, + "asset": asset, + "network": network, + "recipient": to, + "sender": from, + } + + if err != nil { + log.WithFields(fields).ErrorWithContext(ctx, "Failed to save transaction", "value", ret, "error", err) + return err + } + + log.WithFields(fields).DebugWithContext(ctx, "Saved transaction") + return nil +} + func (s *Server) Start() error { return s.mcpHandler.Start(s.listenAddress, s.permissions) } diff --git a/internal/x402/x402.go b/internal/x402/x402.go index bce92c0..6a47b16 100644 --- a/internal/x402/x402.go +++ b/internal/x402/x402.go @@ -37,6 +37,7 @@ type X402Server struct { mcpServer *mcp.Server x402ResourceServer *x402.X402ResourceServer facilitatorClient *x402http.HTTPFacilitatorClient + httpSettlementHook func(w http.ResponseWriter, r *http.Request, resp *x402.SettleResponse) mux *http.ServeMux name string @@ -45,7 +46,9 @@ type X402Server struct { tools []ToolInfo } -func NewX402Server(name, version string, facilitatorURL string, auth x402http.AuthProvider, mws X402Middlewares) (*X402Server, error) { +const VERSION1 = "v1/" + +func NewX402Server(name, version string, facilitatorURL string, auth x402http.AuthProvider, mws X402Middlewares, settleHook func(w http.ResponseWriter, r *http.Request, resp *x402.SettleResponse)) (*X402Server, error) { mcpServer := mcp.NewServer( &mcp.Implementation{ Name: name, @@ -75,6 +78,7 @@ func NewX402Server(name, version string, facilitatorURL string, auth x402http.Au mcpServer: mcpServer, x402ResourceServer: resourceServer, facilitatorClient: facilitator, + httpSettlementHook: settleHook, mux: http.NewServeMux(), name: name, version: version, @@ -112,7 +116,7 @@ func (s *X402Server) AddTool(tool mcp.Tool, handler mcp.ToolHandler) { restHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { s.handleRESTCall(w, r, tool.Name, handler) }) - s.mux.Handle("POST /v1/"+tool.Name, s.middlewareChain(restHandler)) + s.mux.Handle("POST /"+VERSION1+tool.Name, s.middlewareChain(restHandler)) } func (s *X402Server) AddPayableTool( @@ -157,7 +161,7 @@ func (s *X402Server) AddPayableTool( paymentMiddleware := nethttp.X402Payment(nethttp.Config{ Routes: x402http.RoutesConfig{ - "POST /" + tool.Name: { + "POST /" + VERSION1 + tool.Name: { Accepts: options, Description: tool.Description, }, @@ -166,13 +170,14 @@ func (s *X402Server) AddPayableTool( Schemes: []nethttp.SchemeConfig{ {Network: "eip155:8453", Server: evm.NewExactEvmScheme()}, }, + SettlementHandler: s.httpSettlementHook, }) restHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { s.handleRESTCall(w, r, tool.Name, handler) }) chained := s.middlewareChain(restHandler) - s.mux.Handle("POST /"+tool.Name, paymentMiddleware(chained)) + s.mux.Handle("POST /"+VERSION1+tool.Name, paymentMiddleware(chained)) } func (s *X402Server) handleRESTCall(w http.ResponseWriter, r *http.Request, toolName string, handler mcp.ToolHandler) { @@ -182,6 +187,8 @@ func (s *X402Server) handleRESTCall(w http.ResponseWriter, r *http.Request, tool return } + log.DebugWithContext(r.Context(), "RESTful API request.", "RemoteAddr", r.RemoteAddr, "X-Forwarded-For", r.Header.Get("X-Forwarded-For")) + callReq := &mcp.CallToolRequest{ Params: &mcp.CallToolParamsRaw{ Name: toolName, @@ -215,15 +222,11 @@ func (s *X402Server) OnAfterSettle(hook func(ctx x402.SettleResultContext) error s.x402ResourceServer.OnAfterSettle(hook) } -func noOpMiddleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - next.ServeHTTP(w, r) - }) -} - func (s *X402Server) serveOpenAPI(w http.ResponseWriter, r *http.Request) { paths := make(map[string]any) + log.DebugWithContext(r.Context(), "OpenAPI Spec requested.", "RemoteAddr", r.RemoteAddr, "X-Forwarded-For", r.Header.Get("X-Forwarded-For")) + for _, info := range s.tools { inputSchema := parseSchema(info.Tool.InputSchema) outputSchema := parseSchema(info.Tool.OutputSchema) @@ -320,25 +323,34 @@ func (s *X402Server) Start(addr string, permissions os.FileMode) error { func toJSONSchema(val any) json.RawMessage { if val == nil { - return nil + return json.RawMessage(`{"type": "object"}`) } switch v := val.(type) { case json.RawMessage: + if len(v) == 0 { + return json.RawMessage(`{"type": "object"}`) + } return v case []byte: + if len(v) == 0 { + return json.RawMessage(`{"type": "object"}`) + } return json.RawMessage(v) case string: + if v == "" { + return json.RawMessage(`{"type": "object"}`) + } return json.RawMessage(v) } t := reflect.TypeOf(val) schema, err := jsonschema.ForType(t, &jsonschema.ForOptions{}) if err != nil { - return nil + return json.RawMessage(`{"type": "object"}`) } bytes, err := json.Marshal(schema) if err != nil { - return nil + return json.RawMessage(`{"type": "object"}`) } return bytes } diff --git a/internal/x402/x402_test.go b/internal/x402/x402_test.go new file mode 100644 index 0000000..ba6e84a --- /dev/null +++ b/internal/x402/x402_test.go @@ -0,0 +1,206 @@ +package x402 + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/modelcontextprotocol/go-sdk/mcp" + x402 "github.com/x402-foundation/x402/go/v2" + x402http "github.com/x402-foundation/x402/go/v2/http" +) + +type DummyAuthProvider struct{} + +func (d *DummyAuthProvider) GetAuthHeaders(ctx context.Context) (x402http.AuthHeaders, error) { + return x402http.AuthHeaders{}, nil +} + +func TestX402Server_HTTPBoundaries(t *testing.T) { + mockFacilitator := setupMockFacilitator() + defer mockFacilitator.Close() + + srv, err := NewX402Server( + "Test Agent API", + "1.0.0", + mockFacilitator.URL, + &DummyAuthProvider{}, + X402Middlewares{}, + func(w http.ResponseWriter, r *http.Request, resp *x402.SettleResponse) {}, + ) + if err != nil { + t.Fatalf("failed to create server: %v", err) + } + + srv.AddPayableTool( + mcp.Tool{ + Name: "getTestPost", + Description: "Gated test endpoint", + }, + func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) { + return &mcp.CallToolResult{ + Content: []mcp.Content{&mcp.TextContent{Text: `{"status": "success"}`}}, + }, nil + }, + x402.PaymentRequirements{ + Scheme: "exact", + Network: "eip155:8453", + Asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + Amount: "1000", + PayTo: "0x0000000000000000000000000000000000000000", + }, + ) + + reqRec := httptest.NewRecorder() + reqHealth, _ := http.NewRequest("GET", "/healthz", nil) + srv.mux.ServeHTTP(reqRec, reqHealth) + if reqRec.Code != http.StatusOK { + t.Errorf("expected healthz status 200, got %d", reqRec.Code) + } + + reqRecOpenAPI := httptest.NewRecorder() + reqOpenAPI, _ := http.NewRequest("GET", "/openapi.json", nil) + srv.mux.ServeHTTP(reqRecOpenAPI, reqOpenAPI) + if reqRecOpenAPI.Code != http.StatusOK { + t.Errorf("expected OpenAPI status 200, got %d", reqRecOpenAPI.Code) + } + if !strings.Contains(reqRecOpenAPI.Body.String(), "getTestPost") { + t.Error("OpenAPI spec did not render registered tool definitions") + } + + reqRecPayable := httptest.NewRecorder() + reqPayable, _ := http.NewRequest("POST", "/v1/getTestPost", strings.NewReader(`{}`)) + srv.mux.ServeHTTP(reqRecPayable, reqPayable) + + if reqRecPayable.Code != http.StatusPaymentRequired { + t.Errorf("expected status 402 (Payment Required), got %d", reqRecPayable.Code) + } + + payHeader := reqRecPayable.Header().Get("PAYMENT-REQUIRED") + if payHeader == "" { + t.Error("missing PAYMENT-REQUIRED response header on 402 challenge") + } +} + +func TestX402Server_MCPBoundaries(t *testing.T) { + ctx := context.Background() + + serverTransport, clientTransport := mcp.NewInMemoryTransports() + + mockFacilitator := setupMockFacilitator() + defer mockFacilitator.Close() + + srv, err := NewX402Server( + "Test Agent API", + "1.0.0", + mockFacilitator.URL, + &DummyAuthProvider{}, + X402Middlewares{}, + func(w http.ResponseWriter, r *http.Request, resp *x402.SettleResponse) {}, + ) + if err != nil { + t.Fatalf("failed to create server: %v", err) + } + + srv.AddPayableTool( + mcp.Tool{ + Name: "getTestPost", + Description: "Gated test endpoint", + }, + func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) { + return &mcp.CallToolResult{ + Content: []mcp.Content{&mcp.TextContent{Text: `{"status": "success"}`}}, + }, nil + }, + x402.PaymentRequirements{ + Scheme: "exact", + Network: "eip155:8453", + Asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + Amount: "1000", + PayTo: "0x0000000000000000000000000000000000000000", + }, + ) + + serverSession, err := srv.mcpServer.Connect(ctx, serverTransport, nil) + if err != nil { + t.Fatalf("failed to connect server session: %v", err) + } + defer serverSession.Close() + + client := mcp.NewClient(&mcp.Implementation{Name: "test-client", Version: "1.0.0"}, nil) + clientSession, err := client.Connect(ctx, clientTransport, nil) + if err != nil { + t.Fatalf("failed to connect client session: %v", err) + } + defer clientSession.Close() + + toolsResponse, err := clientSession.ListTools(ctx, nil) + if err != nil { + t.Fatalf("failed to list tools over MCP: %v", err) + } + + found := false + for _, tool := range toolsResponse.Tools { + if tool.Name == "getTestPost" { + found = true + break + } + } + if !found { + t.Error("expected tool 'getTestPost' to be discovered by the MCP client, but it was missing") + } + + result, err := clientSession.CallTool(ctx, &mcp.CallToolParams{ + Name: "getTestPost", + Arguments: json.RawMessage(`{}`), + }) + + if err != nil { + t.Errorf("Unexpected protocol-level error: %v", err) + } else { + if !result.IsError { + t.Error("expected CallTool to return a payment challenge, but it returned a successful, free result") + } else { + t.Logf("Intercepted expected application-level payment challenge.") + t.Logf("Content: %v", result.Content) + t.Logf("Meta fields: %v", result.Meta) + } + } +} + +func setupMockFacilitator() *httptest.Server { + s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + switch { + case r.Method == "GET" && r.URL.Path == "/supported": + _, _ = w.Write([]byte(`{ + "kinds": [ + { + "x402Version": 2, + "scheme": "exact", + "network": "eip155:8453" + } + ] + }`)) + case r.Method == "POST" && r.URL.Path == "/verify": + _, _ = w.Write([]byte(`{ + "isValid": true, + "invalidReason": "" + }`)) + case r.Method == "POST" && r.URL.Path == "/settle": + _, _ = w.Write([]byte(`{ + "success": true, + "transaction": "0x1234abcd", + "network": "eip155:8453", + "payer": "0xabcd1234" + }`)) + default: + w.WriteHeader(http.StatusNotFound) + } + })) + return s +}