This is an automated email from the ASF dual-hosted git repository.
alexstocks pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/dubbo-go-pixiu.git
The following commit(s) were added to refs/heads/develop by this push:
new edaa3f62 Feat(MCP): Add MCP Server Filter for Model Context Protocol
support (#702)
edaa3f62 is described below
commit edaa3f620b1089976b40f4de9640f994d9076317
Author: Zerui Yang <[email protected]>
AuthorDate: Sat Aug 16 23:30:03 2025 +0800
Feat(MCP): Add MCP Server Filter for Model Context Protocol support (#702)
* feat: Add MCP executor filter with configuration and request handling
* feat: Implement MCP server filter with configuration, request handling,
and tool registry
* feat: Enhance MCP server filter with new context management and tool
registry features
* feat: Enhance MCP server filter with new context management and tool
registry features
* feat: Simplify MCP server configuration and response handling with
optimized structures and error management
* fix: fix some problems from copilot
* chore: update dependencies in go.mod for MCP integration and improvements
* style: add missing newlines in context and filter files
* chore: update dependencies for MCP integration
* chore: update dependencies for MCP integration
* refactor: update MCP context to use explicit contexthttp package and
improve method naming
* refactor: delete useless method and fix mistakes about function name
---
go.mod | 14 +-
go.sum | 35 ++-
pkg/common/constant/key.go | 2 +
pkg/filter/mcp/mcpserver/config.go | 217 +++++++++++++
pkg/filter/mcp/mcpserver/context.go | 111 +++++++
pkg/filter/mcp/mcpserver/filter.go | 237 +++++++++++++++
pkg/filter/mcp/mcpserver/handlers.go | 571 +++++++++++++++++++++++++++++++++++
pkg/filter/mcp/mcpserver/plugin.go | 50 +++
pkg/filter/mcp/mcpserver/registry.go | 359 ++++++++++++++++++++++
pkg/filter/mcp/mcpserver/response.go | 151 +++++++++
pkg/pluginregistry/registry.go | 1 +
11 files changed, 1739 insertions(+), 9 deletions(-)
diff --git a/go.mod b/go.mod
index 2cde68b6..29b28814 100644
--- a/go.mod
+++ b/go.mod
@@ -32,13 +32,15 @@ require (
github.com/golang/protobuf v1.5.4
github.com/jhump/protoreflect v1.17.0
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible
+ github.com/lestrrat-go/jwx/v2 v2.1.6
+ github.com/mark3labs/mcp-go v0.32.0
github.com/mitchellh/mapstructure v1.5.0
github.com/nacos-group/nacos-sdk-go v1.1.3
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.14.0
github.com/prometheus/common v0.37.0
github.com/smartystreets/goconvey v1.7.2
- github.com/spf13/cast v1.5.0
+ github.com/spf13/cast v1.7.1
github.com/spf13/cobra v1.5.0
github.com/spf13/viper v1.7.1
github.com/stretchr/testify v1.10.0
@@ -94,6 +96,7 @@ require (
github.com/coreos/go-semver v0.3.0 // indirect
github.com/coreos/go-systemd/v22 v22.3.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
github.com/dlclark/regexp2 v1.7.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/eapache/go-resiliency v1.7.0 // indirect
@@ -115,7 +118,7 @@ require (
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.20.0 // indirect
github.com/go-resty/resty/v2 v2.7.0 // indirect
- github.com/goccy/go-json v0.10.2 // indirect
+ github.com/goccy/go-json v0.10.3 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/uuid v1.6.0 // indirect
@@ -148,6 +151,11 @@ require (
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/knadh/koanf v1.5.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
+ github.com/lestrrat-go/blackmagic v1.0.3 // indirect
+ github.com/lestrrat-go/httpcc v1.0.1 // indirect
+ github.com/lestrrat-go/httprc v1.0.6 // indirect
+ github.com/lestrrat-go/iter v1.0.2 // indirect
+ github.com/lestrrat-go/option v1.0.1 // indirect
github.com/lestrrat-go/strftime v1.1.1 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 //
indirect
github.com/magiconair/properties v1.8.5 // indirect
@@ -178,6 +186,7 @@ require (
github.com/prometheus/procfs v0.8.0 // indirect
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 //
indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
+ github.com/segmentio/asm v1.2.0 // indirect
github.com/shirou/gopsutil/v3 v3.22.2 // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
github.com/smartystreets/assertions v1.2.0 // indirect
@@ -193,6 +202,7 @@ require (
github.com/uber/jaeger-lib v2.4.1+incompatible // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
github.com/wasmerio/wasmer-go v1.0.3 // indirect
+ github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
github.com/yusufpapurcu/wmi v1.2.2 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.5.7 // indirect
go.opentelemetry.io/contrib/propagators/b3 v1.10.0 // indirect
diff --git a/go.sum b/go.sum
index b83be784..6a703ba0 100644
--- a/go.sum
+++ b/go.sum
@@ -556,6 +556,8 @@ github.com/creasty/defaults v1.5.2/go.mod
h1:FPZ+Y0WNrbqOVw+c6av63eyHUAl6pMHZwqL
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/decred/dcrd/dcrec/secp256k1/v4 v4.4.0
h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
+github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod
h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
github.com/dgrijalva/jwt-go v3.2.0+incompatible
h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod
h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod
h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
@@ -632,8 +634,8 @@ github.com/franela/goblin
v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVB
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod
h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
github.com/frankban/quicktest v1.10.0/go.mod
h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y=
github.com/frankban/quicktest v1.11.3/go.mod
h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
-github.com/frankban/quicktest v1.14.3
h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
-github.com/frankban/quicktest v1.14.3/go.mod
h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
+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.4.7/go.mod
h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod
h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.6.0
h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
@@ -704,8 +706,8 @@ github.com/go-stack/stack v1.8.0/go.mod
h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod
h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod
h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/go-test/deep v1.0.2/go.mod
h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
-github.com/goccy/go-json v0.10.2
h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
-github.com/goccy/go-json v0.10.2/go.mod
h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/goccy/go-json v0.10.3
h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
+github.com/goccy/go-json v0.10.3/go.mod
h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/godbus/dbus/v5 v5.0.3/go.mod
h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.0.4/go.mod
h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/googleapis v1.1.0/go.mod
h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
@@ -1035,10 +1037,22 @@ github.com/kr/text v0.2.0/go.mod
h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.2/go.mod
h1:kUaIbLZWttglzwNuG0pgsh5vuV6u2YcGBYz1hIPjtOQ=
github.com/leodido/go-urn v1.4.0
h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod
h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
+github.com/lestrrat-go/blackmagic v1.0.3
h1:94HXkVLxkZO9vJI/w2u1T0DAoprShFd13xtnSINtDWs=
+github.com/lestrrat-go/blackmagic v1.0.3/go.mod
h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw=
github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc
h1:RKf14vYWi2ttpEmkA4aQ3j4u9dStX2t4M8UM6qqNsG8=
github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc/go.mod
h1:kopuH9ugFRkIXf3YoqHKyrJ9YfUFsckUU9S7B+XP+is=
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible
h1:Y6sqxHMyB1D2YSzWkLibYKgg+SwmyFU9dF2hn6MdTj4=
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible/go.mod
h1:ZQnN8lSECaebrkQytbHj4xNgtg8CR7RYXnPok8e0EHA=
+github.com/lestrrat-go/httpcc v1.0.1
h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
+github.com/lestrrat-go/httpcc v1.0.1/go.mod
h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
+github.com/lestrrat-go/httprc v1.0.6
h1:qgmgIRhpvBqexMJjA/PmwSvhNk679oqD1RbovdCGW8k=
+github.com/lestrrat-go/httprc v1.0.6/go.mod
h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
+github.com/lestrrat-go/iter v1.0.2
h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
+github.com/lestrrat-go/iter v1.0.2/go.mod
h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
+github.com/lestrrat-go/jwx/v2 v2.1.6
h1:hxM1gfDILk/l5ylers6BX/Eq1m/pnxe9NBwW6lVfecA=
+github.com/lestrrat-go/jwx/v2 v2.1.6/go.mod
h1:Y722kU5r/8mV7fYDifjug0r8FK8mZdw0K0GpJw/l8pU=
+github.com/lestrrat-go/option v1.0.1
h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
+github.com/lestrrat-go/option v1.0.1/go.mod
h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/lestrrat-go/strftime v1.1.1
h1:zgf8QCsgj27GlKBy3SU9/8MMgegZ8UCzlCyHYrUF0QU=
github.com/lestrrat-go/strftime v1.1.1/go.mod
h1:YDrzHJAODYQ+xxvrn5SG01uFIQAeDTzpxNVppCz7Nmw=
github.com/lestrrat/go-envload v0.0.0-20180220120943-6ed08b54a570/go.mod
h1:BLt8L9ld7wVsvEWQbuLrUZnCMnUmLZ+CGDzKtclrTlE=
@@ -1058,6 +1072,8 @@ github.com/mailru/easyjson
v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod
h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.6
h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
github.com/mailru/easyjson v0.7.6/go.mod
h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
+github.com/mark3labs/mcp-go v0.32.0
h1:fgwmbfL2gbd67obg57OfV2Dnrhs1HtSdlY/i5fn7MU8=
+github.com/mark3labs/mcp-go v0.32.0/go.mod
h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4=
github.com/mattn/go-colorable v0.0.9/go.mod
h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.4/go.mod
h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6/go.mod
h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
@@ -1258,8 +1274,9 @@ github.com/rogpeppe/fastuuid
v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6So
github.com/rogpeppe/fastuuid v1.2.0/go.mod
h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod
h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1/go.mod
h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
-github.com/rogpeppe/go-internal v1.8.0
h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod
h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
+github.com/rogpeppe/go-internal v1.9.0
h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
+github.com/rogpeppe/go-internal v1.9.0/go.mod
h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/russross/blackfriday/v2 v2.0.1/go.mod
h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0/go.mod
h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/rwtodd/Go.Sed v0.0.0-20210816025313-55464686f9ef/go.mod
h1:8AEUvGVi2uQ5b24BIhcr0GCcpd/RNAFWaN2CJFrWIIQ=
@@ -1268,6 +1285,8 @@ github.com/ryanuber/columnize v2.1.0+incompatible/go.mod
h1:sm1tb6uqfes/u+d4ooFo
github.com/ryanuber/go-glob v1.0.0/go.mod
h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod
h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod
h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
+github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
+github.com/segmentio/asm v1.2.0/go.mod
h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/shirou/gopsutil v3.20.11+incompatible/go.mod
h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/shirou/gopsutil/v3 v3.21.6/go.mod
h1:JfVbDpIBLVzT8oKbvMg9P3wEIMDDpVn+LwHTKj0ST88=
github.com/shirou/gopsutil/v3 v3.22.2
h1:wCrArWFkHYIdDxx/FSfF5RB4dpJYW6t7rcp3+zL8uks=
@@ -1299,8 +1318,8 @@ github.com/spf13/afero v1.9.2/go.mod
h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcD
github.com/spf13/afero v1.10.0 h1:EaGW2JJh15aKOejeuJ+wpFSHnbd7GE6Wvp3TsNhb6LY=
github.com/spf13/afero v1.10.0/go.mod
h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
github.com/spf13/cast v1.3.0/go.mod
h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
-github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
-github.com/spf13/cast v1.5.0/go.mod
h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
+github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
+github.com/spf13/cast v1.7.1/go.mod
h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v0.0.3/go.mod
h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/cobra v1.1.1/go.mod
h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI=
github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU=
@@ -1382,6 +1401,8 @@ github.com/xdg-go/scram v1.0.2/go.mod
h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+
github.com/xdg-go/stringprep v1.0.2/go.mod
h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2
h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod
h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
+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.25/go.mod
h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod
h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod
h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
diff --git a/pkg/common/constant/key.go b/pkg/common/constant/key.go
index bc60701f..fd542610 100644
--- a/pkg/common/constant/key.go
+++ b/pkg/common/constant/key.go
@@ -57,6 +57,8 @@ const (
LLMProxyFilter = "dgp.filter.llm.proxy"
LLMTokenizerFilter = "dgp.filter.llm.tokenizer"
+
+ MCPServerFilter = "dgp.filter.mcp.mcpserver"
)
const (
diff --git a/pkg/filter/mcp/mcpserver/config.go
b/pkg/filter/mcp/mcpserver/config.go
new file mode 100644
index 00000000..28c97b4e
--- /dev/null
+++ b/pkg/filter/mcp/mcpserver/config.go
@@ -0,0 +1,217 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package mcpserver
+
+import (
+ "regexp"
+)
+
+type (
+ // Config MCP Server Filter configuration
+ Config struct {
+ ServerInfo ServerInfo `yaml:"server_info"
json:"server_info"`
+ Endpoint string `yaml:"endpoint"
json:"endpoint" default:"/mcp"`
+ Tools []ToolConfig
`yaml:"tools,omitempty" json:"tools,omitempty"`
+ Resources []ResourceConfig
`yaml:"resources,omitempty" json:"resources,omitempty"`
+ ResourceTemplates []ResourceTemplateConfig
`yaml:"resource_templates,omitempty" json:"resource_templates,omitempty"`
+ Prompts []PromptConfig
`yaml:"prompts,omitempty" json:"prompts,omitempty"`
+ }
+
+ // ServerInfo server information
+ ServerInfo struct {
+ Name string `yaml:"name" json:"name" default:"Pixiu MCP
Server"`
+ Version string `yaml:"version" json:"version"
default:"1.0.0"`
+ Description string `yaml:"description,omitempty"
json:"description,omitempty" default:"MCP Server powered by Apache
Dubbo-go-pixiu"`
+ Instructions string `yaml:"instructions,omitempty"
json:"instructions,omitempty" default:"Use the provided tools to interact with
backend services."`
+ }
+
+ // ToolConfig tool configuration
+ ToolConfig struct {
+ Name string `yaml:"name" json:"name"`
+ Description string `yaml:"description"
json:"description"`
+ Cluster string `yaml:"cluster" json:"cluster"`
+ Request RequestConfig `yaml:"request" json:"request"`
+ Args []ArgConfig `yaml:"args,omitempty"
json:"args,omitempty"`
+ }
+
+ // RequestConfig request configuration
+ RequestConfig struct {
+ Method string `yaml:"method" json:"method"
default:"GET"`
+ Path string `yaml:"path" json:"path"`
+ Headers map[string]string `yaml:"headers,omitempty"
json:"headers,omitempty"`
+ Timeout string `yaml:"timeout,omitempty"
json:"timeout,omitempty" default:"30s"`
+ }
+
+ // ArgConfig parameter configuration (simplified)
+ ArgConfig struct {
+ Name string `yaml:"name" json:"name"`
+ Type string `yaml:"type" json:"type" default:"string"`
+ In string `yaml:"in" json:"in"`
+ Description string `yaml:"description,omitempty"
json:"description,omitempty"`
+ Required bool `yaml:"required,omitempty"
json:"required,omitempty"`
+ Default any `yaml:"default,omitempty"
json:"default,omitempty"`
+ Enum []string `yaml:"enum,omitempty"
json:"enum,omitempty"`
+ }
+
+ // ResourceConfig resource configuration
+ ResourceConfig struct {
+ Name string `yaml:"name" json:"name"`
+ URI string `yaml:"uri" json:"uri"`
+ Description string `yaml:"description,omitempty"
json:"description,omitempty"`
+ MIMEType string `yaml:"mime_type,omitempty"
json:"mime_type,omitempty"`
+ Source ResourceSource `yaml:"source" json:"source"`
+ }
+
+ // ResourceSource resource source configuration (simplified)
+ ResourceSource struct {
+ Type string `yaml:"type" json:"type"`
+ Path string `yaml:"path,omitempty" json:"path,omitempty"`
// for file type
+ URL string `yaml:"url,omitempty" json:"url,omitempty"`
// for url type
+ Content string `yaml:"content,omitempty"
json:"content,omitempty"` // for inline type
+ Template string `yaml:"template,omitempty"
json:"template,omitempty"` // for template type
+ }
+
+ // ResourceTemplateConfig resource template configuration
+ ResourceTemplateConfig struct {
+ Name string `yaml:"name"
json:"name"`
+ URITemplate string `yaml:"uri_template"
json:"uri_template"`
+ Title string
`yaml:"title,omitempty" json:"title,omitempty"`
+ Description string
`yaml:"description,omitempty" json:"description,omitempty"`
+ MIMEType string
`yaml:"mime_type,omitempty" json:"mime_type,omitempty"`
+ Parameters []ResourceTemplateParameter
`yaml:"parameters,omitempty" json:"parameters,omitempty"`
+ Annotations *ResourceTemplateAnnotations
`yaml:"annotations,omitempty" json:"annotations,omitempty"`
+ }
+
+ // ResourceTemplateParameter resource template parameter
+ ResourceTemplateParameter struct {
+ Name string `yaml:"name" json:"name"`
+ Type string `yaml:"type" json:"type" default:"string"`
+ Description string `yaml:"description,omitempty"
json:"description,omitempty"`
+ Required bool `yaml:"required,omitempty"
json:"required,omitempty" default:"false"`
+ Enum []string `yaml:"enum,omitempty"
json:"enum,omitempty"`
+ Default any `yaml:"default,omitempty"
json:"default,omitempty"`
+ }
+
+ // ResourceTemplateAnnotations resource template annotations
+ ResourceTemplateAnnotations struct {
+ Audience []string `yaml:"audience,omitempty"
json:"audience,omitempty"`
+ Priority *float64 `yaml:"priority,omitempty"
json:"priority,omitempty"`
+ LastModified string `yaml:"last_modified,omitempty"
json:"last_modified,omitempty"`
+ }
+
+ // ComputedParameter computed parameter (for internal processing,
simplified)
+ ComputedParameter struct {
+ Name string
+ Type string
+ In string
+ Description string
+ Required bool
+ Enum []string
+ Default any
+ }
+)
+
+// GetAllParameters gets all parameters of the tool
+func (tc *ToolConfig) GetAllParameters() ([]ComputedParameter, error) {
+ var allParams []ComputedParameter
+
+ // 1. Automatically extract path parameters
+ pathParams := GetPathParameterNames(tc.Request.Path)
+ for _, paramName := range pathParams {
+ // Find corresponding arg configuration
+ var argConfig *ArgConfig
+ for _, arg := range tc.Args {
+ if arg.Name == paramName && arg.In == "path" {
+ argConfig = &arg
+ break
+ }
+ }
+
+ // Create computed parameter
+ computed := ComputedParameter{
+ Name: paramName,
+ Type: "string", // Default type
+ In: "path",
+ Required: true, // Path parameters are always required
+ }
+
+ // Apply arg configuration
+ if argConfig != nil {
+ computed.Type = argConfig.Type
+ computed.Description = argConfig.Description
+ // Simplified: removed Pattern and Format fields
+ }
+
+ allParams = append(allParams, computed)
+ }
+
+ // 2. Add non-path parameters
+ for _, arg := range tc.Args {
+ if arg.In != "path" {
+ computed := ComputedParameter{
+ Name: arg.Name,
+ Type: arg.Type,
+ In: arg.In,
+ Description: arg.Description,
+ Required: arg.Required,
+ Enum: arg.Enum,
+ Default: arg.Default,
+ // Simplified: removed complex validation fields
+ }
+ allParams = append(allParams, computed)
+ }
+ }
+
+ return allParams, nil
+}
+
+// GetPathParameterNames gets all parameter names in the path template
+func GetPathParameterNames(pathTemplate string) []string {
+ re := regexp.MustCompile(`\{([^}]+)}`)
+ matches := re.FindAllStringSubmatch(pathTemplate, -1)
+
+ // Initialize as empty slice instead of nil
+ names := []string{}
+ for _, match := range matches {
+ names = append(names, match[1])
+ }
+
+ return names
+}
+
+// PromptConfig prompt configuration
+type PromptConfig struct {
+ Name string `yaml:"name" json:"name"`
+ Title string `yaml:"title,omitempty"
json:"title,omitempty"`
+ Description string `yaml:"description,omitempty"
json:"description,omitempty"`
+ Arguments []PromptArgumentConfig `yaml:"arguments,omitempty"
json:"arguments,omitempty"`
+ Messages []PromptMessageConfig `yaml:"messages" json:"messages"`
+}
+
+// PromptArgumentConfig prompt argument configuration
+type PromptArgumentConfig struct {
+ Name string `yaml:"name" json:"name"`
+ Description string `yaml:"description,omitempty"
json:"description,omitempty"`
+ Required bool `yaml:"required,omitempty" json:"required,omitempty"`
+}
+
+// PromptMessageConfig prompt message configuration
+type PromptMessageConfig struct {
+ Role string `yaml:"role" json:"role"` // "user" or "assistant"
+ Content string `yaml:"content" json:"content"`
+}
diff --git a/pkg/filter/mcp/mcpserver/context.go
b/pkg/filter/mcp/mcpserver/context.go
new file mode 100644
index 00000000..1404bec2
--- /dev/null
+++ b/pkg/filter/mcp/mcpserver/context.go
@@ -0,0 +1,111 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package mcpserver
+
+import (
+ "github.com/mark3labs/mcp-go/mcp"
+)
+
+import (
+ "github.com/apache/dubbo-go-pixiu/pkg/common/constant"
+ contexthttp "github.com/apache/dubbo-go-pixiu/pkg/context/http"
+)
+
+const MCPDataKey = "mcp_data"
+
+// MCPData stores MCP-related data
+type MCPData struct {
+ // Method stores MCP method name
+ Method string
+ // RequestID stores JSON-RPC request ID
+ RequestID any
+}
+
+// MCPContext MCP context wrapper that composes HttpContext and provides
MCP-specific operations
+type MCPContext struct {
+ *contexthttp.HttpContext
+ mcpData *MCPData
+}
+
+// NewMCPContext creates a new MCP context
+func NewMCPContext(httpCtx *contexthttp.HttpContext) *MCPContext {
+ return &MCPContext{
+ HttpContext: httpCtx,
+ mcpData: &MCPData{},
+ }
+}
+
+// IsMCPRequest checks if it's an MCP request (by method name)
+func (ctx *MCPContext) IsMCPRequest() bool {
+ return ctx.mcpData.Method != ""
+}
+
+// SetMCPMethod sets MCP method name
+func (ctx *MCPContext) SetMCPMethod(method string) {
+ ctx.mcpData.Method = method
+}
+
+// McpMethod gets MCP method name
+func (ctx *MCPContext) McpMethod() string {
+ return ctx.mcpData.Method
+}
+
+// SetMCPRequestID sets JSON-RPC request ID
+func (ctx *MCPContext) SetMCPRequestID(id any) {
+ ctx.mcpData.RequestID = id
+}
+
+// McpRequestID gets JSON-RPC request ID
+func (ctx *MCPContext) McpRequestID() any {
+ return ctx.mcpData.RequestID
+}
+
+// IsMCPToolCall checks if it's a tool call request (by method name)
+func (ctx *MCPContext) IsMCPToolCall() bool {
+ return ctx.mcpData.Method == string(mcp.MethodToolsCall)
+}
+
+// StoreMCPDataInParams stores MCP data in HttpContext.Params for passing
through the filter chain
+func (ctx *MCPContext) StoreMCPDataInParams() {
+ if ctx.HttpContext.Params == nil {
+ ctx.HttpContext.Params = make(map[string]any)
+ }
+ ctx.HttpContext.Params[MCPDataKey] = ctx.mcpData
+}
+
+// LoadMCPDataFromParams loads MCP data from HttpContext.Params
+func (ctx *MCPContext) LoadMCPDataFromParams() {
+ if ctx.HttpContext.Params == nil {
+ return
+ }
+ if data, ok := ctx.HttpContext.Params[MCPDataKey].(*MCPData); ok {
+ ctx.mcpData = data
+ }
+}
+
+// NewMCPContextFromHttpContext creates MCPContext from existing HttpContext
and tries to load stored MCP data
+func NewMCPContextFromHttpContext(httpCtx *contexthttp.HttpContext)
*MCPContext {
+ mcpCtx := NewMCPContext(httpCtx)
+ mcpCtx.LoadMCPDataFromParams()
+ return mcpCtx
+}
+
+// ClearContentLengthHeader removes the Content-Length header to prevent
conflicts with chunked transfer encoding.
+func (ctx *MCPContext) ClearContentLengthHeader() {
+ ctx.Writer.Header().Del(constant.HeaderKeyContentLength)
+}
diff --git a/pkg/filter/mcp/mcpserver/filter.go
b/pkg/filter/mcp/mcpserver/filter.go
new file mode 100644
index 00000000..5392af2e
--- /dev/null
+++ b/pkg/filter/mcp/mcpserver/filter.go
@@ -0,0 +1,237 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package mcpserver
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+)
+
+import (
+ "github.com/mark3labs/mcp-go/mcp"
+)
+
+import (
+ "github.com/apache/dubbo-go-pixiu/pkg/common/extension/filter"
+ contexthttp "github.com/apache/dubbo-go-pixiu/pkg/context/http"
+ "github.com/apache/dubbo-go-pixiu/pkg/logger"
+)
+
+// FilterFactory and MCPServerFilter types
+type (
+ // FilterFactory is a factory to create MCP server filters.
+ FilterFactory struct {
+ cfg *Config
+ registry *ToolRegistry
+ }
+
+ // MCPServerFilter is a filter that handles MCP protocol.
+ MCPServerFilter struct {
+ cfg *Config
+ registry *ToolRegistry
+ errorHandler *ErrorHandler
+ responseBuilder *ResponseBuilder
+ }
+)
+
+// Apply prepares the MCP server and tool registry.
+func (f *FilterFactory) Apply() error {
+ // Initialize tool registry
+ f.registry = NewToolRegistry()
+
+ // Register statically configured tools
+ for _, tool := range f.cfg.Tools {
+ if err := f.registry.RegisterTool(tool); err != nil {
+ return fmt.Errorf("failed to register tool %s: %v",
tool.Name, err)
+ }
+ logger.Debugf("[dubbo-go-pixiu] mcp server registered tool '%s'
-> cluster:%s", tool.Name, tool.Cluster)
+ }
+
+ // Register statically configured resources
+ for _, resource := range f.cfg.Resources {
+ if err := f.registry.RegisterResource(resource); err != nil {
+ return fmt.Errorf("failed to register resource %s: %v",
resource.Name, err)
+ }
+ logger.Debugf("[dubbo-go-pixiu] mcp server registered resource
'%s' -> uri:%s", resource.Name, resource.URI)
+ }
+
+ // Register statically configured resource templates
+ for _, template := range f.cfg.ResourceTemplates {
+ if err := f.registry.RegisterResourceTemplate(template); err !=
nil {
+ return fmt.Errorf("failed to register resource template
%s: %v", template.Name, err)
+ }
+ logger.Debugf("[dubbo-go-pixiu] mcp server registered template
'%s' -> pattern:%s", template.Name, template.URITemplate)
+ }
+
+ // Register statically configured prompts
+ for _, prompt := range f.cfg.Prompts {
+ if err := f.registry.RegisterPrompt(prompt); err != nil {
+ return fmt.Errorf("failed to register prompt %s: %v",
prompt.Name, err)
+ }
+ logger.Debugf("[dubbo-go-pixiu] mcp server registered prompt
'%s'", prompt.Name)
+ }
+
+ return nil
+}
+
+// Config returns the configuration struct
+func (f *FilterFactory) Config() any {
+ return f.cfg
+}
+
+// PrepareFilterChain prepares the filter chain
+func (f *FilterFactory) PrepareFilterChain(ctx *contexthttp.HttpContext, chain
filter.FilterChain) error {
+ mcpFilter := &MCPServerFilter{
+ cfg: f.cfg,
+ registry: f.registry,
+ errorHandler: NewErrorHandler(),
+ responseBuilder: NewResponseBuilder(),
+ }
+ chain.AppendDecodeFilters(mcpFilter)
+ chain.AppendEncodeFilters(mcpFilter) // Add to Encode chain
+ return nil
+}
+
+// Decode processes incoming HTTP requests for MCP protocol.
+func (f *MCPServerFilter) Decode(ctx *contexthttp.HttpContext)
filter.FilterStatus {
+ // Check if it's an MCP request
+ if !f.isMCPRequest(ctx) {
+ return filter.Continue
+ }
+
+ // Create MCP context wrapper
+ mcpCtx := NewMCPContext(ctx)
+
+ // Read request body
+ body, err := io.ReadAll(ctx.Request.Body)
+ if err != nil {
+ logger.Errorf("[dubbo-go-pixiu] mcp server failed to read
request body: %v", err)
+ return f.errorHandler.SendInternalError(mcpCtx, nil, "failed to
read request body")
+ }
+
+ // Parse JSON-RPC request
+ var jsonrpcReq mcp.JSONRPCRequest
+ if err := json.Unmarshal(body, &jsonrpcReq); err != nil {
+ logger.Errorf("[dubbo-go-pixiu] mcp server failed to parse
JSON-RPC request: %v", err)
+ return f.errorHandler.SendInternalError(mcpCtx, nil, "invalid
JSON-RPC request")
+ }
+
+ logger.Infof("[dubbo-go-pixiu] mcp server received request: %s (id:
%v)", jsonrpcReq.Method, jsonrpcReq.ID)
+
+ // Store information in MCP context
+ mcpCtx.SetMCPMethod(jsonrpcReq.Method)
+ mcpCtx.SetMCPRequestID(jsonrpcReq.ID)
+
+ // Handle terminal methods (methods that don't need forwarding to
backend)
+ if f.isTerminalMethod(jsonrpcReq.Method) {
+ return f.handleTerminalMethod(mcpCtx, jsonrpcReq)
+ } else if jsonrpcReq.Method == string(mcp.MethodToolsCall) {
+ // Tool call will be processed in Encode stage (IsMCPToolCall()
checks method)
+ return f.handleToolCall(mcpCtx, jsonrpcReq)
+ } else {
+ // Unknown method
+ logger.Warnf("[dubbo-go-pixiu] mcp server unsupported method:
%s", jsonrpcReq.Method)
+ return f.errorHandler.SendMethodNotFound(mcpCtx, jsonrpcReq.ID)
+ }
+}
+
+// isTerminalMethod checks if it's a terminal method (methods that don't need
forwarding to backend)
+func (f *MCPServerFilter) isTerminalMethod(method string) bool {
+ switch method {
+ case string(mcp.MethodInitialize), string(mcp.MethodToolsList),
string(mcp.MethodResourcesList), string(mcp.MethodResourcesRead),
+ "resources/templates/list", string(mcp.MethodPromptsList),
string(mcp.MethodPromptsGet),
+ "notifications/initialized", string(mcp.MethodPing):
+ return true
+ default:
+ return false
+ }
+}
+
+// handleTerminalMethod handles terminal methods
+func (f *MCPServerFilter) handleTerminalMethod(ctx *MCPContext, req
mcp.JSONRPCRequest) filter.FilterStatus {
+ switch req.Method {
+ case string(mcp.MethodInitialize):
+ return f.handleInitialize(ctx, req)
+ case string(mcp.MethodToolsList):
+ return f.handleToolsList(ctx, req)
+ case string(mcp.MethodResourcesList):
+ return f.handleResourcesList(ctx, req)
+ case string(mcp.MethodResourcesRead):
+ return f.handleResourceRead(ctx, req)
+ case "resources/templates/list":
+ return f.handleResourceTemplatesList(ctx, req)
+ case string(mcp.MethodPromptsList):
+ return f.handlePromptsList(ctx, req)
+ case string(mcp.MethodPromptsGet):
+ return f.handlePromptsGet(ctx, req)
+ case "notifications/initialized":
+ return f.handleNotificationsInitialized(ctx, req)
+ case string(mcp.MethodPing):
+ return f.handlePing(ctx, req)
+ default:
+ logger.Warnf("[dubbo-go-pixiu] mcp server unsupported method:
%s", req.Method)
+ return f.errorHandler.SendMethodNotFound(ctx, req.ID)
+ }
+}
+
+// Encode processes outgoing HTTP responses.
+func (f *MCPServerFilter) Encode(ctx *contexthttp.HttpContext)
filter.FilterStatus {
+ // Create MCP context wrapper and load stored MCP data
+ mcpCtx := NewMCPContextFromHttpContext(ctx)
+
+ // Check if it's a tool call response
+ if mcpCtx.IsMCPToolCall() {
+ logger.Debugf("[dubbo-go-pixiu] mcp server processing tool call
response: %s", ctx.Request.URL.Path)
+ return f.handleToolCallResponse(mcpCtx)
+ }
+
+ // For regular MCP requests, no special processing needed
+ if mcpCtx.IsMCPRequest() {
+ logger.Debugf("[dubbo-go-pixiu] mcp server regular MCP request,
no special processing needed")
+ }
+
+ return filter.Continue
+}
+
+// isMCPRequest checks if it's an MCP request
+func (f *MCPServerFilter) isMCPRequest(ctx *contexthttp.HttpContext) bool {
+ return ctx.Request.URL.Path == f.cfg.Endpoint
+}
+
+// sendJSONResponse sends a JSON response
+func (f *MCPServerFilter) sendJSONResponse(ctx *MCPContext, response any)
filter.FilterStatus {
+ responseBody, err := json.Marshal(response)
+ if err != nil {
+ logger.Errorf("[dubbo-go-pixiu] mcp server failed to marshal
response: %v", err)
+ ctx.SendLocalReply(http.StatusInternalServerError,
[]byte("internal server error"))
+ return filter.Stop
+ }
+
+ // Get method and request ID for logging
+ method := ctx.McpMethod()
+ requestID := ctx.McpRequestID()
+
+ logger.Infof("[dubbo-go-pixiu] mcp server response sent: %s (id: %v)",
method, requestID)
+
+ // Critical: Clear Content-Length header to prevent mismatch errors
+ ctx.ClearContentLengthHeader()
+ ctx.SendLocalReply(http.StatusOK, responseBody)
+ return filter.Stop
+}
diff --git a/pkg/filter/mcp/mcpserver/handlers.go
b/pkg/filter/mcp/mcpserver/handlers.go
new file mode 100644
index 00000000..f468b394
--- /dev/null
+++ b/pkg/filter/mcp/mcpserver/handlers.go
@@ -0,0 +1,571 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package mcpserver
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "strings"
+)
+
+import (
+ "github.com/mark3labs/mcp-go/mcp"
+)
+
+import (
+ "github.com/apache/dubbo-go-pixiu/pkg/client"
+ "github.com/apache/dubbo-go-pixiu/pkg/common/constant"
+ "github.com/apache/dubbo-go-pixiu/pkg/common/extension/filter"
+ "github.com/apache/dubbo-go-pixiu/pkg/logger"
+ "github.com/apache/dubbo-go-pixiu/pkg/model"
+)
+
+const (
+ inPath = "path"
+ inQuery = "query"
+ inBody = "body"
+
+ typeString = "string"
+ typeInteger = "integer"
+ typeNumber = "number"
+ typeBoolean = "boolean"
+)
+
+// handleInitialize handles the initialize method
+func (f *MCPServerFilter) handleInitialize(ctx *MCPContext, req
mcp.JSONRPCRequest) filter.FilterStatus {
+ // Build server capabilities using mcp-go structures
+ capabilities := mcp.ServerCapabilities{
+ Tools: &struct {
+ ListChanged bool `json:"listChanged,omitempty"`
+ }{
+ // TODO: Dynamic update capabilities - enable after
Nacos integration
+ // Currently set to false, future Nacos integration
will support:
+ // 1. Dynamic discovery and registration of new backend
services
+ // 2. Automatic generation of corresponding MCP tools
+ // 3. Send notifications/tools/list_changed
notifications
+ ListChanged: false,
+ },
+ Resources: &struct {
+ Subscribe bool `json:"subscribe,omitempty"`
+ ListChanged bool `json:"listChanged,omitempty"`
+ }{
+ Subscribe: false,
+ ListChanged: false,
+ },
+ Prompts: &struct {
+ ListChanged bool `json:"listChanged,omitempty"`
+ }{
+ ListChanged: false,
+ },
+ }
+
+ // Build server info using mcp-go structures
+ serverInfo := mcp.Implementation{
+ Name: f.cfg.ServerInfo.Name,
+ Version: f.cfg.ServerInfo.Version,
+ }
+
+ // Create initialization result using mcp-go API
+ instructions := f.cfg.ServerInfo.Instructions
+ if instructions == "" {
+ instructions = "This MCP server provides API access through
tools, documentation through resources, and AI assistance through prompts."
+ }
+ result := mcp.NewInitializeResult(mcp.LATEST_PROTOCOL_VERSION,
capabilities, serverInfo, instructions)
+
+ // Create JSON-RPC response
+ response := f.responseBuilder.Success(req.ID, result)
+ return f.sendJSONResponse(ctx, response)
+}
+
+// handleToolsList handles the tools/list method using mcp-go APIs
+func (f *MCPServerFilter) handleToolsList(ctx *MCPContext, req
mcp.JSONRPCRequest) filter.FilterStatus {
+ tools := make([]mcp.Tool, 0, len(f.cfg.Tools))
+
+ // Build tools using mcp-go API for standard compliance
+ for _, toolCfg := range f.cfg.Tools {
+ // Start with basic tool options
+ toolOptions := []mcp.ToolOption{
+ mcp.WithDescription(toolCfg.Description),
+ }
+
+ // Add parameter definitions using mcp-go APIs
+ for _, arg := range toolCfg.Args {
+ opts := f.buildToolParameterOptions(&arg)
+ switch arg.Type {
+ case typeString:
+ toolOptions = append(toolOptions,
mcp.WithString(arg.Name, opts...))
+ case typeInteger, typeNumber:
+ toolOptions = append(toolOptions,
mcp.WithNumber(arg.Name, opts...))
+ case typeBoolean:
+ toolOptions = append(toolOptions,
mcp.WithBoolean(arg.Name, opts...))
+ }
+ }
+
+ // Create tool using mcp-go API
+ tool := mcp.NewTool(toolCfg.Name, toolOptions...)
+ tools = append(tools, tool)
+ }
+
+ // Build standard MCP tools list response using mcp-go structures
+ result := mcp.NewListToolsResult(tools, "") // empty cursor for no
pagination
+
+ response := f.responseBuilder.Success(req.ID, result)
+ return f.sendJSONResponse(ctx, response)
+}
+
+// buildToolParameterOptions builds the mcp.PropertyOption slice for a given
tool argument
+func (f *MCPServerFilter) buildToolParameterOptions(arg *ArgConfig)
[]mcp.PropertyOption {
+ opts := []mcp.PropertyOption{mcp.Description(arg.Description)}
+
+ if arg.Required {
+ opts = append(opts, mcp.Required())
+ }
+
+ if arg.Default != nil {
+ switch arg.Type {
+ case typeString:
+ if defaultStr, ok := arg.Default.(string); ok {
+ opts = append(opts,
mcp.DefaultString(defaultStr))
+ }
+ case typeInteger, typeNumber:
+ switch defaultVal := arg.Default.(type) {
+ case float64:
+ opts = append(opts,
mcp.DefaultNumber(defaultVal))
+ case int:
+ opts = append(opts,
mcp.DefaultNumber(float64(defaultVal)))
+ case int64:
+ opts = append(opts,
mcp.DefaultNumber(float64(defaultVal)))
+ }
+ case typeBoolean:
+ if defaultBool, ok := arg.Default.(bool); ok {
+ opts = append(opts,
mcp.DefaultBool(defaultBool))
+ }
+ }
+ }
+
+ if len(arg.Enum) > 0 && arg.Type == typeString {
+ opts = append(opts, mcp.Enum(arg.Enum...))
+ }
+
+ return opts
+}
+
+// handleResourcesList handles the resources/list method
+func (f *MCPServerFilter) handleResourcesList(ctx *MCPContext, req
mcp.JSONRPCRequest) filter.FilterStatus {
+ // Get all resources
+ mcpResources, err := f.registry.ToMCPResources()
+ if err != nil {
+ logger.Errorf("[dubbo-go-pixiu] mcp server failed to get MCP
resources: %v", err)
+ return f.errorHandler.SendInternalError(ctx, req.ID, "failed to
get resources")
+ }
+
+ // Build resources list response using mcp-go structures
+ result := mcp.NewListResourcesResult(mcpResources, "") // empty cursor
for no pagination
+
+ response := f.responseBuilder.Success(req.ID, result)
+ return f.sendJSONResponse(ctx, response)
+}
+
+// handlePing handles the ping method
+func (f *MCPServerFilter) handlePing(ctx *MCPContext, req mcp.JSONRPCRequest)
filter.FilterStatus {
+ logger.Debugf("[dubbo-go-pixiu] mcp server handling ping request")
+
+ // Simple ping response
+ result := map[string]any{}
+
+ response := f.responseBuilder.Success(req.ID, result)
+ return f.sendJSONResponse(ctx, response)
+}
+
+// handleNotificationsInitialized handles notifications/initialized
notification
+func (f *MCPServerFilter) handleNotificationsInitialized(ctx *MCPContext, req
mcp.JSONRPCRequest) filter.FilterStatus {
+ logger.Debugf("[dubbo-go-pixiu] mcp server received initialized
notification from client")
+
+ // Store client initialization state
+ // This notification indicates that the client has completed
initialization
+ // and is ready to receive requests
+
+ // For notifications, we don't send a response, just return Stop
+ return filter.Stop
+}
+
+// handleResourceRead handles the resources/read method
+func (f *MCPServerFilter) handleResourceRead(ctx *MCPContext, req
mcp.JSONRPCRequest) filter.FilterStatus {
+ logger.Debugf("[dubbo-go-pixiu] mcp server handling resources/read")
+
+ // Parse request parameters
+ paramsBytes, err := json.Marshal(req.Params)
+ if err != nil {
+ logger.Errorf("[dubbo-go-pixiu] mcp server failed to marshal
params: %v", err)
+ return f.errorHandler.SendInvalidParams(ctx, req.ID, "invalid
parameters")
+ }
+
+ var params struct {
+ URI string `json:"uri"`
+ }
+ if err := json.Unmarshal(paramsBytes, ¶ms); err != nil {
+ logger.Errorf("[dubbo-go-pixiu] mcp server failed to parse
resource read params: %v", err)
+ return f.errorHandler.SendInvalidParams(ctx, req.ID, "invalid
parameters")
+ }
+
+ // Find resource (by URI)
+ resource, exists := f.registry.GetResourceByURI(params.URI)
+ if !exists {
+ logger.Warnf("[dubbo-go-pixiu] mcp server resource not found:
%s", params.URI)
+ return f.errorHandler.SendInternalError(ctx, req.ID,
fmt.Sprintf("resource not found: %s", params.URI))
+ }
+
+ // Build resource content response
+ // TODO: Implement actual resource content loading from source
+ content := fmt.Sprintf("Resource content for %s (source: %s)",
resource.URI, resource.Source.Type)
+
+ result := map[string]any{
+ "contents": []map[string]any{
+ {
+ "uri": resource.URI,
+ "mimeType": resource.MIMEType,
+ "text": content,
+ },
+ },
+ }
+
+ response := f.responseBuilder.Success(req.ID, result)
+ return f.sendJSONResponse(ctx, response)
+}
+
+// handleResourceTemplatesList handles the resources/templates/list method
+func (f *MCPServerFilter) handleResourceTemplatesList(ctx *MCPContext, req
mcp.JSONRPCRequest) filter.FilterStatus {
+ // Get all resource templates (parameterized resource patterns)
+ mcpResourceTemplates, err := f.registry.ToMCPResourceTemplates()
+ if err != nil {
+ logger.Errorf("[dubbo-go-pixiu] mcp server failed to get MCP
resource templates: %v", err)
+ return f.errorHandler.SendInternalError(ctx, req.ID, "failed to
get resource templates")
+ }
+
+ // Build resource templates list response
+ result := map[string]any{
+ "resourceTemplates": mcpResourceTemplates,
+ }
+
+ response := f.responseBuilder.Success(req.ID, result)
+ return f.sendJSONResponse(ctx, response)
+}
+
+// handlePromptsList handles the prompts/list method
+func (f *MCPServerFilter) handlePromptsList(ctx *MCPContext, req
mcp.JSONRPCRequest) filter.FilterStatus {
+ logger.Debugf("[dubbo-go-pixiu] mcp server handling prompts/list
request")
+
+ // Get all prompts
+ mcpPrompts, err := f.registry.ToMCPPrompts()
+ if err != nil {
+ logger.Errorf("[dubbo-go-pixiu] mcp server failed to get
prompts: %v", err)
+ return f.errorHandler.SendInternalError(ctx, req.ID, "failed to
get prompts")
+ }
+
+ // Build prompts list response
+ result := map[string]any{
+ "prompts": mcpPrompts,
+ }
+
+ response := f.responseBuilder.Success(req.ID, result)
+ return f.sendJSONResponse(ctx, response)
+}
+
+// handlePromptsGet handles the prompts/get method
+func (f *MCPServerFilter) handlePromptsGet(ctx *MCPContext, req
mcp.JSONRPCRequest) filter.FilterStatus {
+ logger.Debugf("[dubbo-go-pixiu] mcp server handling prompts/get
request")
+
+ // Parse request parameters
+ paramsBytes, err := json.Marshal(req.Params)
+ if err != nil {
+ logger.Errorf("[dubbo-go-pixiu] mcp server failed to marshal
params: %v", err)
+ return f.errorHandler.SendInvalidParams(ctx, req.ID, "invalid
parameters")
+ }
+
+ var params struct {
+ Name string `json:"name"`
+ Arguments map[string]any `json:"arguments,omitempty"`
+ }
+ if err := json.Unmarshal(paramsBytes, ¶ms); err != nil {
+ logger.Errorf("[dubbo-go-pixiu] mcp server failed to parse
prompts/get params: %v", err)
+ return f.errorHandler.SendInvalidParams(ctx, req.ID, "invalid
parameters")
+ }
+
+ // Find prompt configuration
+ promptConfig, exists := f.registry.GetPrompt(params.Name)
+ if !exists {
+ logger.Warnf("[dubbo-go-pixiu] mcp server prompt not found:
%s", params.Name)
+ return f.errorHandler.SendInternalError(ctx, req.ID,
fmt.Sprintf("prompt not found: %s", params.Name))
+ }
+
+ // Build prompt messages with parameter replacement
+ messages, err := f.buildPromptMessages(promptConfig, params.Arguments)
+ if err != nil {
+ logger.Errorf("[dubbo-go-pixiu] mcp server failed to build
prompt messages: %v", err)
+ return f.errorHandler.SendInternalError(ctx, req.ID, "failed to
build prompt messages")
+ }
+
+ // Build prompts/get response
+ result := map[string]any{
+ "description": promptConfig.Description,
+ "messages": messages,
+ }
+
+ response := f.responseBuilder.Success(req.ID, result)
+ return f.sendJSONResponse(ctx, response)
+}
+
+// buildPromptMessages builds prompt messages with parameter replacement
support
+func (f *MCPServerFilter) buildPromptMessages(promptConfig PromptConfig,
arguments map[string]any) ([]map[string]any, error) {
+ messages := make([]map[string]any, 0, len(promptConfig.Messages))
+
+ for _, msg := range promptConfig.Messages {
+ // Replace parameter placeholders in content
+ content := f.replacePromptArguments(msg.Content, arguments)
+
+ message := map[string]any{
+ "role": msg.Role,
+ "content": content,
+ }
+ messages = append(messages, message)
+ }
+
+ return messages, nil
+}
+
+// replacePromptArguments replaces parameter placeholders in prompt content
+func (f *MCPServerFilter) replacePromptArguments(content string, arguments
map[string]any) string {
+ if arguments == nil {
+ return content
+ }
+
+ result := content
+ for key, value := range arguments {
+ placeholder := fmt.Sprintf("{{%s}}", key)
+ replacement := fmt.Sprintf("%v", value)
+ result = strings.ReplaceAll(result, placeholder, replacement)
+ }
+
+ return result
+}
+
+// handleToolCall handles tool call requests by forwarding to backend
+func (f *MCPServerFilter) handleToolCall(ctx *MCPContext, req
mcp.JSONRPCRequest) filter.FilterStatus {
+ // Parse tool call parameters
+ paramsBytes, err := json.Marshal(req.Params)
+ if err != nil {
+ logger.Errorf("[dubbo-go-pixiu] mcp server failed to marshal
tool call params: %v", err)
+ return f.errorHandler.SendInternalError(ctx, req.ID, "invalid
tool call parameters")
+ }
+
+ var params struct {
+ Name string `json:"name"`
+ Arguments map[string]any `json:"arguments,omitempty"`
+ }
+ if err := json.Unmarshal(paramsBytes, ¶ms); err != nil {
+ logger.Errorf("[dubbo-go-pixiu] mcp server failed to parse tool
call params: %v", err)
+ return f.errorHandler.SendInvalidParams(ctx, req.ID, "invalid
tool call parameters")
+ }
+
+ // Find tool configuration
+ toolConfig, exists := f.registry.GetTool(params.Name)
+ if !exists {
+ logger.Warnf("[dubbo-go-pixiu] mcp server tool not found: %s",
params.Name)
+ return f.errorHandler.SendToolCallError(ctx, req.ID,
fmt.Sprintf("tool not found: %s", params.Name))
+ }
+
+ // Build backend request
+ err = f.buildBackendRequest(ctx, toolConfig, params.Arguments)
+ if err != nil {
+ logger.Errorf("[dubbo-go-pixiu] mcp server failed to build
backend request: %v", err)
+ return f.errorHandler.SendToolCallError(ctx, req.ID, "failed to
build backend request")
+ }
+
+ // Set cluster information for routing
+ if ctx.Params == nil {
+ ctx.Params = make(map[string]any)
+ }
+
+ logger.Infof("[dubbo-go-pixiu] mcp server forwarding tool call: %s ->
%s %s (cluster: %s)",
+ params.Name, toolConfig.Request.Method, ctx.Request.URL.Path,
toolConfig.Cluster)
+
+ // Store MCP data for Encode stage processing
+ ctx.StoreMCPDataInParams()
+
+ ctx.Route = &model.RouteAction{
+ Cluster: toolConfig.Cluster,
+ }
+
+ // Continue to next filter for backend forwarding
+ return filter.Continue
+}
+
+// buildBackendRequest builds the complete backend request including path,
body, and headers
+func (f *MCPServerFilter) buildBackendRequest(ctx *MCPContext, toolConfig
ToolConfig, arguments map[string]any) error {
+ // Set HTTP method
+ ctx.Request.Method = toolConfig.Request.Method
+
+ // Build request path and body based on argument locations
+ path := toolConfig.Request.Path
+ bodyParams := make(map[string]any)
+ queryParams := make(map[string]string)
+
+ // Process arguments based on their location (path, query, body)
+ if arguments != nil {
+ for argName, argValue := range arguments {
+ // Find argument configuration
+ var argConfig *ArgConfig
+ for _, arg := range toolConfig.Args {
+ if arg.Name == argName {
+ argConfig = &arg
+ break
+ }
+ }
+
+ if argConfig == nil {
+ continue // Skip unknown arguments
+ }
+
+ switch argConfig.In {
+ case inPath:
+ // Replace path parameters
+ placeholder := fmt.Sprintf("{%s}", argName)
+ replacement := fmt.Sprintf("%v", argValue)
+ path = strings.ReplaceAll(path, placeholder,
replacement)
+
+ case inQuery:
+ // Add to query parameters
+ queryParams[argName] = fmt.Sprintf("%v",
argValue)
+
+ case inBody:
+ // Add to request body
+ bodyParams[argName] = argValue
+ }
+ }
+ }
+
+ // Set the request path
+ ctx.Request.URL.Path = path
+
+ // Add query parameters
+ if len(queryParams) > 0 {
+ query := ctx.Request.URL.Query()
+ for key, value := range queryParams {
+ query.Set(key, value)
+ }
+ ctx.Request.URL.RawQuery = query.Encode()
+ }
+
+ // Build request body for POST/PUT requests
+ if len(bodyParams) > 0 && (toolConfig.Request.Method == constant.Post
|| toolConfig.Request.Method == constant.Put) {
+ bodyJSON, err := json.Marshal(bodyParams)
+ if err != nil {
+ return fmt.Errorf("failed to marshal request body: %v",
err)
+ }
+
+ // Set request body
+ ctx.Request.Body =
io.NopCloser(strings.NewReader(string(bodyJSON)))
+
+ // Set Content-Type header
+ ctx.Request.Header.Set(constant.HeaderKeyContextType,
constant.HeaderValueApplicationJson)
+
+ logger.Debugf("[dubbo-go-pixiu] mcp server built request body:
%s", string(bodyJSON))
+ }
+
+ return nil
+}
+
+// handleToolCallResponse handles tool call responses, wrapping backend
responses in MCP format
+func (f *MCPServerFilter) handleToolCallResponse(ctx *MCPContext)
filter.FilterStatus {
+ logger.Debugf("[dubbo-go-pixiu] mcp server handling tool call response")
+
+ // Extract request information
+ requestID := ctx.McpRequestID()
+ if requestID == nil {
+ logger.Errorf("[dubbo-go-pixiu] mcp server missing request ID
for tool call response")
+ return filter.Continue
+ }
+
+ // Extract backend response
+ responseBody, statusCode, err := f.extractBackendResponse(ctx)
+ if err != nil {
+ logger.Errorf("[dubbo-go-pixiu] mcp server failed to extract
backend response: %v", err)
+ return f.errorHandler.SendToolCallError(ctx, requestID, "failed
to process backend response")
+ }
+
+ // Process the response
+ return f.processToolCallResponse(ctx, requestID, responseBody,
statusCode)
+}
+
+// extractBackendResponse extracts response data from the context
+func (f *MCPServerFilter) extractBackendResponse(ctx *MCPContext) ([]byte,
int, error) {
+ if ctx.TargetResp == nil {
+ return nil, 0, fmt.Errorf("no target response available")
+ }
+
+ unaryResp, ok := ctx.TargetResp.(*client.UnaryResponse)
+ if !ok {
+ return nil, 0, fmt.Errorf("unexpected response type")
+ }
+
+ responseBody := unaryResp.Data
+ statusCode := ctx.GetStatusCode()
+
+ if len(responseBody) == 0 {
+ return nil, statusCode, fmt.Errorf("empty response body")
+ }
+
+ logger.Debugf("[dubbo-go-pixiu] mcp server backend response: status=%d,
size=%d bytes", statusCode, len(responseBody))
+ return responseBody, statusCode, nil
+}
+
+// processToolCallResponse processes the tool call response and sends the
result
+func (f *MCPServerFilter) processToolCallResponse(ctx *MCPContext, requestID
any, responseBody []byte, statusCode int) filter.FilterStatus {
+ // Check for backend errors
+ if statusCode >= 400 {
+ logger.Errorf("[dubbo-go-pixiu] mcp server backend returned
error status: %d", statusCode)
+ return f.errorHandler.SendToolCallError(ctx, requestID,
fmt.Sprintf("backend error: %d", statusCode))
+ }
+
+ // Build successful response using ToolCallSuccess method
+ content := strings.TrimSpace(string(responseBody))
+ mcpResponse := f.responseBuilder.ToolCallSuccess(requestID, content)
+ return f.sendMCPResponse(ctx, mcpResponse)
+}
+
+// sendMCPResponse sends an MCP response and updates the target response
+func (f *MCPServerFilter) sendMCPResponse(ctx *MCPContext, response
mcp.JSONRPCResponse) filter.FilterStatus {
+ mcpResponseBody, err := json.Marshal(response)
+ if err != nil {
+ logger.Errorf("[dubbo-go-pixiu] mcp server failed to marshal
MCP response: %v", err)
+ return filter.Continue
+ }
+
+ // Override TargetResp to ensure MCP format response is sent
+ ctx.TargetResp = &client.UnaryResponse{Data: mcpResponseBody}
+ ctx.StatusCode(http.StatusOK)
+ ctx.AddHeader(constant.HeaderKeyContextType,
constant.HeaderValueApplicationJson)
+
+ // Critical: Clear Content-Length header to prevent mismatch errors
+ ctx.ClearContentLengthHeader()
+
+ logger.Debugf("[dubbo-go-pixiu] mcp server successfully wrapped backend
response in MCP format")
+ return filter.Continue
+}
diff --git a/pkg/filter/mcp/mcpserver/plugin.go
b/pkg/filter/mcp/mcpserver/plugin.go
new file mode 100644
index 00000000..bfef12bc
--- /dev/null
+++ b/pkg/filter/mcp/mcpserver/plugin.go
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package mcpserver
+
+import (
+ "github.com/apache/dubbo-go-pixiu/pkg/common/constant"
+ "github.com/apache/dubbo-go-pixiu/pkg/common/extension/filter"
+)
+
+const (
+ // Kind is the type identifier for MCP Server Filter
+ Kind = constant.MCPServerFilter
+)
+
+func init() {
+ filter.RegisterHttpFilter(&Plugin{})
+}
+
+// Plugin implements filter.HttpFilterPlugin interface
+type Plugin struct{}
+
+// Kind returns the plugin type
+func (p *Plugin) Kind() string {
+ return Kind
+}
+
+// CreateFilterFactory creates FilterFactory
+func (p *Plugin) CreateFilterFactory() (filter.HttpFilterFactory, error) {
+ return &FilterFactory{cfg: &Config{}}, nil
+}
+
+// Config returns the configuration struct
+func (p *Plugin) Config() any {
+ return &Config{}
+}
diff --git a/pkg/filter/mcp/mcpserver/registry.go
b/pkg/filter/mcp/mcpserver/registry.go
new file mode 100644
index 00000000..6d3e0b79
--- /dev/null
+++ b/pkg/filter/mcp/mcpserver/registry.go
@@ -0,0 +1,359 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package mcpserver
+
+import (
+ "fmt"
+ "sync"
+)
+
+import (
+ "github.com/mark3labs/mcp-go/mcp"
+)
+
+import (
+ "github.com/apache/dubbo-go-pixiu/pkg/logger"
+)
+
+// ToolRegistry tool registry, thread-safe (optimized with single indexing)
+type ToolRegistry struct {
+ mu sync.RWMutex
+ tools map[string]ToolConfig
+ resources map[string]ResourceConfig // indexed by URI
+ resourceTemplates map[string]ResourceTemplateConfig // indexed by name
+ prompts map[string]PromptConfig
+
+ // TODO: Dynamic update support - add when integrating with Nacos
+ // changeListeners []ChangeListener // change listeners
+ // nacosClient nacos.ConfigClient // Nacos config client
+ // serviceDiscovery nacos.NamingClient // Nacos service
discovery client
+}
+
+// NewToolRegistry creates a new tool registry
+func NewToolRegistry() *ToolRegistry {
+ return &ToolRegistry{
+ tools: make(map[string]ToolConfig),
+ resources: make(map[string]ResourceConfig),
+ resourceTemplates: make(map[string]ResourceTemplateConfig),
+ prompts: make(map[string]PromptConfig),
+ }
+}
+
+// RegisterTool registers a tool
+func (r *ToolRegistry) RegisterTool(tool ToolConfig) error {
+ r.mu.Lock()
+ defer r.mu.Unlock()
+
+ if _, exists := r.tools[tool.Name]; exists {
+ return fmt.Errorf("tool %s already exists", tool.Name)
+ }
+
+ r.tools[tool.Name] = tool
+
+ // TODO: Dynamic update notification - enable when integrating with
Nacos
+ // r.notifyToolsListChanged()
+
+ return nil
+}
+
+// RegisterResource registers a resource (indexed by URI as per MCP
specification)
+func (r *ToolRegistry) RegisterResource(resource ResourceConfig) error {
+ r.mu.Lock()
+ defer r.mu.Unlock()
+
+ if _, exists := r.resources[resource.URI]; exists {
+ return fmt.Errorf("resource with URI %s already exists",
resource.URI)
+ }
+
+ // Register resource by URI as per MCP specification
+ r.resources[resource.URI] = resource
+ return nil
+}
+
+// GetTool gets tool configuration
+func (r *ToolRegistry) GetTool(name string) (ToolConfig, bool) {
+ r.mu.RLock()
+ defer r.mu.RUnlock()
+
+ tool, exists := r.tools[name]
+ return tool, exists
+}
+
+// GetResourceByURI gets resource configuration (by URI, O(1) lookup)
+func (r *ToolRegistry) GetResourceByURI(uri string) (ResourceConfig, bool) {
+ r.mu.RLock()
+ defer r.mu.RUnlock()
+
+ // Direct O(1) lookup by URI
+ resource, exists := r.resources[uri]
+ return resource, exists
+}
+
+// RegisterResourceTemplate registers a resource template
+func (r *ToolRegistry) RegisterResourceTemplate(template
ResourceTemplateConfig) error {
+ r.mu.Lock()
+ defer r.mu.Unlock()
+
+ if _, exists := r.resourceTemplates[template.Name]; exists {
+ return fmt.Errorf("resource template %s already exists",
template.Name)
+ }
+
+ r.resourceTemplates[template.Name] = template
+ return nil
+}
+
+// GetResourceTemplate gets resource template configuration
+func (r *ToolRegistry) GetResourceTemplate(name string)
(ResourceTemplateConfig, bool) {
+ r.mu.RLock()
+ defer r.mu.RUnlock()
+
+ template, exists := r.resourceTemplates[name]
+ return template, exists
+}
+
+// ListResourceTemplates lists all resource templates
+func (r *ToolRegistry) ListResourceTemplates() []ResourceTemplateConfig {
+ r.mu.RLock()
+ defer r.mu.RUnlock()
+
+ templates := make([]ResourceTemplateConfig, 0, len(r.resourceTemplates))
+ for _, template := range r.resourceTemplates {
+ templates = append(templates, template)
+ }
+ return templates
+}
+
+// ListTools lists all tools
+func (r *ToolRegistry) ListTools() []ToolConfig {
+ r.mu.RLock()
+ defer r.mu.RUnlock()
+
+ tools := make([]ToolConfig, 0, len(r.tools))
+ for _, tool := range r.tools {
+ tools = append(tools, tool)
+ }
+ return tools
+}
+
+// ListResources lists all resources
+func (r *ToolRegistry) ListResources() []ResourceConfig {
+ r.mu.RLock()
+ defer r.mu.RUnlock()
+
+ resources := make([]ResourceConfig, 0, len(r.resources))
+ for _, resource := range r.resources {
+ resources = append(resources, resource)
+ }
+ return resources
+}
+
+// ToMCPTools converts tool configurations to tool list
+func (r *ToolRegistry) ToMCPTools() ([]map[string]any, error) {
+ tools := r.ListTools()
+ mcpTools := make([]map[string]any, 0, len(tools))
+
+ for _, tool := range tools {
+ // Build tool according to MCP protocol specification
+ mcpTool := map[string]any{
+ "name": tool.Name,
+ "description": tool.Description,
+ "inputSchema": r.convertToInputSchema(tool),
+ }
+ mcpTools = append(mcpTools, mcpTool)
+ }
+
+ return mcpTools, nil
+}
+
+// convertToInputSchema converts tool parameters to MCP inputSchema format
+func (r *ToolRegistry) convertToInputSchema(tool ToolConfig) map[string]any {
+ allParams, err := tool.GetAllParameters()
+ if err != nil {
+ logger.Errorf("failed to get parameters for tool %s: %v",
tool.Name, err)
+ return map[string]any{
+ "type": "object",
+ "properties": map[string]any{},
+ }
+ }
+
+ properties := make(map[string]any)
+ required := make([]string, 0)
+
+ for _, param := range allParams {
+ propSchema := map[string]any{
+ "type": param.Type,
+ "description": param.Description,
+ }
+
+ if len(param.Enum) > 0 {
+ propSchema["enum"] = param.Enum
+ }
+
+ if param.Default != nil {
+ propSchema["default"] = param.Default
+ }
+
+ properties[param.Name] = propSchema
+
+ if param.Required {
+ required = append(required, param.Name)
+ }
+ }
+
+ schema := map[string]any{
+ "type": "object",
+ "properties": properties,
+ }
+
+ if len(required) > 0 {
+ schema["required"] = required
+ }
+
+ return schema
+}
+
+// ToMCPResources converts resource configurations to MCP resource list using
mcp-go structures
+func (r *ToolRegistry) ToMCPResources() ([]mcp.Resource, error) {
+ resources := r.ListResources()
+ mcpResources := make([]mcp.Resource, 0, len(resources))
+
+ for _, resource := range resources {
+ // Use mcp-go Resource structure
+ mcpResource := mcp.Resource{
+ URI: resource.URI,
+ Name: resource.Name,
+ Description: resource.Description,
+ MIMEType: resource.MIMEType,
+ }
+ mcpResources = append(mcpResources, mcpResource)
+ }
+
+ return mcpResources, nil
+}
+
+// ToMCPResourceTemplates converts resource template configurations to MCP
resource template list
+func (r *ToolRegistry) ToMCPResourceTemplates() ([]map[string]any, error) {
+ templates := r.ListResourceTemplates()
+ mcpTemplates := make([]map[string]any, 0, len(templates))
+
+ for _, template := range templates {
+ mcpTemplate := map[string]any{
+ "uriTemplate": template.URITemplate,
+ "name": template.Name,
+ "description": template.Description,
+ "mimeType": template.MIMEType,
+ }
+
+ // Add optional fields
+ if template.Title != "" {
+ mcpTemplate["title"] = template.Title
+ }
+
+ // Add annotations
+ if template.Annotations != nil {
+ annotations := make(map[string]any)
+ if len(template.Annotations.Audience) > 0 {
+ annotations["audience"] =
template.Annotations.Audience
+ }
+ if template.Annotations.Priority != nil {
+ annotations["priority"] =
*template.Annotations.Priority
+ }
+ if template.Annotations.LastModified != "" {
+ annotations["lastModified"] =
template.Annotations.LastModified
+ }
+ if len(annotations) > 0 {
+ mcpTemplate["annotations"] = annotations
+ }
+ }
+
+ mcpTemplates = append(mcpTemplates, mcpTemplate)
+ }
+
+ return mcpTemplates, nil
+}
+
+// RegisterPrompt registers a prompt
+func (r *ToolRegistry) RegisterPrompt(prompt PromptConfig) error {
+ r.mu.Lock()
+ defer r.mu.Unlock()
+
+ if _, exists := r.prompts[prompt.Name]; exists {
+ return fmt.Errorf("prompt %s already exists", prompt.Name)
+ }
+
+ r.prompts[prompt.Name] = prompt
+ return nil
+}
+
+// GetPrompt gets a prompt
+func (r *ToolRegistry) GetPrompt(name string) (PromptConfig, bool) {
+ r.mu.RLock()
+ defer r.mu.RUnlock()
+
+ prompt, exists := r.prompts[name]
+ return prompt, exists
+}
+
+// ListPrompts lists all prompts
+func (r *ToolRegistry) ListPrompts() []PromptConfig {
+ r.mu.RLock()
+ defer r.mu.RUnlock()
+
+ prompts := make([]PromptConfig, 0, len(r.prompts))
+ for _, prompt := range r.prompts {
+ prompts = append(prompts, prompt)
+ }
+
+ return prompts
+}
+
+// ToMCPPrompts converts prompt configurations to MCP prompt list
+func (r *ToolRegistry) ToMCPPrompts() ([]map[string]any, error) {
+ prompts := r.ListPrompts()
+ mcpPrompts := make([]map[string]any, 0, len(prompts))
+
+ for _, prompt := range prompts {
+ mcpPrompt := map[string]any{
+ "name": prompt.Name,
+ "description": prompt.Description,
+ }
+
+ if prompt.Title != "" {
+ mcpPrompt["title"] = prompt.Title
+ }
+
+ if len(prompt.Arguments) > 0 {
+ args := make([]map[string]any, 0, len(prompt.Arguments))
+ for _, arg := range prompt.Arguments {
+ argMap := map[string]any{
+ "name": arg.Name,
+ "description": arg.Description,
+ "required": arg.Required,
+ }
+ args = append(args, argMap)
+ }
+ mcpPrompt["arguments"] = args
+ }
+
+ mcpPrompts = append(mcpPrompts, mcpPrompt)
+ }
+
+ return mcpPrompts, nil
+}
+
+// TODO: Dynamic update functionality - implement when integrating with Nacos
diff --git a/pkg/filter/mcp/mcpserver/response.go
b/pkg/filter/mcp/mcpserver/response.go
new file mode 100644
index 00000000..429976b0
--- /dev/null
+++ b/pkg/filter/mcp/mcpserver/response.go
@@ -0,0 +1,151 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package mcpserver
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "sync"
+)
+
+import (
+ "github.com/mark3labs/mcp-go/mcp"
+)
+
+import (
+ "github.com/apache/dubbo-go-pixiu/pkg/common/extension/filter"
+ "github.com/apache/dubbo-go-pixiu/pkg/logger"
+)
+
+// ResponseBuilder provides methods to create standardized MCP responses
+type ResponseBuilder struct{}
+
+var (
+ responseBuilderInstance *ResponseBuilder
+ responseBuilderOnce sync.Once
+)
+
+// NewResponseBuilder returns the singleton ResponseBuilder instance
+func NewResponseBuilder() *ResponseBuilder {
+ responseBuilderOnce.Do(func() {
+ responseBuilderInstance = &ResponseBuilder{}
+ })
+ return responseBuilderInstance
+}
+
+// Success creates a successful JSON-RPC response
+func (rb *ResponseBuilder) Success(id any, result any) mcp.JSONRPCResponse {
+ return mcp.JSONRPCResponse{
+ JSONRPC: mcp.JSONRPC_VERSION,
+ ID: mcp.NewRequestId(id),
+ Result: result,
+ }
+}
+
+// Error creates an error JSON-RPC response
+func (rb *ResponseBuilder) Error(id any, code int, message string)
mcp.JSONRPCError {
+ return mcp.NewJSONRPCError(mcp.NewRequestId(id), code, message, nil)
+}
+
+// ToolCallSuccess creates a successful tool call response
+func (rb *ResponseBuilder) ToolCallSuccess(id any, content string)
mcp.JSONRPCResponse {
+ // Use mcp-go API to create text content
+ textContent := mcp.NewTextContent(content)
+
+ // Build MCP tool call result using mcp-go structures
+ result := mcp.CallToolResult{
+ Content: []mcp.Content{textContent},
+ IsError: false,
+ }
+
+ return rb.Success(id, result)
+}
+
+// ToolCallError creates an error tool call response
+func (rb *ResponseBuilder) ToolCallError(id any, message string)
mcp.JSONRPCResponse {
+ errorText := fmt.Sprintf("Error: %s", message)
+ textContent := mcp.NewTextContent(errorText)
+
+ // Build MCP tool call error result using mcp-go structures
+ result := mcp.CallToolResult{
+ Content: []mcp.Content{textContent},
+ IsError: true,
+ }
+
+ return rb.Success(id, result)
+}
+
+// ErrorHandler provides centralized error handling for MCP responses
+type ErrorHandler struct {
+ responseBuilder *ResponseBuilder
+}
+
+var (
+ errorHandlerInstance *ErrorHandler
+ errorHandlerOnce sync.Once
+)
+
+// NewErrorHandler returns the singleton ErrorHandler instance
+func NewErrorHandler() *ErrorHandler {
+ errorHandlerOnce.Do(func() {
+ errorHandlerInstance = &ErrorHandler{
+ responseBuilder: NewResponseBuilder(),
+ }
+ })
+ return errorHandlerInstance
+}
+
+// SendInternalError sends an internal server error response
+func (eh *ErrorHandler) SendInternalError(ctx *MCPContext, id any, message
string) filter.FilterStatus {
+ response := eh.responseBuilder.Error(id, mcp.INTERNAL_ERROR, message)
+ return eh.sendResponse(ctx, response)
+}
+
+// SendMethodNotFound sends a method not found error response
+func (eh *ErrorHandler) SendMethodNotFound(ctx *MCPContext, id any)
filter.FilterStatus {
+ response := eh.responseBuilder.Error(id, mcp.METHOD_NOT_FOUND, "Method
not found")
+ return eh.sendResponse(ctx, response)
+}
+
+// SendInvalidParams sends an invalid parameters error response
+func (eh *ErrorHandler) SendInvalidParams(ctx *MCPContext, id any, message
string) filter.FilterStatus {
+ response := eh.responseBuilder.Error(id, mcp.INVALID_PARAMS,
fmt.Sprintf("Invalid params: %s", message))
+ return eh.sendResponse(ctx, response)
+}
+
+// SendToolCallError sends a tool call error response
+func (eh *ErrorHandler) SendToolCallError(ctx *MCPContext, id any, message
string) filter.FilterStatus {
+ response := eh.responseBuilder.ToolCallError(id, message)
+ return eh.sendResponse(ctx, response)
+}
+
+// sendResponse sends any response and handles Content-Length cleanup
+func (eh *ErrorHandler) sendResponse(ctx *MCPContext, response any)
filter.FilterStatus {
+ responseBody, err := json.Marshal(response)
+ if err != nil {
+ logger.Errorf("[dubbo-go-pixiu] mcp server failed to marshal
response: %v", err)
+ ctx.SendLocalReply(http.StatusInternalServerError,
[]byte("internal server error"))
+ return filter.Stop
+ }
+
+ // Critical: Clear Content-Length header to prevent mismatch errors
+ ctx.ClearContentLengthHeader()
+ ctx.SendLocalReply(http.StatusOK, responseBody)
+ return filter.Stop
+}
diff --git a/pkg/pluginregistry/registry.go b/pkg/pluginregistry/registry.go
index fd32183a..775c1b97 100644
--- a/pkg/pluginregistry/registry.go
+++ b/pkg/pluginregistry/registry.go
@@ -46,6 +46,7 @@ import (
_
"github.com/apache/dubbo-go-pixiu/pkg/filter/http/remote/resolver/dubboresolver"
_ "github.com/apache/dubbo-go-pixiu/pkg/filter/llm/proxy"
_ "github.com/apache/dubbo-go-pixiu/pkg/filter/llm/tokenizer"
+ _ "github.com/apache/dubbo-go-pixiu/pkg/filter/mcp/mcpserver"
_ "github.com/apache/dubbo-go-pixiu/pkg/filter/metric"
_ "github.com/apache/dubbo-go-pixiu/pkg/filter/network/dubboproxy"
_
"github.com/apache/dubbo-go-pixiu/pkg/filter/network/dubboproxy/filter/http"